From 433670a75439804f1d6aedde3fbc60a7dc5ca3c3 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 19 Jul 2026 22:13:50 +0800 Subject: [PATCH 01/12] feat(invariants): require package-owned companions --- docs/config-catalog.md | 2 +- docs/cordis-catalog/services.md | 2 +- docs/core-data-structures/session.md | 2 +- docs/module-graph.md | 251 ++++++++++------ ...06-11-dev-invariants-over-deep-readonly.md | 2 +- .../2026-06-15-turn-enclosure-invariant.md | 2 +- .../2026-07-05-reconstructable-requests.md | 2 +- .../2026-07-12-agent-scope-runtime-design.md | 2 +- ...-package-owned-invariant-service.i18n.yaml | 4 +- ...6-07-19-package-owned-invariant-service.md | 18 +- ...7-19-package-owned-invariant-service.zh.md | 18 +- .../2026-06-16-typed-event-schemas.md | 6 +- docs/testing.md | 2 + knip.json | 21 +- package.json | 4 +- packages/AGENTS.md | 1 + packages/bash/bash-local/package.json | 7 + packages/bash/bash-local/src/invariant.ts | 30 ++ packages/bash/bash-local/tsconfig.json | 3 + packages/bash/bash-sandbox/package.json | 11 +- packages/bash/bash-sandbox/src/invariant.ts | 30 ++ packages/bash/bash-sandbox/tsconfig.json | 3 + packages/bash/bash/package.json | 7 + packages/bash/bash/src/invariant.ts | 30 ++ packages/bash/bash/tsconfig.json | 3 + packages/bash/tool-bash/package.json | 13 +- packages/bash/tool-bash/src/invariant.ts | 30 ++ packages/bash/tool-bash/tsconfig.json | 3 + .../code-runtime-worker/package.json | 7 + .../code-runtime-worker/src/invariant.ts | 30 ++ .../code-runtime-worker/tsconfig.json | 3 + .../code-runtime-worker/tsdown.config.ts | 2 +- .../code-runtime/code-runtime/package.json | 7 + .../code-runtime/src/invariant.ts | 30 ++ .../code-runtime/code-runtime/tsconfig.json | 3 + packages/compact/compact-basic/package.json | 6 + .../compact/compact-basic/src/invariant.ts | 30 ++ .../compact-basic/tests/compact-basic.spec.ts | 8 +- packages/compact/compact-basic/tsconfig.json | 35 ++- packages/compact/compact/package.json | 7 + packages/compact/compact/src/invariant.ts | 30 ++ packages/compact/compact/tsconfig.json | 3 + packages/context/time-context/package.json | 7 + .../context/time-context/src/invariant.ts | 30 ++ .../time-context/tests/time-context.spec.ts | 5 +- packages/context/time-context/tsconfig.json | 35 ++- .../context/workspace-context/package.json | 7 + .../workspace-context/src/invariant.ts | 30 ++ .../tests/workspace-context.spec.ts | 60 ++-- .../context/workspace-context/tsconfig.json | 3 + packages/cordis/tool-cordis/package.json | 15 +- packages/cordis/tool-cordis/src/invariant.ts | 30 ++ packages/cordis/tool-cordis/tsconfig.json | 3 + packages/core/agent-loop/src/invariant.ts | 6 +- packages/core/agent-loop/tests/resume.spec.ts | 4 +- packages/core/session/src/invariant.ts | 6 +- packages/core/session/tests/session.spec.ts | 23 +- packages/core/system-prompt/package.json | 7 + packages/core/system-prompt/src/invariant.ts | 30 ++ packages/core/system-prompt/tsconfig.json | 3 + packages/core/tools/package.json | 11 +- packages/core/tools/src/invariant.ts | 30 ++ packages/core/tools/tsconfig.json | 3 + packages/examples/acp-demo/package.json | 19 +- packages/examples/acp-demo/src/invariant.ts | 30 ++ packages/examples/acp-demo/tsconfig.json | 3 + packages/examples/acp-demo/tsdown.config.ts | 2 +- .../examples/agent-spine-demo/package.json | 17 +- .../agent-spine-demo/src/invariant.ts | 30 ++ packages/examples/cli-demo/package.json | 7 + packages/examples/cli-demo/src/invariant.ts | 30 ++ packages/examples/cli-demo/tsconfig.json | 43 ++- packages/examples/cli-demo/tsdown.config.ts | 2 +- packages/examples/jsonrpc-demo/package.json | 7 + .../examples/jsonrpc-demo/src/invariant.ts | 30 ++ packages/examples/jsonrpc-demo/tsconfig.json | 3 + .../examples/jsonrpc-demo/tsdown.config.ts | 2 +- packages/examples/stdio-demo/package.json | 25 +- packages/examples/stdio-demo/src/invariant.ts | 30 ++ packages/examples/stdio-demo/tsconfig.json | 3 + packages/examples/stdio-demo/tsdown.config.ts | 2 +- packages/fs/fs-local/package.json | 7 + packages/fs/fs-local/src/invariant.ts | 30 ++ packages/fs/fs-local/tsconfig.json | 23 +- packages/fs/fs-policy/package.json | 7 + packages/fs/fs-policy/src/invariant.ts | 30 ++ packages/fs/fs-policy/tsconfig.json | 19 +- packages/fs/fs/package.json | 7 + packages/fs/fs/src/invariant.ts | 30 ++ packages/fs/fs/tsconfig.json | 19 +- packages/fs/tool-fs-search/package.json | 7 + packages/fs/tool-fs-search/src/invariant.ts | 30 ++ packages/fs/tool-fs-search/tsconfig.json | 43 ++- packages/fs/tool-fs/package.json | 9 +- packages/fs/tool-fs/src/invariant.ts | 30 ++ packages/fs/tool-fs/tsconfig.json | 35 ++- packages/guard/repeat-tool-guard/package.json | 7 + .../guard/repeat-tool-guard/src/invariant.ts | 30 ++ .../guard/repeat-tool-guard/tsconfig.json | 3 + packages/hooks/hook-protocol/package.json | 7 + packages/hooks/hook-protocol/src/invariant.ts | 30 ++ packages/hooks/hook-protocol/tsconfig.json | 3 + packages/hooks/hooks-claude/package.json | 7 + packages/hooks/hooks-claude/src/invariant.ts | 30 ++ .../hooks/hooks-claude/tests/bridge.spec.ts | 13 +- .../hooks-claude/tests/coverage-cases.ts | 15 +- packages/hooks/hooks-claude/tsconfig.json | 3 + packages/hooks/hooks-codex/package.json | 7 + packages/hooks/hooks-codex/src/invariant.ts | 30 ++ packages/hooks/hooks-codex/tsconfig.json | 3 + packages/llm/llm-deepseek/package.json | 7 + packages/llm/llm-deepseek/src/invariant.ts | 30 ++ packages/llm/llm-deepseek/tsconfig.json | 3 + packages/llm/llm-pi-ai/package.json | 7 + packages/llm/llm-pi-ai/src/invariant.ts | 30 ++ packages/llm/llm-pi-ai/tsconfig.json | 3 + packages/llm/llm/package.json | 7 + packages/llm/llm/src/invariant.ts | 30 ++ packages/llm/llm/tsconfig.json | 3 + packages/llm/token-meter/package.json | 7 + packages/llm/token-meter/src/invariant.ts | 30 ++ .../llm/token-meter/tests/token-meter.spec.ts | 13 +- packages/llm/token-meter/tsconfig.json | 3 + packages/mcp/mcp-client/package.json | 11 +- packages/mcp/mcp-client/src/invariant.ts | 30 ++ packages/mcp/mcp-client/tsconfig.json | 23 +- packages/sandbox/sandbox-local/package.json | 7 + .../sandbox/sandbox-local/src/invariant.ts | 30 ++ packages/sandbox/sandbox-local/tsconfig.json | 3 + packages/sandbox/sandbox/package.json | 7 + packages/sandbox/sandbox/src/invariant.ts | 30 ++ packages/sandbox/sandbox/tsconfig.json | 3 + packages/sdk/create-sdk/package.json | 7 + packages/sdk/create-sdk/src/invariant.ts | 30 ++ packages/sdk/create-sdk/tsconfig.json | 11 +- packages/sdk/create-sdk/tsdown.config.ts | 2 +- packages/sdk/helper/package.json | 7 + packages/sdk/helper/src/invariant.ts | 30 ++ packages/sdk/helper/tsconfig.json | 39 ++- packages/sdk/helper/tsdown.config.ts | 2 +- packages/sdk/scripts/package.json | 15 +- packages/sdk/scripts/src/invariant.ts | 30 ++ packages/sdk/scripts/tsconfig.json | 15 +- packages/sdk/scripts/tsdown.config.ts | 4 + .../session-persistence-jsonl/package.json | 7 + .../src/invariant.ts | 30 ++ .../tests/jsonl.spec.ts | 29 +- .../session-persistence-jsonl/tsconfig.json | 3 + .../session-persistence-sqlite/package.json | 7 + .../src/invariant.ts | 30 ++ .../tests/sqlite.spec.ts | 32 +- .../session-persistence-sqlite/tsconfig.json | 3 + .../session-persistence/package.json | 8 + .../session-persistence/src/invariant.ts | 30 ++ .../tests/coordinator-contract.ts | 53 ++-- .../session-persistence/tsconfig.json | 3 + .../session-query/session-query/package.json | 7 + .../session-query/src/invariant.ts | 30 ++ .../session-query/tests/session-query.spec.ts | 32 +- .../session-query/tests/tracing.spec.ts | 59 ++-- .../session-query/session-query/tsconfig.json | 3 + packages/skill/skill-local/package.json | 7 + packages/skill/skill-local/src/invariant.ts | 30 ++ packages/skill/skill-local/tsconfig.json | 27 +- packages/skill/skill/package.json | 7 + packages/skill/skill/src/invariant.ts | 30 ++ packages/skill/skill/tsconfig.json | 15 +- packages/skill/tool-skill/package.json | 7 + packages/skill/tool-skill/src/invariant.ts | 30 ++ packages/skill/tool-skill/tsconfig.json | 35 ++- packages/spill/spill-local/package.json | 7 + packages/spill/spill-local/src/invariant.ts | 30 ++ packages/spill/spill-local/tsconfig.json | 19 +- packages/spill/spill-policy/package.json | 7 + packages/spill/spill-policy/src/invariant.ts | 30 ++ packages/spill/spill-policy/tsconfig.json | 35 ++- packages/spill/spill/package.json | 7 + packages/spill/spill/src/invariant.ts | 30 ++ packages/spill/spill/tsconfig.json | 23 +- packages/subagent/subagent-acp/package.json | 9 +- .../subagent/subagent-acp/src/invariant.ts | 30 ++ packages/subagent/subagent-acp/tsconfig.json | 3 + packages/subagent/subagent-fork/package.json | 8 +- .../subagent/subagent-fork/src/invariant.ts | 30 ++ packages/subagent/subagent-fork/tsconfig.json | 3 + .../subagent/subagent-inprocess/package.json | 6 + .../subagent-inprocess/src/invariant.ts | 30 ++ .../subagent/subagent-inprocess/tsconfig.json | 3 + packages/subagent/subagent-spawn/package.json | 8 +- .../subagent/subagent-spawn/src/invariant.ts | 30 ++ .../subagent/subagent-spawn/tsconfig.json | 3 + .../subagent/subagent-subprocess/package.json | 7 + .../subagent-subprocess/src/invariant.ts | 30 ++ .../subagent-subprocess/tsconfig.json | 6 +- packages/subagent/subagent/package.json | 7 + packages/subagent/subagent/src/invariant.ts | 30 ++ packages/subagent/subagent/tsconfig.json | 3 + packages/subagent/tool-subagent/package.json | 7 + .../subagent/tool-subagent/src/invariant.ts | 30 ++ packages/subagent/tool-subagent/tsconfig.json | 3 + packages/support/acp-snapshot/package.json | 7 + .../support/acp-snapshot/src/invariant.ts | 30 ++ packages/support/acp-snapshot/tsconfig.json | 7 +- .../support/agent-loop-testkit/package.json | 7 + .../agent-loop-testkit/src/invariant.ts | 30 ++ .../support/agent-loop-testkit/tsconfig.json | 3 + packages/support/invariants/README.md | 10 +- packages/support/invariants/package.json | 5 + packages/support/invariants/src/index.ts | 5 +- packages/support/invariants/src/invariant.ts | 30 ++ packages/support/llm-replay/package.json | 7 + packages/support/llm-replay/src/invariant.ts | 30 ++ packages/support/llm-replay/tsconfig.json | 3 + packages/support/loader-smoke/package.json | 7 + .../support/loader-smoke/src/invariant.ts | 30 ++ packages/support/loader-smoke/tsconfig.json | 6 +- packages/tasks/tasks/package.json | 9 +- packages/tasks/tasks/src/invariant.ts | 30 ++ packages/tasks/tasks/tsconfig.json | 3 + packages/tasks/tool-tasks/package.json | 7 + packages/tasks/tool-tasks/src/invariant.ts | 30 ++ packages/tasks/tool-tasks/tsconfig.json | 3 + packages/timeout/timeout-policy/package.json | 7 + .../timeout/timeout-policy/src/invariant.ts | 30 ++ packages/timeout/timeout-policy/tsconfig.json | 27 +- packages/todo/tool-todo/package.json | 7 + packages/todo/tool-todo/src/invariant.ts | 30 ++ packages/todo/tool-todo/tsconfig.json | 3 + packages/ui/acp/package.json | 6 + packages/ui/acp/src/invariant.ts | 30 ++ packages/ui/acp/tsconfig.json | 3 + packages/ui/app-boot/package.json | 7 + packages/ui/app-boot/src/invariant.ts | 30 ++ packages/ui/app-boot/tsconfig.json | 3 + packages/ui/jsonrpc/package.json | 7 + packages/ui/jsonrpc/src/invariant.ts | 30 ++ packages/ui/jsonrpc/tsconfig.json | 3 + packages/ui/permission/package.json | 7 + packages/ui/permission/src/invariant.ts | 30 ++ packages/ui/permission/tsconfig.json | 3 + packages/ui/stdio/package.json | 7 + packages/ui/stdio/src/invariant.ts | 30 ++ packages/ui/stdio/tests/stdio.spec.ts | 112 ++++--- packages/ui/stdio/tsconfig.json | 3 + packages/ui/tool-ask-user/package.json | 7 + packages/ui/tool-ask-user/src/invariant.ts | 30 ++ packages/ui/tool-ask-user/tsconfig.json | 3 + packages/ui/tui/package.json | 7 + packages/ui/tui/src/invariant.ts | 30 ++ packages/ui/tui/tests/harness.ts | 9 +- packages/ui/tui/tests/tui.snapshot.ts | 42 +-- packages/ui/tui/tests/tui.spec.ts | 138 +++++---- packages/ui/tui/tsconfig.json | 3 + packages/ui/user-approval/package.json | 7 + packages/ui/user-approval/src/invariant.ts | 30 ++ .../ui/user-approval/tests/approval.spec.ts | 4 +- packages/ui/user-approval/tsconfig.json | 3 + packages/ui/user-interaction/package.json | 7 + packages/ui/user-interaction/src/invariant.ts | 30 ++ packages/ui/user-interaction/tsconfig.json | 3 + packages/util/brand/package.json | 7 + packages/util/brand/src/invariant.ts | 30 ++ packages/util/brand/tsconfig.json | 6 +- packages/util/home/package.json | 7 + packages/util/home/src/invariant.ts | 30 ++ packages/util/home/tsconfig.json | 6 +- packages/util/paths/package.json | 7 + packages/util/paths/src/invariant.ts | 30 ++ packages/util/paths/tsconfig.json | 6 +- packages/util/retention/package.json | 7 + packages/util/retention/src/invariant.ts | 30 ++ packages/util/retention/tsconfig.json | 6 +- packages/util/timeout/package.json | 7 + packages/util/timeout/src/invariant.ts | 30 ++ packages/util/timeout/tsconfig.json | 6 +- packages/web/tool-web/package.json | 9 +- packages/web/tool-web/src/invariant.ts | 30 ++ packages/web/tool-web/tsconfig.json | 35 ++- packages/web/web-fetch-local/package.json | 7 + packages/web/web-fetch-local/src/invariant.ts | 30 ++ packages/web/web-fetch-local/tsconfig.json | 3 + packages/web/web-search-deepseek/package.json | 7 + .../web/web-search-deepseek/src/invariant.ts | 30 ++ .../web/web-search-deepseek/tsconfig.json | 3 + packages/web/web-search-exa/package.json | 7 + packages/web/web-search-exa/src/invariant.ts | 30 ++ packages/web/web-search-exa/tsconfig.json | 3 + .../web/web-search-perplexity/package.json | 7 + .../web-search-perplexity/src/invariant.ts | 30 ++ .../web/web-search-perplexity/tsconfig.json | 3 + packages/web/web/package.json | 7 + packages/web/web/src/invariant.ts | 30 ++ packages/web/web/tsconfig.json | 3 + packages/workflow/tool-workflow/package.json | 7 + .../workflow/tool-workflow/src/invariant.ts | 30 ++ packages/workflow/tool-workflow/tsconfig.json | 3 + .../workflow-workerthread/package.json | 6 + .../workflow-workerthread/src/invariant.ts | 30 ++ .../workflow-workerthread/tsconfig.json | 3 + .../workflow-workerthread/tsdown.config.ts | 2 +- packages/workflow/workflow/package.json | 7 + packages/workflow/workflow/src/invariant.ts | 30 ++ packages/workflow/workflow/tsconfig.json | 3 + pnpm-lock.yaml | 237 +++++++++++++++ scripts/check-workspace-constraints.ts | 62 +--- scripts/gen-package-invariants.ts | 44 +++ scripts/package-invariants.spec.ts | 104 +++++++ scripts/package-invariants.ts | 283 ++++++++++++++++++ scripts/run-gates.ts | 3 + scripts/test-invariants.spec.ts | 70 +++++ scripts/test-invariants.ts | 104 +++++++ tsdown.config.ts | 9 +- vitest.config.ts | 1 + vitest.e2e.config.ts | 1 + vitest.snapshot.config.ts | 1 + website/zh-CN/api/harness/invariants.md | 4 +- 316 files changed, 5348 insertions(+), 646 deletions(-) create mode 100644 packages/bash/bash-local/src/invariant.ts create mode 100644 packages/bash/bash-sandbox/src/invariant.ts create mode 100644 packages/bash/bash/src/invariant.ts create mode 100644 packages/bash/tool-bash/src/invariant.ts create mode 100644 packages/code-runtime/code-runtime-worker/src/invariant.ts create mode 100644 packages/code-runtime/code-runtime/src/invariant.ts create mode 100644 packages/compact/compact-basic/src/invariant.ts create mode 100644 packages/compact/compact/src/invariant.ts create mode 100644 packages/context/time-context/src/invariant.ts create mode 100644 packages/context/workspace-context/src/invariant.ts create mode 100644 packages/cordis/tool-cordis/src/invariant.ts create mode 100644 packages/core/system-prompt/src/invariant.ts create mode 100644 packages/core/tools/src/invariant.ts create mode 100644 packages/examples/acp-demo/src/invariant.ts create mode 100644 packages/examples/agent-spine-demo/src/invariant.ts create mode 100644 packages/examples/cli-demo/src/invariant.ts create mode 100644 packages/examples/jsonrpc-demo/src/invariant.ts create mode 100644 packages/examples/stdio-demo/src/invariant.ts create mode 100644 packages/fs/fs-local/src/invariant.ts create mode 100644 packages/fs/fs-policy/src/invariant.ts create mode 100644 packages/fs/fs/src/invariant.ts create mode 100644 packages/fs/tool-fs-search/src/invariant.ts create mode 100644 packages/fs/tool-fs/src/invariant.ts create mode 100644 packages/guard/repeat-tool-guard/src/invariant.ts create mode 100644 packages/hooks/hook-protocol/src/invariant.ts create mode 100644 packages/hooks/hooks-claude/src/invariant.ts create mode 100644 packages/hooks/hooks-codex/src/invariant.ts create mode 100644 packages/llm/llm-deepseek/src/invariant.ts create mode 100644 packages/llm/llm-pi-ai/src/invariant.ts create mode 100644 packages/llm/llm/src/invariant.ts create mode 100644 packages/llm/token-meter/src/invariant.ts create mode 100644 packages/mcp/mcp-client/src/invariant.ts create mode 100644 packages/sandbox/sandbox-local/src/invariant.ts create mode 100644 packages/sandbox/sandbox/src/invariant.ts create mode 100644 packages/sdk/create-sdk/src/invariant.ts create mode 100644 packages/sdk/helper/src/invariant.ts create mode 100644 packages/sdk/scripts/src/invariant.ts create mode 100644 packages/session-persistence/session-persistence-jsonl/src/invariant.ts create mode 100644 packages/session-persistence/session-persistence-sqlite/src/invariant.ts create mode 100644 packages/session-persistence/session-persistence/src/invariant.ts create mode 100644 packages/session-query/session-query/src/invariant.ts create mode 100644 packages/skill/skill-local/src/invariant.ts create mode 100644 packages/skill/skill/src/invariant.ts create mode 100644 packages/skill/tool-skill/src/invariant.ts create mode 100644 packages/spill/spill-local/src/invariant.ts create mode 100644 packages/spill/spill-policy/src/invariant.ts create mode 100644 packages/spill/spill/src/invariant.ts create mode 100644 packages/subagent/subagent-acp/src/invariant.ts create mode 100644 packages/subagent/subagent-fork/src/invariant.ts create mode 100644 packages/subagent/subagent-inprocess/src/invariant.ts create mode 100644 packages/subagent/subagent-spawn/src/invariant.ts create mode 100644 packages/subagent/subagent-subprocess/src/invariant.ts create mode 100644 packages/subagent/subagent/src/invariant.ts create mode 100644 packages/subagent/tool-subagent/src/invariant.ts create mode 100644 packages/support/acp-snapshot/src/invariant.ts create mode 100644 packages/support/agent-loop-testkit/src/invariant.ts create mode 100644 packages/support/invariants/src/invariant.ts create mode 100644 packages/support/llm-replay/src/invariant.ts create mode 100644 packages/support/loader-smoke/src/invariant.ts create mode 100644 packages/tasks/tasks/src/invariant.ts create mode 100644 packages/tasks/tool-tasks/src/invariant.ts create mode 100644 packages/timeout/timeout-policy/src/invariant.ts create mode 100644 packages/todo/tool-todo/src/invariant.ts create mode 100644 packages/ui/acp/src/invariant.ts create mode 100644 packages/ui/app-boot/src/invariant.ts create mode 100644 packages/ui/jsonrpc/src/invariant.ts create mode 100644 packages/ui/permission/src/invariant.ts create mode 100644 packages/ui/stdio/src/invariant.ts create mode 100644 packages/ui/tool-ask-user/src/invariant.ts create mode 100644 packages/ui/tui/src/invariant.ts create mode 100644 packages/ui/user-approval/src/invariant.ts create mode 100644 packages/ui/user-interaction/src/invariant.ts create mode 100644 packages/util/brand/src/invariant.ts create mode 100644 packages/util/home/src/invariant.ts create mode 100644 packages/util/paths/src/invariant.ts create mode 100644 packages/util/retention/src/invariant.ts create mode 100644 packages/util/timeout/src/invariant.ts create mode 100644 packages/web/tool-web/src/invariant.ts create mode 100644 packages/web/web-fetch-local/src/invariant.ts create mode 100644 packages/web/web-search-deepseek/src/invariant.ts create mode 100644 packages/web/web-search-exa/src/invariant.ts create mode 100644 packages/web/web-search-perplexity/src/invariant.ts create mode 100644 packages/web/web/src/invariant.ts create mode 100644 packages/workflow/tool-workflow/src/invariant.ts create mode 100644 packages/workflow/workflow-workerthread/src/invariant.ts create mode 100644 packages/workflow/workflow/src/invariant.ts create mode 100644 scripts/gen-package-invariants.ts create mode 100644 scripts/package-invariants.spec.ts create mode 100644 scripts/package-invariants.ts create mode 100644 scripts/test-invariants.spec.ts create mode 100644 scripts/test-invariants.ts diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 76663117b8..4ea9c7a300 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:15`](../packages/support/invariants/src/index.ts) +Source: [`packages/support/invariants/src/index.ts:16`](../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 5a868e1eff..ca5eb4232b 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:94`](../../packages/support/invariants/src/index.ts) +Source: [`packages/support/invariants/src/index.ts:95`](../../packages/support/invariants/src/index.ts) ## `ctx.llm` — `LlmService` diff --git a/docs/core-data-structures/session.md b/docs/core-data-structures/session.md index 2dc5a1c34e..fb99f5c15e 100644 --- a/docs/core-data-structures/session.md +++ b/docs/core-data-structures/session.md @@ -502,7 +502,7 @@ interface TurnEndReasonMap { ## The turn-enclosure invariant -Every session event lives **inside** a turn (between a `turn/start` and its `turn/end`). The loop appends queued `user/message` events *after* `turn/start`, and an idle `agent.inject()` wraps its `context/message` in a one-shot `injection` turn. This makes the turn the single durability/replay boundary: a backend can treat anything after the last `turn/end` as an interrupted-crash tail without risking the loss of legitimately-recorded between-turn context. The optional `dsh-session/invariant` companion enforces it in dev through `ctx.invariants` (a message event outside an open turn throws). See [the turn-enclosure invariant RFC](../rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.md). +Every session event lives **inside** a turn (between a `turn/start` and its `turn/end`). The loop appends queued `user/message` events *after* `turn/start`, and an idle `agent.inject()` wraps its `context/message` in a one-shot `injection` turn. This makes the turn the single durability/replay boundary: a backend can treat anything after the last `turn/end` as an interrupted-crash tail without risking the loss of legitimately-recorded between-turn context. The `dsh-session/invariant` companion enforces it when selected through `ctx.invariants` (a message event outside an open turn throws). See [the turn-enclosure invariant RFC](../rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.md). ## Plugin-contributed log-only events diff --git a/docs/module-graph.md b/docs/module-graph.md index af8504be18..07ef0f4da9 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -150,23 +150,46 @@ flowchart TD pkg_workflow["workflow"] pkg_workflow_workerthread["workflow-workerthread"] end - pkg_llm --> pkg_brand + pkg_brand --> pkg_invariants + pkg_home --> pkg_invariants + pkg_paths --> pkg_invariants + pkg_retention --> pkg_invariants + pkg_timeout --> pkg_invariants pkg_scope --> pkg_invariants + pkg_skill --> pkg_invariants + pkg_subagent_subprocess --> pkg_invariants + pkg_acp_snapshot --> pkg_invariants + pkg_loader_smoke --> pkg_invariants + pkg_app_boot --> pkg_invariants + pkg_code_runtime --> pkg_invariants + pkg_jsonrpc_demo --> pkg_invariants + pkg_llm --> pkg_brand + pkg_llm --> pkg_invariants pkg_code_runtime_worker --> pkg_code_runtime + pkg_code_runtime_worker --> pkg_invariants pkg_helper --> pkg_brand + pkg_helper --> pkg_invariants pkg_scripts --> pkg_app_boot + pkg_scripts --> pkg_invariants + pkg_llm_deepseek --> pkg_invariants pkg_llm_deepseek --> pkg_llm + pkg_llm_pi_ai --> pkg_invariants pkg_llm_pi_ai --> pkg_llm pkg_session --> pkg_brand pkg_session --> pkg_invariants pkg_session --> pkg_llm pkg_session --> pkg_scope + pkg_system_prompt --> pkg_invariants pkg_system_prompt --> pkg_llm pkg_system_prompt --> pkg_scope pkg_fs --> pkg_brand + pkg_fs --> pkg_invariants pkg_fs --> pkg_llm + pkg_web --> pkg_invariants pkg_web --> pkg_llm + pkg_sandbox --> pkg_invariants pkg_sandbox --> pkg_llm + pkg_token_meter --> pkg_invariants pkg_token_meter --> pkg_llm pkg_token_meter --> pkg_session pkg_agent --> pkg_brand @@ -175,64 +198,90 @@ flowchart TD pkg_agent --> pkg_scope pkg_agent --> pkg_session pkg_agent --> pkg_system_prompt + pkg_bash --> pkg_invariants pkg_bash --> pkg_sandbox pkg_bash --> pkg_session pkg_fs_local --> pkg_fs + pkg_fs_local --> pkg_invariants pkg_fs_policy --> pkg_fs + pkg_fs_policy --> pkg_invariants pkg_skill_local --> pkg_fs pkg_skill_local --> pkg_home + pkg_skill_local --> pkg_invariants pkg_skill_local --> pkg_skill + pkg_compact --> pkg_invariants pkg_compact --> pkg_llm pkg_compact --> pkg_session + pkg_web_fetch_local --> pkg_invariants pkg_web_fetch_local --> pkg_timeout pkg_web_fetch_local --> pkg_web + pkg_web_search_deepseek --> pkg_invariants pkg_web_search_deepseek --> pkg_web + pkg_web_search_exa --> pkg_invariants pkg_web_search_exa --> pkg_web + pkg_web_search_perplexity --> pkg_invariants pkg_web_search_perplexity --> pkg_web pkg_spill --> pkg_brand + pkg_spill --> pkg_invariants pkg_spill --> pkg_llm pkg_spill --> pkg_session + pkg_session_persistence --> pkg_invariants pkg_session_persistence --> pkg_session + pkg_llm_replay --> pkg_invariants pkg_llm_replay --> pkg_llm pkg_llm_replay --> pkg_session + pkg_sandbox_local --> pkg_invariants pkg_sandbox_local --> pkg_llm pkg_sandbox_local --> pkg_sandbox pkg_bash_local --> pkg_bash + pkg_bash_local --> pkg_invariants pkg_bash_local --> pkg_timeout pkg_compact_basic --> pkg_agent pkg_compact_basic --> pkg_compact + pkg_compact_basic --> pkg_invariants pkg_compact_basic --> pkg_llm pkg_compact_basic --> pkg_session pkg_compact_basic --> pkg_token_meter + pkg_spill_local --> pkg_invariants pkg_spill_local --> pkg_spill pkg_hook_protocol --> pkg_bash + pkg_hook_protocol --> pkg_invariants pkg_hook_protocol --> pkg_session + pkg_session_persistence_jsonl --> pkg_invariants pkg_session_persistence_jsonl --> pkg_session pkg_session_persistence_jsonl --> pkg_session_persistence + pkg_session_persistence_sqlite --> pkg_invariants pkg_session_persistence_sqlite --> pkg_session pkg_session_persistence_sqlite --> pkg_session_persistence + pkg_session_query --> pkg_invariants pkg_session_query --> pkg_llm pkg_session_query --> pkg_session pkg_session_query --> pkg_session_persistence pkg_user_approval --> pkg_agent pkg_user_approval --> pkg_brand + pkg_user_approval --> pkg_invariants pkg_user_approval --> pkg_llm pkg_user_approval --> pkg_scope pkg_user_approval --> pkg_session pkg_user_approval --> pkg_system_prompt pkg_user_interaction --> pkg_agent + pkg_user_interaction --> pkg_invariants pkg_user_interaction --> pkg_llm pkg_time_context --> pkg_agent + pkg_time_context --> pkg_invariants pkg_tasks --> pkg_agent pkg_tasks --> pkg_brand + pkg_tasks --> pkg_invariants pkg_tasks --> pkg_session pkg_tasks --> pkg_timeout pkg_workflow --> pkg_agent pkg_workflow --> pkg_brand + pkg_workflow --> pkg_invariants pkg_workflow --> pkg_llm pkg_workflow --> pkg_session pkg_tools --> pkg_agent pkg_tools --> pkg_code_runtime + pkg_tools --> pkg_invariants pkg_tools --> pkg_llm pkg_tools --> pkg_scope pkg_tools --> pkg_session @@ -240,8 +289,10 @@ flowchart TD pkg_tools --> pkg_user_approval pkg_bash_sandbox --> pkg_bash pkg_bash_sandbox --> pkg_bash_local + pkg_bash_sandbox --> pkg_invariants pkg_bash_sandbox --> pkg_sandbox pkg_permission --> pkg_bash + pkg_permission --> pkg_invariants pkg_permission --> pkg_sandbox pkg_permission --> pkg_session pkg_permission --> pkg_user_approval @@ -256,6 +307,7 @@ flowchart TD pkg_tool_bash --> pkg_agent pkg_tool_bash --> pkg_bash pkg_tool_bash --> pkg_home + pkg_tool_bash --> pkg_invariants pkg_tool_bash --> pkg_llm pkg_tool_bash --> pkg_sandbox pkg_tool_bash --> pkg_session_persistence @@ -264,11 +316,13 @@ flowchart TD pkg_tool_bash --> pkg_tools pkg_tool_bash --> pkg_user_approval pkg_tool_fs --> pkg_fs + pkg_tool_fs --> pkg_invariants pkg_tool_fs --> pkg_llm pkg_tool_fs --> pkg_session pkg_tool_fs --> pkg_system_prompt pkg_tool_fs --> pkg_tools pkg_tool_fs_search --> pkg_bash + pkg_tool_fs_search --> pkg_invariants pkg_tool_fs_search --> pkg_llm pkg_tool_fs_search --> pkg_retention pkg_tool_fs_search --> pkg_session @@ -276,45 +330,55 @@ flowchart TD pkg_tool_fs_search --> pkg_system_prompt pkg_tool_fs_search --> pkg_tools pkg_tool_skill --> pkg_agent + pkg_tool_skill --> pkg_invariants pkg_tool_skill --> pkg_llm pkg_tool_skill --> pkg_skill pkg_tool_skill --> pkg_tools pkg_subagent --> pkg_agent pkg_subagent --> pkg_brand + pkg_subagent --> pkg_invariants pkg_subagent --> pkg_llm pkg_subagent --> pkg_scope pkg_subagent --> pkg_session pkg_subagent --> pkg_tools + pkg_tool_web --> pkg_invariants pkg_tool_web --> pkg_llm pkg_tool_web --> pkg_system_prompt pkg_tool_web --> pkg_tools pkg_tool_web --> pkg_web + pkg_spill_policy --> pkg_invariants pkg_spill_policy --> pkg_llm pkg_spill_policy --> pkg_retention pkg_spill_policy --> pkg_session pkg_spill_policy --> pkg_spill pkg_spill_policy --> pkg_tools + pkg_timeout_policy --> pkg_invariants pkg_timeout_policy --> pkg_llm pkg_timeout_policy --> pkg_timeout pkg_timeout_policy --> pkg_tools pkg_tool_todo --> pkg_agent + pkg_tool_todo --> pkg_invariants pkg_tool_todo --> pkg_session pkg_tool_todo --> pkg_tools + pkg_tool_cordis --> pkg_invariants pkg_tool_cordis --> pkg_scope pkg_tool_cordis --> pkg_tools pkg_hooks_codex --> pkg_agent pkg_hooks_codex --> pkg_hook_protocol + pkg_hooks_codex --> pkg_invariants pkg_hooks_codex --> pkg_llm pkg_hooks_codex --> pkg_session pkg_hooks_codex --> pkg_session_persistence pkg_hooks_codex --> pkg_tools pkg_agent_loop_testkit --> pkg_agent + pkg_agent_loop_testkit --> pkg_invariants pkg_agent_loop_testkit --> pkg_llm pkg_agent_loop_testkit --> pkg_session pkg_agent_loop_testkit --> pkg_system_prompt pkg_agent_loop_testkit --> pkg_tools pkg_acp --> pkg_agent pkg_acp --> pkg_bash + pkg_acp --> pkg_invariants pkg_acp --> pkg_llm pkg_acp --> pkg_permission pkg_acp --> pkg_sandbox @@ -325,51 +389,62 @@ flowchart TD pkg_acp --> pkg_user_approval pkg_acp --> pkg_user_interaction pkg_tool_ask_user --> pkg_agent + pkg_tool_ask_user --> pkg_invariants pkg_tool_ask_user --> pkg_tools pkg_tool_ask_user --> pkg_user_interaction pkg_workspace_context --> pkg_agent pkg_workspace_context --> pkg_fs + pkg_workspace_context --> pkg_invariants pkg_workspace_context --> pkg_llm pkg_workspace_context --> pkg_paths pkg_workspace_context --> pkg_session pkg_workspace_context --> pkg_tools pkg_repeat_tool_guard --> pkg_agent + pkg_repeat_tool_guard --> pkg_invariants pkg_repeat_tool_guard --> pkg_tools + pkg_mcp_client --> pkg_invariants pkg_mcp_client --> pkg_llm pkg_mcp_client --> pkg_tools pkg_tool_tasks --> pkg_agent + pkg_tool_tasks --> pkg_invariants pkg_tool_tasks --> pkg_system_prompt pkg_tool_tasks --> pkg_tasks pkg_tool_tasks --> pkg_tools pkg_tool_workflow --> pkg_agent + pkg_tool_workflow --> pkg_invariants pkg_tool_workflow --> pkg_llm pkg_tool_workflow --> pkg_system_prompt pkg_tool_workflow --> pkg_tools pkg_tool_workflow --> pkg_workflow pkg_subagent_acp --> pkg_agent + pkg_subagent_acp --> pkg_invariants pkg_subagent_acp --> pkg_llm pkg_subagent_acp --> pkg_session pkg_subagent_acp --> pkg_subagent pkg_subagent_acp --> pkg_subagent_subprocess pkg_subagent_inprocess --> pkg_agent + pkg_subagent_inprocess --> pkg_invariants pkg_subagent_inprocess --> pkg_llm pkg_subagent_inprocess --> pkg_session pkg_subagent_inprocess --> pkg_subagent pkg_subagent_inprocess --> pkg_system_prompt pkg_subagent_inprocess --> pkg_tools pkg_tool_subagent --> pkg_agent + pkg_tool_subagent --> pkg_invariants pkg_tool_subagent --> pkg_llm pkg_tool_subagent --> pkg_subagent pkg_tool_subagent --> pkg_tasks pkg_tool_subagent --> pkg_tools pkg_hooks_claude --> pkg_agent pkg_hooks_claude --> pkg_hook_protocol + pkg_hooks_claude --> pkg_invariants pkg_hooks_claude --> pkg_llm pkg_hooks_claude --> pkg_session pkg_hooks_claude --> pkg_session_persistence pkg_hooks_claude --> pkg_subagent pkg_hooks_claude --> pkg_tools pkg_jsonrpc --> pkg_agent + pkg_jsonrpc --> pkg_invariants pkg_jsonrpc --> pkg_llm pkg_jsonrpc --> pkg_llm_deepseek pkg_jsonrpc --> pkg_scope @@ -377,11 +452,13 @@ flowchart TD pkg_jsonrpc --> pkg_subagent pkg_stdio --> pkg_agent pkg_stdio --> pkg_agent_loop + pkg_stdio --> pkg_invariants pkg_stdio --> pkg_llm pkg_stdio --> pkg_session pkg_stdio --> pkg_user_interaction pkg_tui --> pkg_agent pkg_tui --> pkg_agent_loop + pkg_tui --> pkg_invariants pkg_tui --> pkg_llm pkg_tui --> pkg_session pkg_tui --> pkg_tools @@ -404,20 +481,24 @@ flowchart TD pkg_agent_spine_demo --> pkg_workspace_context pkg_workflow_workerthread --> pkg_agent pkg_workflow_workerthread --> pkg_brand + pkg_workflow_workerthread --> pkg_invariants pkg_workflow_workerthread --> pkg_llm pkg_workflow_workerthread --> pkg_session pkg_workflow_workerthread --> pkg_subagent pkg_workflow_workerthread --> pkg_tools pkg_workflow_workerthread --> pkg_workflow pkg_subagent_fork --> pkg_agent + pkg_subagent_fork --> pkg_invariants pkg_subagent_fork --> pkg_session pkg_subagent_fork --> pkg_subagent pkg_subagent_fork --> pkg_subagent_inprocess + pkg_subagent_spawn --> pkg_invariants pkg_subagent_spawn --> pkg_subagent pkg_subagent_spawn --> pkg_subagent_inprocess pkg_acp_demo --> pkg_acp pkg_acp_demo --> pkg_agent_spine_demo pkg_acp_demo --> pkg_app_boot + pkg_acp_demo --> pkg_invariants pkg_acp_demo --> pkg_session_persistence_jsonl pkg_acp_demo --> pkg_tools pkg_acp_demo --> pkg_user_interaction @@ -425,6 +506,7 @@ flowchart TD pkg_cli_demo --> pkg_agent pkg_cli_demo --> pkg_agent_spine_demo pkg_cli_demo --> pkg_app_boot + pkg_cli_demo --> pkg_invariants pkg_cli_demo --> pkg_llm pkg_cli_demo --> pkg_session pkg_cli_demo --> pkg_session_persistence_jsonl @@ -434,6 +516,7 @@ flowchart TD pkg_stdio_demo --> pkg_agent_loop pkg_stdio_demo --> pkg_agent_spine_demo pkg_stdio_demo --> pkg_app_boot + pkg_stdio_demo --> pkg_invariants pkg_stdio_demo --> pkg_llm pkg_stdio_demo --> pkg_session pkg_stdio_demo --> pkg_session_persistence_jsonl @@ -447,92 +530,92 @@ flowchart TD | Package | Group | Depends on | | --- | --- | --- | -| [`brand`](../packages/util/brand) | `util` | — | -| [`home`](../packages/util/home) | `util` | — | -| [`paths`](../packages/util/paths) | `util` | — | -| [`retention`](../packages/util/retention) | `util` | — | -| [`timeout`](../packages/util/timeout) | `util` | — | -| [`skill`](../packages/skill/skill) | `skill` | — | -| [`subagent-subprocess`](../packages/subagent/subagent-subprocess) | `subagent` | — | -| [`acp-snapshot`](../packages/support/acp-snapshot) | `support` | — | | [`invariants`](../packages/support/invariants) | `support` | — | -| [`loader-smoke`](../packages/support/loader-smoke) | `support` | — | -| [`app-boot`](../packages/ui/app-boot) | `ui` | — | -| [`code-runtime`](../packages/code-runtime/code-runtime) | `code-runtime` | — | -| [`jsonrpc-demo`](../packages/examples/jsonrpc-demo) | `examples` | — | -| [`llm`](../packages/llm/llm) | `llm` | [`brand`](../packages/util/brand) | +| [`brand`](../packages/util/brand) | `util` | [`invariants`](../packages/support/invariants) | +| [`home`](../packages/util/home) | `util` | [`invariants`](../packages/support/invariants) | +| [`paths`](../packages/util/paths) | `util` | [`invariants`](../packages/support/invariants) | +| [`retention`](../packages/util/retention) | `util` | [`invariants`](../packages/support/invariants) | +| [`timeout`](../packages/util/timeout) | `util` | [`invariants`](../packages/support/invariants) | | [`scope`](../packages/core/scope) | `core` | [`invariants`](../packages/support/invariants) | -| [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime) | -| [`helper`](../packages/sdk/helper) | `sdk` | [`brand`](../packages/util/brand) | -| [`scripts`](../packages/sdk/scripts) | `sdk` | [`app-boot`](../packages/ui/app-boot) | -| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`llm`](../packages/llm/llm) | -| [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`llm`](../packages/llm/llm) | +| [`skill`](../packages/skill/skill) | `skill` | [`invariants`](../packages/support/invariants) | +| [`subagent-subprocess`](../packages/subagent/subagent-subprocess) | `subagent` | [`invariants`](../packages/support/invariants) | +| [`acp-snapshot`](../packages/support/acp-snapshot) | `support` | [`invariants`](../packages/support/invariants) | +| [`loader-smoke`](../packages/support/loader-smoke) | `support` | [`invariants`](../packages/support/invariants) | +| [`app-boot`](../packages/ui/app-boot) | `ui` | [`invariants`](../packages/support/invariants) | +| [`code-runtime`](../packages/code-runtime/code-runtime) | `code-runtime` | [`invariants`](../packages/support/invariants) | +| [`jsonrpc-demo`](../packages/examples/jsonrpc-demo) | `examples` | [`invariants`](../packages/support/invariants) | +| [`llm`](../packages/llm/llm) | `llm` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) | +| [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants) | +| [`helper`](../packages/sdk/helper) | `sdk` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) | +| [`scripts`](../packages/sdk/scripts) | `sdk` | [`app-boot`](../packages/ui/app-boot), [`invariants`](../packages/support/invariants) | +| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) | +| [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) | | [`session`](../packages/core/session) | `core` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) | -| [`system-prompt`](../packages/core/system-prompt) | `core` | [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) | -| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) | -| [`web`](../packages/web/web) | `web` | [`llm`](../packages/llm/llm) | -| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`llm`](../packages/llm/llm) | -| [`token-meter`](../packages/llm/token-meter) | `llm` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | +| [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) | +| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) | +| [`web`](../packages/web/web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) | +| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) | +| [`token-meter`](../packages/llm/token-meter) | `llm` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`agent`](../packages/core/agent) | `core` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) | -| [`bash`](../packages/bash/bash) | `bash` | [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) | -| [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs) | -| [`fs-policy`](../packages/fs/fs-policy) | `fs` | [`fs`](../packages/fs/fs) | -| [`skill-local`](../packages/skill/skill-local) | `skill` | [`fs`](../packages/fs/fs), [`home`](../packages/util/home), [`skill`](../packages/skill/skill) | -| [`compact`](../packages/compact/compact) | `compact` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | -| [`web-fetch-local`](../packages/web/web-fetch-local) | `web` | [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) | -| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`web`](../packages/web/web) | -| [`web-search-exa`](../packages/web/web-search-exa) | `web` | [`web`](../packages/web/web) | -| [`web-search-perplexity`](../packages/web/web-search-perplexity) | `web` | [`web`](../packages/web/web) | -| [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | -| [`session-persistence`](../packages/session-persistence/session-persistence) | `session-persistence` | [`session`](../packages/core/session) | -| [`llm-replay`](../packages/support/llm-replay) | `support` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | -| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) | -| [`bash-local`](../packages/bash/bash-local) | `bash` | [`bash`](../packages/bash/bash), [`timeout`](../packages/util/timeout) | -| [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | -| [`spill-local`](../packages/spill/spill-local) | `spill` | [`spill`](../packages/spill/spill) | -| [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`bash`](../packages/bash/bash), [`session`](../packages/core/session) | -| [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl) | `session-persistence` | [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence) | -| [`session-persistence-sqlite`](../packages/session-persistence/session-persistence-sqlite) | `session-persistence` | [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence) | -| [`session-query`](../packages/session-query/session-query) | `session-query` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence) | -| [`user-approval`](../packages/ui/user-approval) | `ui` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) | -| [`user-interaction`](../packages/ui/user-interaction) | `ui` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm) | -| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent) | -| [`tasks`](../packages/tasks/tasks) | `tasks` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) | -| [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | -| [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`code-runtime`](../packages/code-runtime/code-runtime), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/ui/user-approval) | -| [`bash-sandbox`](../packages/bash/bash-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`bash-local`](../packages/bash/bash-local), [`sandbox`](../packages/sandbox/sandbox) | -| [`permission`](../packages/ui/permission) | `ui` | [`bash`](../packages/bash/bash), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`user-approval`](../packages/ui/user-approval) | +| [`bash`](../packages/bash/bash) | `bash` | [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) | +| [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) | +| [`fs-policy`](../packages/fs/fs-policy) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) | +| [`skill-local`](../packages/skill/skill-local) | `skill` | [`fs`](../packages/fs/fs), [`home`](../packages/util/home), [`invariants`](../packages/support/invariants), [`skill`](../packages/skill/skill) | +| [`compact`](../packages/compact/compact) | `compact` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | +| [`web-fetch-local`](../packages/web/web-fetch-local) | `web` | [`invariants`](../packages/support/invariants), [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) | +| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`invariants`](../packages/support/invariants), [`web`](../packages/web/web) | +| [`web-search-exa`](../packages/web/web-search-exa) | `web` | [`invariants`](../packages/support/invariants), [`web`](../packages/web/web) | +| [`web-search-perplexity`](../packages/web/web-search-perplexity) | `web` | [`invariants`](../packages/support/invariants), [`web`](../packages/web/web) | +| [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | +| [`session-persistence`](../packages/session-persistence/session-persistence) | `session-persistence` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | +| [`llm-replay`](../packages/support/llm-replay) | `support` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | +| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) | +| [`bash-local`](../packages/bash/bash-local) | `bash` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`timeout`](../packages/util/timeout) | +| [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | +| [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/support/invariants), [`spill`](../packages/spill/spill) | +| [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | +| [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl) | `session-persistence` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence) | +| [`session-persistence-sqlite`](../packages/session-persistence/session-persistence-sqlite) | `session-persistence` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence) | +| [`session-query`](../packages/session-query/session-query) | `session-query` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence) | +| [`user-approval`](../packages/ui/user-approval) | `ui` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) | +| [`user-interaction`](../packages/ui/user-interaction) | `ui` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) | +| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants) | +| [`tasks`](../packages/tasks/tasks) | `tasks` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) | +| [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | +| [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/ui/user-approval) | +| [`bash-sandbox`](../packages/bash/bash-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`bash-local`](../packages/bash/bash-local), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox) | +| [`permission`](../packages/ui/permission) | `ui` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`user-approval`](../packages/ui/user-approval) | | [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`tool-bash`](../packages/bash/tool-bash) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`home`](../packages/util/home), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session-persistence`](../packages/session-persistence/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) | -| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`bash`](../packages/bash/bash), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) | -| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | -| [`tool-web`](../packages/web/tool-web) | `web` | [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) | -| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) | -| [`timeout-policy`](../packages/timeout/timeout-policy) | `timeout` | [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | -| [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | -| [`tool-cordis`](../packages/cordis/tool-cordis) | `cordis` | [`scope`](../packages/core/scope), [`tools`](../packages/core/tools) | -| [`hooks-codex`](../packages/hooks/hooks-codex) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`tools`](../packages/core/tools) | -| [`agent-loop-testkit`](../packages/support/agent-loop-testkit) | `support` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`acp`](../packages/ui/acp) | `ui` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`llm`](../packages/llm/llm), [`permission`](../packages/ui/permission), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval), [`user-interaction`](../packages/ui/user-interaction) | -| [`tool-ask-user`](../packages/ui/tool-ask-user) | `ui` | [`agent`](../packages/core/agent), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) | -| [`workspace-context`](../packages/context/workspace-context) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`paths`](../packages/util/paths), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | -| [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | `guard` | [`agent`](../packages/core/agent), [`tools`](../packages/core/tools) | -| [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`llm`](../packages/llm/llm), [`tools`](../packages/core/tools) | -| [`tool-tasks`](../packages/tasks/tool-tasks) | `tasks` | [`agent`](../packages/core/agent), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) | -| [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | -| [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-subprocess`](../packages/subagent/subagent-subprocess) | -| [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) | -| [`hooks-claude`](../packages/hooks/hooks-claude) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | -| [`jsonrpc`](../packages/ui/jsonrpc) | `ui` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | -| [`stdio`](../packages/ui/stdio) | `ui` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`user-interaction`](../packages/ui/user-interaction) | -| [`tui`](../packages/ui/tui) | `ui` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) | +| [`tool-bash`](../packages/bash/tool-bash) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`home`](../packages/util/home), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session-persistence`](../packages/session-persistence/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) | +| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) | +| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | +| [`tool-web`](../packages/web/tool-web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) | +| [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) | +| [`timeout-policy`](../packages/timeout/timeout-policy) | `timeout` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | +| [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | +| [`tool-cordis`](../packages/cordis/tool-cordis) | `cordis` | [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`tools`](../packages/core/tools) | +| [`hooks-codex`](../packages/hooks/hooks-codex) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`tools`](../packages/core/tools) | +| [`agent-loop-testkit`](../packages/support/agent-loop-testkit) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`acp`](../packages/ui/acp) | `ui` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`permission`](../packages/ui/permission), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval), [`user-interaction`](../packages/ui/user-interaction) | +| [`tool-ask-user`](../packages/ui/tool-ask-user) | `ui` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) | +| [`workspace-context`](../packages/context/workspace-context) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`paths`](../packages/util/paths), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | +| [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | `guard` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) | +| [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`tools`](../packages/core/tools) | +| [`tool-tasks`](../packages/tasks/tool-tasks) | `tasks` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) | +| [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | +| [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-subprocess`](../packages/subagent/subagent-subprocess) | +| [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) | +| [`hooks-claude`](../packages/hooks/hooks-claude) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | +| [`jsonrpc`](../packages/ui/jsonrpc) | `ui` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | +| [`stdio`](../packages/ui/stdio) | `ui` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`user-interaction`](../packages/ui/user-interaction) | +| [`tui`](../packages/ui/tui) | `ui` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) | | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`home`](../packages/util/home), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`skill`](../packages/skill/skill), [`skill-local`](../packages/skill/skill-local), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tool-bash`](../packages/bash/tool-bash), [`tool-skill`](../packages/skill/tool-skill), [`tool-tasks`](../packages/tasks/tool-tasks), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) | -| [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | -| [`subagent-fork`](../packages/subagent/subagent-fork) | `subagent` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | -| [`subagent-spawn`](../packages/subagent/subagent-spawn) | `subagent` | [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | -| [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/ui/acp), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/ui/app-boot), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction), [`workspace-context`](../packages/context/workspace-context) | -| [`cli-demo`](../packages/examples/cli-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/ui/app-boot), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) | -| [`stdio-demo`](../packages/examples/stdio-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/ui/app-boot), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`stdio`](../packages/ui/stdio), [`tool-ask-user`](../packages/ui/tool-ask-user), [`tools`](../packages/core/tools), [`tui`](../packages/ui/tui), [`user-interaction`](../packages/ui/user-interaction), [`workspace-context`](../packages/context/workspace-context) | +| [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | +| [`subagent-fork`](../packages/subagent/subagent-fork) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | +| [`subagent-spawn`](../packages/subagent/subagent-spawn) | `subagent` | [`invariants`](../packages/support/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | +| [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/ui/acp), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/ui/app-boot), [`invariants`](../packages/support/invariants), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction), [`workspace-context`](../packages/context/workspace-context) | +| [`cli-demo`](../packages/examples/cli-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/ui/app-boot), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) | +| [`stdio-demo`](../packages/examples/stdio-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/ui/app-boot), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`stdio`](../packages/ui/stdio), [`tool-ask-user`](../packages/ui/tool-ask-user), [`tools`](../packages/core/tools), [`tui`](../packages/ui/tui), [`user-interaction`](../packages/ui/user-interaction), [`workspace-context`](../packages/context/workspace-context) | diff --git a/docs/rfc/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md b/docs/rfc/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md index 9e272456fc..4b77930280 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md +++ b/docs/rfc/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md @@ -30,7 +30,7 @@ This guarantee belongs in `Session`, not in an optional listener, because every ### Package-owned invariant companions check relationships -`dsh-invariants` registers the configurable `ctx.invariants` service and contains no product checks. Optional `./invariant` companions from `dsh-session`, `dsh-agent`, `dsh-scope`, and `dsh-agent-loop` own the rules that require trace state or observation of another seam: monotonic sequence numbers, turn and step nesting, tool-call/result pairing, legal agent-status transitions, subject-correct scoped dispatch, and equality between a loop-built request and the request reconstructed from its session-log prefix. Global enablement and package-name regex filters belong to the service ([package-owned invariant service](2026-07-19-package-owned-invariant-service.md)). +`dsh-invariants` registers the configurable `ctx.invariants` service and contains no product checks. Every package publishes a `./invariant` ownership companion; `dsh-session`, `dsh-agent`, `dsh-scope`, and `dsh-agent-loop` currently add the rules that require trace state or observation of another seam: monotonic sequence numbers, turn and step nesting, tool-call/result pairing, legal agent-status transitions, subject-correct scoped dispatch, and equality between a loop-built request and the request reconstructed from its session-log prefix. Global enablement and package-name regex filters belong to the service ([package-owned invariant service](2026-07-19-package-owned-invariant-service.md)). When the session companion attaches to an existing or seeded session, it replays the immutable log to rebuild trace state. The service gives each contribution a disposable child fiber, so hot reload is safe in the middle of a turn without giving diagnostics ownership of session storage. diff --git a/docs/rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.md b/docs/rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.md index 81b1bd25df..4ee29e8fb8 100644 --- a/docs/rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.md +++ b/docs/rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.md @@ -21,7 +21,7 @@ In case 2, if the injected `context/message` is the last event before a flush/di - An `agent.inject()` made while the agent is **running** joins the already-open turn. While the current step executes assistant tool calls, accepted context waits in arrival order until that batch settles, then appends after every recorded result and before the turn closes even when execution is interrupted. - An `agent.inject()` made while **idle** wraps its `context/message` in a one-shot turn: `turn/start{trigger:{kind:'injection'}}` → `context/message` → `turn/end{completed}`. A new `injection` variant joins the merge-extensible `TurnTriggerMap`. - The loop derives the next turn number from the log each iteration (`lastTurnNumber(session) + 1`) instead of keeping a private counter, so an idle injection's one-shot turn cannot collide with the next real turn's number. -- The optional `dsh-session/invariant` companion registers the dev check with `ctx.invariants`: a `user/message` / `context/message` / `steering/message` appended while no turn is open throws an `InvariantError` attributed to `@deepseek-ai/dsh-session`. +- The `dsh-session/invariant` companion registers the check with `ctx.invariants`: when selected, a `user/message` / `context/message` / `steering/message` appended while no turn is open throws an `InvariantError` attributed to `@deepseek-ai/dsh-session`. The serializability invariant is enforced at the same source boundary (`Session.append` throws on non-JSON-serializable data), so "what may enter the log" is now governed in one place rather than discovered downstream by whichever backend happens to be watching. diff --git a/docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md b/docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md index e86d6b9610..62630152c9 100644 --- a/docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md +++ b/docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md @@ -26,7 +26,7 @@ Each step rebuilds prompt assembly. On the instance's first step, `agent/session **`step/start` is the reconstruction boundary.** A step derives messages from events before that sequence. Injection after the snapshot joins the next request, and reentrant appends are rejected during event publication. `agent/pre-step(agent, turn, step, signal)` remains the generic seam for content needed by the current request. Header reconstruction selects the step's `request/header`, or carries the prior snapshot when no new header is written. -**Enforcement.** In development, the optional `dsh-agent-loop/invariant` companion registers with `ctx.invariants` and independently rebuilds each loop request through a fresh `Session`, so the live cache cannot vouch for itself, then compares messages and folded header fields at `llm/stream`. Loop requests are identified by their frozen shape and session id; direct one-shots are excluded. Correctness depends on sequence-bounded reconstruction rather than listener order. A with-key e2e requires positive cache-read tokens after the first request; per-step usage is the production signal, and a header change or compaction appears as a cache-read drop on the next step. +**Enforcement.** The `dsh-agent-loop/invariant` companion registers with `ctx.invariants` and, when selected, independently rebuilds each loop request through a fresh `Session`, so the live cache cannot vouch for itself, then compares messages and folded header fields at `llm/stream`. Loop requests are identified by their frozen shape and session id; direct one-shots are excluded. Correctness depends on sequence-bounded reconstruction rather than listener order. A with-key e2e requires positive cache-read tokens after the first request; per-step usage is the production signal, and a header change or compaction appears as a cache-read drop on the next step. ### The MiniCode shape: adopted, with the provenance arrow inverted diff --git a/docs/rfc/implemented/architecture/2026-07-12-agent-scope-runtime-design.md b/docs/rfc/implemented/architecture/2026-07-12-agent-scope-runtime-design.md index e7e43cc1eb..2be6be1c53 100644 --- a/docs/rfc/implemented/architecture/2026-07-12-agent-scope-runtime-design.md +++ b/docs/rfc/implemented/architecture/2026-07-12-agent-scope-runtime-design.md @@ -322,7 +322,7 @@ TypeScript cannot govern JavaScript casts, direct Cordis dispatch, process messa ### Runtime invariants cover cross-service facts -The optional `dsh-scope/invariant` companion verifies that every declared scoped event uses a marked carrier and that event families exposing a subject use the matching key. The separate `dsh-session/invariant` contribution stages trace validation before append commit and advances after the same event commits; both register through `ctx.invariants`. +The `dsh-scope/invariant` companion verifies, when selected, that every declared scoped event uses a marked carrier and that event families exposing a subject use the matching key. The separate `dsh-session/invariant` contribution stages trace validation before append commit and advances after the same event commits; both register through `ctx.invariants`. The plugin does not police trusted setup by scanning registries or reject prompt assembly objects fabricated through casts. Those checks would turn composition contracts into speculative runtime machinery without protecting a real external boundary. 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 ce8fabdc40..dfe64b7110 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: f147cd91134509e17df39d1acbb28ae8b521c2f8 -2026-07-19-package-owned-invariant-service.zh.md: a106b57137c36009d0d4b4d39f109b7abf857f42 +2026-07-19-package-owned-invariant-service.md: bf27bb4e951988bc124509b49dea9b5509f8e6f3 +2026-07-19-package-owned-invariant-service.zh.md: c9dfbb65bf4fd497f2f592f7741923dcef3e51fb 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 f147cd9113..bf27bb4e95 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 @@ -10,13 +10,15 @@ Runtime invariant checks span session traces, agent state, scoped dispatch, and Deployments also need more than presence or absence of one plugin. A standard composition should carry the known invariant contributions while permitting a global off switch and package-selective diagnostics. Selection must remain stable when a package loads later or reloads under HMR, and disabled contributions must not allow two plugins to claim the same package name silently. +Package ownership must also be exhaustive. Without a mechanical repository rule, a new package can omit the companion, dependency, or publication wiring and remain invisible to diagnostics until a maintainer notices the gap. + ## Decision ### One registry service, package-owned contributions `@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. -Packages expose diagnostics through optional `./invariant` companion plugins. Their root entrypoints do not import or register diagnostics implicitly, so loading a product 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. 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. ### Configuration and selection @@ -53,7 +55,7 @@ Registration setup is transactional. If an installer fails after registering lis The former functional-plugin entrypoint and one-argument `InvariantError` constructor are not retained as compatibility surfaces. The repository is pre-release and all call sites move to the service and package-attributed error together. -### Shipped companions +### Stateful companions and exhaustive ownership | Companion entry | Registration name | Owned checks | |---|---|---| @@ -62,7 +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 | -Each owner contains its source and focused tests. Every companion is a separately bundled `./invariant` export with its own declarations and Loader-safe namespace plugin shape. The service package retains only registry tests. +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. + +`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. ### Scoped-event semantic map @@ -70,7 +74,7 @@ The generated scoped-event subject resolver lives in `dsh-scope`, beside the con ### Standard composition and SDK output -The standard agent spine mounts the service and all four companion subpaths, forwarding `enabled`, `package_allowlist`, and `package_blocklist` to the service. Generated SDK Cordis composition emits the same entries. A subpath entry adds its installable root npm package rather than treating the subpath as a package name. +The standard agent spine mounts the service and all four stateful companion subpaths, forwarding `enabled`, `package_allowlist`, and `package_blocklist` to the service. Generated SDK Cordis composition emits the same entries. A subpath entry adds its installable root npm package rather than treating the subpath as a package name. Workspace constraints recognize the separate invariant bundle, and package exports, project references, build configuration, dependency declarations, and the lockfile describe the same publication surface. Generated config catalogs, module graphs, and API documentation derive from those sources. @@ -80,18 +84,22 @@ Service tests cover defaults, global disablement, allow/block selection, blockli Composition tests cover standard-spine forwarding and generated SDK entries. Loader tests preserve each companion namespace, while built plain-Node smokes exercise the compiled subpath exports. The scoped-event freshness gate reruns its semantic Program analysis. +Every Vitest configuration loads a test host that mounts an explicitly enabled service and all package companions before an ordinary Cordis root's first plugin. Focused service and owner tests construct their own invariant topology so they can exercise disablement, filtering, rollback, and reload without duplicate ownership. Gate tests also execute every companion's `apply` function and verify that it calls `register` with its manifest name, rather than accepting source text alone. + ## Alternatives considered - **Keep all checks in `dsh-invariants`.** Rejected because the registry would continue importing every checked product domain, owner changes would require central edits, and package tests would remain detached from the contracts they protect. - **Let root package entrypoints register checks implicitly when `ctx.invariants` happens to exist.** Rejected because root behavior would depend on composition order and optional service presence, diagnostics could not be selected independently, and package loading would hide a registration effect outside an explicit companion. -- **Discover every `invariant.ts` file automatically.** Rejected because filesystem/package discovery is not a runtime ownership contract, makes bundled publication ambiguous, and cannot express explicit Cordis load order or dependency installation. +- **Discover every `invariant.ts` file automatically at runtime.** Rejected because filesystem/package discovery is not a runtime ownership contract, makes bundled publication ambiguous, and cannot express explicit Cordis load order or dependency installation. Build-time generation, verification, and the test host may enumerate the source tree because they validate repository completeness rather than composing a shipped deployment. - **Validate allow/block entries against the currently loaded package set.** Rejected because a zero-match pattern can intentionally target a later or HMR-loaded contribution; current load order must not determine config validity. ## 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. - 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. - Regex sources are deployment configuration and remain fixed until the service reloads. +- Ordinary Vitest roots install every selected companion, trading extra child fibers during tests for repository-wide invariant coverage and immediate fixture failures. - Session storage validation, snapshotting, freezing, provenance, and surface acceptance remain always on and are not affected by invariant selection. 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 a106b57137..c9dfbb65bf 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 @@ -10,13 +10,15 @@ Status: implemented 部署还需要比“是否加载一个插件”更细的控制。标准组合应携带已知的不变式贡献,同时允许全局关闭或按包选择诊断。包稍后加载或在 HMR 下重载时,选择结果必须保持稳定;被过滤的贡献也不能让两个插件静默占用同一个包名。 +包所有权还必须覆盖完整。若没有机械化的仓库规则,新包可能遗漏伴随插件、依赖或发布配置,并一直不会进入诊断范围,直到维护者发现这一缺口。 + ## 决策 ### 一个注册服务,贡献归包所有 `@deepseek-ai/dsh-invariants` 是与产品无关的 Cordis 服务插件,注册 `ctx.invariants`。它只负责配置、注册唯一性、子 fiber 生命周期和带包归属的失败;不导入 session、agent、scope 或 agent-loop 包,也不包含这些包的检查。 -各包通过可选的 `./invariant` 伴随插件公开诊断。根入口不会隐式导入或注册诊断,因此加载产品包本身不会改变运行时检查,也不要求不变式服务存在。 +工作区内的每个包都发布 `./invariant` 伴随插件,并注册自己完整且准确的 npm 包名。没有关系检查的包使用生成的仅声明所有权 installer:它通过真实服务边界占用包名,但不安装监听器。包的根入口不会隐式导入或注册诊断,因此加载根包不会改变运行时检查,也不要求不变式服务存在。 ### 配置与选择 @@ -53,7 +55,7 @@ blocklist 匹配优先于 allowlist 匹配。每个条目都是区分大小写 原有函数式插件入口与单参数 `InvariantError` 构造函数不作为兼容表面保留。仓库尚未发布,所有调用方会一起迁移到服务和带包归属的错误。 -### 已提供的伴随插件 +### 有状态伴随插件与完整所有权 | 伴随入口 | 注册名 | 所属检查 | |---|---|---| @@ -62,7 +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 安全的命名空间插件形态。服务包只保留注册服务测试。 +这四个所有者保存有状态检查与聚焦测试。其他每个包在获得关系断言之前,都带有生成的基线伴随插件。每个伴随入口都是单独打包的 `./invariant` export,具有独立声明和对 Loader 安全的命名空间插件形态;服务包自身的伴随插件导入本地服务类型,避免形成自依赖。 + +`verify-package-invariants` 会发现每个工作区包,并拒绝缺失或陈旧的伴随插件源码、外部或无法解析的注册名、缺失的 `./invariant` export 或发布文件、缺失的不变式对等依赖(peer dependency)、开发依赖及项目引用,以及遗漏伴随入口的自定义构建配置。生成器只写入缺失或带生成标记的所有权基线,因此绝不会替换包自行维护的实现。 ### Scoped event 语义映射 @@ -70,7 +74,7 @@ blocklist 匹配优先于 allowlist 匹配。每个条目都是区分大小写 ### 标准组合与 SDK 输出 -标准 agent spine 会挂载服务和四个伴随子路径,并把 `enabled`、`package_allowlist` 与 `package_blocklist` 转发给服务。生成的 SDK Cordis 组合输出相同条目。子路径条目添加可安装的根 npm 包,而不会把子路径误当成包名。 +标准 agent spine 会挂载服务和四个有状态伴随子路径,并把 `enabled`、`package_allowlist` 与 `package_blocklist` 转发给服务。生成的 SDK Cordis 组合输出相同条目。子路径条目添加可安装的根 npm 包,而不会把子路径误当成包名。 Workspace 约束识别独立的不变式 bundle;包 exports、项目引用、构建配置、依赖声明和 lockfile 描述同一发布表面。生成的配置目录、模块图和 API 文档都从这些源派生。 @@ -80,18 +84,22 @@ Workspace 约束识别独立的不变式 bundle;包 exports、项目引用、 组合测试覆盖标准 spine 转发和生成的 SDK 条目。Loader 测试固定每个伴随命名空间,构建后的纯 Node smoke 覆盖编译子路径 export。scoped event 新鲜度门禁会重新执行语义 Program 分析。 +每个 Vitest 配置都会加载测试宿主;在普通 Cordis 根上下文启动第一个插件之前,宿主会挂载显式启用的服务以及所有包的伴随插件。服务与所有者的聚焦测试自行构建不变式拓扑,从而在不发生重复所有权冲突的前提下覆盖关闭、过滤、回滚与重载。门禁测试还会执行每个伴随插件的 `apply` 函数,并验证它调用 `register` 时使用包清单中的包名,而不是只检查源码文本。 + ## 考虑过的替代方案 - **把所有检查保留在 `dsh-invariants`。** 不予采纳,因为注册包仍要导入所有被检查的产品领域,所有者变更仍需中央编辑,测试也继续远离被保护的契约。 - **当 `ctx.invariants` 恰好存在时,让根包入口隐式注册检查。** 不予采纳,因为根入口行为会依赖组合顺序与可选服务是否存在,诊断无法独立选择,而且包加载会隐藏一个不在显式伴随插件中的注册 effect。 -- **自动发现所有 `invariant.ts` 文件。** 不予采纳,因为文件系统或包发现不是运行时所有权契约,会让 bundle 发布含义不清,也无法表达显式 Cordis 加载顺序或依赖安装。 +- **在运行时自动发现所有 `invariant.ts` 文件。** 不予采纳,因为文件系统或包发现不是运行时所有权契约,会让 bundle 发布含义不清,也无法表达显式 Cordis 加载顺序或依赖安装。构建期生成与校验以及测试 host 可以枚举源码树,因为它们验证的是仓库完整性,而不是组合已发布的部署。 - **根据当前已加载包集合验证 allow/block 条目。** 不予采纳,因为零匹配模式可能有意指向稍后加载或 HMR 加载的贡献;当前加载顺序不能决定配置有效性。 ## 后果 - 产品包拥有并测试自己的关系断言,服务保持与产品无关。 +- 每个包都要承担不变式伴随插件带来的少量发布与依赖成本,包括目前只安装生成基线、不添加监听器的包。 - 标准组合无需改变插件树即可关闭全部检查或按包名选择。 - 显式伴随条目让诊断成本和所有权在 Cordis 配置与包 export 中可见。 - 每个选中贡献增加一个子 fiber 及其监听器和状态成本;被过滤注册只保留包名占用。 - 正则表达式源属于部署配置,在服务重载前保持固定。 +- 普通 Vitest 根上下文会安装每个被选中的伴随插件,以增加测试期间的子 fiber 为代价,换取覆盖整个仓库的不变式检查和对 fixture(测试前置数据)错误的即时反馈。 - 会话存储验证、快照、冻结、provenance 与 surface 接受规则始终启用,不受不变式选择影响。 diff --git a/docs/rfc/proposed/architecture/2026-06-16-typed-event-schemas.md b/docs/rfc/proposed/architecture/2026-06-16-typed-event-schemas.md index 5db00851ae..026273c088 100644 --- a/docs/rfc/proposed/architecture/2026-06-16-typed-event-schemas.md +++ b/docs/rfc/proposed/architecture/2026-06-16-typed-event-schemas.md @@ -28,7 +28,7 @@ A migration of the event/vocabulary surface to runtime schemas touches, at minim - **Six merge-extensible maps** (~370 LOC of core types): `ContentBlockMap`, `MessageSourceMap`, `FinishReasonMap` (in `dsh-llm`); `TurnTriggerMap`, `TurnEndReasonMap`, `SessionEventMap` (in `dsh-session`). - **~10 `declare module` augmentation sites** across `dsh-agent`, `dsh-agent-loop`, `dsh-bash`, `dsh-llm`, `dsh-session`, `dsh-session-persistence`, `dsh-system-prompt`, `dsh-tools` — each would move from declaration merging to a runtime `register()` call. - **The event producers** — 16 `session.append(...)` call sites in the loop — unchanged in shape but now validated at the boundary. -- **~7 switch-consumers** that branch on these unions: `deriveMessages` and the optional invariant companion (`dsh-session`), `BlockAssembler` (`dsh-llm`), both LLM adapters (`dsh-llm-deepseek`, `dsh-llm-pi-ai`), and the tool schema layer (`dsh-tools`). The `assertNever`-on-closed-unions vs fall-through-on-extensible-unions convention (a documented lint rule) would need rethinking — runtime variants are not statically exhaustive. +- **~7 switch-consumers** that branch on these unions: `deriveMessages` and the package-owned invariant companion (`dsh-session`), `BlockAssembler` (`dsh-llm`), both LLM adapters (`dsh-llm-deepseek`, `dsh-llm-pi-ai`), and the tool schema layer (`dsh-tools`). The `assertNever`-on-closed-unions vs fall-through-on-extensible-unions convention (a documented lint rule) would need rethinking — runtime variants are not statically exhaustive. - **The `defineTool` `InferArgs` DSL** (`dsh-tools`), which derives zero-cast `execute` arg types from a compile-time schema spec — the showcase of the current approach. - **Docs**: architecture.md (the pattern is described as foundational), [dev-mode invariants](../../implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md), and any RFC that references the pattern. @@ -37,7 +37,7 @@ This is a repository-wide vocabulary redesign, not a persistence implementation ## Alternatives considered ### A. Status quo — merge-extensible types + `isJsonValue` at the durable boundary -Keep the compile-time pattern. Persistence stays opaque-JSON + serializability guard. Plugins extend via declaration merging; correctness of event *shape* is the producer's responsibility and is enforced by TypeScript at compile time. Package-owned invariant companions check selected cross-record relationships in dev but do not provide general runtime shape schemas. +Keep the compile-time pattern. Persistence stays opaque-JSON + serializability guard. Plugins extend via declaration merging; correctness of event *shape* is the producer's responsibility and is enforced by TypeScript at compile time. Package-owned invariant companions check selected cross-record relationships when enabled but do not provide general runtime shape schemas. - **Pros**: zero churn; plugin extension is a one-line `interface` augmentation with full type inference and no runtime registration ceremony; no new runtime dependency; the `defineTool` DSL and `assertNever` exhaustiveness keep working. - **Cons**: no runtime structural validation at the persistence boundary or at plugin seams; a malformed-but-JSON datum is caught late. @@ -72,4 +72,4 @@ Defer. If runtime validation is wanted at the durable boundary, **Option B** (sc - If a registry is adopted, is the library **schemastery** (already in the tree, already the config schema lib) or **Zod** (richer ecosystem, currently only transitive)? Adopting two schema libraries is a cost in itself. - Can a hybrid keep compile-time inference (so `defineTool` and plugin DX survive) while adding an *optional* runtime schema per variant, validated only at the persistence/wire boundary rather than on every in-process append? -- Does the `dsh-invariants` plugin already cover enough of the runtime-shape gap in dev that boundary validation is only needed for genuinely untrusted input (reload of an externally-modified log)? +- Does the `ctx.invariants` service already cover enough of the runtime-shape gap when enabled that boundary validation is only needed for genuinely untrusted input (reload of an externally-modified log)? diff --git a/docs/testing.md b/docs/testing.md index cd3b1aa9e8..97ec20e99f 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -9,6 +9,8 @@ How this repo tests, tier by tier, and the rules that keep a green suite meaning - **Real-API e2e** (`pnpm run test:e2e`): with-key tests against live provider APIs — the DeepSeek model plus provider-specific smokes that gate on their own keys (`EXA_API_KEY`, `PERPLEXITY_API_KEY`, …); each suite self-skips without its key so keyless CI stays green ([real-API e2e RFC](rfc/implemented/testing/2026-06-19-real-api-e2e-ci.md)). - **Snapshot** (`pnpm run test:snapshot`): transport-specific keyless goldens cover external presentation. ACP suites boot the real example subprocess, replay a recorded session, and diff normalized JSON-RPC plus the re-persisted log ([ACP snapshot RFC](rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md)); the headless suite independently pins `stream-json` through its real one-shot subprocess. TUI completed journeys replay recorded primary/child JSONL through the real agent loop and tools before projecting ANSI into semantic terminal-state goldens; package-local snapshots retain transient renderer states, and a real PTY conversation covers the process boundary ([TUI snapshot RFC](rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md)). Use `pnpm run test:snapshot:record` when a model transcript must change and `pnpm run test:snapshot:refresh` when committed replay input remains correct; review every JSONL and golden diff. System-prompt/tool-schema content is pinned by one ACP scenario (`text-turn`) and tokenized in every other fixture, so a prompt or schema edit churns one committed line ([pinned-header RFC](rfc/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)). +All Vitest configurations mount enabled `ctx.invariants` and every package companion before ordinary Cordis roots start. Focused invariant topology tests compose enabled or deliberately disabled services explicitly, without competing global registrations. + ## The with-key policy: inference is cheap here We are DeepSeek — do not ration real-API tests. A no-key test proves plumbing; only a with-key run proves the agent works against a real model. Write many: file-writing prompts, multi-turn conversations, tool use, cancellation mid-stream. Highest-value are **smoke tests** that boot the real example, send one real prompt, and check the world — they catch the "green unit tests, broken product" class that mocks structurally cannot ([postmortem 0001](postmortem/0001-acp-default-export-drops-inject.md)). The self-skip exists only so secretless CI and keyless contributors aren't blocked; it is not a cost signal. Every example ships a keyless smoke and — unless keyless-by-nature — a with-key smoke ([examples/AGENTS.md](../examples/AGENTS.md)). diff --git a/knip.json b/knip.json index 2c3465ab38..3735303d8f 100644 --- a/knip.json +++ b/knip.json @@ -35,23 +35,19 @@ "project": ["src/**/*.ts", "tests/**/*.ts"] }, "packages/util/brand": { - "project": ["src/**/*.ts"], - "ignoreDependencies": ["cordis"] + "project": ["src/**/*.ts"] }, "packages/util/home": { "entry": ["tests/**/*.spec.ts"], - "project": ["src/**/*.ts", "tests/**/*.ts"], - "ignoreDependencies": ["cordis"] + "project": ["src/**/*.ts", "tests/**/*.ts"] }, "packages/util/timeout": { "entry": ["tests/**/*.spec.ts"], - "project": ["src/**/*.ts", "tests/**/*.ts"], - "ignoreDependencies": ["cordis"] + "project": ["src/**/*.ts", "tests/**/*.ts"] }, "packages/util/retention": { "entry": ["tests/**/*.spec.ts"], - "project": ["src/**/*.ts", "tests/**/*.ts"], - "ignoreDependencies": ["cordis"] + "project": ["src/**/*.ts", "tests/**/*.ts"] }, "packages/support/acp-snapshot": { "entry": ["tests/**/*.spec.ts", "tests/fixtures/fake-acp-agent.ts"], @@ -59,8 +55,7 @@ }, "packages/support/loader-smoke": { "entry": ["tests/**/*.spec.ts", "tests/fixtures/*.ts"], - "project": ["src/**/*.ts", "tests/**/*.ts"], - "ignoreDependencies": ["cordis"] + "project": ["src/**/*.ts", "tests/**/*.ts"] }, "packages/core/agent-loop": { "entry": ["tests/**/*.spec.ts", "tests/**/*.e2e.ts"], @@ -84,8 +79,7 @@ }, "packages/util/paths": { "entry": ["tests/**/*.spec.ts"], - "project": ["src/**/*.ts", "tests/**/*.ts"], - "ignoreDependencies": ["cordis"] + "project": ["src/**/*.ts", "tests/**/*.ts"] }, "packages/web/web-search-exa": { "entry": ["tests/**/*.spec.ts", "tests/**/*.e2e.ts"], @@ -149,8 +143,7 @@ }, "packages/subagent/subagent-subprocess": { "entry": ["tests/**/*.spec.ts"], - "project": ["src/**/*.ts", "tests/**/*.ts"], - "ignoreDependencies": ["cordis"] + "project": ["src/**/*.ts", "tests/**/*.ts"] }, "packages/fs/tool-fs": { "entry": ["tests/**/*.spec.ts", "tests/**/*.e2e.ts"], diff --git a/package.json b/package.json index e736e3f8d1..abf1e59178 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,8 @@ "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-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", @@ -77,7 +79,7 @@ "website:build": "pnpm --filter @deepseek-ai/website run build", "constraints": "tsx scripts/check-workspace-constraints.ts", "doc-sync": "pnpm run doc-typecheck && pnpm run verify-cordis-catalog && pnpm run verify-cordis-api && pnpm run verify-export-jsdoc && pnpm run verify-tool-catalog && pnpm run verify-config-catalog && pnpm run verify-persistence-catalog && pnpm run verify-doc-graphs && pnpm run verify-scoped-events && pnpm run verify-website-api && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-package-readme-model-experience && pnpm run verify-mermaid && pnpm run verify-rfc-classification && pnpm run verify-rfc-format && pnpm run verify-type-equiv && pnpm run verify-translation-prompt && pnpm run verify-translation-pairing && pnpm run verify-doc-budgets && pnpm run verify-package-readme-limitations && pnpm run verify-website-yaml", - "hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-cordis-config && pnpm run verify-node-next-types && pnpm run verify-runtime-closure", + "hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-package-invariants && pnpm run verify-cordis-config && pnpm run verify-node-next-types && pnpm run verify-runtime-closure", "demo:echo": "node --expose-internals --import tsx packages/examples/stdio-demo/src/bin.ts examples/echo-agent/cordis.yml", "demo:repl": "node --expose-internals --import tsx packages/examples/stdio-demo/src/bin.ts examples/repl-agent/cordis.yml", "demo:headless": "node --expose-internals --import tsx packages/examples/cli-demo/src/bin.ts --config examples/headless-agent/cordis.yml", diff --git a/packages/AGENTS.md b/packages/AGENTS.md index e4b5ded6a3..1735c44978 100644 --- a/packages/AGENTS.md +++ b/packages/AGENTS.md @@ -16,6 +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)). Naming notes: diff --git a/packages/bash/bash-local/package.json b/packages/bash/bash-local/package.json index 381855b465..152c2db28b 100644 --- a/packages/bash/bash-local/package.json +++ b/packages/bash/bash-local/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -23,6 +28,7 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-bash": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-timeout": "^0.0.1", "cordis": "^4.0.0-rc.7" }, @@ -31,6 +37,7 @@ }, "devDependencies": { "@deepseek-ai/dsh-bash": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-timeout": "workspace:^", "cordis": "^4.0.0-rc.7" } diff --git a/packages/bash/bash-local/src/invariant.ts b/packages/bash/bash-local/src/invariant.ts new file mode 100644 index 0000000000..8af5f91b22 --- /dev/null +++ b/packages/bash/bash-local/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-bash-local' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/bash/bash-local/tsconfig.json b/packages/bash/bash-local/tsconfig.json index 02448770f4..a55c76f00a 100644 --- a/packages/bash/bash-local/tsconfig.json +++ b/packages/bash/bash-local/tsconfig.json @@ -25,6 +25,9 @@ }, { "path": "../../bash/bash" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/bash/bash-sandbox/package.json b/packages/bash/bash-sandbox/package.json index b4f61abcbe..42ba34c6a2 100644 --- a/packages/bash/bash-sandbox/package.json +++ b/packages/bash/bash-sandbox/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -24,6 +29,7 @@ "peerDependencies": { "@deepseek-ai/dsh-bash": "^0.0.1", "@deepseek-ai/dsh-bash-local": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-sandbox": "^0.0.1", "cordis": "^4.0.0-rc.7" }, @@ -33,9 +39,10 @@ "devDependencies": { "@deepseek-ai/dsh-bash": "workspace:^", "@deepseek-ai/dsh-bash-local": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-sandbox": "workspace:^", "@deepseek-ai/dsh-sandbox-local": "workspace:^", - "node-addon-landlock-run": "0.0.0-test.0", - "cordis": "^4.0.0-rc.7" + "cordis": "^4.0.0-rc.7", + "node-addon-landlock-run": "0.0.0-test.0" } } diff --git a/packages/bash/bash-sandbox/src/invariant.ts b/packages/bash/bash-sandbox/src/invariant.ts new file mode 100644 index 0000000000..e74190fa82 --- /dev/null +++ b/packages/bash/bash-sandbox/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-bash-sandbox' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/bash/bash-sandbox/tsconfig.json b/packages/bash/bash-sandbox/tsconfig.json index 6dad98d54f..0b0e350ce9 100644 --- a/packages/bash/bash-sandbox/tsconfig.json +++ b/packages/bash/bash-sandbox/tsconfig.json @@ -31,6 +31,9 @@ }, { "path": "../../bash/bash-local" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/bash/bash/package.json b/packages/bash/bash/package.json index 0783995161..8bee74f632 100644 --- a/packages/bash/bash/package.json +++ b/packages/bash/bash/package.json @@ -11,22 +11,29 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-sandbox": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-sandbox": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "cordis": "^4.0.0-rc.7" diff --git a/packages/bash/bash/src/invariant.ts b/packages/bash/bash/src/invariant.ts new file mode 100644 index 0000000000..350f68bfc7 --- /dev/null +++ b/packages/bash/bash/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-bash' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/bash/bash/tsconfig.json b/packages/bash/bash/tsconfig.json index b9ab094edb..0e228ada67 100644 --- a/packages/bash/bash/tsconfig.json +++ b/packages/bash/bash/tsconfig.json @@ -19,6 +19,9 @@ }, { "path": "../../core/session" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/bash/tool-bash/package.json b/packages/bash/tool-bash/package.json index 816be1ef88..ec17dbacdb 100644 --- a/packages/bash/tool-bash/package.json +++ b/packages/bash/tool-bash/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -23,15 +28,16 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", - "@deepseek-ai/dsh-user-approval": "^0.0.1", "@deepseek-ai/dsh-bash": "^0.0.1", "@deepseek-ai/dsh-home": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", - "@deepseek-ai/dsh-session-persistence": "^0.0.1", "@deepseek-ai/dsh-sandbox": "^0.0.1", + "@deepseek-ai/dsh-session-persistence": "^0.0.1", "@deepseek-ai/dsh-system-prompt": "^0.0.1", "@deepseek-ai/dsh-tasks": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", + "@deepseek-ai/dsh-user-approval": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "dependencies": { @@ -41,10 +47,10 @@ "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-agent-loop-testkit": "workspace:^", - "@deepseek-ai/dsh-user-approval": "workspace:^", "@deepseek-ai/dsh-bash": "workspace:^", "@deepseek-ai/dsh-bash-local": "workspace:^", "@deepseek-ai/dsh-home": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-sandbox": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", @@ -54,6 +60,7 @@ "@deepseek-ai/dsh-tasks": "workspace:^", "@deepseek-ai/dsh-tool-tasks": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", + "@deepseek-ai/dsh-user-approval": "workspace:^", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/bash/tool-bash/src/invariant.ts b/packages/bash/tool-bash/src/invariant.ts new file mode 100644 index 0000000000..286089c7ff --- /dev/null +++ b/packages/bash/tool-bash/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-tool-bash' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/bash/tool-bash/tsconfig.json b/packages/bash/tool-bash/tsconfig.json index 407e78ebd8..6854bb9f7e 100644 --- a/packages/bash/tool-bash/tsconfig.json +++ b/packages/bash/tool-bash/tsconfig.json @@ -46,6 +46,9 @@ }, { "path": "../../sandbox/sandbox" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/code-runtime/code-runtime-worker/package.json b/packages/code-runtime/code-runtime-worker/package.json index 77169f8eab..f9c0f6be4c 100644 --- a/packages/code-runtime/code-runtime-worker/package.json +++ b/packages/code-runtime/code-runtime-worker/package.json @@ -11,6 +11,10 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./worker": { "types": "./lib/types/worker.d.ts", "default": "./lib/worker.cjs" @@ -19,6 +23,7 @@ }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/worker.cjs", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", @@ -27,6 +32,7 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-code-runtime": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "dependencies": { @@ -34,6 +40,7 @@ }, "devDependencies": { "@deepseek-ai/dsh-code-runtime": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/code-runtime/code-runtime-worker/src/invariant.ts b/packages/code-runtime/code-runtime-worker/src/invariant.ts new file mode 100644 index 0000000000..41b3eab511 --- /dev/null +++ b/packages/code-runtime/code-runtime-worker/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + * @module @deepseek-ai/dsh-code-runtime-worker/invariant + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-code-runtime-worker' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/code-runtime/code-runtime-worker/tsconfig.json b/packages/code-runtime/code-runtime-worker/tsconfig.json index af962eda4f..4a201c70e1 100644 --- a/packages/code-runtime/code-runtime-worker/tsconfig.json +++ b/packages/code-runtime/code-runtime-worker/tsconfig.json @@ -19,6 +19,9 @@ }, { "path": "../code-runtime" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/code-runtime/code-runtime-worker/tsdown.config.ts b/packages/code-runtime/code-runtime-worker/tsdown.config.ts index 6fee724195..1c40637722 100644 --- a/packages/code-runtime/code-runtime-worker/tsdown.config.ts +++ b/packages/code-runtime/code-runtime-worker/tsdown.config.ts @@ -7,7 +7,7 @@ import { defineConfig } from 'tsdown' */ export default defineConfig([ { - entry: ['lib/types/index.js'], + entry: ['lib/types/index.js', 'lib/types/invariant.js'], outDir: 'lib', format: ['esm'], platform: 'node', diff --git a/packages/code-runtime/code-runtime/package.json b/packages/code-runtime/code-runtime/package.json index 5380d26ace..2d59302ec9 100644 --- a/packages/code-runtime/code-runtime/package.json +++ b/packages/code-runtime/code-runtime/package.json @@ -11,20 +11,27 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/code-runtime/code-runtime/src/invariant.ts b/packages/code-runtime/code-runtime/src/invariant.ts new file mode 100644 index 0000000000..6102927d77 --- /dev/null +++ b/packages/code-runtime/code-runtime/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-code-runtime' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/code-runtime/code-runtime/tsconfig.json b/packages/code-runtime/code-runtime/tsconfig.json index 754725418e..9966c8ca8a 100644 --- a/packages/code-runtime/code-runtime/tsconfig.json +++ b/packages/code-runtime/code-runtime/tsconfig.json @@ -13,6 +13,9 @@ }, { "path": "../../../vendor/cordis" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/compact/compact-basic/package.json b/packages/compact/compact-basic/package.json index a0ee2b4036..44274238de 100644 --- a/packages/compact/compact-basic/package.json +++ b/packages/compact/compact-basic/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -24,6 +29,7 @@ "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-compact": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-token-meter": "^0.0.1", diff --git a/packages/compact/compact-basic/src/invariant.ts b/packages/compact/compact-basic/src/invariant.ts new file mode 100644 index 0000000000..f72c0e5898 --- /dev/null +++ b/packages/compact/compact-basic/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-compact-basic' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/compact/compact-basic/tests/compact-basic.spec.ts b/packages/compact/compact-basic/tests/compact-basic.spec.ts index 0a411440b3..31992793f0 100644 --- a/packages/compact/compact-basic/tests/compact-basic.spec.ts +++ b/packages/compact/compact-basic/tests/compact-basic.spec.ts @@ -10,7 +10,7 @@ import LlmService, { CallId, CONTEXT_WINDOW_EXCEEDED_CODE, LlmAdapter } from '@d import type { ContentBlock, GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' import { Session, SessionId } from '@deepseek-ai/dsh-session' import TokenMeterService from '@deepseek-ai/dsh-token-meter' -import type { Agent } from '@deepseek-ai/dsh-agent' +import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent' const SIGNAL = new AbortController().signal const MODEL = 'test-model' @@ -791,7 +791,7 @@ describe('default one-shot summarizer', () => { describe('automatic listener and loader composition', () => { function postStep(ctx: Context, owner: Agent, signal = SIGNAL): Promise { - return ctx.serial('agent/post-step', owner, 1, 1, signal) + return agentEvents(ctx, owner).serial('agent/post-step', 1, 1, signal) } function recover( @@ -802,7 +802,9 @@ describe('automatic listener and loader composition', () => { signal = SIGNAL, next: () => Promise<{ action: 'fail' | 'retry' }> = () => Promise.resolve({ action: 'fail' }), ): Promise<{ action: 'fail' | 'retry' }> { - return ctx.waterfall('agent/request-error', owner, 1, 1, error, retryAttempt, signal, next) + return agentEvents(ctx, owner).waterfall( + 'agent/request-error', 1, 1, error, retryAttempt, signal, next, + ) } function overflow(message = 'provider overflow'): Error & { code: string } { diff --git a/packages/compact/compact-basic/tsconfig.json b/packages/compact/compact-basic/tsconfig.json index 0103ad82a8..a009b9ecb8 100644 --- a/packages/compact/compact-basic/tsconfig.json +++ b/packages/compact/compact-basic/tsconfig.json @@ -6,13 +6,32 @@ }, "include": ["src"], "references": [ - { "path": "../../../vendor/cosmokit" }, - { "path": "../../../vendor/cordis" }, - { "path": "../../../vendor/schemastery" }, - { "path": "../../llm/llm" }, - { "path": "../../llm/token-meter" }, - { "path": "../../core/session" }, - { "path": "../../core/agent" }, - { "path": "../compact" } + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../../vendor/schemastery" + }, + { + "path": "../../llm/llm" + }, + { + "path": "../../llm/token-meter" + }, + { + "path": "../../core/session" + }, + { + "path": "../../core/agent" + }, + { + "path": "../compact" + }, + { + "path": "../../support/invariants" + } ] } diff --git a/packages/compact/compact/package.json b/packages/compact/compact/package.json index 985c42d3b2..135c688507 100644 --- a/packages/compact/compact/package.json +++ b/packages/compact/compact/package.json @@ -11,22 +11,29 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "cordis": "^4.0.0-rc.7" diff --git a/packages/compact/compact/src/invariant.ts b/packages/compact/compact/src/invariant.ts new file mode 100644 index 0000000000..f7a2cbffd1 --- /dev/null +++ b/packages/compact/compact/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-compact' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/compact/compact/tsconfig.json b/packages/compact/compact/tsconfig.json index 95245937ec..673ee51547 100644 --- a/packages/compact/compact/tsconfig.json +++ b/packages/compact/compact/tsconfig.json @@ -19,6 +19,9 @@ }, { "path": "../../core/session" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/context/time-context/package.json b/packages/context/time-context/package.json index c0d69a75cb..ffbc3a87ee 100644 --- a/packages/context/time-context/package.json +++ b/packages/context/time-context/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -26,12 +31,14 @@ }, "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-agent-loop-testkit": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-loader-smoke": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", diff --git a/packages/context/time-context/src/invariant.ts b/packages/context/time-context/src/invariant.ts new file mode 100644 index 0000000000..64fb98ac81 --- /dev/null +++ b/packages/context/time-context/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-time-context' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/context/time-context/tests/time-context.spec.ts b/packages/context/time-context/tests/time-context.spec.ts index 06ae13818d..07c2fe54ae 100644 --- a/packages/context/time-context/tests/time-context.spec.ts +++ b/packages/context/time-context/tests/time-context.spec.ts @@ -4,8 +4,7 @@ import Loader from '@cordisjs/plugin-loader' import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm' import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' import { Session, SessionId } from '@deepseek-ai/dsh-session' -import AgentRegistry from '@deepseek-ai/dsh-agent' -import type { Agent } from '@deepseek-ai/dsh-agent' +import AgentRegistry, { agentEvents, type Agent } from '@deepseek-ai/dsh-agent' import { defineTool } from '@deepseek-ai/dsh-tools' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' @@ -83,7 +82,7 @@ async function fire( step: number, signal: AbortSignal = SIGNAL, ): Promise { - await ctx.serial('agent/pre-step', agent, turn, step, signal) + await agentEvents(ctx, agent).serial('agent/pre-step', turn, step, signal) } function textResponse(text: string): StreamChunk[] { diff --git a/packages/context/time-context/tsconfig.json b/packages/context/time-context/tsconfig.json index 7815242b55..491552def0 100644 --- a/packages/context/time-context/tsconfig.json +++ b/packages/context/time-context/tsconfig.json @@ -6,13 +6,32 @@ }, "include": ["src"], "references": [ - { "path": "../../../vendor/cosmokit" }, - { "path": "../../../vendor/cordis" }, - { "path": "../../../vendor/schemastery" }, - { "path": "../../llm/llm" }, - { "path": "../../core/agent" }, - { "path": "../../core/system-prompt" }, - { "path": "../../core/agent" }, - { "path": "../../support/loader-smoke" } + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../../vendor/schemastery" + }, + { + "path": "../../llm/llm" + }, + { + "path": "../../core/agent" + }, + { + "path": "../../core/system-prompt" + }, + { + "path": "../../core/agent" + }, + { + "path": "../../support/loader-smoke" + }, + { + "path": "../../support/invariants" + } ] } diff --git a/packages/context/workspace-context/package.json b/packages/context/workspace-context/package.json index 7f704c838a..0c50b8cc17 100644 --- a/packages/context/workspace-context/package.json +++ b/packages/context/workspace-context/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -24,6 +29,7 @@ "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-fs": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-paths": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", @@ -39,6 +45,7 @@ "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-fs": "workspace:^", "@deepseek-ai/dsh-fs-local": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-llm-deepseek": "workspace:^", "@deepseek-ai/dsh-paths": "workspace:^", diff --git a/packages/context/workspace-context/src/invariant.ts b/packages/context/workspace-context/src/invariant.ts new file mode 100644 index 0000000000..f3e76225c0 --- /dev/null +++ b/packages/context/workspace-context/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-workspace-context' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/context/workspace-context/tests/workspace-context.spec.ts b/packages/context/workspace-context/tests/workspace-context.spec.ts index 2ff2067cf5..bdeeabe810 100644 --- a/packages/context/workspace-context/tests/workspace-context.spec.ts +++ b/packages/context/workspace-context/tests/workspace-context.spec.ts @@ -7,8 +7,9 @@ import Loader from '@cordisjs/plugin-loader' import * as workspaceContext from '@deepseek-ai/dsh-workspace-context' import LlmService, { CallId, type Message, type StreamChunk } from '@deepseek-ai/dsh-llm' import SessionStore, { Session, SessionId, SESSION_FORMAT_VERSION, type SessionEvent } from '@deepseek-ai/dsh-session' -import AgentRegistry, { type Agent, type HookContext } from '@deepseek-ai/dsh-agent' +import AgentRegistry, { agentEvents, type Agent, type HookContext } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' +import { scopeTarget } from '@deepseek-ai/dsh-scope' import { FileSystem, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs' import type { FsDirEntry, @@ -23,7 +24,12 @@ import type { import LocalFileSystem from '@deepseek-ai/dsh-fs-local' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools' -import type { ToolExecution, ToolExecutionToken } from '@deepseek-ai/dsh-tools' +import type { + PostToolDecision, + ToolExecution, + ToolExecutionResult, + ToolExecutionToken, +} from '@deepseek-ai/dsh-tools' import * as ToolFs from '@deepseek-ai/dsh-tool-fs' import { discoverBaselineInstructionFiles, @@ -228,14 +234,31 @@ const composedPrefixes = new WeakMap() async function composeBaselinePrefix(ctx: Context, agent: Agent): Promise { const empty: Message[] = [] - const prefix = await ctx.waterfall( - 'agent/session-prefix', agent, empty, AbortSignal.timeout(1000), + const prefix = await agentEvents(ctx, agent).waterfall( + 'agent/session-prefix', empty, AbortSignal.timeout(1000), () => Promise.resolve(empty), ) composedPrefixes.set(agent, prefix) return prefix } +function toolEventCarrier(ctx: Context, exec: ToolExecution) { + return scopeTarget(ctx.get('tools') ?? ctx as unknown as ToolRegistry, exec.agent) +} + +function postExecute( + ctx: Context, + exec: ToolExecution, + result: Readonly, + next: () => Promise, +): Promise { + return ctx.waterfall(toolEventCarrier(ctx, exec), 'tools/post-execute', exec, result, next) +} + +function emitToolResult(ctx: Context, exec: ToolExecution, result: Readonly): void { + ctx.emit(toolEventCarrier(ctx, exec), 'tools/result', exec, result) +} + function derivedText(agent: Agent): string { return blocksText(composedPrefixes.get(agent)?.[0]?.content) } @@ -796,7 +819,7 @@ describe('workspace context request injection', () => { try { await ctx.plugin(workspaceContext, { maxBytes: 65536 }) - const decision = await ctx.waterfall('tools/post-execute', stubToolExecution({ + const decision = await postExecute(ctx, stubToolExecution({ callId: CallId('no-fs-post-execute'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, @@ -843,7 +866,7 @@ describe('workspace context request injection', () => { } // A later PostToolUse-style policy blocks this otherwise-successful read. - const blocked = await ctx.waterfall('tools/post-execute', exec, result, async () => ({ + const blocked = await postExecute(ctx, exec, result, async () => ({ kind: 'block' as const, feedback: [{ type: 'text' as const, text: 'blocked by policy' }], })) @@ -857,7 +880,7 @@ describe('workspace context request injection', () => { // The same read, when the downstream accepts, DOES surface the nested // instructions — proving the block branch above is what suppressed them, // and that the block did not consume the pending nested change. - const accepted = await ctx.waterfall('tools/post-execute', exec, result, async () => ({ + const accepted = await postExecute(ctx, exec, result, async () => ({ kind: 'accept' as const, })) expect(accepted.kind).toBe('accept') @@ -1169,8 +1192,9 @@ describe('workspace context request injection', () => { const controller = new AbortController() const reason = new Error('cancel prefix') const empty: Message[] = [] - const pending = ctx.waterfall( - 'agent/session-prefix', stubAgent(root), empty, controller.signal, + const agent = stubAgent(root) + const pending = agentEvents(ctx, agent).waterfall( + 'agent/session-prefix', empty, controller.signal, () => Promise.resolve(empty), ) @@ -1660,7 +1684,7 @@ describe('dynamic nested workspace context injection', () => { signal: controller.signal, }) - const pending = ctx.waterfall('tools/post-execute', exec, { + const pending = postExecute(ctx, exec, { content: [{ type: 'text', text: 'ok' }], isError: false, }, () => Promise.resolve({ kind: 'accept' as const })) @@ -2387,12 +2411,12 @@ describe('dynamic nested workspace context injection', () => { isError: false, } - const failedStat = await ctx.waterfall('tools/post-execute', stubToolExecution({ + const failedStat = await postExecute(ctx, stubToolExecution({ callId: CallId('provider-stat-failure'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent, }), result, async () => ({ kind: 'accept' as const })) fs.throwOnStat.clear() fs.entries.set(join(root, 'pkg/AGENTS.md'), { type: 'directory' }) - const mismatchedStat = await ctx.waterfall('tools/post-execute', stubToolExecution({ + const mismatchedStat = await postExecute(ctx, stubToolExecution({ callId: CallId('provider-stat-mismatch'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent, }), result, async () => ({ kind: 'accept' as const })) @@ -2626,19 +2650,19 @@ describe('dynamic nested workspace context injection', () => { const parent = Symbol('parent') as ToolExecutionToken const plainResult = { callId: CallId('plain'), content: [], isError: false } - ctx.emit('tools/result', stubToolExecution({ + emitToolResult(ctx, stubToolExecution({ callId: CallId('agentless-child'), name: 'read', arguments: {}, parent, }), plainResult) - ctx.emit('tools/result', stubToolExecution({ + emitToolResult(ctx, stubToolExecution({ callId: CallId('contextless-child'), name: 'read', arguments: {}, agent, parent, }), { ...plainResult, additionalContexts: [{ content: [], source: { kind: 'plugin', plugin: 'workspace-context' } }] }) - ctx.emit('tools/result', stubToolExecution({ + emitToolResult(ctx, stubToolExecution({ callId: CallId('first-child'), name: 'read', arguments: {}, agent, parent, }), { ...plainResult, additionalContexts: [workspaceChangeContext('first', 'one')] }) - ctx.emit('tools/result', stubToolExecution({ + emitToolResult(ctx, stubToolExecution({ callId: CallId('second-child'), name: 'read', arguments: {}, agent, parent, }), { ...plainResult, additionalContexts: [workspaceChangeContext('second', 'two')] }) - ctx.emit('tools/result', { + emitToolResult(ctx, { ...stubToolExecution({ callId: CallId('agentless-parent'), name: 'composite', arguments: {} }), token: parent, }, plainResult) @@ -2674,7 +2698,7 @@ describe('dynamic nested workspace context injection', () => { ] for (const item of cases) { - const decision = await ctx.waterfall('tools/post-execute', stubToolExecution({ + const decision = await postExecute(ctx, stubToolExecution({ callId: CallId(`manual-${item.name}-${cases.indexOf(item)}`), name: item.name, arguments: item.arguments, diff --git a/packages/context/workspace-context/tsconfig.json b/packages/context/workspace-context/tsconfig.json index b4807ded65..b5aca1dfc8 100644 --- a/packages/context/workspace-context/tsconfig.json +++ b/packages/context/workspace-context/tsconfig.json @@ -31,6 +31,9 @@ }, { "path": "../../util/paths" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/cordis/tool-cordis/package.json b/packages/cordis/tool-cordis/package.json index e13de3e58b..7e7b75cab3 100644 --- a/packages/cordis/tool-cordis/package.json +++ b/packages/cordis/tool-cordis/package.json @@ -11,17 +11,23 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-scope": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", "cordis": "^4.0.0-rc.7" @@ -30,16 +36,17 @@ "schemastery": "^3.18.0" }, "devDependencies": { + "@cordisjs/plugin-loader": "^1.0.0-rc.5", + "@cordisjs/plugin-timer": "workspace:^", "@deepseek-ai/dsh-agent": "workspace:^", - "@deepseek-ai/dsh-scope": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-agent-loop-testkit": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", + "@deepseek-ai/dsh-scope": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", - "@cordisjs/plugin-loader": "^1.0.0-rc.5", - "cordis": "^4.0.0-rc.7", - "@cordisjs/plugin-timer": "workspace:^" + "cordis": "^4.0.0-rc.7" } } diff --git a/packages/cordis/tool-cordis/src/invariant.ts b/packages/cordis/tool-cordis/src/invariant.ts new file mode 100644 index 0000000000..a3f375b69f --- /dev/null +++ b/packages/cordis/tool-cordis/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-tool-cordis' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/cordis/tool-cordis/tsconfig.json b/packages/cordis/tool-cordis/tsconfig.json index cc9928d81f..4f10b49622 100644 --- a/packages/cordis/tool-cordis/tsconfig.json +++ b/packages/cordis/tool-cordis/tsconfig.json @@ -25,6 +25,9 @@ }, { "path": "../../core/tools" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/core/agent-loop/src/invariant.ts b/packages/core/agent-loop/src/invariant.ts index 41b9112a4e..ab6a885dc0 100644 --- a/packages/core/agent-loop/src/invariant.ts +++ b/packages/core/agent-loop/src/invariant.ts @@ -12,8 +12,8 @@ const PACKAGE_NAME = '@deepseek-ai/dsh-agent-loop' /** Cordis companion plugin name. */ export const name = 'agent-loop-invariant' -/** Services required before the companion can register. */ -export const inject = ['invariants', 'sessions'] +/** Service required before the companion can reserve package ownership. */ +export const inject = ['invariants'] /** Install the request-reconstruction contribution into its child registration fiber. */ const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => { @@ -66,7 +66,7 @@ const install: InvariantInstaller = Object.assign((ctx: Context, fail: Invariant /** * Register the agent-loop invariant companion. - * @param ctx - Cordis context carrying the invariant and session services. + * @param ctx - Cordis context carrying the invariant service. * @returns the installed registration's disposer after setup succeeds. */ export const apply = (ctx: Context): Promise<() => void> => diff --git a/packages/core/agent-loop/tests/resume.spec.ts b/packages/core/agent-loop/tests/resume.spec.ts index 74192dde90..be98eb8447 100644 --- a/packages/core/agent-loop/tests/resume.spec.ts +++ b/packages/core/agent-loop/tests/resume.spec.ts @@ -425,7 +425,7 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => { seed, meta: { cwd: '/w', parentSession: SessionId('parent-sess'), seedLength: seed.length }, }) - await ctx1.parallel('session/flush', forked) + await ctx1.sessions.flush(forked) await ctx1.fiber.dispose() // Lifecycle 2: resume it; the parentSession + seedLength header survives the @@ -482,7 +482,7 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => { a1.send([{ type: 'text', text: 'q' }], { source: { kind: 'user' } }) await waitForIdle(ctx1, a1) a1.inject([{ type: 'text', text: 'background task 42 finished' }], { source: { kind: 'plugin', plugin: 'tool-bash' } }) - await ctx1.parallel('session/flush', a1.session) + await ctx1.sessions.flush(a1.session) await ctx1.fiber.dispose() // Lifecycle 2: resume; the injected context is still in the derived history. diff --git a/packages/core/session/src/invariant.ts b/packages/core/session/src/invariant.ts index 2887d6f9f0..22b567183e 100644 --- a/packages/core/session/src/invariant.ts +++ b/packages/core/session/src/invariant.ts @@ -15,8 +15,8 @@ const PACKAGE_NAME = '@deepseek-ai/dsh-session' /** Cordis companion plugin name. */ export const name = 'session-invariant' -/** Services required before the companion can register. */ -export const inject = ['invariants', 'sessions'] +/** Service required before the companion can reserve package ownership. */ +export const inject = ['invariants'] /** Per-session bookkeeping for relational log checks. */ interface SessionTrace { @@ -223,7 +223,7 @@ const install: InvariantInstaller = Object.assign((ctx: Context, fail: Invariant /** * Register the session invariant companion. - * @param ctx - Cordis context carrying the invariant and session services. + * @param ctx - Cordis context carrying the invariant service. * @returns the installed registration's disposer after setup succeeds. */ export const apply = (ctx: Context): Promise<() => void> => diff --git a/packages/core/session/tests/session.spec.ts b/packages/core/session/tests/session.spec.ts index 3b15157bde..a9addbf1cb 100644 --- a/packages/core/session/tests/session.spec.ts +++ b/packages/core/session/tests/session.spec.ts @@ -718,10 +718,11 @@ describe('SessionStore', () => { // may create an unrelated property with the old implementation's name, // but cannot suppress the durable event feed. expect(Reflect.set(session, 'onAppend', undefined)).toBe(true) + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) session.append('user/message', { content: [{ type: 'text', text: 'x' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) - expect(events).toHaveLength(1) - expect(events[0]![0]).toBe(session) - expect(events[0]![1].type).toBe('user/message') + expect(events).toHaveLength(2) + expect(events[1]![0]).toBe(session) + expect(events[1]![1].type).toBe('user/message') expect(ctx.sessions.get(session.id)).toBe(session) expect(ctx.sessions.list()).toEqual([session]) @@ -733,6 +734,7 @@ describe('SessionStore', () => { const a = ctx.sessions.create(SessionId('fixed')) expect(() => ctx.sessions.create(SessionId('fixed'))).toThrow('already exists') + a.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) a.append('user/message', { content: [{ type: 'text', text: 'q' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) const forked = ctx.sessions.create(SessionId('fork'), { seed: [...a.events] }) expect(forked.deriveMessages()).toEqual(a.deriveMessages()) @@ -958,8 +960,9 @@ describe('SessionStore', () => { ctx.on('session/event', (_session, event) => void events.push(event)) const session = ctx.sessions.create(SessionId('fixed')) expect(ctx.sessions.get(SessionId('fixed'))).toBe(session) + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) session.append('user/message', { content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) - expect(events).toHaveLength(1) + expect(events.at(-1)?.type).toBe('user/message') }) it('contains session/event observer failures after the append commit point', async () => { @@ -1043,6 +1046,8 @@ describe('SessionStore', () => { const ctx = new Context() await ctx.plugin(SessionStore) const session = ctx.sessions.create(SessionId('surface-dispatch-veto')) + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) + session.append('step/start', { turn: 1, step: 1 }) session.append('user/message', { content: [{ type: 'text', text: 'source' }], source: { kind: 'user' }, @@ -1062,19 +1067,19 @@ describe('SessionStore', () => { step: 1, content: [{ type: 'text', text: 'replacement' }], }, { - surfaceOp: { op: 'replace', start: 0, end: 0 }, - sourceEventSeqs: [0], + surfaceOp: { op: 'replace', start: 2, end: 2 }, + sourceEventSeqs: [2], })).toThrow('reject surface candidate') - expect(session.events).toHaveLength(1) - expect(surface.nodes).toEqual([0]) + expect(session.events).toHaveLength(3) + expect(surface.nodes).toEqual([2]) expect(surface.replaceGeneration).toBe(0) session.append('user/message', { content: [{ type: 'text', text: 'next' }], source: { kind: 'user' }, }, { surfaceOp: 'append' }) - expect(surface.nodes).toEqual([0, 1]) + expect(surface.nodes).toEqual([2, 3]) expect(surface.replaceGeneration).toBe(0) }) diff --git a/packages/core/system-prompt/package.json b/packages/core/system-prompt/package.json index 67161b79b4..6cf94f477e 100644 --- a/packages/core/system-prompt/package.json +++ b/packages/core/system-prompt/package.json @@ -11,17 +11,23 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-scope": "^0.0.1", "cordis": "^4.0.0-rc.7" @@ -30,6 +36,7 @@ "schemastery": "^3.18.0" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-scope": "workspace:^", "cordis": "^4.0.0-rc.7" diff --git a/packages/core/system-prompt/src/invariant.ts b/packages/core/system-prompt/src/invariant.ts new file mode 100644 index 0000000000..5117f93ce0 --- /dev/null +++ b/packages/core/system-prompt/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-system-prompt' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/core/system-prompt/tsconfig.json b/packages/core/system-prompt/tsconfig.json index 91e7bf1ba4..c7de9a1b69 100644 --- a/packages/core/system-prompt/tsconfig.json +++ b/packages/core/system-prompt/tsconfig.json @@ -22,6 +22,9 @@ }, { "path": "../../core/scope" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/core/tools/package.json b/packages/core/tools/package.json index 2fe3cbd448..535e60f60b 100644 --- a/packages/core/tools/package.json +++ b/packages/core/tools/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -23,12 +28,13 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", - "@deepseek-ai/dsh-user-approval": "^0.0.1", "@deepseek-ai/dsh-code-runtime": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-scope": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-system-prompt": "^0.0.1", + "@deepseek-ai/dsh-user-approval": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "dependencies": { @@ -36,12 +42,13 @@ }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", - "@deepseek-ai/dsh-user-approval": "workspace:^", "@deepseek-ai/dsh-code-runtime": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-scope": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", + "@deepseek-ai/dsh-user-approval": "workspace:^", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/core/tools/src/invariant.ts b/packages/core/tools/src/invariant.ts new file mode 100644 index 0000000000..7c5743e1d4 --- /dev/null +++ b/packages/core/tools/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-tools' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/core/tools/tsconfig.json b/packages/core/tools/tsconfig.json index c94b270c8c..918112d7d0 100644 --- a/packages/core/tools/tsconfig.json +++ b/packages/core/tools/tsconfig.json @@ -34,6 +34,9 @@ }, { "path": "../../ui/user-approval" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/examples/acp-demo/package.json b/packages/examples/acp-demo/package.json index 28d057c0a0..6743c55a3e 100644 --- a/packages/examples/acp-demo/package.json +++ b/packages/examples/acp-demo/package.json @@ -14,6 +14,10 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./bin": { "types": "./lib/types/bin.d.ts", "default": "./lib/bin.js" @@ -23,6 +27,7 @@ }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/bin.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", @@ -32,28 +37,30 @@ "peerDependencies": { "@cordisjs/plugin-include": "^1.0.4", "@cordisjs/plugin-loader": "^1.0.0-rc.5", - "@deepseek-ai/dsh-app-boot": "^0.0.1", "@deepseek-ai/dsh-acp": "^0.0.1", "@deepseek-ai/dsh-agent-spine-demo": "^0.0.1", - "@deepseek-ai/dsh-workspace-context": "^0.0.1", + "@deepseek-ai/dsh-app-boot": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", "@deepseek-ai/dsh-user-interaction": "^0.0.1", + "@deepseek-ai/dsh-workspace-context": "^0.0.1", "cordis": "^4.0.0-rc.7", "schemastery": "^3.17.0" }, "devDependencies": { "@cordisjs/plugin-include": "workspace:^", "@cordisjs/plugin-loader": "workspace:^", - "@deepseek-ai/dsh-app-boot": "workspace:^", "@deepseek-ai/dsh-acp": "workspace:^", "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-agent-spine-demo": "workspace:^", - "@deepseek-ai/dsh-tools": "workspace:^", - "@deepseek-ai/dsh-system-prompt": "workspace:^", - "@deepseek-ai/dsh-workspace-context": "workspace:^", + "@deepseek-ai/dsh-app-boot": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^", + "@deepseek-ai/dsh-system-prompt": "workspace:^", + "@deepseek-ai/dsh-tools": "workspace:^", "@deepseek-ai/dsh-user-interaction": "workspace:^", + "@deepseek-ai/dsh-workspace-context": "workspace:^", "cordis": "^4.0.0-rc.7", "schemastery": "^3.17.0" } diff --git a/packages/examples/acp-demo/src/invariant.ts b/packages/examples/acp-demo/src/invariant.ts new file mode 100644 index 0000000000..d8f2ff17dc --- /dev/null +++ b/packages/examples/acp-demo/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-acp-demo' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/examples/acp-demo/tsconfig.json b/packages/examples/acp-demo/tsconfig.json index b0e537574a..2f6acd4a17 100644 --- a/packages/examples/acp-demo/tsconfig.json +++ b/packages/examples/acp-demo/tsconfig.json @@ -40,6 +40,9 @@ }, { "path": "../../session-persistence/session-persistence-jsonl" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/examples/acp-demo/tsdown.config.ts b/packages/examples/acp-demo/tsdown.config.ts index 9dd130b30d..2fa93780be 100644 --- a/packages/examples/acp-demo/tsdown.config.ts +++ b/packages/examples/acp-demo/tsdown.config.ts @@ -8,7 +8,7 @@ import { defineConfig } from 'tsdown' * matching every package. */ export default defineConfig({ - entry: ['lib/types/index.js', 'lib/types/bin.js'], + entry: ['lib/types/index.js', 'lib/types/invariant.js', 'lib/types/bin.js'], outDir: 'lib', format: ['esm'], platform: 'node', diff --git a/packages/examples/agent-spine-demo/package.json b/packages/examples/agent-spine-demo/package.json index cae0aab6d4..1cc6214599 100644 --- a/packages/examples/agent-spine-demo/package.json +++ b/packages/examples/agent-spine-demo/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -25,12 +30,11 @@ "@cordisjs/plugin-timer": "^1.1.2", "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-agent-loop": "^0.0.1", - "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-home": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", - "@deepseek-ai/dsh-workspace-context": "^0.0.1", - "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-scope": "^0.0.1", + "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-skill": "^0.0.1", "@deepseek-ai/dsh-skill-local": "^0.0.1", "@deepseek-ai/dsh-system-prompt": "^0.0.1", @@ -39,6 +43,7 @@ "@deepseek-ai/dsh-tool-skill": "^0.0.1", "@deepseek-ai/dsh-tool-tasks": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", + "@deepseek-ai/dsh-workspace-context": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { @@ -46,12 +51,11 @@ "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-fs-local": "workspace:^", - "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-home": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", - "@deepseek-ai/dsh-workspace-context": "workspace:^", - "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-scope": "workspace:^", + "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-skill": "workspace:^", "@deepseek-ai/dsh-skill-local": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", @@ -60,6 +64,7 @@ "@deepseek-ai/dsh-tool-skill": "workspace:^", "@deepseek-ai/dsh-tool-tasks": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", + "@deepseek-ai/dsh-workspace-context": "workspace:^", "cordis": "^4.0.0-rc.7" }, "dependencies": { diff --git a/packages/examples/agent-spine-demo/src/invariant.ts b/packages/examples/agent-spine-demo/src/invariant.ts new file mode 100644 index 0000000000..913b5ca2ab --- /dev/null +++ b/packages/examples/agent-spine-demo/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-agent-spine-demo' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/examples/cli-demo/package.json b/packages/examples/cli-demo/package.json index 7b035e2524..1c00a32891 100644 --- a/packages/examples/cli-demo/package.json +++ b/packages/examples/cli-demo/package.json @@ -14,6 +14,10 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./bin": { "types": "./lib/types/bin.d.ts", "default": "./lib/bin.js" @@ -23,6 +27,7 @@ }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/bin.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", @@ -35,6 +40,7 @@ "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-agent-spine-demo": "^0.0.1", "@deepseek-ai/dsh-app-boot": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1", @@ -49,6 +55,7 @@ "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-agent-spine-demo": "workspace:^", "@deepseek-ai/dsh-app-boot": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^", diff --git a/packages/examples/cli-demo/src/invariant.ts b/packages/examples/cli-demo/src/invariant.ts new file mode 100644 index 0000000000..5681362dcc --- /dev/null +++ b/packages/examples/cli-demo/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * Generated invariant ownership companion for `@deepseek-ai/dsh-cli-demo`. + * Replace this file with package-owned checks while preserving its registration. + * + * @generated scripts/gen-package-invariants.ts + * @module @deepseek-ai/dsh-cli-demo/invariant + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-cli-demo' + +/** Cordis companion plugin name. */ +export const name = 'cli-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 = () => {} + +/** + * 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 */ diff --git a/packages/examples/cli-demo/tsconfig.json b/packages/examples/cli-demo/tsconfig.json index f25b1592ca..c7e3aed914 100644 --- a/packages/examples/cli-demo/tsconfig.json +++ b/packages/examples/cli-demo/tsconfig.json @@ -8,15 +8,38 @@ }, "include": ["src/**/*.ts"], "references": [ - { "path": "../../../vendor/schemastery" }, - { "path": "../../../vendor/cordis" }, - { "path": "../../llm/llm" }, - { "path": "../../core/session" }, - { "path": "../../core/agent" }, - { "path": "../../core/system-prompt" }, - { "path": "../../core/tools" }, - { "path": "../agent-spine-demo" }, - { "path": "../../session-persistence/session-persistence-jsonl" }, - { "path": "../../ui/app-boot" } + { + "path": "../../../vendor/schemastery" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../llm/llm" + }, + { + "path": "../../core/session" + }, + { + "path": "../../core/agent" + }, + { + "path": "../../core/system-prompt" + }, + { + "path": "../../core/tools" + }, + { + "path": "../agent-spine-demo" + }, + { + "path": "../../session-persistence/session-persistence-jsonl" + }, + { + "path": "../../ui/app-boot" + }, + { + "path": "../../support/invariants" + } ] } diff --git a/packages/examples/cli-demo/tsdown.config.ts b/packages/examples/cli-demo/tsdown.config.ts index e5b164d46f..646855bea9 100644 --- a/packages/examples/cli-demo/tsdown.config.ts +++ b/packages/examples/cli-demo/tsdown.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from 'tsdown' /** Builds the plugin and executable entries from declarations emitted by `tsc -b`. */ export default defineConfig({ - entry: ['lib/types/index.js', 'lib/types/bin.js'], + entry: ['lib/types/index.js', 'lib/types/invariant.js', 'lib/types/bin.js'], outDir: 'lib', format: ['esm'], platform: 'node', diff --git a/packages/examples/jsonrpc-demo/package.json b/packages/examples/jsonrpc-demo/package.json index 3b04fcc977..d103e3bd00 100644 --- a/packages/examples/jsonrpc-demo/package.json +++ b/packages/examples/jsonrpc-demo/package.json @@ -14,6 +14,10 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./bin": { "types": "./lib/types/bin.d.ts", "default": "./lib/bin.js" @@ -23,6 +27,7 @@ }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/bin.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", @@ -33,9 +38,11 @@ "@deepseek-ai/dsh-app-boot": "workspace:^" }, "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/examples/jsonrpc-demo/src/invariant.ts b/packages/examples/jsonrpc-demo/src/invariant.ts new file mode 100644 index 0000000000..21f19faf0f --- /dev/null +++ b/packages/examples/jsonrpc-demo/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-jsonrpc-demo' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/examples/jsonrpc-demo/tsconfig.json b/packages/examples/jsonrpc-demo/tsconfig.json index 83d6cf5aa2..aba279d405 100644 --- a/packages/examples/jsonrpc-demo/tsconfig.json +++ b/packages/examples/jsonrpc-demo/tsconfig.json @@ -16,6 +16,9 @@ }, { "path": "../../ui/app-boot" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/examples/jsonrpc-demo/tsdown.config.ts b/packages/examples/jsonrpc-demo/tsdown.config.ts index aaa860edd0..a8864a84a9 100644 --- a/packages/examples/jsonrpc-demo/tsdown.config.ts +++ b/packages/examples/jsonrpc-demo/tsdown.config.ts @@ -4,7 +4,7 @@ import { defineConfig } from 'tsdown' * Build the doc-only module and CLI entry; `tsc -b` supplies declarations. */ export default defineConfig({ - entry: ['lib/types/index.js', 'lib/types/bin.js'], + entry: ['lib/types/index.js', 'lib/types/invariant.js', 'lib/types/bin.js'], outDir: 'lib', format: ['esm'], platform: 'node', diff --git a/packages/examples/stdio-demo/package.json b/packages/examples/stdio-demo/package.json index 94554e3f8e..69c45bb362 100644 --- a/packages/examples/stdio-demo/package.json +++ b/packages/examples/stdio-demo/package.json @@ -14,6 +14,10 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./bin": { "types": "./lib/types/bin.d.ts", "default": "./lib/bin.js" @@ -23,6 +27,7 @@ }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/bin.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", @@ -33,19 +38,20 @@ "@cordisjs/plugin-include": "^1.0.4", "@cordisjs/plugin-loader": "^1.0.0-rc.5", "@cordisjs/plugin-logger-console": "^1.0.0", - "@deepseek-ai/dsh-app-boot": "^0.0.1", "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-agent-loop": "^0.0.1", - "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-agent-spine-demo": "^0.0.1", - "@deepseek-ai/dsh-workspace-context": "^0.0.1", + "@deepseek-ai/dsh-app-boot": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", + "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1", "@deepseek-ai/dsh-stdio": "^0.0.1", - "@deepseek-ai/dsh-tui": "^0.0.1", "@deepseek-ai/dsh-tool-ask-user": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", + "@deepseek-ai/dsh-tui": "^0.0.1", "@deepseek-ai/dsh-user-interaction": "^0.0.1", + "@deepseek-ai/dsh-workspace-context": "^0.0.1", "cordis": "^4.0.0-rc.7", "schemastery": "^3.17.0" }, @@ -53,20 +59,21 @@ "@cordisjs/plugin-include": "workspace:^", "@cordisjs/plugin-loader": "workspace:^", "@cordisjs/plugin-logger-console": "workspace:^", - "@deepseek-ai/dsh-app-boot": "workspace:^", "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", - "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-agent-spine-demo": "workspace:^", - "@deepseek-ai/dsh-system-prompt": "workspace:^", - "@deepseek-ai/dsh-workspace-context": "workspace:^", + "@deepseek-ai/dsh-app-boot": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^", "@deepseek-ai/dsh-stdio": "workspace:^", - "@deepseek-ai/dsh-tui": "workspace:^", + "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tool-ask-user": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", + "@deepseek-ai/dsh-tui": "workspace:^", "@deepseek-ai/dsh-user-interaction": "workspace:^", + "@deepseek-ai/dsh-workspace-context": "workspace:^", "cordis": "^4.0.0-rc.7", "schemastery": "^3.17.0" } diff --git a/packages/examples/stdio-demo/src/invariant.ts b/packages/examples/stdio-demo/src/invariant.ts new file mode 100644 index 0000000000..5d51c3cd9b --- /dev/null +++ b/packages/examples/stdio-demo/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-stdio-demo' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/examples/stdio-demo/tsconfig.json b/packages/examples/stdio-demo/tsconfig.json index fc6711ffb9..265ee5f35b 100644 --- a/packages/examples/stdio-demo/tsconfig.json +++ b/packages/examples/stdio-demo/tsconfig.json @@ -49,6 +49,9 @@ }, { "path": "../../session-persistence/session-persistence-jsonl" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/examples/stdio-demo/tsdown.config.ts b/packages/examples/stdio-demo/tsdown.config.ts index 53797cdd79..7e907e8a18 100644 --- a/packages/examples/stdio-demo/tsdown.config.ts +++ b/packages/examples/stdio-demo/tsdown.config.ts @@ -8,7 +8,7 @@ import { defineConfig } from 'tsdown' * matching every package. */ export default defineConfig({ - entry: ['lib/types/index.js', 'lib/types/bin.js'], + entry: ['lib/types/index.js', 'lib/types/invariant.js', 'lib/types/bin.js'], outDir: 'lib', format: ['esm'], platform: 'node', diff --git a/packages/fs/fs-local/package.json b/packages/fs/fs-local/package.json index dd80cb4d9c..75a0400afb 100644 --- a/packages/fs/fs-local/package.json +++ b/packages/fs/fs-local/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -23,6 +28,7 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-fs": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "dependencies": { @@ -30,6 +36,7 @@ }, "devDependencies": { "@deepseek-ai/dsh-fs": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "cordis": "^4.0.0-rc.7" } diff --git a/packages/fs/fs-local/src/invariant.ts b/packages/fs/fs-local/src/invariant.ts new file mode 100644 index 0000000000..0c89f2299b --- /dev/null +++ b/packages/fs/fs-local/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-fs-local' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/fs/fs-local/tsconfig.json b/packages/fs/fs-local/tsconfig.json index 0808fd29ca..b249913d43 100644 --- a/packages/fs/fs-local/tsconfig.json +++ b/packages/fs/fs-local/tsconfig.json @@ -6,10 +6,23 @@ }, "include": ["src"], "references": [ - { "path": "../../../vendor/cosmokit" }, - { "path": "../../../vendor/cordis" }, - { "path": "../../../vendor/schemastery" }, - { "path": "../../llm/llm" }, - { "path": "../fs" } + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../../vendor/schemastery" + }, + { + "path": "../../llm/llm" + }, + { + "path": "../fs" + }, + { + "path": "../../support/invariants" + } ] } diff --git a/packages/fs/fs-policy/package.json b/packages/fs/fs-policy/package.json index e27302e4a6..e74852ef7d 100644 --- a/packages/fs/fs-policy/package.json +++ b/packages/fs/fs-policy/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -23,10 +28,12 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-fs": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-fs": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "cordis": "^4.0.0-rc.7" } diff --git a/packages/fs/fs-policy/src/invariant.ts b/packages/fs/fs-policy/src/invariant.ts new file mode 100644 index 0000000000..11f155d2ec --- /dev/null +++ b/packages/fs/fs-policy/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-fs-policy' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/fs/fs-policy/tsconfig.json b/packages/fs/fs-policy/tsconfig.json index fcc1307a36..3f22545107 100644 --- a/packages/fs/fs-policy/tsconfig.json +++ b/packages/fs/fs-policy/tsconfig.json @@ -6,9 +6,20 @@ }, "include": ["src"], "references": [ - { "path": "../../../vendor/cosmokit" }, - { "path": "../../../vendor/cordis" }, - { "path": "../../llm/llm" }, - { "path": "../fs" } + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../llm/llm" + }, + { + "path": "../fs" + }, + { + "path": "../../support/invariants" + } ] } diff --git a/packages/fs/fs/package.json b/packages/fs/fs/package.json index f1efde152a..3f15e0762a 100644 --- a/packages/fs/fs/package.json +++ b/packages/fs/fs/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -23,11 +28,13 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-brand": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-brand": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "cordis": "^4.0.0-rc.7" } diff --git a/packages/fs/fs/src/invariant.ts b/packages/fs/fs/src/invariant.ts new file mode 100644 index 0000000000..55895b4b24 --- /dev/null +++ b/packages/fs/fs/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-fs' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/fs/fs/tsconfig.json b/packages/fs/fs/tsconfig.json index a352aea65a..5fb6fec5a3 100644 --- a/packages/fs/fs/tsconfig.json +++ b/packages/fs/fs/tsconfig.json @@ -6,9 +6,20 @@ }, "include": ["src"], "references": [ - { "path": "../../../vendor/cosmokit" }, - { "path": "../../../vendor/cordis" }, - { "path": "../../util/brand" }, - { "path": "../../llm/llm" } + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../util/brand" + }, + { + "path": "../../llm/llm" + }, + { + "path": "../../support/invariants" + } ] } diff --git a/packages/fs/tool-fs-search/package.json b/packages/fs/tool-fs-search/package.json index 002d54569a..45de8f5e4a 100644 --- a/packages/fs/tool-fs-search/package.json +++ b/packages/fs/tool-fs-search/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -26,6 +31,7 @@ }, "peerDependencies": { "@deepseek-ai/dsh-bash": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-retention": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", @@ -38,6 +44,7 @@ "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-bash": "workspace:^", "@deepseek-ai/dsh-bash-local": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-retention": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", diff --git a/packages/fs/tool-fs-search/src/invariant.ts b/packages/fs/tool-fs-search/src/invariant.ts new file mode 100644 index 0000000000..f0055b611a --- /dev/null +++ b/packages/fs/tool-fs-search/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-tool-fs-search' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/fs/tool-fs-search/tsconfig.json b/packages/fs/tool-fs-search/tsconfig.json index 9241aca15b..ad0c703117 100644 --- a/packages/fs/tool-fs-search/tsconfig.json +++ b/packages/fs/tool-fs-search/tsconfig.json @@ -6,15 +6,38 @@ }, "include": ["src"], "references": [ - { "path": "../../../vendor/cosmokit" }, - { "path": "../../../vendor/cordis" }, - { "path": "../../../vendor/schemastery" }, - { "path": "../../util/retention" }, - { "path": "../../llm/llm" }, - { "path": "../../core/session" }, - { "path": "../../core/tools" }, - { "path": "../../core/system-prompt" }, - { "path": "../../bash/bash" }, - { "path": "../../spill/spill" } + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../../vendor/schemastery" + }, + { + "path": "../../util/retention" + }, + { + "path": "../../llm/llm" + }, + { + "path": "../../core/session" + }, + { + "path": "../../core/tools" + }, + { + "path": "../../core/system-prompt" + }, + { + "path": "../../bash/bash" + }, + { + "path": "../../spill/spill" + }, + { + "path": "../../support/invariants" + } ] } diff --git a/packages/fs/tool-fs/package.json b/packages/fs/tool-fs/package.json index f9ef3136bf..51586836c9 100644 --- a/packages/fs/tool-fs/package.json +++ b/packages/fs/tool-fs/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -27,6 +32,7 @@ }, "peerDependencies": { "@deepseek-ai/dsh-fs": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-system-prompt": "^0.0.1", @@ -37,9 +43,10 @@ "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-agent-loop-testkit": "workspace:^", - "@deepseek-ai/dsh-fs-policy": "workspace:^", "@deepseek-ai/dsh-fs": "workspace:^", "@deepseek-ai/dsh-fs-local": "workspace:^", + "@deepseek-ai/dsh-fs-policy": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-llm-deepseek": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", diff --git a/packages/fs/tool-fs/src/invariant.ts b/packages/fs/tool-fs/src/invariant.ts new file mode 100644 index 0000000000..7a683d978f --- /dev/null +++ b/packages/fs/tool-fs/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-tool-fs' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/fs/tool-fs/tsconfig.json b/packages/fs/tool-fs/tsconfig.json index f0133b1d2b..43c91b2dd6 100644 --- a/packages/fs/tool-fs/tsconfig.json +++ b/packages/fs/tool-fs/tsconfig.json @@ -6,13 +6,32 @@ }, "include": ["src"], "references": [ - { "path": "../../../vendor/cosmokit" }, - { "path": "../../../vendor/cordis" }, - { "path": "../../../vendor/schemastery" }, - { "path": "../../llm/llm" }, - { "path": "../../core/tools" }, - { "path": "../../core/system-prompt" }, - { "path": "../fs" }, - { "path": "../fs-policy" } + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../../vendor/schemastery" + }, + { + "path": "../../llm/llm" + }, + { + "path": "../../core/tools" + }, + { + "path": "../../core/system-prompt" + }, + { + "path": "../fs" + }, + { + "path": "../fs-policy" + }, + { + "path": "../../support/invariants" + } ] } diff --git a/packages/guard/repeat-tool-guard/package.json b/packages/guard/repeat-tool-guard/package.json index 92d49c548f..c892ca99e4 100644 --- a/packages/guard/repeat-tool-guard/package.json +++ b/packages/guard/repeat-tool-guard/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -26,6 +31,7 @@ }, "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", "cordis": "^4.0.0-rc.7" }, @@ -33,6 +39,7 @@ "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-agent-loop-testkit": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", diff --git a/packages/guard/repeat-tool-guard/src/invariant.ts b/packages/guard/repeat-tool-guard/src/invariant.ts new file mode 100644 index 0000000000..05c4df1a66 --- /dev/null +++ b/packages/guard/repeat-tool-guard/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-repeat-tool-guard' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/guard/repeat-tool-guard/tsconfig.json b/packages/guard/repeat-tool-guard/tsconfig.json index 66439bcd5f..9ca11b7119 100644 --- a/packages/guard/repeat-tool-guard/tsconfig.json +++ b/packages/guard/repeat-tool-guard/tsconfig.json @@ -25,6 +25,9 @@ }, { "path": "../../llm/llm" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/hooks/hook-protocol/package.json b/packages/hooks/hook-protocol/package.json index 201c744219..f357278db3 100644 --- a/packages/hooks/hook-protocol/package.json +++ b/packages/hooks/hook-protocol/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -23,11 +28,13 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-bash": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-bash": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "cordis": "^4.0.0-rc.7" } diff --git a/packages/hooks/hook-protocol/src/invariant.ts b/packages/hooks/hook-protocol/src/invariant.ts new file mode 100644 index 0000000000..9893493644 --- /dev/null +++ b/packages/hooks/hook-protocol/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-hook-protocol' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/hooks/hook-protocol/tsconfig.json b/packages/hooks/hook-protocol/tsconfig.json index dc4f8d9e16..220748cb0f 100644 --- a/packages/hooks/hook-protocol/tsconfig.json +++ b/packages/hooks/hook-protocol/tsconfig.json @@ -19,6 +19,9 @@ }, { "path": "../../core/session" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/hooks/hooks-claude/package.json b/packages/hooks/hooks-claude/package.json index c6870a6471..34696ab625 100644 --- a/packages/hooks/hooks-claude/package.json +++ b/packages/hooks/hooks-claude/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -27,6 +32,7 @@ "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-hook-protocol": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-session-persistence": "^0.0.1", @@ -41,6 +47,7 @@ "@deepseek-ai/dsh-bash": "workspace:^", "@deepseek-ai/dsh-bash-local": "workspace:^", "@deepseek-ai/dsh-hook-protocol": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-persistence": "workspace:^", diff --git a/packages/hooks/hooks-claude/src/invariant.ts b/packages/hooks/hooks-claude/src/invariant.ts new file mode 100644 index 0000000000..5c6002f7f5 --- /dev/null +++ b/packages/hooks/hooks-claude/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-hooks-claude' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/hooks/hooks-claude/tests/bridge.spec.ts b/packages/hooks/hooks-claude/tests/bridge.spec.ts index 65dd29d146..953b4befd0 100644 --- a/packages/hooks/hooks-claude/tests/bridge.spec.ts +++ b/packages/hooks/hooks-claude/tests/bridge.spec.ts @@ -10,7 +10,8 @@ import type { Agent } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local' -import { SubagentRunId } from '@deepseek-ai/dsh-subagent' +import { scopeTarget } from '@deepseek-ai/dsh-scope' +import SubagentService, { SubagentRunId } from '@deepseek-ai/dsh-subagent' import * as HooksClaude from '@deepseek-ai/dsh-hooks-claude' import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' @@ -25,6 +26,10 @@ import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent const dirs: string[] = [] afterEach(() => { for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true }) }) +function subagentCarrier(ctx: Context) { + return scopeTarget(ctx as unknown as SubagentService, undefined) +} + /** Write a hooks.json + named executable scripts into a fresh temp dir. */ function writeConfig(hooks: unknown, scripts: Record = {}): string { const dir = mkdtempSync(join(tmpdir(), 'dsh-hooks-claude-')) @@ -290,8 +295,8 @@ describe('hooks-claude bridge — SubagentStart / SubagentStop (observe)', () => // Drive the observe-only lifecycle events directly (no real child needed — the // bridge just listens). No child agent is registered, so SubagentStart's // child lookup yields undefined and it simply runs the hook. - ctx.emit('subagent/start', { runId: SubagentRunId('run-1'), provider: 'inproc', id: SessionId('child-1'), local: false }) - ctx.emit('subagent/end', { runId: SubagentRunId('run-1'), provider: 'inproc', id: SessionId('child-1'), local: false, stopReason: 'completed', lastAssistantMessage: [{ type: 'text', text: 'done' }] }) + ctx.emit(subagentCarrier(ctx), 'subagent/start', { runId: SubagentRunId('run-1'), provider: 'inproc', id: SessionId('child-1'), local: false }) + ctx.emit(subagentCarrier(ctx), 'subagent/end', { runId: SubagentRunId('run-1'), provider: 'inproc', id: SessionId('child-1'), local: false, stopReason: 'completed', lastAssistantMessage: [{ type: 'text', text: 'done' }] }) // Both hooks run async (detached .then); poll for their marker files rather // than a fixed sleep that flakes under load. @@ -326,7 +331,7 @@ describe('hooks-claude bridge — SubagentStart / SubagentStop (observe)', () => const { ctx, hooks } = await harnessWithFiber(dir, new MockAdapter([])) const warn = vi.fn() ctx.logger.warn = warn as never - ctx.emit('subagent/start', { runId: SubagentRunId('run-1'), provider: 'inproc', id: SessionId('child-1'), local: false }) + ctx.emit(subagentCarrier(ctx), 'subagent/start', { runId: SubagentRunId('run-1'), provider: 'inproc', id: SessionId('child-1'), local: false }) await waitFor(() => existsSync(marker)) const pid = Number(readFileSync(pidFile, 'utf8').trim()) await hooks.dispose() diff --git a/packages/hooks/hooks-claude/tests/coverage-cases.ts b/packages/hooks/hooks-claude/tests/coverage-cases.ts index ec7d18b4c4..4b8036f3a5 100644 --- a/packages/hooks/hooks-claude/tests/coverage-cases.ts +++ b/packages/hooks/hooks-claude/tests/coverage-cases.ts @@ -10,7 +10,8 @@ import type { Agent } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local' -import { SubagentRunId } from '@deepseek-ai/dsh-subagent' +import { scopeTarget } from '@deepseek-ai/dsh-scope' +import SubagentService, { SubagentRunId } from '@deepseek-ai/dsh-subagent' import * as HooksClaude from '@deepseek-ai/dsh-hooks-claude' import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' @@ -20,6 +21,10 @@ import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent const dirs: string[] = [] afterEach(() => { for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true }) }) +function subagentCarrier(ctx: Context) { + return scopeTarget(ctx as unknown as SubagentService, undefined) +} + function dir(): string { const d = mkdtempSync(join(tmpdir(), 'dsh-hc-cov-')); dirs.push(d); return d } function sh(d: string, name: string, body: string): string { const p = join(d, name); writeFileSync(p, body); chmodSync(p, 0o755); return p @@ -233,7 +238,7 @@ export function defineCoverageCases(group: CoverageGroup): void { const injected: string[] = [] const child = { id: SessionId('child-x'), inject: (content: { type: string; text?: string }[]) => { injected.push(content.map(b => b.text ?? '').join('')) }, session: { id: SessionId('child-x'), header: { id: 'child-x' } } } as unknown as Parameters[0] ctx.agents.register(child) - ctx.emit('subagent/start', { runId: SubagentRunId('run-x'), provider: 'p', id: SessionId('child-x'), local: true }) + ctx.emit(subagentCarrier(ctx), 'subagent/start', { runId: SubagentRunId('run-x'), provider: 'p', id: SessionId('child-x'), local: true }) await waitFor(() => injected.includes('child guidance')) expect(injected).toContain('child guidance') }) @@ -249,7 +254,7 @@ export function defineCoverageCases(group: CoverageGroup): void { const warn = vi.fn(); ctx.logger.warn = warn as never const child = { id: SessionId('child-y'), inject: () => { throw new Error('inject boom') }, session: { id: SessionId('child-y'), header: { id: 'child-y' } } } as unknown as Parameters[0] ctx.agents.register(child) - ctx.emit('subagent/start', { runId: SubagentRunId('run-y'), provider: 'p', id: SessionId('child-y'), local: true }) + ctx.emit(subagentCarrier(ctx), 'subagent/start', { runId: SubagentRunId('run-y'), provider: 'p', id: SessionId('child-y'), local: true }) await waitFor(() => warn.mock.calls.some(c => String(c[0]).includes('SubagentStart hook failed'))) expect(warn).toHaveBeenCalledWith(expect.stringContaining('SubagentStart hook failed')) }) @@ -293,7 +298,7 @@ export function defineCoverageCases(group: CoverageGroup): void { const s = sh(d, 'stop.sh', `#!/usr/bin/env bash\ntouch "${marker}"\n`) const path = hooks(d, { SubagentStop: [{ hooks: [{ type: 'command', command: s }] }] }) const ctx = await harness(path, new MockAdapter([])) - ctx.emit('subagent/end', { runId: SubagentRunId('run-z'), provider: 'p', id: SessionId('child-z'), local: false, stopReason: 'completed' }) + ctx.emit(subagentCarrier(ctx), 'subagent/end', { runId: SubagentRunId('run-z'), provider: 'p', id: SessionId('child-z'), local: false, stopReason: 'completed' }) await waitFor(() => existsSync(marker)) expect(existsSync(marker)).toBe(true) }) @@ -693,7 +698,7 @@ export function defineCoverageCases(group: CoverageGroup): void { // Register a live child on its own session cwd; emit subagent/end with its id. const { SessionId } = await import('@deepseek-ai/dsh-session') const childHandle = await ctx.agents.create({ sessionId: SessionId('child-stop-session'), meta: { cwd: childDir }, agentOptions: { provider: 'mock', model: 'mock' } }) - ctx.emit('subagent/end', { runId: SubagentRunId('run-stop'), provider: 'inproc', id: childHandle.agent.id, local: true, stopReason: 'completed' }) + ctx.emit(subagentCarrier(ctx), 'subagent/end', { runId: SubagentRunId('run-stop'), provider: 'inproc', id: childHandle.agent.id, local: true, stopReason: 'completed' }) await waitFor(() => existsSync(marker)) expect(existsSync(marker)).toBe(true) // the marker landed in the CHILD dir diff --git a/packages/hooks/hooks-claude/tsconfig.json b/packages/hooks/hooks-claude/tsconfig.json index 07c88610f9..445d0f68b1 100644 --- a/packages/hooks/hooks-claude/tsconfig.json +++ b/packages/hooks/hooks-claude/tsconfig.json @@ -40,6 +40,9 @@ }, { "path": "../../bash/bash" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/hooks/hooks-codex/package.json b/packages/hooks/hooks-codex/package.json index 8c8686c539..5d583baafe 100644 --- a/packages/hooks/hooks-codex/package.json +++ b/packages/hooks/hooks-codex/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -27,6 +32,7 @@ "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-hook-protocol": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-session-persistence": "^0.0.1", @@ -40,6 +46,7 @@ "@deepseek-ai/dsh-bash": "workspace:^", "@deepseek-ai/dsh-bash-local": "workspace:^", "@deepseek-ai/dsh-hook-protocol": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-persistence": "workspace:^", diff --git a/packages/hooks/hooks-codex/src/invariant.ts b/packages/hooks/hooks-codex/src/invariant.ts new file mode 100644 index 0000000000..1b8f03a057 --- /dev/null +++ b/packages/hooks/hooks-codex/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-hooks-codex' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/hooks/hooks-codex/tsconfig.json b/packages/hooks/hooks-codex/tsconfig.json index ae3c91e9dd..3bd9bd91e5 100644 --- a/packages/hooks/hooks-codex/tsconfig.json +++ b/packages/hooks/hooks-codex/tsconfig.json @@ -37,6 +37,9 @@ }, { "path": "../../bash/bash" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/llm/llm-deepseek/package.json b/packages/llm/llm-deepseek/package.json index 1461ad0f44..af8ca45085 100644 --- a/packages/llm/llm-deepseek/package.json +++ b/packages/llm/llm-deepseek/package.json @@ -11,17 +11,23 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "cordis": "^4.0.0-rc.7" }, @@ -29,6 +35,7 @@ "schemastery": "^3.18.0" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "cordis": "^4.0.0-rc.7" } diff --git a/packages/llm/llm-deepseek/src/invariant.ts b/packages/llm/llm-deepseek/src/invariant.ts new file mode 100644 index 0000000000..c3e6b5e153 --- /dev/null +++ b/packages/llm/llm-deepseek/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-llm-deepseek' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/llm/llm-deepseek/tsconfig.json b/packages/llm/llm-deepseek/tsconfig.json index e9de391ba1..d145ddb6ee 100644 --- a/packages/llm/llm-deepseek/tsconfig.json +++ b/packages/llm/llm-deepseek/tsconfig.json @@ -19,6 +19,9 @@ }, { "path": "../../llm/llm" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/llm/llm-pi-ai/package.json b/packages/llm/llm-pi-ai/package.json index c922467deb..6901441490 100644 --- a/packages/llm/llm-pi-ai/package.json +++ b/packages/llm/llm-pi-ai/package.json @@ -11,17 +11,23 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "cordis": "^4.0.0-rc.7" }, @@ -30,6 +36,7 @@ "schemastery": "^3.18.0" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-llm-deepseek": "workspace:^", "cordis": "^4.0.0-rc.7" diff --git a/packages/llm/llm-pi-ai/src/invariant.ts b/packages/llm/llm-pi-ai/src/invariant.ts new file mode 100644 index 0000000000..4ada7a1a51 --- /dev/null +++ b/packages/llm/llm-pi-ai/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-llm-pi-ai' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/llm/llm-pi-ai/tsconfig.json b/packages/llm/llm-pi-ai/tsconfig.json index e9de391ba1..d145ddb6ee 100644 --- a/packages/llm/llm-pi-ai/tsconfig.json +++ b/packages/llm/llm-pi-ai/tsconfig.json @@ -19,6 +19,9 @@ }, { "path": "../../llm/llm" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/llm/llm/package.json b/packages/llm/llm/package.json index ab11c8574f..9ef3e3323e 100644 --- a/packages/llm/llm/package.json +++ b/packages/llm/llm/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -23,10 +28,12 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-brand": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-brand": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/llm/llm/src/invariant.ts b/packages/llm/llm/src/invariant.ts new file mode 100644 index 0000000000..a8a56ce245 --- /dev/null +++ b/packages/llm/llm/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-llm' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/llm/llm/tsconfig.json b/packages/llm/llm/tsconfig.json index 342f636170..5bc7a9fcf5 100644 --- a/packages/llm/llm/tsconfig.json +++ b/packages/llm/llm/tsconfig.json @@ -16,6 +16,9 @@ }, { "path": "../../util/brand" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/llm/token-meter/package.json b/packages/llm/token-meter/package.json index 13fa4e3cdc..dadd5e8f8d 100644 --- a/packages/llm/token-meter/package.json +++ b/packages/llm/token-meter/package.json @@ -11,17 +11,23 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "cordis": "^4.0.0-rc.7" @@ -30,6 +36,7 @@ "schemastery": "^3.18.0" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "cordis": "^4.0.0-rc.7" diff --git a/packages/llm/token-meter/src/invariant.ts b/packages/llm/token-meter/src/invariant.ts new file mode 100644 index 0000000000..00ceb567be --- /dev/null +++ b/packages/llm/token-meter/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-token-meter' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/llm/token-meter/tests/token-meter.spec.ts b/packages/llm/token-meter/tests/token-meter.spec.ts index e7ddca6696..d49c96fdb0 100644 --- a/packages/llm/token-meter/tests/token-meter.spec.ts +++ b/packages/llm/token-meter/tests/token-meter.spec.ts @@ -631,19 +631,24 @@ describe('malformed replay and listener lifecycle', () => { }) const firstFiber = await ctx.plugin(TokenMeterService) activeMeter = ctx.tokenMeter - const session = ctx.sessions.create(SessionId('listener-order')) + const session = ctx.sessions.create(SessionId('listener-order'), { seed: [{ + type: 'turn/start', + seq: 0, + time: 1, + data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }, + }] }) activeMeter.measure(session) session.append('user/message', { content: [{ type: 'text', text: 'one' }], source: { kind: 'user' }, }, { surfaceOp: 'append' }) - expect(revisions).toEqual([1]) - expect(activeMeter.measure(session).logRevision).toBe(1) + expect(revisions).toEqual([2]) + expect(activeMeter.measure(session).logRevision).toBe(2) await firstFiber.dispose() const secondFiber = await ctx.plugin(TokenMeterService) activeMeter = ctx.tokenMeter - expect(activeMeter.measure(session).logRevision).toBe(1) + expect(activeMeter.measure(session).logRevision).toBe(2) await secondFiber.dispose() }) }) diff --git a/packages/llm/token-meter/tsconfig.json b/packages/llm/token-meter/tsconfig.json index 5e1604e02f..481fad6e15 100644 --- a/packages/llm/token-meter/tsconfig.json +++ b/packages/llm/token-meter/tsconfig.json @@ -22,6 +22,9 @@ }, { "path": "../../core/session" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/mcp/mcp-client/package.json b/packages/mcp/mcp-client/package.json index 6b8f145108..cb3b51e1aa 100644 --- a/packages/mcp/mcp-client/package.json +++ b/packages/mcp/mcp-client/package.json @@ -11,19 +11,25 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { - "@deepseek-ai/dsh-tools": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", + "@deepseek-ai/dsh-tools": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "dependencies": { @@ -31,8 +37,9 @@ "schemastery": "^3.18.0" }, "devDependencies": { - "@deepseek-ai/dsh-tools": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", + "@deepseek-ai/dsh-tools": "workspace:^", "@modelcontextprotocol/server-everything": "^2026.7.4", "@modelcontextprotocol/server-filesystem": "^2026.7.4", "cordis": "^4.0.0-rc.7", diff --git a/packages/mcp/mcp-client/src/invariant.ts b/packages/mcp/mcp-client/src/invariant.ts new file mode 100644 index 0000000000..d9e75e9955 --- /dev/null +++ b/packages/mcp/mcp-client/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-mcp-client' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/mcp/mcp-client/tsconfig.json b/packages/mcp/mcp-client/tsconfig.json index e9c9266415..668ee2c3cb 100644 --- a/packages/mcp/mcp-client/tsconfig.json +++ b/packages/mcp/mcp-client/tsconfig.json @@ -6,10 +6,23 @@ }, "include": ["src"], "references": [ - { "path": "../../../vendor/cosmokit" }, - { "path": "../../../vendor/cordis" }, - { "path": "../../../vendor/schemastery" }, - { "path": "../../llm/llm" }, - { "path": "../../core/tools" } + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../../vendor/schemastery" + }, + { + "path": "../../llm/llm" + }, + { + "path": "../../core/tools" + }, + { + "path": "../../support/invariants" + } ] } diff --git a/packages/sandbox/sandbox-local/package.json b/packages/sandbox/sandbox-local/package.json index 9dd90a3e9a..6750b92577 100644 --- a/packages/sandbox/sandbox-local/package.json +++ b/packages/sandbox/sandbox-local/package.json @@ -11,17 +11,23 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-sandbox": "^0.0.1", "cordis": "^4.0.0-rc.7" @@ -31,6 +37,7 @@ "schemastery": "^3.18.0" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-sandbox": "workspace:^", "cordis": "^4.0.0-rc.7" diff --git a/packages/sandbox/sandbox-local/src/invariant.ts b/packages/sandbox/sandbox-local/src/invariant.ts new file mode 100644 index 0000000000..3582962f94 --- /dev/null +++ b/packages/sandbox/sandbox-local/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-sandbox-local' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/sandbox/sandbox-local/tsconfig.json b/packages/sandbox/sandbox-local/tsconfig.json index c756b6af69..608f0e9568 100644 --- a/packages/sandbox/sandbox-local/tsconfig.json +++ b/packages/sandbox/sandbox-local/tsconfig.json @@ -22,6 +22,9 @@ }, { "path": "../sandbox" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/sandbox/sandbox/package.json b/packages/sandbox/sandbox/package.json index 50c5b443ba..84266499b3 100644 --- a/packages/sandbox/sandbox/package.json +++ b/packages/sandbox/sandbox/package.json @@ -11,21 +11,28 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "cordis": "^4.0.0-rc.7" } diff --git a/packages/sandbox/sandbox/src/invariant.ts b/packages/sandbox/sandbox/src/invariant.ts new file mode 100644 index 0000000000..b220f11717 --- /dev/null +++ b/packages/sandbox/sandbox/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-sandbox' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/sandbox/sandbox/tsconfig.json b/packages/sandbox/sandbox/tsconfig.json index 9f687793d7..af4de1c016 100644 --- a/packages/sandbox/sandbox/tsconfig.json +++ b/packages/sandbox/sandbox/tsconfig.json @@ -16,6 +16,9 @@ }, { "path": "../../llm/llm" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/sdk/create-sdk/package.json b/packages/sdk/create-sdk/package.json index e23415f8dc..27102154fc 100644 --- a/packages/sdk/create-sdk/package.json +++ b/packages/sdk/create-sdk/package.json @@ -13,10 +13,15 @@ ".": { "types": "./lib/types/index.d.ts", "default": "./lib/index.js" + }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" } }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/bin.js", "lib/assets", "lib/types/**/*.d.ts", @@ -29,9 +34,11 @@ "commander": "^15.0.0" }, "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/sdk/create-sdk/src/invariant.ts b/packages/sdk/create-sdk/src/invariant.ts new file mode 100644 index 0000000000..87a697e438 --- /dev/null +++ b/packages/sdk/create-sdk/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/create-sdk' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/sdk/create-sdk/tsconfig.json b/packages/sdk/create-sdk/tsconfig.json index f77f711880..e2ed951fa4 100644 --- a/packages/sdk/create-sdk/tsconfig.json +++ b/packages/sdk/create-sdk/tsconfig.json @@ -6,7 +6,14 @@ }, "include": ["src"], "references": [ - { "path": "../helper" }, - { "path": "../../../vendor/cordis" } + { + "path": "../helper" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../support/invariants" + } ] } diff --git a/packages/sdk/create-sdk/tsdown.config.ts b/packages/sdk/create-sdk/tsdown.config.ts index cdf9ef6d4f..08d522590e 100644 --- a/packages/sdk/create-sdk/tsdown.config.ts +++ b/packages/sdk/create-sdk/tsdown.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from 'tsdown' /** Bundle the library and create bin, then mirror package-owned terminal templates. */ export default defineConfig({ - entry: ['lib/types/index.js', 'lib/types/bin.js'], + entry: ['lib/types/index.js', 'lib/types/invariant.js', 'lib/types/bin.js'], outDir: 'lib', format: ['esm'], platform: 'node', diff --git a/packages/sdk/helper/package.json b/packages/sdk/helper/package.json index 8d95dafe66..1d9c6a2be2 100644 --- a/packages/sdk/helper/package.json +++ b/packages/sdk/helper/package.json @@ -10,10 +10,15 @@ ".": { "types": "./lib/types/index.d.ts", "default": "./lib/index.js" + }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" } }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/assets", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", @@ -29,12 +34,14 @@ }, "peerDependencies": { "@deepseek-ai/dsh-brand": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-brand": "workspace:^", "@deepseek-ai/dsh-hooks-claude": "workspace:^", "@deepseek-ai/dsh-hooks-codex": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^", "@deepseek-ai/dsh-session-persistence-sqlite": "workspace:^", "@deepseek-ai/dsh-tool-subagent": "workspace:^", diff --git a/packages/sdk/helper/src/invariant.ts b/packages/sdk/helper/src/invariant.ts new file mode 100644 index 0000000000..63a6fc2055 --- /dev/null +++ b/packages/sdk/helper/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-helper' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/sdk/helper/tsconfig.json b/packages/sdk/helper/tsconfig.json index e4a8604575..18e79898c7 100644 --- a/packages/sdk/helper/tsconfig.json +++ b/packages/sdk/helper/tsconfig.json @@ -6,14 +6,35 @@ }, "include": ["src"], "references": [ - { "path": "../../util/brand" }, - { "path": "../../compact/compact-basic" }, - { "path": "../../hooks/hooks-claude" }, - { "path": "../../hooks/hooks-codex" }, - { "path": "../../session-persistence/session-persistence-jsonl" }, - { "path": "../../session-persistence/session-persistence-sqlite" }, - { "path": "../../subagent/tool-subagent" }, - { "path": "../../web/tool-web" }, - { "path": "../../../vendor/cordis" } + { + "path": "../../util/brand" + }, + { + "path": "../../compact/compact-basic" + }, + { + "path": "../../hooks/hooks-claude" + }, + { + "path": "../../hooks/hooks-codex" + }, + { + "path": "../../session-persistence/session-persistence-jsonl" + }, + { + "path": "../../session-persistence/session-persistence-sqlite" + }, + { + "path": "../../subagent/tool-subagent" + }, + { + "path": "../../web/tool-web" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../support/invariants" + } ] } diff --git a/packages/sdk/helper/tsdown.config.ts b/packages/sdk/helper/tsdown.config.ts index cb8b9fafef..b8ba9cb652 100644 --- a/packages/sdk/helper/tsdown.config.ts +++ b/packages/sdk/helper/tsdown.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from 'tsdown' /** Bundle helper runtime and mirror template assets beside the bundle. */ export default defineConfig({ - entry: ['lib/types/index.js'], + entry: ['lib/types/index.js', 'lib/types/invariant.js'], outDir: 'lib', format: ['esm'], platform: 'node', diff --git a/packages/sdk/scripts/package.json b/packages/sdk/scripts/package.json index ba441a528c..3f607f7849 100644 --- a/packages/sdk/scripts/package.json +++ b/packages/sdk/scripts/package.json @@ -14,6 +14,10 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./dev/tsdown-config": { "types": "./lib/types/dev/tsdown-config.d.ts", "default": "./lib/dev/tsdown-config.js" @@ -21,6 +25,7 @@ }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/bin.js", "lib/dev/tsdown-config.js", "lib/local-plugin-loader-hooks.js", @@ -37,16 +42,22 @@ }, "peerDependencies": { "@deepseek-ai/dsh-app-boot": "workspace:^", + "@deepseek-ai/dsh-invariants": "^0.0.1", "cordis": "^4.0.0-rc.7", "tsdown": "^0.22.2", "tsx": "^4.22.4" }, "peerDependenciesMeta": { - "tsdown": { "optional": true }, - "tsx": { "optional": true } + "tsdown": { + "optional": true + }, + "tsx": { + "optional": true + } }, "devDependencies": { "@deepseek-ai/dsh-app-boot": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "cordis": "^4.0.0-rc.7", "tsdown": "^0.22.2", "tsx": "^4.22.4" diff --git a/packages/sdk/scripts/src/invariant.ts b/packages/sdk/scripts/src/invariant.ts new file mode 100644 index 0000000000..fd0a0ef55b --- /dev/null +++ b/packages/sdk/scripts/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-scripts' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/sdk/scripts/tsconfig.json b/packages/sdk/scripts/tsconfig.json index 848de9a314..6ada45e1ac 100644 --- a/packages/sdk/scripts/tsconfig.json +++ b/packages/sdk/scripts/tsconfig.json @@ -6,8 +6,17 @@ }, "include": ["src"], "references": [ - { "path": "../helper" }, - { "path": "../../ui/app-boot" }, - { "path": "../../../vendor/cordis" } + { + "path": "../helper" + }, + { + "path": "../../ui/app-boot" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../support/invariants" + } ] } diff --git a/packages/sdk/scripts/tsdown.config.ts b/packages/sdk/scripts/tsdown.config.ts index 2218faa7be..14bb59b8bc 100644 --- a/packages/sdk/scripts/tsdown.config.ts +++ b/packages/sdk/scripts/tsdown.config.ts @@ -7,6 +7,10 @@ export default defineConfig([ fixedExtension: false, outputOptions: { codeSplitting: false }, dts: false, clean: false, copy: [{ from: 'src/templates/assets/*', to: 'lib/assets' }], }, + { + entry: ['lib/types/invariant.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', + fixedExtension: false, outputOptions: { codeSplitting: false }, dts: false, clean: false, + }, { entry: ['lib/types/bin.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', fixedExtension: false, outputOptions: { codeSplitting: false }, dts: false, clean: false, diff --git a/packages/session-persistence/session-persistence-jsonl/package.json b/packages/session-persistence/session-persistence-jsonl/package.json index ddb9f2af4d..bc5c6d2c4c 100644 --- a/packages/session-persistence/session-persistence-jsonl/package.json +++ b/packages/session-persistence/session-persistence-jsonl/package.json @@ -11,17 +11,23 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-session-persistence": "^0.0.1", "cordis": "^4.0.0-rc.7" @@ -30,6 +36,7 @@ "schemastery": "^3.18.0" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-persistence": "workspace:^", "cordis": "^4.0.0-rc.7" diff --git a/packages/session-persistence/session-persistence-jsonl/src/invariant.ts b/packages/session-persistence/session-persistence-jsonl/src/invariant.ts new file mode 100644 index 0000000000..12c65db1c4 --- /dev/null +++ b/packages/session-persistence/session-persistence-jsonl/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + * @module @deepseek-ai/dsh-session-persistence-jsonl/invariant + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-session-persistence-jsonl' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/session-persistence/session-persistence-jsonl/tests/jsonl.spec.ts b/packages/session-persistence/session-persistence-jsonl/tests/jsonl.spec.ts index e7dc469132..710a0ec9f1 100644 --- a/packages/session-persistence/session-persistence-jsonl/tests/jsonl.spec.ts +++ b/packages/session-persistence/session-persistence-jsonl/tests/jsonl.spec.ts @@ -21,17 +21,15 @@ function mutableHeader(header: SessionHeader): MutableSessionHeader { return header } -async function expectParallelFlushError(promise: Promise, message: RegExp): Promise { +async function expectFlushError(promise: Promise, message: RegExp): Promise { try { await promise } catch (error) { - expect(error).toBeInstanceOf(AggregateError) - const [cause] = (error as AggregateError).errors as unknown[] - expect(cause).toBeInstanceOf(Error) - expect((cause as Error).message).toMatch(message) + expect(error).toBeInstanceOf(Error) + expect((error as Error).message).toMatch(message) return } - throw new Error('expected parallel flush to reject') + throw new Error('expected flush to reject') } async function freshRoot(): Promise { @@ -250,7 +248,7 @@ describe('SessionPersistenceJsonl: durability and crash semantics', () => { appendClosedTurn(source) const child = ctx.sessions.fork(source, undefined, SessionId('persist-child')) - await ctx.parallel('session/flush', child) + await ctx.sessions.flush(child) const loaded = await ctx.sessionPersistence.load(child.id) expect(loaded.events).toEqual(source.events) @@ -429,12 +427,14 @@ describe('SessionPersistenceJsonl: write path (session/event → flush)', () => const a = ctx.sessions.create(SessionId('sa')) const b = ctx.sessions.create(SessionId('sb')) + a.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) + b.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) a.append('user/message', { content: [{ type: 'text', text: 'A' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) b.append('user/message', { content: [{ type: 'text', text: 'B' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) a.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) b.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) - await ctx.parallel('session/flush', a) - await ctx.parallel('session/flush', b) + await ctx.sessions.flush(a) + await ctx.sessions.flush(b) const la = await ctx.sessionPersistence.load(SessionId('sa')) const lb = await ctx.sessionPersistence.load(SessionId('sb')) @@ -607,7 +607,7 @@ describe('SessionPersistenceJsonl: edge cases', () => { }, { inject: ['sessions'] })) // Drain A, then dispose ITS fiber (the live session A is gone) while the // backend stays loaded. - for (const s of ctx.sessions.list()) await ctx.parallel('session/flush', s) + for (const s of ctx.sessions.list()) await ctx.sessions.flush(s) await sessFiberA.dispose() // A new Session object reuses the id. Object-keyed initialization must run independently, @@ -675,7 +675,7 @@ describe('SessionPersistenceJsonl: edge cases', () => { a.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) a.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) }, { inject: ['sessions'] })) - for (const s of ctx.sessions.list()) await ctx.parallel('session/flush', s) + for (const s of ctx.sessions.list()) await ctx.sessions.flush(s) await firstFiber.dispose() let second!: Session @@ -783,18 +783,19 @@ describe('SessionPersistenceJsonl: edge cases', () => { await ctx2.plugin(SessionPersistenceJsonl, { root }) const session = ctx2.sessions.create(SessionId('flush-fail')) // A full turn lands in the write-behind buffer. + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) session.append('user/message', { content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) // Make the durable materialize fail on the next flush. const backend = ctx2.sessionPersistence as unknown as { materialize: (...args: unknown[]) => Promise } const origMat = backend.materialize.bind(backend) backend.materialize = () => Promise.reject(new Error('disk full')) - await expectParallelFlushError(ctx2.parallel('session/flush', session), /disk full/) + await expectFlushError(ctx2.sessions.flush(session), /disk full/) // The events are STILL buffered (not silently dropped): a retry persists them. backend.materialize = origMat - await ctx2.parallel('session/flush', session) + await ctx2.sessions.flush(session) const loaded = await ctx2.sessionPersistence.load(SessionId('flush-fail')) - expect(loaded.events.map(e => e.seq)).toEqual([0, 1]) + expect(loaded.events.map(e => e.seq)).toEqual([0, 1, 2]) await ctx2.fiber.dispose() }) diff --git a/packages/session-persistence/session-persistence-jsonl/tsconfig.json b/packages/session-persistence/session-persistence-jsonl/tsconfig.json index 23970f5a57..044156938b 100644 --- a/packages/session-persistence/session-persistence-jsonl/tsconfig.json +++ b/packages/session-persistence/session-persistence-jsonl/tsconfig.json @@ -22,6 +22,9 @@ }, { "path": "../../session-persistence/session-persistence" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/session-persistence/session-persistence-sqlite/package.json b/packages/session-persistence/session-persistence-sqlite/package.json index f367b737b3..f65bdebd16 100644 --- a/packages/session-persistence/session-persistence-sqlite/package.json +++ b/packages/session-persistence/session-persistence-sqlite/package.json @@ -11,17 +11,23 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-session-persistence": "^0.0.1", "cordis": "^4.0.0-rc.7" @@ -30,6 +36,7 @@ "schemastery": "^3.18.0" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-persistence": "workspace:^", "cordis": "^4.0.0-rc.7" diff --git a/packages/session-persistence/session-persistence-sqlite/src/invariant.ts b/packages/session-persistence/session-persistence-sqlite/src/invariant.ts new file mode 100644 index 0000000000..a9b04cf5f5 --- /dev/null +++ b/packages/session-persistence/session-persistence-sqlite/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + * @module @deepseek-ai/dsh-session-persistence-sqlite/invariant + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-session-persistence-sqlite' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/session-persistence/session-persistence-sqlite/tests/sqlite.spec.ts b/packages/session-persistence/session-persistence-sqlite/tests/sqlite.spec.ts index f26edfa54d..ee599987bb 100644 --- a/packages/session-persistence/session-persistence-sqlite/tests/sqlite.spec.ts +++ b/packages/session-persistence/session-persistence-sqlite/tests/sqlite.spec.ts @@ -14,17 +14,15 @@ import { runCoordinatorContract, type CoordinatorFixture } from '../../session-p const dirs: string[] = [] afterEach(async () => { for (const d of dirs.splice(0)) await rm(d, { recursive: true, force: true }) }) -async function expectParallelFlushError(promise: Promise, message: RegExp): Promise { +async function expectFlushError(promise: Promise, message: RegExp): Promise { try { await promise } catch (error) { - expect(error).toBeInstanceOf(AggregateError) - const [cause] = (error as AggregateError).errors as unknown[] - expect(cause).toBeInstanceOf(Error) - expect((cause as Error).message).toMatch(message) + expect(error).toBeInstanceOf(Error) + expect((error as Error).message).toMatch(message) return } - throw new Error('expected parallel flush to reject') + throw new Error('expected flush to reject') } async function freshDbPath(): Promise { @@ -502,7 +500,7 @@ describe('SessionPersistenceSqlite: edge cases', () => { const b1 = await backend(path) const s1 = b1.ctx.sessions.create(SessionId('hmr-collide')) appendLog(s1, oneTurnLog()) - await b1.ctx.parallel('session/flush', s1) + await b1.ctx.sessions.flush(s1) await b1.dispose() // A fresh context with an UNRELATED live session reusing the id meets a @@ -513,9 +511,9 @@ describe('SessionPersistenceSqlite: edge cases', () => { await ctx.plugin(Object.assign((inner: Context) => { session = inner.sessions.create(SessionId('hmr-collide')) }, { inject: ['sessions'] })) - session.append('turn/start', { turn: 9, trigger: { kind: 'message', source: { kind: 'user' } } }) + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) await ctx.plugin(SessionPersistenceSqlite, { path }) - await expectParallelFlushError(ctx.parallel('session/flush', session), /id collision/) + await expectFlushError(ctx.sessions.flush(session), /id collision/) await ctx.fiber.dispose() }) }) @@ -567,18 +565,20 @@ describe('surface field round-trip', () => { const fiber = await ctx.plugin(SessionPersistenceSqlite, { path: ':memory:' }) const session = ctx.sessions.create(SessionId('roundtrip-surface')) session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) + session.append('step/start', { turn: 1, step: 1 }) session.append('user/message', { content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) - session.append('assistant/message', { provenance: { provider: 'mock', model: 'mock' }, turn: 1, step: 1, content: [] }, { surfaceOp: 'append', sourceEventSeqs: [0] }) + session.append('assistant/message', { provenance: { provider: 'mock', model: 'mock' }, turn: 1, step: 1, content: [] }, { surfaceOp: 'append', sourceEventSeqs: [2] }) + session.append('step/end', { turn: 1, step: 1 }) session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) - await ctx.parallel('session/flush', session) + await ctx.sessions.flush(session) const loaded = await ctx.sessionPersistence.load(SessionId('roundtrip-surface')) - expect(loaded.events).toHaveLength(4) - const um = loaded.events[1]! + expect(loaded.events).toHaveLength(6) + const um = loaded.events[2]! expect((um as SurfaceEvent).surfaceOp).toBe('append') expect((um as SurfaceEvent).sourceEventSeqs).toBeUndefined() - const am = loaded.events[2]! + const am = loaded.events[3]! expect((am as SurfaceEvent).surfaceOp).toBe('append') - expect((am as SurfaceEvent).sourceEventSeqs).toEqual([0]) + expect((am as SurfaceEvent).sourceEventSeqs).toEqual([2]) await fiber.dispose() }) @@ -590,7 +590,7 @@ describe('surface field round-trip', () => { session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) session.append('steering/message', { turn: 1, content: [], source: { kind: 'user' } }, { surfaceOp: 'append' }) session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) - await ctx.parallel('session/flush', session) + await ctx.sessions.flush(session) const loaded = await ctx.sessionPersistence.load(SessionId('surface-noseq')) expect((loaded.events[1]! as SurfaceEvent).surfaceOp).toBe('append') expect((loaded.events[1]! as SurfaceEvent).sourceEventSeqs).toBeUndefined() diff --git a/packages/session-persistence/session-persistence-sqlite/tsconfig.json b/packages/session-persistence/session-persistence-sqlite/tsconfig.json index 23970f5a57..044156938b 100644 --- a/packages/session-persistence/session-persistence-sqlite/tsconfig.json +++ b/packages/session-persistence/session-persistence-sqlite/tsconfig.json @@ -22,6 +22,9 @@ }, { "path": "../../session-persistence/session-persistence" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/session-persistence/session-persistence/package.json b/packages/session-persistence/session-persistence/package.json index 91eef09007..a503b146ae 100644 --- a/packages/session-persistence/session-persistence/package.json +++ b/packages/session-persistence/session-persistence/package.json @@ -11,21 +11,29 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-scope": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "cordis": "^4.0.0-rc.7" } diff --git a/packages/session-persistence/session-persistence/src/invariant.ts b/packages/session-persistence/session-persistence/src/invariant.ts new file mode 100644 index 0000000000..cc7cc8fa2c --- /dev/null +++ b/packages/session-persistence/session-persistence/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + * @module @deepseek-ai/dsh-session-persistence/invariant + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-session-persistence' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/session-persistence/session-persistence/tests/coordinator-contract.ts b/packages/session-persistence/session-persistence/tests/coordinator-contract.ts index defcaefb6d..47867c2f56 100644 --- a/packages/session-persistence/session-persistence/tests/coordinator-contract.ts +++ b/packages/session-persistence/session-persistence/tests/coordinator-contract.ts @@ -11,6 +11,7 @@ import { describe, expect, it, vi } from 'vitest' import { Context, type Fiber } from 'cordis' +import { scopeTarget } from '@deepseek-ai/dsh-scope' import SessionStore, { SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/dsh-session' import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' import { meta, oneTurnLog, appendLog } from './contract.ts' @@ -76,7 +77,7 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise< try { const session = ctx.sessions.create(SessionId('live'), { meta: { cwd: WORK } }) send(session, oneTurnLog()) - await ctx.parallel('session/flush', session) + await ctx.sessions.flush(session) const loaded = await ctx.sessionPersistence.load(SessionId('live')) expect(loaded.events).toHaveLength(6) @@ -96,7 +97,7 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise< try { const session = ctx.sessions.create(SessionId('forked-child'), { meta: { cwd: WORK, seedLength: 3 } }) send(session, oneTurnLog()) - await ctx.parallel('session/flush', session) + await ctx.sessions.flush(session) const loaded = await ctx.sessionPersistence.load(SessionId('forked-child')) expect(loaded.meta.seedLength).toBe(3) @@ -111,17 +112,17 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise< const { ctx, fiber } = await freshCtx(fix) try { const session = ctx.sessions.create(SessionId('mutate'), { meta: { cwd: WORK } }) + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) const ev = session.append('user/message', { content: [{ type: 'text', text: 'original' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) expect(() => { ;(ev.data as { content: { type: 'text'; text: string }[] }).content[0]!.text = 'HACKED' }).toThrow(TypeError) - session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) - await ctx.parallel('session/flush', session) + await ctx.sessions.flush(session) const loaded = await ctx.sessionPersistence.load(SessionId('mutate')) - const first = loaded.events[0] - expect(first?.type === 'user/message' && (first.data.content[0] as { text: string }).text).toBe('original') + const message = loaded.events.find(event => event.type === 'user/message') + expect(message?.type === 'user/message' && (message.data.content[0] as { text: string }).text).toBe('original') } finally { await fiber.dispose() await fix.cleanup() @@ -166,7 +167,7 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise< const loaded = await ctx.sessionPersistence.load(SessionId('forked')) expect(loaded.events).toEqual(seed) // A flush with no NEW events must not double-write. - await ctx.parallel('session/flush', forked) + await ctx.sessions.flush(forked) const reloaded = await ctx.sessionPersistence.load(SessionId('forked')) expect(reloaded.events).toEqual(seed) } finally { @@ -182,7 +183,7 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise< try { const s1 = first.ctx.sessions.create(SessionId('resumed'), { meta: { cwd: WORK } }) send(s1, oneTurnLog()) - await first.ctx.parallel('session/flush', s1) + await first.ctx.sessions.flush(s1) } finally { await first.fiber.dispose() } @@ -194,7 +195,7 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise< await second.ctx.sessions.flush(s2) // let onCreated adopt s2.append('turn/start', { turn: 2, trigger: { kind: 'message', source: { kind: 'user' } } }) s2.append('turn/end', { turn: 2, reason: { kind: 'completed' } }) - await second.ctx.parallel('session/flush', s2) + await second.ctx.sessions.flush(s2) const reloaded = await second.ctx.sessionPersistence.load(SessionId('resumed')) expect(reloaded.events.map(e => e.seq)).toEqual([0, 1, 2, 3, 4, 5, 6, 7]) @@ -212,13 +213,14 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise< await ctx.plugin(SessionStore) // A session exists BEFORE the persistence plugin is applied. const session = ctx.sessions.create(SessionId('pre-existing'), { meta: { cwd: WORK } }) + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) session.append('user/message', { content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) const fiber = await fix.mount(ctx) try { // The plugin seeded it on apply; a subsequent flush persists its events. - await ctx.parallel('session/flush', session) + await ctx.sessions.flush(session) const loaded = await ctx.sessionPersistence.load(SessionId('pre-existing')) expect(loaded.events.length).toBeGreaterThanOrEqual(2) } finally { @@ -233,6 +235,7 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise< await ctx.plugin(SessionStore) const fiber = await fix.mount(ctx) const session = await liveSessionInFiber(ctx, 'drain', WORK) + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) session.append('user/message', { content: [{ type: 'text', text: 'buffered' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) // No explicit flush — dispose must drain. @@ -261,7 +264,7 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise< session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) session.append('user/message', { content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) - await ctx.parallel('session/flush', session) + await ctx.sessions.flush(session) // Hot-reload: dispose instance 1, mount instance 2 over the same storage while the // session stays live. The new instance has no coordinator state but must adopt the @@ -271,7 +274,7 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise< session.append('turn/start', { turn: 2, trigger: { kind: 'message', source: { kind: 'user' } } }) session.append('user/message', { content: [{ type: 'text', text: 'again' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) session.append('turn/end', { turn: 2, reason: { kind: 'completed' } }) - await expect(ctx.parallel('session/flush', session)).resolves.not.toThrow() + await expect(ctx.sessions.flush(session)).resolves.not.toThrow() const loaded = await ctx.sessionPersistence.load(SessionId('hmr-adopt')) expect(loaded.events.filter(e => e.type === 'turn/start')).toHaveLength(2) @@ -291,7 +294,7 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise< const backend1 = await fix.mount(ctx) session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) - await ctx.parallel('session/flush', session) + await ctx.sessions.flush(session) // Append turn 2 to the LIVE session, then dispose instance 1 WITHOUT // flushing turn 2: it is now ONLY in the live session's events; the new @@ -303,7 +306,7 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise< // Instance 2 adopts the stored prefix (turn 1) and MUST also persist the // live suffix (turn 2) carried in the session's events. await fix.mount(ctx) - await ctx.parallel('session/flush', session) + await ctx.sessions.flush(session) const loaded = await ctx.sessionPersistence.load(SessionId('hmr-suffix')) expect(loaded.events.map(e => e.seq)).toEqual([0, 1, 2, 3]) expect(loaded.events.filter(e => e.type === 'turn/start')).toHaveLength(2) @@ -322,7 +325,7 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise< const first = await fix.mount(ctx) session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) session.append('step/start', { turn: 1, step: 1 }) - await ctx.parallel('session/flush', session) + await ctx.sessions.flush(session) // Crash-tail a torn fragment past the (open) committed turn, then reload. await first.dispose() @@ -332,7 +335,7 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise< // end. Adoption must truncate the torn tail but NOT synthesize closers. session.append('step/end', { turn: 1, step: 1 }) session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) - await ctx.parallel('session/flush', session) + await ctx.sessions.flush(session) const loaded = await ctx.sessionPersistence.load(SessionId('hmr-open')) expect(loaded.events.map(e => e.type)).toEqual(['turn/start', 'step/start', 'step/end', 'turn/end']) @@ -352,7 +355,7 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise< try { const s1 = first.ctx.sessions.create(SessionId('collide'), { meta: { cwd: WORK } }) send(s1, oneTurnLog()) - await first.ctx.parallel('session/flush', s1) + await first.ctx.sessions.flush(s1) } finally { await first.fiber.dispose() } @@ -392,7 +395,7 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise< await expect(ctx.sessions.flush(reuse)).resolves.toBeUndefined() reuse.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) reuse.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) - await ctx.parallel('session/flush', reuse) + await ctx.sessions.flush(reuse) const loaded = await ctx.sessionPersistence.load(SessionId('abandoned')) expect(loaded.events.map(e => e.seq)).toEqual([0, 1]) } finally { @@ -438,14 +441,15 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise< const { ctx, fiber } = await freshCtx(fix) try { const session = ctx.sessions.create(SessionId('idem'), { meta: { cwd: WORK } }) + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) session.append('user/message', { content: [{ type: 'text', text: 'x' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) - await ctx.parallel('session/flush', session) + await ctx.sessions.flush(session) // Re-emit session/created for the SAME live session (idempotent initFor). - ctx.emit('session/created', session) - await ctx.parallel('session/flush', session) + ctx.emit(scopeTarget(session, undefined), 'session/created', session) + await ctx.sessions.flush(session) const loaded = await ctx.sessionPersistence.load(SessionId('idem')) - expect(loaded.events).toHaveLength(2) // not doubled + expect(loaded.events).toHaveLength(3) // not doubled } finally { await fiber.dispose() await fix.cleanup() @@ -690,11 +694,12 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise< // async onCreated init has necessarily set state (exercises the // state-undefined cursor path). const session = ctx.sessions.create(SessionId('flush-nostate'), { meta: { cwd: WORK } }) + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) session.append('user/message', { content: [{ type: 'text', text: 'q' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) - await ctx.parallel('session/flush', session) + await ctx.sessions.flush(session) const loaded = await ctx.sessionPersistence.load(SessionId('flush-nostate')) - expect(loaded.events).toHaveLength(2) + expect(loaded.events).toHaveLength(3) } finally { await fiber.dispose() await fix.cleanup() diff --git a/packages/session-persistence/session-persistence/tsconfig.json b/packages/session-persistence/session-persistence/tsconfig.json index e817086a6a..cbd74a19e7 100644 --- a/packages/session-persistence/session-persistence/tsconfig.json +++ b/packages/session-persistence/session-persistence/tsconfig.json @@ -16,6 +16,9 @@ }, { "path": "../../core/session" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/session-query/session-query/package.json b/packages/session-query/session-query/package.json index ae2767598a..040697391f 100644 --- a/packages/session-query/session-query/package.json +++ b/packages/session-query/session-query/package.json @@ -11,17 +11,23 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-session-persistence": "^0.0.1", @@ -36,6 +42,7 @@ "schemastery": "^3.18.0" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-persistence": "workspace:^", diff --git a/packages/session-query/session-query/src/invariant.ts b/packages/session-query/session-query/src/invariant.ts new file mode 100644 index 0000000000..91bcee721e --- /dev/null +++ b/packages/session-query/session-query/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-session-query' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/session-query/session-query/tests/session-query.spec.ts b/packages/session-query/session-query/tests/session-query.spec.ts index f532edf168..658eb709d9 100644 --- a/packages/session-query/session-query/tests/session-query.spec.ts +++ b/packages/session-query/session-query/tests/session-query.spec.ts @@ -101,6 +101,8 @@ describe('session-query exact reads', () => { it('classifies current, shadowed, and raw-log-only events through foldSurface', async () => { const ctx = await liveContext() const session = ctx.sessions.create(SessionId('surface')) + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) + session.append('step/start', { turn: 1, step: 1 }) const first = session.append( 'user/message', { content: [{ type: 'text', text: 'first' }], source: { kind: 'user' } }, @@ -117,13 +119,14 @@ describe('session-query exact reads', () => { { surfaceOp: { op: 'replace', start: first.seq, end: first.seq }, sourceEventSeqs: [first.seq] }, ) - expect((await ctx.sessionQuery.listEvents(session.id)).map(record => record.surface)) + expect((await ctx.sessionQuery.listEvents(session.id)).slice(2).map(record => record.surface)) .toEqual(['shadowed', 'log-only', 'current']) }) it('returns a bounded detached raw-event window and validates the request', async () => { const ctx = await liveContext({ readWindowMax: 1 }) const session = ctx.sessions.create(SessionId('window'), { meta: { cwd: '/work' } }) + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) for (const text of ['one', 'two', 'three']) { session.append( 'user/message', @@ -132,14 +135,14 @@ describe('session-query exact reads', () => { ) } - const result = await ctx.sessionQuery.readEvent({ sessionId: session.id, seq: 1, before: 1, after: 1 }) - expect([result.startSeq, result.endSeq, result.target.seq]).toEqual([0, 2, 1]) + const result = await ctx.sessionQuery.readEvent({ sessionId: session.id, seq: 2, before: 1, after: 1 }) + expect([result.startSeq, result.endSeq, result.target.seq]).toEqual([1, 3, 2]) expect(result.session).toEqual(session.header) Object.assign(result.session, { createdAt: -1 }) if (result.events[0]?.type !== 'user/message') throw new Error('expected user message') result.events[0].data.content = [] expect(session.header.createdAt).not.toBe(-1) - expect(session.events[0]?.type === 'user/message' && session.events[0].data.content).toHaveLength(1) + expect(session.events[1]?.type === 'user/message' && session.events[1].data.content).toHaveLength(1) await expect(ctx.sessionQuery.readEvent({ sessionId: session.id, seq: 9 })) .rejects.toThrow(expectCode('SESSION_QUERY_EVENT_NOT_FOUND')) @@ -161,6 +164,7 @@ describe('session-query exact reads', () => { ]) const ctx = await liveContext() const live = ctx.sessions.create(shared.id, { meta: { createdAt: 3, cwd: '/same' } }) + live.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) live.append( 'user/message', { content: [{ type: 'text', text: 'live' }], source: { kind: 'user' } }, @@ -170,7 +174,7 @@ describe('session-query exact reads', () => { expect((await ctx.sessionQuery.listSessions()).map(record => [record.header.id, record.live, record.persisted])) .toEqual([[shared.id, true, true], [durable.id, false, true]]) - const liveRead = await ctx.sessionQuery.readEvent({ sessionId: shared.id, seq: 0 }) + const liveRead = await ctx.sessionQuery.readEvent({ sessionId: shared.id, seq: 1 }) expect(liveRead.target.type === 'user/message' && liveRead.target.data.content[0]) .toMatchObject({ text: 'live' }) await expect(ctx.sessionQuery.readEvent({ sessionId: durable.id, seq: 0 })) @@ -189,6 +193,7 @@ describe('session-query exact reads', () => { TestPersistence.reset() const ctx = await liveContext() const live = ctx.sessions.create(SessionId('live')) + live.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) live.append( 'user/message', { content: [{ type: 'text', text: 'available' }], source: { kind: 'user' } }, @@ -198,8 +203,8 @@ describe('session-query exact reads', () => { TestPersistence.listFailure = new Error('list unavailable') TestPersistence.loadFailure = new Error('load unavailable') - await expect(ctx.sessionQuery.listEvents(live.id)).resolves.toHaveLength(1) - await expect(ctx.sessionQuery.readEvent({ sessionId: live.id, seq: 0 })).resolves.toMatchObject({ target: { seq: 0 } }) + await expect(ctx.sessionQuery.listEvents(live.id)).resolves.toHaveLength(2) + await expect(ctx.sessionQuery.readEvent({ sessionId: live.id, seq: 1 })).resolves.toMatchObject({ target: { seq: 1 } }) await expect(ctx.sessionQuery.listSessions()).rejects.toThrow(expectCode('SESSION_QUERY_PERSISTENCE_FAILED')) await expect(ctx.sessionQuery.listEvents(SessionId('durable'))).rejects.toThrow(expectCode('SESSION_QUERY_PERSISTENCE_FAILED')) }) @@ -228,19 +233,8 @@ describe('session-query exact reads', () => { .rejects.toThrow(expectCode('SESSION_QUERY_SOURCE_CONFLICT')) }) - it('turns malformed surfaces and direct invalid config into typed errors', async () => { + it('turns persisted malformed surfaces and direct invalid config into typed errors', async () => { const ctx = await liveContext() - const session = ctx.sessions.create(SessionId('bad-surface')) - ;(session as unknown as { log: SessionEvent[] }).log.push({ - type: 'assistant/message', - seq: 0, - time: 1, - data: { turn: 1, step: 1, content: [], provenance: { provider: 'mock', model: 'mock' } }, - surfaceOp: { op: 'replace', start: 9, end: 9 }, - }) - await expect(ctx.sessionQuery.listEvents(session.id)) - .rejects.toThrow(expectCode('SESSION_QUERY_INVALID_SURFACE')) - const persisted = header('bad-persisted-surface') TestPersistence.reset([{ meta: persisted, diff --git a/packages/session-query/session-query/tests/tracing.spec.ts b/packages/session-query/session-query/tests/tracing.spec.ts index 03e1adad89..ee8b1d833f 100644 --- a/packages/session-query/session-query/tests/tracing.spec.ts +++ b/packages/session-query/session-query/tests/tracing.spec.ts @@ -89,6 +89,8 @@ function expectCode(code: SessionQueryErrorCode): Error { } function appendTraceEvents(session: Session): void { + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) + session.append('step/start', { turn: 1, step: 1 }) session.append('assistant/chunk', { turn: 1, step: 1, @@ -97,22 +99,24 @@ function appendTraceEvents(session: Session): void { session.append( 'user/message', { content: [{ type: 'text', text: 'original' }], source: { kind: 'user' } }, - { surfaceOp: 'append', sourceEventSeqs: [0] }, + { surfaceOp: 'append', sourceEventSeqs: [2] }, ) session.append( 'assistant/message', { provenance: { provider: 'mock', model: 'mock' }, turn: 1, step: 1, content: [{ type: 'text', text: 'summary one' }] }, - { surfaceOp: { op: 'replace', start: 1, end: 1 }, sourceEventSeqs: [1, 0] }, + { surfaceOp: { op: 'replace', start: 3, end: 3 }, sourceEventSeqs: [3, 2] }, ) session.append( 'context/message', { content: [{ type: 'text', text: 'context' }], source: { kind: 'plugin', plugin: 'test' } }, { surfaceOp: 'append' }, ) + session.append('step/end', { turn: 1, step: 1 }) + session.append('step/start', { turn: 1, step: 2 }) session.append( 'assistant/message', { provenance: { provider: 'mock', model: 'mock' }, turn: 1, step: 2, content: [{ type: 'text', text: 'summary two' }] }, - { surfaceOp: { op: 'replace', start: 2, end: 2 }, sourceEventSeqs: [0, 2] }, + { surfaceOp: { op: 'replace', start: 4, end: 4 }, sourceEventSeqs: [2, 4] }, ) } @@ -235,40 +239,40 @@ describe('session event tracing', () => { const session = ctx.sessions.create(SessionId('trace')) appendTraceEvents(session) - const original = await ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 1 }) + const original = await ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 3 }) expect(original.target).toMatchObject({ sessionId: session.id, - seq: 1, + seq: 3, type: 'user/message', surface: 'shadowed', }) expect(original).toMatchObject({ - replacedBy: 2, - replacementChain: [2, 4], + replacedBy: 4, + replacementChain: [4, 8], replacedEventSeqs: [], - sourceEventSeqs: [0], - derivedEventSeqs: [2], + sourceEventSeqs: [2], + derivedEventSeqs: [4], }) - await expect(ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 2 })) + await expect(ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 4 })) .resolves.toMatchObject({ - replacedBy: 4, - replacementChain: [4], - replacedEventSeqs: [1], - sourceEventSeqs: [1, 0], - derivedEventSeqs: [4], + replacedBy: 8, + replacementChain: [8], + replacedEventSeqs: [3], + sourceEventSeqs: [3, 2], + derivedEventSeqs: [8], }) - await expect(ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 0 })) + await expect(ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 2 })) .resolves.toMatchObject({ target: { surface: 'log-only' }, replacementChain: [], sourceEventSeqs: [], - derivedEventSeqs: [1, 2, 4], + derivedEventSeqs: [3, 4, 8], }) - await expect(ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 4 })) + await expect(ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 8 })) .resolves.toMatchObject({ replacementChain: [], - replacedEventSeqs: [2], - sourceEventSeqs: [0, 2], + replacedEventSeqs: [4], + sourceEventSeqs: [2, 4], derivedEventSeqs: [], }) }) @@ -278,18 +282,18 @@ describe('session event tracing', () => { const session = ctx.sessions.create(SessionId('detached')) appendTraceEvents(session) - const first = await ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 2 }) + const first = await ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 4 }) first.target.time = -1 first.replacementChain.push(99) first.replacedEventSeqs.push(99) first.sourceEventSeqs.push(99) first.derivedEventSeqs.push(99) - const repeated = await ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 2 }) + const repeated = await ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 4 }) expect(repeated.target.time).not.toBe(-1) - expect(repeated.replacementChain).toEqual([4]) - expect(repeated.replacedEventSeqs).toEqual([1]) - expect(repeated.sourceEventSeqs).toEqual([1, 0]) - expect(repeated.derivedEventSeqs).toEqual([4]) + expect(repeated.replacementChain).toEqual([8]) + expect(repeated.replacedEventSeqs).toEqual([3]) + expect(repeated.sourceEventSeqs).toEqual([3, 2]) + expect(repeated.derivedEventSeqs).toEqual([8]) }) it('loads persisted logs once, prefers live logs, and preserves failures and conflicts', async () => { @@ -303,6 +307,7 @@ describe('session event tracing', () => { expect([TracePersistence.listCalls, TracePersistence.loadCalls]).toEqual([1, 1]) const live = ctx.sessions.create(durable.id, { meta: { createdAt: 1, cwd: '/same' } }) + live.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) live.append( 'context/message', { content: [{ type: 'text', text: 'live' }], source: { kind: 'plugin', plugin: 'test' } }, @@ -310,7 +315,7 @@ describe('session event tracing', () => { ) TracePersistence.listFailure = new Error('list unavailable') TracePersistence.loadFailure = new Error('load unavailable') - await expect(ctx.sessionQuery.traceEvent({ sessionId: durable.id, seq: 0 })) + await expect(ctx.sessionQuery.traceEvent({ sessionId: durable.id, seq: 1 })) .resolves.toMatchObject({ target: { type: 'context/message' } }) expect([TracePersistence.listCalls, TracePersistence.loadCalls]).toEqual([1, 1]) diff --git a/packages/session-query/session-query/tsconfig.json b/packages/session-query/session-query/tsconfig.json index 7153dae8bb..d7205907e1 100644 --- a/packages/session-query/session-query/tsconfig.json +++ b/packages/session-query/session-query/tsconfig.json @@ -25,6 +25,9 @@ }, { "path": "../../session-persistence/session-persistence" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/skill/skill-local/package.json b/packages/skill/skill-local/package.json index d490438c51..be7c7baae2 100644 --- a/packages/skill/skill-local/package.json +++ b/packages/skill/skill-local/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -24,6 +29,7 @@ "peerDependencies": { "@deepseek-ai/dsh-fs": "^0.0.1", "@deepseek-ai/dsh-home": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-skill": "^0.0.1", "cordis": "^4.0.0-rc.7" }, @@ -34,6 +40,7 @@ "devDependencies": { "@deepseek-ai/dsh-fs": "workspace:^", "@deepseek-ai/dsh-home": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-skill": "workspace:^", "cordis": "^4.0.0-rc.7" } diff --git a/packages/skill/skill-local/src/invariant.ts b/packages/skill/skill-local/src/invariant.ts new file mode 100644 index 0000000000..475d02bb8f --- /dev/null +++ b/packages/skill/skill-local/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-skill-local' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/skill/skill-local/tsconfig.json b/packages/skill/skill-local/tsconfig.json index f51147abce..d471e269d0 100644 --- a/packages/skill/skill-local/tsconfig.json +++ b/packages/skill/skill-local/tsconfig.json @@ -6,11 +6,26 @@ }, "include": ["src"], "references": [ - { "path": "../../../vendor/cosmokit" }, - { "path": "../../../vendor/cordis" }, - { "path": "../../../vendor/schemastery" }, - { "path": "../../util/home" }, - { "path": "../../fs/fs" }, - { "path": "../skill" } + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../../vendor/schemastery" + }, + { + "path": "../../util/home" + }, + { + "path": "../../fs/fs" + }, + { + "path": "../skill" + }, + { + "path": "../../support/invariants" + } ] } diff --git a/packages/skill/skill/package.json b/packages/skill/skill/package.json index c025de6ee9..3148a3f577 100644 --- a/packages/skill/skill/package.json +++ b/packages/skill/skill/package.json @@ -11,23 +11,30 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "dependencies": { "schemastery": "^3.18.0" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/skill/skill/src/invariant.ts b/packages/skill/skill/src/invariant.ts new file mode 100644 index 0000000000..c1d4091bb2 --- /dev/null +++ b/packages/skill/skill/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-skill' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/skill/skill/tsconfig.json b/packages/skill/skill/tsconfig.json index 1b1855dcc4..e882ed2d72 100644 --- a/packages/skill/skill/tsconfig.json +++ b/packages/skill/skill/tsconfig.json @@ -6,8 +6,17 @@ }, "include": ["src"], "references": [ - { "path": "../../../vendor/cosmokit" }, - { "path": "../../../vendor/cordis" }, - { "path": "../../../vendor/schemastery" } + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../../vendor/schemastery" + }, + { + "path": "../../support/invariants" + } ] } diff --git a/packages/skill/tool-skill/package.json b/packages/skill/tool-skill/package.json index 3d6ddc6b7e..47421bf19c 100644 --- a/packages/skill/tool-skill/package.json +++ b/packages/skill/tool-skill/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -23,6 +28,7 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-skill": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", @@ -33,6 +39,7 @@ }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-scope": "workspace:^", "@deepseek-ai/dsh-skill": "workspace:^", diff --git a/packages/skill/tool-skill/src/invariant.ts b/packages/skill/tool-skill/src/invariant.ts new file mode 100644 index 0000000000..abf4ba3961 --- /dev/null +++ b/packages/skill/tool-skill/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-tool-skill' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/skill/tool-skill/tsconfig.json b/packages/skill/tool-skill/tsconfig.json index 52ebb8bf9d..fed1ffa5f5 100644 --- a/packages/skill/tool-skill/tsconfig.json +++ b/packages/skill/tool-skill/tsconfig.json @@ -6,13 +6,32 @@ }, "include": ["src"], "references": [ - { "path": "../../../vendor/cosmokit" }, - { "path": "../../../vendor/cordis" }, - { "path": "../../../vendor/schemastery" }, - { "path": "../../core/scope" }, - { "path": "../../llm/llm" }, - { "path": "../../core/agent" }, - { "path": "../skill" }, - { "path": "../../core/tools" } + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../../vendor/schemastery" + }, + { + "path": "../../core/scope" + }, + { + "path": "../../llm/llm" + }, + { + "path": "../../core/agent" + }, + { + "path": "../skill" + }, + { + "path": "../../core/tools" + }, + { + "path": "../../support/invariants" + } ] } diff --git a/packages/spill/spill-local/package.json b/packages/spill/spill-local/package.json index a75c4bfc0b..25a20db1d5 100644 --- a/packages/spill/spill-local/package.json +++ b/packages/spill/spill-local/package.json @@ -11,17 +11,23 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-spill": "^0.0.1", "cordis": "^4.0.0-rc.6" }, @@ -30,6 +36,7 @@ }, "devDependencies": { "@deepseek-ai/dsh-brand": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-spill": "workspace:^", diff --git a/packages/spill/spill-local/src/invariant.ts b/packages/spill/spill-local/src/invariant.ts new file mode 100644 index 0000000000..d638ffa2a9 --- /dev/null +++ b/packages/spill/spill-local/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-spill-local' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/spill/spill-local/tsconfig.json b/packages/spill/spill-local/tsconfig.json index 8e818212f5..0cb209d8d0 100644 --- a/packages/spill/spill-local/tsconfig.json +++ b/packages/spill/spill-local/tsconfig.json @@ -6,9 +6,20 @@ }, "include": ["src"], "references": [ - { "path": "../../../vendor/cosmokit" }, - { "path": "../../../vendor/cordis" }, - { "path": "../../../vendor/schemastery" }, - { "path": "../spill" } + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../../vendor/schemastery" + }, + { + "path": "../spill" + }, + { + "path": "../../support/invariants" + } ] } diff --git a/packages/spill/spill-policy/package.json b/packages/spill/spill-policy/package.json index 9c28ea5382..dac7d4311d 100644 --- a/packages/spill/spill-policy/package.json +++ b/packages/spill/spill-policy/package.json @@ -11,17 +11,23 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-retention": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", @@ -34,6 +40,7 @@ }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-retention": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", diff --git a/packages/spill/spill-policy/src/invariant.ts b/packages/spill/spill-policy/src/invariant.ts new file mode 100644 index 0000000000..d4aa544ecd --- /dev/null +++ b/packages/spill/spill-policy/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-spill-policy' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/spill/spill-policy/tsconfig.json b/packages/spill/spill-policy/tsconfig.json index 6a81ab2f3c..71f19d381c 100644 --- a/packages/spill/spill-policy/tsconfig.json +++ b/packages/spill/spill-policy/tsconfig.json @@ -6,13 +6,32 @@ }, "include": ["src"], "references": [ - { "path": "../../../vendor/cosmokit" }, - { "path": "../../../vendor/cordis" }, - { "path": "../../../vendor/schemastery" }, - { "path": "../../util/retention" }, - { "path": "../../llm/llm" }, - { "path": "../../core/session" }, - { "path": "../spill" }, - { "path": "../../core/tools" } + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../../vendor/schemastery" + }, + { + "path": "../../util/retention" + }, + { + "path": "../../llm/llm" + }, + { + "path": "../../core/session" + }, + { + "path": "../spill" + }, + { + "path": "../../core/tools" + }, + { + "path": "../../support/invariants" + } ] } diff --git a/packages/spill/spill/package.json b/packages/spill/spill/package.json index 3103c9cd11..c66306ff0a 100644 --- a/packages/spill/spill/package.json +++ b/packages/spill/spill/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -23,12 +28,14 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-brand": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "cordis": "^4.0.0-rc.6" }, "devDependencies": { "@deepseek-ai/dsh-brand": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "cordis": "^4.0.0-rc.6" diff --git a/packages/spill/spill/src/invariant.ts b/packages/spill/spill/src/invariant.ts new file mode 100644 index 0000000000..714e43f3a6 --- /dev/null +++ b/packages/spill/spill/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-spill' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/spill/spill/tsconfig.json b/packages/spill/spill/tsconfig.json index 0c2fd5c57f..30d0d29f0f 100644 --- a/packages/spill/spill/tsconfig.json +++ b/packages/spill/spill/tsconfig.json @@ -6,10 +6,23 @@ }, "include": ["src"], "references": [ - { "path": "../../../vendor/cosmokit" }, - { "path": "../../../vendor/cordis" }, - { "path": "../../util/brand" }, - { "path": "../../llm/llm" }, - { "path": "../../core/session" } + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../util/brand" + }, + { + "path": "../../llm/llm" + }, + { + "path": "../../core/session" + }, + { + "path": "../../support/invariants" + } ] } diff --git a/packages/subagent/subagent-acp/package.json b/packages/subagent/subagent-acp/package.json index fa16edcf60..2564afa8da 100644 --- a/packages/subagent/subagent-acp/package.json +++ b/packages/subagent/subagent-acp/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -23,6 +28,7 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-subagent": "^0.0.1", @@ -34,13 +40,14 @@ "schemastery": "^3.18.0" }, "devDependencies": { + "@cordisjs/plugin-loader": "^1.0.0-rc.5", "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-loader-smoke": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-subagent": "workspace:^", "@deepseek-ai/dsh-subagent-subprocess": "workspace:^", - "@cordisjs/plugin-loader": "^1.0.0-rc.5", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/subagent/subagent-acp/src/invariant.ts b/packages/subagent/subagent-acp/src/invariant.ts new file mode 100644 index 0000000000..a5828fd4c1 --- /dev/null +++ b/packages/subagent/subagent-acp/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-acp' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/subagent/subagent-acp/tsconfig.json b/packages/subagent/subagent-acp/tsconfig.json index 5aa28528ac..175eb78e2f 100644 --- a/packages/subagent/subagent-acp/tsconfig.json +++ b/packages/subagent/subagent-acp/tsconfig.json @@ -31,6 +31,9 @@ }, { "path": "../../support/loader-smoke" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/subagent/subagent-fork/package.json b/packages/subagent/subagent-fork/package.json index 63f548d217..aa93b83e03 100644 --- a/packages/subagent/subagent-fork/package.json +++ b/packages/subagent/subagent-fork/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -23,6 +28,7 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-subagent": "^0.0.1", "@deepseek-ai/dsh-subagent-inprocess": "^0.0.1", @@ -32,6 +38,7 @@ "schemastery": "^3.18.0" }, "devDependencies": { + "@cordisjs/plugin-loader": "^1.0.0-rc.5", "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-agent-loop-testkit": "workspace:^", @@ -41,7 +48,6 @@ "@deepseek-ai/dsh-subagent": "workspace:^", "@deepseek-ai/dsh-subagent-inprocess": "workspace:^", "@deepseek-ai/dsh-subagent-spawn": "workspace:^", - "@cordisjs/plugin-loader": "^1.0.0-rc.5", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/subagent/subagent-fork/src/invariant.ts b/packages/subagent/subagent-fork/src/invariant.ts new file mode 100644 index 0000000000..c6903fd82d --- /dev/null +++ b/packages/subagent/subagent-fork/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-fork' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/subagent/subagent-fork/tsconfig.json b/packages/subagent/subagent-fork/tsconfig.json index bac12550af..a07a2319ef 100644 --- a/packages/subagent/subagent-fork/tsconfig.json +++ b/packages/subagent/subagent-fork/tsconfig.json @@ -28,6 +28,9 @@ }, { "path": "../subagent-inprocess" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/subagent/subagent-inprocess/package.json b/packages/subagent/subagent-inprocess/package.json index b9397e300c..69b573ecdd 100644 --- a/packages/subagent/subagent-inprocess/package.json +++ b/packages/subagent/subagent-inprocess/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -23,6 +28,7 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-subagent": "^0.0.1", diff --git a/packages/subagent/subagent-inprocess/src/invariant.ts b/packages/subagent/subagent-inprocess/src/invariant.ts new file mode 100644 index 0000000000..0eac204104 --- /dev/null +++ b/packages/subagent/subagent-inprocess/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-inprocess' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/subagent/subagent-inprocess/tsconfig.json b/packages/subagent/subagent-inprocess/tsconfig.json index 7b7a015cc9..02fd8e53d0 100644 --- a/packages/subagent/subagent-inprocess/tsconfig.json +++ b/packages/subagent/subagent-inprocess/tsconfig.json @@ -31,6 +31,9 @@ }, { "path": "../../core/tools" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/subagent/subagent-spawn/package.json b/packages/subagent/subagent-spawn/package.json index 1a986e69aa..f429025a5d 100644 --- a/packages/subagent/subagent-spawn/package.json +++ b/packages/subagent/subagent-spawn/package.json @@ -11,17 +11,23 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-subagent": "^0.0.1", "@deepseek-ai/dsh-subagent-inprocess": "^0.0.1", "cordis": "^4.0.0-rc.7" @@ -30,6 +36,7 @@ "schemastery": "^3.18.0" }, "devDependencies": { + "@cordisjs/plugin-loader": "^1.0.0-rc.5", "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-agent-loop-testkit": "workspace:^", @@ -42,7 +49,6 @@ "@deepseek-ai/dsh-subagent-inprocess": "workspace:^", "@deepseek-ai/dsh-tool-bash": "workspace:^", "@deepseek-ai/dsh-tool-subagent": "workspace:^", - "@cordisjs/plugin-loader": "^1.0.0-rc.5", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/subagent/subagent-spawn/src/invariant.ts b/packages/subagent/subagent-spawn/src/invariant.ts new file mode 100644 index 0000000000..d179a2b72a --- /dev/null +++ b/packages/subagent/subagent-spawn/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-spawn' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/subagent/subagent-spawn/tsconfig.json b/packages/subagent/subagent-spawn/tsconfig.json index 219bf2a0c9..ee9ab096c2 100644 --- a/packages/subagent/subagent-spawn/tsconfig.json +++ b/packages/subagent/subagent-spawn/tsconfig.json @@ -22,6 +22,9 @@ }, { "path": "../subagent-inprocess" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/subagent/subagent-subprocess/package.json b/packages/subagent/subagent-subprocess/package.json index 5f17459276..bd573b3c0c 100644 --- a/packages/subagent/subagent-subprocess/package.json +++ b/packages/subagent/subagent-subprocess/package.json @@ -11,20 +11,27 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/subagent/subagent-subprocess/src/invariant.ts b/packages/subagent/subagent-subprocess/src/invariant.ts new file mode 100644 index 0000000000..22dac32814 --- /dev/null +++ b/packages/subagent/subagent-subprocess/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-subprocess' + +/** 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 = () => {} + +/** + * 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 */ diff --git a/packages/subagent/subagent-subprocess/tsconfig.json b/packages/subagent/subagent-subprocess/tsconfig.json index 749cb0208e..d970a00263 100644 --- a/packages/subagent/subagent-subprocess/tsconfig.json +++ b/packages/subagent/subagent-subprocess/tsconfig.json @@ -7,5 +7,9 @@ "include": [ "src" ], - "references": [] + "references": [ + { + "path": "../../support/invariants" + } + ] } diff --git a/packages/subagent/subagent/package.json b/packages/subagent/subagent/package.json index aea05553e4..58b51d1888 100644 --- a/packages/subagent/subagent/package.json +++ b/packages/subagent/subagent/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -24,6 +29,7 @@ "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-brand": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-scope": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", @@ -33,6 +39,7 @@ "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-brand": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-scope": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", diff --git a/packages/subagent/subagent/src/invariant.ts b/packages/subagent/subagent/src/invariant.ts new file mode 100644 index 0000000000..3a79592ee1 --- /dev/null +++ b/packages/subagent/subagent/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-subagent' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/subagent/subagent/tsconfig.json b/packages/subagent/subagent/tsconfig.json index f93f929241..713e214f04 100644 --- a/packages/subagent/subagent/tsconfig.json +++ b/packages/subagent/subagent/tsconfig.json @@ -25,6 +25,9 @@ }, { "path": "../../core/scope" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/subagent/tool-subagent/package.json b/packages/subagent/tool-subagent/package.json index e2d4378743..6ed447dd56 100644 --- a/packages/subagent/tool-subagent/package.json +++ b/packages/subagent/tool-subagent/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -23,6 +28,7 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-subagent": "^0.0.1", "@deepseek-ai/dsh-tasks": "^0.0.1", @@ -35,6 +41,7 @@ "devDependencies": { "@cordisjs/plugin-loader": "^1.0.0-rc.5", "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-subagent": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", diff --git a/packages/subagent/tool-subagent/src/invariant.ts b/packages/subagent/tool-subagent/src/invariant.ts new file mode 100644 index 0000000000..08881e4b2b --- /dev/null +++ b/packages/subagent/tool-subagent/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-tool-subagent' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/subagent/tool-subagent/tsconfig.json b/packages/subagent/tool-subagent/tsconfig.json index 2aa9d4f14e..25780c367f 100644 --- a/packages/subagent/tool-subagent/tsconfig.json +++ b/packages/subagent/tool-subagent/tsconfig.json @@ -31,6 +31,9 @@ }, { "path": "../../tasks/tasks" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/support/acp-snapshot/package.json b/packages/support/acp-snapshot/package.json index 64e1b0cbd7..abffcc2708 100644 --- a/packages/support/acp-snapshot/package.json +++ b/packages/support/acp-snapshot/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -27,9 +32,11 @@ "vitest": "^4.1.8" }, "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/support/acp-snapshot/src/invariant.ts b/packages/support/acp-snapshot/src/invariant.ts new file mode 100644 index 0000000000..3579b96cf5 --- /dev/null +++ b/packages/support/acp-snapshot/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-acp-snapshot' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/support/acp-snapshot/tsconfig.json b/packages/support/acp-snapshot/tsconfig.json index 9120df0ad1..893282ce51 100644 --- a/packages/support/acp-snapshot/tsconfig.json +++ b/packages/support/acp-snapshot/tsconfig.json @@ -8,6 +8,11 @@ "src" ], "references": [ - { "path": "../loader-smoke" } + { + "path": "../loader-smoke" + }, + { + "path": "../../support/invariants" + } ] } diff --git a/packages/support/agent-loop-testkit/package.json b/packages/support/agent-loop-testkit/package.json index 423bd3e80d..aa04d85832 100644 --- a/packages/support/agent-loop-testkit/package.json +++ b/packages/support/agent-loop-testkit/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -23,6 +28,7 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-system-prompt": "^0.0.1", @@ -32,6 +38,7 @@ "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", diff --git a/packages/support/agent-loop-testkit/src/invariant.ts b/packages/support/agent-loop-testkit/src/invariant.ts new file mode 100644 index 0000000000..fc2554aa77 --- /dev/null +++ b/packages/support/agent-loop-testkit/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-agent-loop-testkit' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/support/agent-loop-testkit/tsconfig.json b/packages/support/agent-loop-testkit/tsconfig.json index 5e5b3c47f2..d24b4dd988 100644 --- a/packages/support/agent-loop-testkit/tsconfig.json +++ b/packages/support/agent-loop-testkit/tsconfig.json @@ -28,6 +28,9 @@ }, { "path": "../../core/tools" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/support/invariants/README.md b/packages/support/invariants/README.md index 1cb0e57838..c49771ded9 100644 --- a/packages/support/invariants/README.md +++ b/packages/support/invariants/README.md @@ -1,6 +1,6 @@ # dsh-invariants -Configurable registry service for package-owned runtime invariant checks. The root plugin registers `ctx.invariants`; it contains no product checks or product-package imports. Packages publish optional `./invariant` companion plugins that contribute their own assertions. +Configurable registry service for package-owned runtime invariant checks. The root plugin registers `ctx.invariants`; it contains no product checks or product-package imports. Every workspace package publishes a `./invariant` companion that registers its exact npm package name. ## Service: `InvariantService` (`ctx.invariants`) @@ -24,6 +24,10 @@ 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. + +Four companions currently install stateful checks: + | Companion | Registration | Checks | |---|---|---| | `@deepseek-ai/dsh-session/invariant` | `@deepseek-ai/dsh-session` | sequence, turn/step enclosure, and same-step tool call/result trace | @@ -50,7 +54,7 @@ ctx.plugin(InvariantService, { ctx.plugin(SessionInvariant) ``` -The standard agent spine mounts the service and all four companions. Custom compositions choose the companions they want and may disable or filter them without changing package entrypoints. +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. ## Model Experience @@ -58,6 +62,6 @@ None, as the service and companions observe runtime events and requests but neve ## Known Limitations and Deferred Work -- The shipped checks cover only the four listed package contracts; a merge-extended event family has no family-specific assertion until its owner publishes one. +- Stateful checks cover only the four listed package contracts; other companions reserve ownership but add no listeners until their packages gain relational assertions. - 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/package.json b/packages/support/invariants/package.json index e06e9a3db7..d52dd0a14d 100644 --- a/packages/support/invariants/package.json +++ b/packages/support/invariants/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" diff --git a/packages/support/invariants/src/index.ts b/packages/support/invariants/src/index.ts index 754cb46ab9..4bbf218e97 100644 --- a/packages/support/invariants/src/index.ts +++ b/packages/support/invariants/src/index.ts @@ -1,7 +1,8 @@ /** * Configurable registry for package-owned runtime invariant contributions. - * Packages register checks from optional `./invariant` companion plugins; - * ordinary package entrypoints stay independent of diagnostics. + * 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. * * @module @deepseek-ai/dsh-invariants */ diff --git a/packages/support/invariants/src/invariant.ts b/packages/support/invariants/src/invariant.ts new file mode 100644 index 0000000000..b6b4d8ae48 --- /dev/null +++ b/packages/support/invariants/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from './index.ts' + +const PACKAGE_NAME = '@deepseek-ai/dsh-invariants' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/support/llm-replay/package.json b/packages/support/llm-replay/package.json index 403f3bda92..4a7e7dd8d8 100644 --- a/packages/support/llm-replay/package.json +++ b/packages/support/llm-replay/package.json @@ -11,22 +11,29 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "cordis": "^4.0.0-rc.7" diff --git a/packages/support/llm-replay/src/invariant.ts b/packages/support/llm-replay/src/invariant.ts new file mode 100644 index 0000000000..50295cbedf --- /dev/null +++ b/packages/support/llm-replay/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-llm-replay' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/support/llm-replay/tsconfig.json b/packages/support/llm-replay/tsconfig.json index 95245937ec..673ee51547 100644 --- a/packages/support/llm-replay/tsconfig.json +++ b/packages/support/llm-replay/tsconfig.json @@ -19,6 +19,9 @@ }, { "path": "../../core/session" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/support/loader-smoke/package.json b/packages/support/loader-smoke/package.json index ddba421b41..570ee2c5ca 100644 --- a/packages/support/loader-smoke/package.json +++ b/packages/support/loader-smoke/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -25,9 +30,11 @@ "tsx": "^4.22.4" }, "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "cordis": "^4.0.0-rc.6" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "cordis": "^4.0.0-rc.6" } } diff --git a/packages/support/loader-smoke/src/invariant.ts b/packages/support/loader-smoke/src/invariant.ts new file mode 100644 index 0000000000..9265a5f8b4 --- /dev/null +++ b/packages/support/loader-smoke/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-loader-smoke' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/support/loader-smoke/tsconfig.json b/packages/support/loader-smoke/tsconfig.json index 749cb0208e..d970a00263 100644 --- a/packages/support/loader-smoke/tsconfig.json +++ b/packages/support/loader-smoke/tsconfig.json @@ -7,5 +7,9 @@ "include": [ "src" ], - "references": [] + "references": [ + { + "path": "../../support/invariants" + } + ] } diff --git a/packages/tasks/tasks/package.json b/packages/tasks/tasks/package.json index 9e5f824418..128a8d2c4e 100644 --- a/packages/tasks/tasks/package.json +++ b/packages/tasks/tasks/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-tasks", - "description": "Background task registry (ctx.tasks) for the DeepSeek Harness \u2014 shared ids, owner isolation, polling, cancellation, and completion listeners for long-running tool work", + "description": "Background task registry (ctx.tasks) for the DeepSeek Harness — shared ids, owner isolation, polling, cancellation, and completion listeners for long-running tool work", "version": "0.0.1", "private": true, "type": "module", @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -24,6 +29,7 @@ "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-brand": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-timeout": "^0.0.1", "cordis": "^4.0.0-rc.6" @@ -31,6 +37,7 @@ "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-brand": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-timeout": "workspace:^", "cordis": "^4.0.0-rc.6" diff --git a/packages/tasks/tasks/src/invariant.ts b/packages/tasks/tasks/src/invariant.ts new file mode 100644 index 0000000000..468fe664b9 --- /dev/null +++ b/packages/tasks/tasks/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-tasks' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/tasks/tasks/tsconfig.json b/packages/tasks/tasks/tsconfig.json index 392d3f8d8a..e29262ca74 100644 --- a/packages/tasks/tasks/tsconfig.json +++ b/packages/tasks/tasks/tsconfig.json @@ -25,6 +25,9 @@ }, { "path": "../../util/timeout" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/tasks/tool-tasks/package.json b/packages/tasks/tool-tasks/package.json index 9e0b19d307..fc1f96417d 100644 --- a/packages/tasks/tool-tasks/package.json +++ b/packages/tasks/tool-tasks/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -23,6 +28,7 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-system-prompt": "^0.0.1", "@deepseek-ai/dsh-tasks": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", @@ -33,6 +39,7 @@ }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", diff --git a/packages/tasks/tool-tasks/src/invariant.ts b/packages/tasks/tool-tasks/src/invariant.ts new file mode 100644 index 0000000000..fded38c895 --- /dev/null +++ b/packages/tasks/tool-tasks/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-tool-tasks' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/tasks/tool-tasks/tsconfig.json b/packages/tasks/tool-tasks/tsconfig.json index 9e5411df25..feab4f3be8 100644 --- a/packages/tasks/tool-tasks/tsconfig.json +++ b/packages/tasks/tool-tasks/tsconfig.json @@ -28,6 +28,9 @@ }, { "path": "../tasks" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/timeout/timeout-policy/package.json b/packages/timeout/timeout-policy/package.json index aa351cb7a6..43dc9dfa28 100644 --- a/packages/timeout/timeout-policy/package.json +++ b/packages/timeout/timeout-policy/package.json @@ -11,23 +11,30 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-timeout": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-timeout": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", diff --git a/packages/timeout/timeout-policy/src/invariant.ts b/packages/timeout/timeout-policy/src/invariant.ts new file mode 100644 index 0000000000..9e7b5b7d4b --- /dev/null +++ b/packages/timeout/timeout-policy/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-timeout-policy' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/timeout/timeout-policy/tsconfig.json b/packages/timeout/timeout-policy/tsconfig.json index 8c0b47716e..55e50befde 100644 --- a/packages/timeout/timeout-policy/tsconfig.json +++ b/packages/timeout/timeout-policy/tsconfig.json @@ -6,11 +6,26 @@ }, "include": ["src"], "references": [ - { "path": "../../../vendor/cosmokit" }, - { "path": "../../../vendor/cordis" }, - { "path": "../../../vendor/schemastery" }, - { "path": "../../llm/llm" }, - { "path": "../../util/timeout" }, - { "path": "../../core/tools" } + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../../vendor/schemastery" + }, + { + "path": "../../llm/llm" + }, + { + "path": "../../util/timeout" + }, + { + "path": "../../core/tools" + }, + { + "path": "../../support/invariants" + } ] } diff --git a/packages/todo/tool-todo/package.json b/packages/todo/tool-todo/package.json index bab0f1230c..88d5e9b9c9 100644 --- a/packages/todo/tool-todo/package.json +++ b/packages/todo/tool-todo/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -23,6 +28,7 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", "cordis": "^4.0.0-rc.7" @@ -31,6 +37,7 @@ "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-agent-loop-testkit": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", diff --git a/packages/todo/tool-todo/src/invariant.ts b/packages/todo/tool-todo/src/invariant.ts new file mode 100644 index 0000000000..a5980342f3 --- /dev/null +++ b/packages/todo/tool-todo/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-tool-todo' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/todo/tool-todo/tsconfig.json b/packages/todo/tool-todo/tsconfig.json index adf2f25dec..f980e5ead1 100644 --- a/packages/todo/tool-todo/tsconfig.json +++ b/packages/todo/tool-todo/tsconfig.json @@ -22,6 +22,9 @@ }, { "path": "../../core/session" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/ui/acp/package.json b/packages/ui/acp/package.json index 84e4dcbda9..7d324d2e6f 100644 --- a/packages/ui/acp/package.json +++ b/packages/ui/acp/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -29,6 +34,7 @@ "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-bash": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-permission": "^0.0.1", "@deepseek-ai/dsh-sandbox": "^0.0.1", diff --git a/packages/ui/acp/src/invariant.ts b/packages/ui/acp/src/invariant.ts new file mode 100644 index 0000000000..2c081fc48c --- /dev/null +++ b/packages/ui/acp/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-acp' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/ui/acp/tsconfig.json b/packages/ui/acp/tsconfig.json index 387e0d0c53..5f93e1589d 100644 --- a/packages/ui/acp/tsconfig.json +++ b/packages/ui/acp/tsconfig.json @@ -46,6 +46,9 @@ }, { "path": "../../bash/bash" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/ui/app-boot/package.json b/packages/ui/app-boot/package.json index 1eef56ae93..e2f0631e8b 100644 --- a/packages/ui/app-boot/package.json +++ b/packages/ui/app-boot/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -24,11 +29,13 @@ "peerDependencies": { "@cordisjs/plugin-include": "^1.0.4", "@cordisjs/plugin-loader": "^1.0.0-rc.5", + "@deepseek-ai/dsh-invariants": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@cordisjs/plugin-include": "workspace:^", "@cordisjs/plugin-loader": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/ui/app-boot/src/invariant.ts b/packages/ui/app-boot/src/invariant.ts new file mode 100644 index 0000000000..498d967799 --- /dev/null +++ b/packages/ui/app-boot/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-app-boot' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/ui/app-boot/tsconfig.json b/packages/ui/app-boot/tsconfig.json index 3171312de4..b85dc7f6a2 100644 --- a/packages/ui/app-boot/tsconfig.json +++ b/packages/ui/app-boot/tsconfig.json @@ -16,6 +16,9 @@ }, { "path": "../../../vendor/include" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/ui/jsonrpc/package.json b/packages/ui/jsonrpc/package.json index e59fc4aaea..72dd9fe8c3 100644 --- a/packages/ui/jsonrpc/package.json +++ b/packages/ui/jsonrpc/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -26,6 +31,7 @@ }, "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-llm-deepseek": "^0.0.1", "@deepseek-ai/dsh-scope": "^0.0.1", @@ -37,6 +43,7 @@ "@cordisjs/plugin-loader": "workspace:^", "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-agent-spine-demo": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-llm-deepseek": "workspace:^", "@deepseek-ai/dsh-scope": "workspace:^", diff --git a/packages/ui/jsonrpc/src/invariant.ts b/packages/ui/jsonrpc/src/invariant.ts new file mode 100644 index 0000000000..552c312481 --- /dev/null +++ b/packages/ui/jsonrpc/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-jsonrpc' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/ui/jsonrpc/tsconfig.json b/packages/ui/jsonrpc/tsconfig.json index dcd57ef9af..14a70d8eaa 100644 --- a/packages/ui/jsonrpc/tsconfig.json +++ b/packages/ui/jsonrpc/tsconfig.json @@ -28,6 +28,9 @@ }, { "path": "../../subagent/subagent" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/ui/permission/package.json b/packages/ui/permission/package.json index e38e5a7bf1..bd8164f427 100644 --- a/packages/ui/permission/package.json +++ b/packages/ui/permission/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -23,6 +28,7 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-bash": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-sandbox": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-user-approval": "^0.0.1", @@ -33,6 +39,7 @@ }, "devDependencies": { "@deepseek-ai/dsh-bash": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-sandbox": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-user-approval": "workspace:^", diff --git a/packages/ui/permission/src/invariant.ts b/packages/ui/permission/src/invariant.ts new file mode 100644 index 0000000000..1a774e2e4c --- /dev/null +++ b/packages/ui/permission/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-permission' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/ui/permission/tsconfig.json b/packages/ui/permission/tsconfig.json index 8b9cff62b4..3e7b747b2c 100644 --- a/packages/ui/permission/tsconfig.json +++ b/packages/ui/permission/tsconfig.json @@ -28,6 +28,9 @@ }, { "path": "../user-approval" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/ui/stdio/package.json b/packages/ui/stdio/package.json index e1bffdf171..f8473af7a3 100644 --- a/packages/ui/stdio/package.json +++ b/packages/ui/stdio/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -24,6 +29,7 @@ "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-agent-loop": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-user-interaction": "^0.0.1", @@ -41,6 +47,7 @@ "@cordisjs/plugin-loader": "workspace:^", "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-user-interaction": "workspace:^", diff --git a/packages/ui/stdio/src/invariant.ts b/packages/ui/stdio/src/invariant.ts new file mode 100644 index 0000000000..443440a215 --- /dev/null +++ b/packages/ui/stdio/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-stdio' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/ui/stdio/tests/stdio.spec.ts b/packages/ui/stdio/tests/stdio.spec.ts index a3069462ff..4f8cf49e71 100644 --- a/packages/ui/stdio/tests/stdio.spec.ts +++ b/packages/ui/stdio/tests/stdio.spec.ts @@ -1,9 +1,9 @@ import { Readable, Writable } from 'node:stream' import { describe, expect, it, vi } from 'vitest' import { Context } from 'cordis' -import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent' -import AgentRegistry from '@deepseek-ai/dsh-agent' +import AgentRegistry, { agentEvents, type Agent, type AgentStatus } from '@deepseek-ai/dsh-agent' import type { ContentBlock, StreamChunk } from '@deepseek-ai/dsh-llm' +import { scopeTarget } from '@deepseek-ai/dsh-scope' import { SessionId, type Session, type SessionEvent } from '@deepseek-ai/dsh-session' import UserInteractionService from '@deepseek-ai/dsh-user-interaction' import { createStdioChat, mountStdio, type Config, type StdioRuntime } from '../src/index.ts' @@ -67,10 +67,26 @@ function makeAgent(id: string, status: AgentStatus = 'idle'): Agent & { /** Register a fake configured agent and cross the supported startup-work boundary. */ function registerReady(ctx: Context, agent: Agent, source: 'startup' | 'resume' = 'startup'): () => void { const dispose = ctx.agents.register(agent) - ctx.emit('agent/session-start', agent, source) + agentEvents(ctx, agent).emit('agent/session-start', source) return dispose } +function emitAgentSessionStart(ctx: Context, agent: Agent, source: 'startup' | 'resume'): void { + agentEvents(ctx, agent).emit('agent/session-start', source) +} + +function emitAgentStatus(ctx: Context, agent: Agent, status: AgentStatus): void { + agentEvents(ctx, agent).emit('agent/status', status) +} + +function emitAgentDisposed(ctx: Context, agent: Agent): void { + agentEvents(ctx, agent).emit('agent/disposed') +} + +function emitSessionEvent(ctx: Context, session: Session, event: SessionEvent): void { + ctx.emit(scopeTarget(session, undefined), 'session/event', session, event) +} + /** A session stub whose `header.id` matches an agent's, for `session/event` emits. */ function makeSession(id: string): Session { return { id, header: { id } } as Session @@ -192,23 +208,23 @@ describe('createStdioChat rendering', () => { it('renders text-delta chunks verbatim', async () => { const { ctx, out } = await setup() - ctx.emit('session/event', makeSession('main'), chunkEvent({ type: 'text-delta', index: 0, text: 'hello' })) + emitSessionEvent(ctx, makeSession('main'), chunkEvent({ type: 'text-delta', index: 0, text: 'hello' })) expect(out.text()).toContain('hello') }) it('wraps reasoning-delta in the dim SGR and resets on the following text-delta', async () => { const { ctx, out } = await setup() const session = makeSession('main') - ctx.emit('session/event', session, chunkEvent({ type: 'reasoning-delta', index: 0, text: 'think' })) - ctx.emit('session/event', session, chunkEvent({ type: 'reasoning-delta', index: 0, text: 'more' })) - ctx.emit('session/event', session, chunkEvent({ type: 'text-delta', index: 0, text: 'answer' })) + emitSessionEvent(ctx, session, chunkEvent({ type: 'reasoning-delta', index: 0, text: 'think' })) + emitSessionEvent(ctx, session, chunkEvent({ type: 'reasoning-delta', index: 0, text: 'more' })) + emitSessionEvent(ctx, session, chunkEvent({ type: 'text-delta', index: 0, text: 'answer' })) expect(out.text()).toContain('\x1B[2mthinkmore\x1B[0m\nanswer') }) it('ignores stream-chunk types it does not render', async () => { const { ctx, out } = await setup() const before = out.text() - ctx.emit('session/event', makeSession('main'), chunkEvent({ type: 'block-start', index: 0, blockType: 'text' })) + emitSessionEvent(ctx, makeSession('main'), chunkEvent({ type: 'block-start', index: 0, blockType: 'text' })) expect(out.text()).toBe(before) }) @@ -217,20 +233,20 @@ describe('createStdioChat rendering', () => { const agent = makeAgent('main') ctx.agents.register(agent) const session = agent.session - ctx.emit('session/event', session, { + emitSessionEvent(ctx, session, { type: 'turn/start', seq: 1, time: 0, data: { turn: 3, trigger: { kind: 'message' } }, } as SessionEvent) expect(out.text()).toContain('[main turn 3] ') - ctx.emit('session/event', session, { + emitSessionEvent(ctx, session, { type: 'turn/end', seq: 2, time: 0, data: { turn: 3, reason: { kind: 'completed' } }, - } as SessionEvent) + }) expect(out.text()).toContain('\n> ') }) it('uses the session id as the label for a non-target session', async () => { const { ctx, out } = await setup() // No target exists, so the event's durable identity is the label. - ctx.emit('session/event', makeSession('orphan'), { + emitSessionEvent(ctx, makeSession('orphan'), { type: 'turn/start', seq: 1, time: 0, data: { turn: 1, trigger: { kind: 'message' } }, } as SessionEvent) expect(out.text()).toContain('[orphan turn 1] ') @@ -253,7 +269,7 @@ describe('createStdioChat rendering', () => { await ctx.plugin(Object.assign((inner: Context) => { createStdioChat(inner, CONFIG, runtime) }, { inject: ['agents', 'userInteraction'] })) - ctx.emit('session/event', agent.session, { + emitSessionEvent(ctx, agent.session, { type: 'turn/start', seq: 1, time: 0, data: { turn: 5, trigger: { kind: 'message' } }, } as SessionEvent) expect(out.text()).toContain('[main turn 5] ') @@ -266,14 +282,14 @@ describe('createStdioChat rendering', () => { const unrelated = makeAgent('unrelated') ctx.agents.register(unrelated) - ctx.emit('agent/session-start', unrelated, 'startup') + emitAgentSessionStart(ctx, unrelated, 'startup') const resumed = makeAgent('resumed') ;(resumed.session.header as { parentSession?: string }).parentSession = 'persisted-parent' ctx.agents.register(resumed) await new Promise(resolve => setImmediate(resolve)) expect(resumed.sent).toEqual([]) - ctx.emit('agent/session-start', resumed, 'resume') + emitAgentSessionStart(ctx, resumed, 'resume') await new Promise(resolve => setImmediate(resolve)) expect(unrelated.sent).toEqual([]) @@ -283,10 +299,10 @@ describe('createStdioChat rendering', () => { it('resets dim styling at turn/end if a turn ends mid-reasoning', async () => { const { ctx, out } = await setup() const session = makeSession('main') - ctx.emit('session/event', session, chunkEvent({ type: 'reasoning-delta', index: 0, text: 'mid' })) - ctx.emit('session/event', session, { + emitSessionEvent(ctx, session, chunkEvent({ type: 'reasoning-delta', index: 0, text: 'mid' })) + emitSessionEvent(ctx, session, { type: 'turn/end', seq: 1, time: 0, data: { turn: 1, reason: { kind: 'completed' } }, - } as SessionEvent) + }) expect(out.text()).toContain('\x1B[2mmid\x1B[0m') }) @@ -297,7 +313,7 @@ describe('createStdioChat rendering', () => { dispose() // After disposal the event belongs to a non-target session, so its durable // identity is rendered directly. - ctx.emit('session/event', agent.session, { + emitSessionEvent(ctx, agent.session, { type: 'turn/start', seq: 1, time: 0, data: { turn: 1, trigger: { kind: 'message' } }, } as SessionEvent) expect(out.text()).toContain('[main turn 1] ') @@ -307,8 +323,8 @@ describe('createStdioChat rendering', () => { const { ctx, out } = await setup() const target = makeAgent('main') ctx.agents.register(target) - ctx.emit('agent/disposed', makeAgent('other')) - ctx.emit('session/event', target.session, { + emitAgentDisposed(ctx, makeAgent('other')) + emitSessionEvent(ctx, target.session, { type: 'turn/start', seq: 1, time: 0, data: { turn: 1, trigger: { kind: 'message' } }, } as SessionEvent) expect(out.text()).toContain('[main turn 1] ') @@ -326,7 +342,7 @@ describe('createStdioChat rendering', () => { input.feed('after hmr') await new Promise(resolve => setImmediate(resolve)) expect(replacement.sent).toEqual([]) - ctx.emit('agent/session-start', replacement, 'resume') + emitAgentSessionStart(ctx, replacement, 'resume') await new Promise(resolve => setImmediate(resolve)) expect(prefixCollision.sent).toEqual([]) @@ -356,28 +372,28 @@ describe('createStdioChat rendering', () => { type: 'tool/call', seq: 1, time: 0, data: { turn: 1, step: 0, callId: 'c1', name: 'bash', arguments: '{"command":"ls"}' }, } as SessionEvent - ctx.emit('session/event', session, callEvent) + emitSessionEvent(ctx, session, callEvent) expect(out.text()).toContain('[tool call] bash({"command":"ls"})') const resultEvent = { type: 'tool/result', seq: 2, time: 0, data: { turn: 1, step: 0, callId: 'c1', content: [{ type: 'text', text: 'file.txt' }], isError: false }, } as SessionEvent - ctx.emit('session/event', session, resultEvent) + emitSessionEvent(ctx, session, resultEvent) expect(out.text()).toContain('[tool result] file.txt') }) it('renders a todo/write session event as a glyphed checklist', async () => { const { ctx, out } = await setup() const session = {} as Session - ctx.emit('session/event', session, { + emitSessionEvent(ctx, session, { type: 'todo/write', seq: 1, time: 0, data: { todos: [ { content: 'read the code', status: 'completed' }, { content: 'write the fix', status: 'in_progress' }, { content: 'run the tests', status: 'pending' }, ] }, - } as SessionEvent) + }) const text = out.text() expect(text).toContain('[todos]') expect(text).toContain('[x] read the code') @@ -387,19 +403,19 @@ describe('createStdioChat rendering', () => { it('resets dim styling when a todo/write interrupts reasoning', async () => { const { ctx, out } = await setup() - ctx.emit('session/event', {} as Session, chunkEvent({ type: 'reasoning-delta', index: 0, text: 'r' })) - ctx.emit('session/event', {} as Session, { + emitSessionEvent(ctx, {} as Session, chunkEvent({ type: 'reasoning-delta', index: 0, text: 'r' })) + emitSessionEvent(ctx, {} as Session, { type: 'todo/write', seq: 1, time: 0, data: { todos: [{ content: 'a task', status: 'pending' }] }, - } as SessionEvent) + }) expect(out.text()).toContain('\x1B[2mr\x1B[0m') }) it('resets dim styling when a tool/call interrupts reasoning', async () => { const { ctx, out } = await setup() const session = {} as Session - ctx.emit('session/event', session, chunkEvent({ type: 'reasoning-delta', index: 0, text: 'r' })) - ctx.emit('session/event', session, { + emitSessionEvent(ctx, session, chunkEvent({ type: 'reasoning-delta', index: 0, text: 'r' })) + emitSessionEvent(ctx, session, { type: 'tool/call', seq: 1, time: 0, data: { turn: 1, step: 0, callId: 'c1', name: 'bash', arguments: '{}' }, } as SessionEvent) @@ -409,10 +425,10 @@ describe('createStdioChat rendering', () => { it('ignores session events it does not render', async () => { const { ctx, out } = await setup() const before = out.text() - ctx.emit('session/event', {} as Session, { + emitSessionEvent(ctx, {} as Session, { type: 'user/message', seq: 1, time: 0, data: { content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }, - } as SessionEvent) + }) expect(out.text()).toBe(before) }) }) @@ -799,7 +815,7 @@ describe('createStdioChat input', () => { ctx.agents.register(agent) await new Promise(r => setImmediate(r)) expect(agent.sent).toEqual([]) - ctx.emit('agent/session-start', agent, 'startup') + emitAgentSessionStart(ctx, agent, 'startup') await new Promise(r => setImmediate(r)) expect(agent.sent).toEqual([[{ type: 'text', text: 'nobody home' }]]) }) @@ -860,9 +876,9 @@ describe('createStdioChat EOF exit', () => { // Work submitted but no 'running' observed yet — must NOT exit. expect(exit).not.toHaveBeenCalled() // The turn starts, then settles. - ctx.emit('agent/status', agent, 'running') + emitAgentStatus(ctx, agent, 'running') ;(agent as { status: AgentStatus }).status = 'idle' - ctx.emit('agent/status', agent, 'idle') + emitAgentStatus(ctx, agent, 'idle') await flushExit() expect(exit).toHaveBeenCalledWith(0) }) @@ -878,12 +894,12 @@ describe('createStdioChat EOF exit', () => { ctx.agents.register(agent) await new Promise(r => setImmediate(r)) expect(agent.sent).toEqual([]) - ctx.emit('agent/session-start', agent, 'startup') + emitAgentSessionStart(ctx, agent, 'startup') await new Promise(r => setImmediate(r)) expect(agent.sent).toEqual([[{ type: 'text', text: 'work' }]]) - ctx.emit('agent/status', agent, 'running') + emitAgentStatus(ctx, agent, 'running') ;(agent as { status: AgentStatus }).status = 'idle' - ctx.emit('agent/status', agent, 'idle') + emitAgentStatus(ctx, agent, 'idle') await flushExit() expect(exit).toHaveBeenCalledWith(0) }) @@ -913,14 +929,14 @@ describe('createStdioChat EOF exit', () => { registerReady(ctx, agent) input.feed('work') await new Promise(r => setImmediate(r)) - ctx.emit('agent/status', agent, 'running') // sawRunning = true + emitAgentStatus(ctx, agent, 'running') // sawRunning = true input.finish() await new Promise(r => setImmediate(r)) // let readline 'close' set stdinClosed ;(agent as { status: AgentStatus }).status = 'idle' // Two idle signals while stdin is already closed: the first arms the timer, // the second must hit the already-scheduled guard, not arm a second. - ctx.emit('agent/status', agent, 'idle') - ctx.emit('agent/status', agent, 'idle') + emitAgentStatus(ctx, agent, 'idle') + emitAgentStatus(ctx, agent, 'idle') await flushExit() expect(exit).toHaveBeenCalledTimes(1) }) @@ -933,8 +949,8 @@ describe('createStdioChat EOF exit', () => { await new Promise(r => setImmediate(r)) input.finish() const other = makeAgent('other') - ctx.emit('agent/status', other, 'running') - ctx.emit('agent/status', other, 'idle') + emitAgentStatus(ctx, other, 'running') + emitAgentStatus(ctx, other, 'idle') await flushExit() expect(exit).not.toHaveBeenCalled() }) @@ -945,11 +961,11 @@ describe('createStdioChat EOF exit', () => { registerReady(ctx, agent) input.feed('work') await new Promise(r => setImmediate(r)) - ctx.emit('agent/status', agent, 'running') + emitAgentStatus(ctx, agent, 'running') ;(agent as { status: AgentStatus }).status = 'running' input.finish() // sawRunning is true, but the agent is still running — the idle gate holds. - ctx.emit('agent/status', agent, 'idle') // a stale/duplicate signal while status stays 'running' + emitAgentStatus(ctx, agent, 'idle') // a stale/duplicate signal while status stays 'running' await flushExit() expect(exit).not.toHaveBeenCalled() }) @@ -998,8 +1014,8 @@ describe('createStdioChat disposal (HMR safety)', () => { // After dispose, status transitions must neither throw nor schedule an exit // (the listener and the EOF-exit path are both torn down). expect(() => { - ctx.emit('agent/status', agent, 'running') - ctx.emit('agent/status', agent, 'idle') + emitAgentStatus(ctx, agent, 'running') + emitAgentStatus(ctx, agent, 'idle') }).not.toThrow() await flushExit() expect(exit).not.toHaveBeenCalled() diff --git a/packages/ui/stdio/tsconfig.json b/packages/ui/stdio/tsconfig.json index e0c578ed32..c37d7ed522 100644 --- a/packages/ui/stdio/tsconfig.json +++ b/packages/ui/stdio/tsconfig.json @@ -28,6 +28,9 @@ }, { "path": "../user-interaction" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/ui/tool-ask-user/package.json b/packages/ui/tool-ask-user/package.json index 5ee90f0818..d2523ce220 100644 --- a/packages/ui/tool-ask-user/package.json +++ b/packages/ui/tool-ask-user/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -23,12 +28,14 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", "@deepseek-ai/dsh-user-interaction": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", diff --git a/packages/ui/tool-ask-user/src/invariant.ts b/packages/ui/tool-ask-user/src/invariant.ts new file mode 100644 index 0000000000..eebb1ced42 --- /dev/null +++ b/packages/ui/tool-ask-user/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-tool-ask-user' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/ui/tool-ask-user/tsconfig.json b/packages/ui/tool-ask-user/tsconfig.json index c779bad37f..6a55e89abe 100644 --- a/packages/ui/tool-ask-user/tsconfig.json +++ b/packages/ui/tool-ask-user/tsconfig.json @@ -31,6 +31,9 @@ }, { "path": "../user-interaction" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/ui/tui/package.json b/packages/ui/tui/package.json index fd4f187e35..35d51fbd2f 100644 --- a/packages/ui/tui/package.json +++ b/packages/ui/tui/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -24,6 +29,7 @@ "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-agent-loop": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", @@ -38,6 +44,7 @@ "@cordisjs/plugin-loader": "workspace:^", "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", diff --git a/packages/ui/tui/src/invariant.ts b/packages/ui/tui/src/invariant.ts new file mode 100644 index 0000000000..cba5fd9d2e --- /dev/null +++ b/packages/ui/tui/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-tui' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/ui/tui/tests/harness.ts b/packages/ui/tui/tests/harness.ts index 9994833308..fcf8105350 100644 --- a/packages/ui/tui/tests/harness.ts +++ b/packages/ui/tui/tests/harness.ts @@ -63,6 +63,11 @@ export async function createTuiTestHarness { await renderAfter(harness, () => { appendUser(harness.session, 'Show the live update.') harness.session.append('assistant/chunk', { - turn: 2, - step: 0, + turn: 1, + step: 1, chunk: { type: 'block-start', index: 0, blockType: 'reasoning' }, }) harness.session.append('assistant/chunk', { - turn: 2, - step: 0, + turn: 1, + step: 1, chunk: { type: 'reasoning-delta', index: 0, text: 'Inspecting width and styles.' }, }) harness.session.append('assistant/chunk', { - turn: 2, - step: 0, + turn: 1, + step: 1, chunk: { type: 'block-start', index: 1, blockType: 'text' }, }) harness.session.append('assistant/chunk', { - turn: 2, - step: 0, + turn: 1, + step: 1, chunk: { type: 'text-delta', index: 1, text: 'Streaming **visible state**…' }, }) }) @@ -345,9 +346,10 @@ describe('TUI terminal-state snapshots', () => { source: { kind: 'user' }, reason: `Unsafe policy ${CONTROL_PROBE}`, }) + session.append('step/end', { turn: 1, step: 1 }) session.append('turn/end', { - turn: 7, - reason: { kind: 'error', step: 2, message: `Unsafe turn error ${CONTROL_PROBE}` }, + turn: 1, + reason: { kind: 'error', step: 1, message: `Unsafe turn error ${CONTROL_PROBE}` }, }) }, }, { columns: 100, rows: 34 }) @@ -369,7 +371,7 @@ describe('TUI terminal-state snapshots', () => { const rejected = expect(answer).rejects.toMatchObject({ code: 'ASK_ABORTED' }) await harness.terminal.waitForFrame(beforeQuestion) await renderAfter(harness, () => { - harness.ctx.emit('agent/error', harness.agent, 8, 3, new Error(`Unsafe live error ${CONTROL_PROBE}`)) + agentEvents(harness.ctx, harness.agent).emit('agent/error', 8, 3, new Error(`Unsafe live error ${CONTROL_PROBE}`)) }) await checkpoint('untrusted-controls', harness.terminal, { includeScrollback: true }) @@ -427,14 +429,14 @@ describe('TUI terminal-state snapshots', () => { }, { surfaceOp: 'append' }) const assistant = session.append('assistant/message', { turn: 1, - step: 0, + step: 1, provenance: { provider: 'mock', model: 'deepseek-v4-flash' }, content: [{ type: 'tool-call', id: CallId('old-tool'), name: 'bash', arguments: '{}' }], }, { surfaceOp: 'append' }) - session.append('tool/call', { turn: 1, step: 0, callId: CallId('old-tool'), name: 'bash', arguments: '{}' }) + session.append('tool/call', { turn: 1, step: 1, callId: CallId('old-tool'), name: 'bash', arguments: '{}' }) const result = session.append('tool/result', { turn: 1, - step: 0, + step: 1, callId: CallId('old-tool'), content: [{ type: 'text', text: 'obsolete output that must disappear' }], isError: false, @@ -470,13 +472,15 @@ describe('TUI terminal-state snapshots', () => { harness.terminal.send('\r') harness.terminal.send('/unknown-advanced-command') harness.terminal.send('\r') - harness.ctx.emit('agent/error', harness.agent, 3, 1, new Error('provider stream failed after partial output')) + agentEvents(harness.ctx, harness.agent).emit('agent/error', 1, 1, new Error('provider stream failed after partial output')) + harness.session.append('step/end', { turn: 1, step: 1 }) harness.session.append('turn/end', { - turn: 3, + turn: 1, reason: { kind: 'error', step: 1, message: 'provider stream failed after partial output' }, }) + harness.session.append('turn/start', { turn: 2, trigger: { kind: 'message', source: { kind: 'user' } } }) harness.session.append('turn/end', { - turn: 4, + turn: 2, reason: { kind: 'interrupted' }, }) }) diff --git a/packages/ui/tui/tests/tui.spec.ts b/packages/ui/tui/tests/tui.spec.ts index 27200e0fa9..c4520090d7 100644 --- a/packages/ui/tui/tests/tui.spec.ts +++ b/packages/ui/tui/tests/tui.spec.ts @@ -3,7 +3,7 @@ import { join } from 'node:path' import { describe, expect, it, vi } from 'vitest' import { Context } from 'cordis' import type { Terminal } from '@earendil-works/pi-tui' -import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' +import AgentRegistry, { agentEvents, type Agent } from '@deepseek-ai/dsh-agent' import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import type { ToolDefinition } from '@deepseek-ai/dsh-tools' import UserInteractionService from '@deepseek-ai/dsh-user-interaction' @@ -174,7 +174,7 @@ describe('pi-tui chat lifecycle and transcript', () => { expect(result.terminal.output).toContain('↑1.3k ↓42') result.agent.status = 'running' - result.ctx.emit('agent/status', result.agent, 'running') + agentEvents(result.ctx, result.agent).emit('agent/status', 'running') result.session.append('user/message', { content: [{ type: 'text', text: ' ' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) result.session.append('steering/message', { turn: 2, content: [{ type: 'text', text: 'steering note' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) result.session.append('steering/message', { turn: 2, content: [{ type: 'text', text: '' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) @@ -182,69 +182,77 @@ describe('pi-tui chat lifecycle and transcript', () => { result.session.append('context/message', { content: [{ type: 'text', text: '' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) result.session.append('prompt/blocked', { content: [{ type: 'text', text: 'blocked' }], source: { kind: 'user' }, reason: 'test policy' }) appendAssistant(result.session, []) - result.session.append('turn/end', { turn: 9, reason: { kind: 'aborted' } }) - result.session.append('turn/end', { turn: 10, reason: { kind: 'completed' } }) - result.session.append('step/start', { turn: 11, step: 0 }) + result.session.append('step/end', { turn: 1, step: 1 }) + result.session.append('turn/end', { turn: 1, reason: { kind: 'aborted' } }) + result.session.append('turn/start', { turn: 2, trigger: { kind: 'message', source: { kind: 'user' } } }) + result.session.append('turn/end', { turn: 2, reason: { kind: 'completed' } }) + result.session.append('turn/start', { turn: 3, trigger: { kind: 'message', source: { kind: 'user' } } }) + result.session.append('step/start', { turn: 3, step: 1 }) result.session.append('assistant/chunk', { - turn: 2, - step: 0, + turn: 3, + step: 1, chunk: { type: 'block-start', index: 0, blockType: 'reasoning' }, }) result.session.append('assistant/chunk', { - turn: 2, - step: 0, + turn: 3, + step: 1, chunk: { type: 'reasoning-delta', index: 0, text: 'live thought' }, }) result.session.append('assistant/chunk', { - turn: 2, - step: 0, + turn: 3, + step: 1, chunk: { type: 'reasoning-delta', index: 9, text: 'unannounced thought' }, }) result.session.append('assistant/chunk', { - turn: 2, - step: 0, + turn: 3, + step: 1, chunk: { type: 'block-end', index: 0, block: { type: 'reasoning', text: 'live thought complete' } }, }) result.session.append('assistant/chunk', { - turn: 2, - step: 0, + turn: 3, + step: 1, chunk: { type: 'block-start', index: 1, blockType: 'text' }, }) result.session.append('assistant/chunk', { - turn: 2, - step: 0, + turn: 3, + step: 1, chunk: { type: 'text-delta', index: 1, text: 'live answer' }, }) result.session.append('assistant/chunk', { - turn: 2, - step: 0, + turn: 3, + step: 1, chunk: { type: 'block-end', index: 1, block: { type: 'text', text: 'live answer done' } }, }) result.session.append('assistant/chunk', { - turn: 2, - step: 0, + turn: 3, + step: 1, chunk: { type: 'block-start', index: 2, blockType: 'tool-call' }, }) result.session.append('assistant/chunk', { - turn: 2, - step: 0, + turn: 3, + step: 1, chunk: { type: 'block-end', index: 2, block: { type: 'tool-call', id: 'stream-tool' as never, name: 'tool', arguments: '{}' } }, }) result.session.append('assistant/chunk', { - turn: 2, - step: 0, + turn: 3, + step: 1, chunk: { type: 'tool-call-delta', index: 2, id: 'stream-tool' as never, argumentsDelta: '{}' }, }) result.session.append('assistant/chunk', { - turn: 2, - step: 0, + turn: 3, + step: 1, chunk: { type: 'usage', usage: { inputTokens: 1, outputTokens: 2 } }, }) await tick() expect(result.terminal.output).toContain('live thought') result.terminal.send('\x12') await tick() - appendAssistant(result.session, [{ type: 'text', text: 'final live answer' }], { inputTokens: 500, outputTokens: 8 }) + appendAssistant( + result.session, + [{ type: 'text', text: 'final live answer' }], + { inputTokens: 500, outputTokens: 8 }, + { turn: 3, step: 1 }, + ) await tick() expect(result.terminal.output).toContain('Working') @@ -258,17 +266,17 @@ describe('pi-tui chat lifecycle and transcript', () => { result.session.append('assistant/chunk', { turn: 3, - step: 0, + step: 1, chunk: { type: 'text-delta', index: 0, text: 'cleared stream' }, }) result.terminal.send('/clear') result.terminal.send('\r') - appendAssistant(result.session, [{ type: 'text', text: 'answer after clear' }]) + appendAssistant(result.session, [{ type: 'text', text: 'answer after clear' }], undefined, { turn: 3, step: 1 }) await tick() expect(result.terminal.output).toContain('answer after clear') result.agent.status = 'idle' - result.ctx.emit('agent/status', result.agent, 'idle') + agentEvents(result.ctx, result.agent).emit('agent/status', 'idle') await tick() expect(result.terminal.progress.at(-1)).toBe(false) await dispose(result) @@ -324,8 +332,8 @@ describe('pi-tui chat lifecycle and transcript', () => { appendUser(session, 'first prompt') appendUser(session, 'second prompt') session.append('assistant/chunk', { - turn: 2, - step: 0, + turn: 1, + step: 1, chunk: { type: 'text-delta', index: 0, text: 'stale partial response' }, }) }, @@ -442,18 +450,25 @@ describe('pi-tui chat lifecycle and transcript', () => { const events = await setup() const unrelatedSession = events.ctx.sessions.create(SessionId('unrelated-session')) const unrelatedAgent = { ...events.agent, id: unrelatedSession.id, session: unrelatedSession } + unrelatedSession.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) unrelatedSession.append('todo/write', { todos: [{ content: 'hidden', status: 'pending' }] }) - events.ctx.emit('agent/status', unrelatedAgent, 'running') - events.ctx.emit('agent/error', unrelatedAgent, 1, 1, new Error('hidden error')) - events.ctx.emit('agent/disposed', unrelatedAgent) - events.ctx.emit('agent/error', events.agent, 3, 2, new Error('live failure')) - events.session.append('turn/end', { turn: 3, reason: { kind: 'error', step: 2, message: 'live failure' } }) - events.session.append('turn/end', { turn: 4, reason: { kind: 'error', step: 1, message: 'durable failure' } }) - events.session.append('turn/end', { turn: 5, reason: { kind: 'aborted', reason: 'stopped' } }) - events.session.append('turn/end', { turn: 6, reason: { kind: 'max-tokens' } }) - events.session.append('turn/end', { turn: 7, reason: { kind: 'rejected', reason: 'policy' } }) - events.session.append('turn/end', { turn: 8, reason: { kind: 'interrupted' } }) - events.ctx.emit('agent/disposed', events.agent) + agentEvents(events.ctx, unrelatedAgent).emit('agent/status', 'running') + agentEvents(events.ctx, unrelatedAgent).emit('agent/error', 1, 1, new Error('hidden error')) + agentEvents(events.ctx, unrelatedAgent).emit('agent/disposed') + agentEvents(events.ctx, events.agent).emit('agent/error', 1, 1, new Error('live failure')) + events.session.append('step/end', { turn: 1, step: 1 }) + events.session.append('turn/end', { turn: 1, reason: { kind: 'error', step: 1, message: 'live failure' } }) + events.session.append('turn/start', { turn: 2, trigger: { kind: 'message', source: { kind: 'user' } } }) + events.session.append('turn/end', { turn: 2, reason: { kind: 'error', step: 1, message: 'durable failure' } }) + events.session.append('turn/start', { turn: 3, trigger: { kind: 'message', source: { kind: 'user' } } }) + events.session.append('turn/end', { turn: 3, reason: { kind: 'aborted', reason: 'stopped' } }) + events.session.append('turn/start', { turn: 4, trigger: { kind: 'message', source: { kind: 'user' } } }) + events.session.append('turn/end', { turn: 4, reason: { kind: 'max-tokens' } }) + events.session.append('turn/start', { turn: 5, trigger: { kind: 'message', source: { kind: 'user' } } }) + events.session.append('turn/end', { turn: 5, reason: { kind: 'rejected', reason: 'policy' } }) + events.session.append('turn/start', { turn: 6, trigger: { kind: 'message', source: { kind: 'user' } } }) + events.session.append('turn/end', { turn: 6, reason: { kind: 'interrupted' } }) + agentEvents(events.ctx, events.agent).emit('agent/disposed') await tick() expect(events.terminal.output).toContain('live failure') expect(events.terminal.output).toContain('durable failure') @@ -546,7 +561,7 @@ describe('tool cards and surface replay', () => { })), ]) for (const [id, name, args] of calls) { - result.session.append('tool/call', { turn: 1, step: 0, callId: id as never, name, arguments: args }) + result.session.append('tool/call', { turn: 1, step: 1, callId: id as never, name, arguments: args }) } await tick() expect(result.terminal.output).toContain('$ raw command') @@ -556,23 +571,23 @@ describe('tool cards and surface replay', () => { expect(result.terminal.output).toContain('call presenter boom') expect(result.terminal.output).toContain('Symbol(input)') result.session.append('tool/result', { - turn: 1, step: 0, callId: 'c1' as never, content: [{ type: 'text', text: 'raw bash' }], isError: false, + turn: 1, step: 1, callId: 'c1' as never, content: [{ type: 'text', text: 'raw bash' }], isError: false, }, { surfaceOp: 'append' }) result.session.append('tool/result', { - turn: 1, step: 0, callId: 'c2' as never, content: [{ type: 'text', text: 'stopped' }], isError: true, + turn: 1, step: 1, callId: 'c2' as never, content: [{ type: 'text', text: 'stopped' }], isError: true, }, { surfaceOp: 'append' }) result.session.append('tool/result', { - turn: 1, step: 0, callId: 'c3' as never, content: [{ type: 'text', text: 'done' }], isError: false, + turn: 1, step: 1, callId: 'c3' as never, content: [{ type: 'text', text: 'done' }], isError: false, }, { surfaceOp: 'append' }) result.session.append('tool/result', { - turn: 1, step: 0, callId: 'c4' as never, content: [{ type: 'text', text: 'raw generic' }], isError: false, + turn: 1, step: 1, callId: 'c4' as never, content: [{ type: 'text', text: 'raw generic' }], isError: false, }, { surfaceOp: 'append' }) result.session.append('tool/result', { - turn: 1, step: 0, callId: 'c5' as never, content: [{ type: 'text', text: 'raw throwing' }], isError: false, + turn: 1, step: 1, callId: 'c5' as never, content: [{ type: 'text', text: 'raw throwing' }], isError: false, meta: { value: 1 }, }, { surfaceOp: 'append' }) result.session.append('tool/result', { - turn: 1, step: 0, callId: 'c7' as never, + turn: 1, step: 1, callId: 'c7' as never, content: [ { type: 'tool-call', id: 'inner' as never, name: 'inner', arguments: '{}' }, { type: 'tool-result', toolCallId: 'inner' as never, content: [{ type: 'text', text: 'nested output' }] }, @@ -581,13 +596,18 @@ describe('tool cards and surface replay', () => { isError: false, }, { surfaceOp: 'append' }) result.session.append('tool/result', { - turn: 1, step: 0, callId: 'c8' as never, content: [{ type: 'text', text: '\nundefined presenter output\n\nkept tail\n' }], isError: false, + turn: 1, step: 1, callId: 'c8' as never, content: [{ type: 'text', text: '\nundefined presenter output\n\nkept tail\n' }], isError: false, }, { surfaceOp: 'append' }) result.session.append('tool/result', { - turn: 1, step: 0, callId: 'c11' as never, content: [{ type: 'text', text: '\nconverted terminal\n\nfinished\n' }], isError: false, + turn: 1, step: 1, callId: 'c11' as never, content: [{ type: 'text', text: '\nconverted terminal\n\nfinished\n' }], isError: false, }, { surfaceOp: 'append' }) result.session.append('tool/result', { - turn: 1, step: 0, callId: 'orphan' as never, content: [{ type: 'text', text: 'orphan result' }], isError: false, + turn: 1, + step: 1, + callId: 'orphan' as never, + content: [{ type: 'text', text: 'orphan result' }], + isError: true, + error: { name: 'InterruptedError', code: 'interrupted' }, }, { surfaceOp: 'append' }) await tick() @@ -623,15 +643,15 @@ describe('tool cards and surface replay', () => { appendUser(result.session, 'old prompt') const assistant = result.session.append('assistant/message', { turn: 1, - step: 0, + step: 1, provenance: { provider: 'mock', model: 'deepseek-v4-flash' }, content: [{ type: 'tool-call', id: 'old-call' as never, name: 'bash', arguments: '{}' }], }, { surfaceOp: 'append' }) result.session.append('tool/call', { - turn: 1, step: 0, callId: 'old-call' as never, name: 'bash', arguments: '{}', + turn: 1, step: 1, callId: 'old-call' as never, name: 'bash', arguments: '{}', }) const toolResult = result.session.append('tool/result', { - turn: 1, step: 0, callId: 'old-call' as never, content: [{ type: 'text', text: 'old output' }], isError: false, + turn: 1, step: 1, callId: 'old-call' as never, content: [{ type: 'text', text: 'old output' }], isError: false, }, { surfaceOp: 'append' }) const start = result.session.surface.nodes[0] as number result.session.append('context/message', { @@ -904,6 +924,8 @@ describe('terminal mounting', () => { await ctx.plugin(UserInteractionService) ctx.provide('tools', { get: () => undefined } as never) const session = ctx.sessions.create(SessionId('failed-start-session')) + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) + session.append('step/start', { turn: 1, step: 1 }) ctx.agents.register({ id: session.id, options: {}, session, status: 'running', ctx, send() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(), @@ -919,7 +941,7 @@ describe('terminal mounting', () => { .rejects.toMatchObject({ code: 'NO_PROVIDER' }) session.append('assistant/chunk', { turn: 1, - step: 0, + step: 1, chunk: { type: 'text-delta', index: 0, text: 'must not render' }, }) await tick() diff --git a/packages/ui/tui/tsconfig.json b/packages/ui/tui/tsconfig.json index 3a09f80ad8..134b87248b 100644 --- a/packages/ui/tui/tsconfig.json +++ b/packages/ui/tui/tsconfig.json @@ -31,6 +31,9 @@ }, { "path": "../user-interaction" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/ui/user-approval/package.json b/packages/ui/user-approval/package.json index 1696a7b603..098a0cc44b 100644 --- a/packages/ui/user-approval/package.json +++ b/packages/ui/user-approval/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -24,6 +29,7 @@ "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-brand": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-scope": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", @@ -36,6 +42,7 @@ "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-brand": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-scope": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", diff --git a/packages/ui/user-approval/src/invariant.ts b/packages/ui/user-approval/src/invariant.ts new file mode 100644 index 0000000000..73dee4f9f1 --- /dev/null +++ b/packages/ui/user-approval/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-user-approval' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/ui/user-approval/tests/approval.spec.ts b/packages/ui/user-approval/tests/approval.spec.ts index 5592063e45..fe2643cb3f 100644 --- a/packages/ui/user-approval/tests/approval.spec.ts +++ b/packages/ui/user-approval/tests/approval.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import { Context } from 'cordis' -import type { Agent } from '@deepseek-ai/dsh-agent' +import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent' import { CallId } from '@deepseek-ai/dsh-llm' import { carrierKeyOf, createScope } from '@deepseek-ai/dsh-scope' import type { Scope } from '@deepseek-ai/dsh-scope' @@ -371,7 +371,7 @@ describe('approval policy (the approval/policy fold)', () => { } const preStep = (ctx: Context, agent: Agent): Promise => - ctx.serial('agent/pre-step', agent, 1, 1, new AbortController().signal) + agentEvents(ctx, agent).serial('agent/pre-step', 1, 1, new AbortController().signal) /** Append a `request/header` snapshot whose system text is exactly `system`. */ function appendHeader(session: Session, system: string): void { diff --git a/packages/ui/user-approval/tsconfig.json b/packages/ui/user-approval/tsconfig.json index fb9a9e6e2d..2fe19338ca 100644 --- a/packages/ui/user-approval/tsconfig.json +++ b/packages/ui/user-approval/tsconfig.json @@ -34,6 +34,9 @@ }, { "path": "../../core/system-prompt" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/ui/user-interaction/package.json b/packages/ui/user-interaction/package.json index f4c9c411fd..bcabc951df 100644 --- a/packages/ui/user-interaction/package.json +++ b/packages/ui/user-interaction/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -23,11 +28,13 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "cordis": "^4.0.0-rc.7" } diff --git a/packages/ui/user-interaction/src/invariant.ts b/packages/ui/user-interaction/src/invariant.ts new file mode 100644 index 0000000000..262681fc06 --- /dev/null +++ b/packages/ui/user-interaction/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-user-interaction' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/ui/user-interaction/tsconfig.json b/packages/ui/user-interaction/tsconfig.json index 178ff39f3f..1361d87c20 100644 --- a/packages/ui/user-interaction/tsconfig.json +++ b/packages/ui/user-interaction/tsconfig.json @@ -19,6 +19,9 @@ }, { "path": "../../llm/llm" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/util/brand/package.json b/packages/util/brand/package.json index 7074aaa621..51ce4d795d 100644 --- a/packages/util/brand/package.json +++ b/packages/util/brand/package.json @@ -11,20 +11,27 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/util/brand/src/invariant.ts b/packages/util/brand/src/invariant.ts new file mode 100644 index 0000000000..e932e7e35e --- /dev/null +++ b/packages/util/brand/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-brand' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/util/brand/tsconfig.json b/packages/util/brand/tsconfig.json index 749cb0208e..d970a00263 100644 --- a/packages/util/brand/tsconfig.json +++ b/packages/util/brand/tsconfig.json @@ -7,5 +7,9 @@ "include": [ "src" ], - "references": [] + "references": [ + { + "path": "../../support/invariants" + } + ] } diff --git a/packages/util/home/package.json b/packages/util/home/package.json index efeaf4832c..07795b1a07 100644 --- a/packages/util/home/package.json +++ b/packages/util/home/package.json @@ -11,20 +11,27 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "cordis": "^4.0.0-rc.6" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "cordis": "^4.0.0-rc.6" } } diff --git a/packages/util/home/src/invariant.ts b/packages/util/home/src/invariant.ts new file mode 100644 index 0000000000..5874a57c1c --- /dev/null +++ b/packages/util/home/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-home' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/util/home/tsconfig.json b/packages/util/home/tsconfig.json index 9770ef25d6..67d4461281 100644 --- a/packages/util/home/tsconfig.json +++ b/packages/util/home/tsconfig.json @@ -5,5 +5,9 @@ "outDir": "lib/types" }, "include": ["src"], - "references": [] + "references": [ + { + "path": "../../support/invariants" + } + ] } diff --git a/packages/util/paths/package.json b/packages/util/paths/package.json index b4f760afe9..601a2941b2 100644 --- a/packages/util/paths/package.json +++ b/packages/util/paths/package.json @@ -11,20 +11,27 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "cordis": "^4.0.0-rc.6" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "cordis": "^4.0.0-rc.6" } } diff --git a/packages/util/paths/src/invariant.ts b/packages/util/paths/src/invariant.ts new file mode 100644 index 0000000000..c2cedfbb0d --- /dev/null +++ b/packages/util/paths/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-paths' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/util/paths/tsconfig.json b/packages/util/paths/tsconfig.json index 749cb0208e..d970a00263 100644 --- a/packages/util/paths/tsconfig.json +++ b/packages/util/paths/tsconfig.json @@ -7,5 +7,9 @@ "include": [ "src" ], - "references": [] + "references": [ + { + "path": "../../support/invariants" + } + ] } diff --git a/packages/util/retention/package.json b/packages/util/retention/package.json index db8bab3342..312bebd624 100644 --- a/packages/util/retention/package.json +++ b/packages/util/retention/package.json @@ -11,20 +11,27 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "cordis": "^4.0.0-rc.6" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "cordis": "^4.0.0-rc.6" } } diff --git a/packages/util/retention/src/invariant.ts b/packages/util/retention/src/invariant.ts new file mode 100644 index 0000000000..7516f9e5ed --- /dev/null +++ b/packages/util/retention/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-retention' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/util/retention/tsconfig.json b/packages/util/retention/tsconfig.json index 749cb0208e..d970a00263 100644 --- a/packages/util/retention/tsconfig.json +++ b/packages/util/retention/tsconfig.json @@ -7,5 +7,9 @@ "include": [ "src" ], - "references": [] + "references": [ + { + "path": "../../support/invariants" + } + ] } diff --git a/packages/util/timeout/package.json b/packages/util/timeout/package.json index 381b9d269e..615d21759e 100644 --- a/packages/util/timeout/package.json +++ b/packages/util/timeout/package.json @@ -11,20 +11,27 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/util/timeout/src/invariant.ts b/packages/util/timeout/src/invariant.ts new file mode 100644 index 0000000000..15140bb880 --- /dev/null +++ b/packages/util/timeout/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-timeout' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/util/timeout/tsconfig.json b/packages/util/timeout/tsconfig.json index 749cb0208e..d970a00263 100644 --- a/packages/util/timeout/tsconfig.json +++ b/packages/util/timeout/tsconfig.json @@ -7,5 +7,9 @@ "include": [ "src" ], - "references": [] + "references": [ + { + "path": "../../support/invariants" + } + ] } diff --git a/packages/web/tool-web/package.json b/packages/web/tool-web/package.json index 23ed7157a2..ec1d33f4d4 100644 --- a/packages/web/tool-web/package.json +++ b/packages/web/tool-web/package.json @@ -11,17 +11,23 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-system-prompt": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", @@ -33,13 +39,14 @@ }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-spill-local": "workspace:^", "@deepseek-ai/dsh-spill-policy": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", - "@deepseek-ai/dsh-tools": "workspace:^", "@deepseek-ai/dsh-timeout-policy": "workspace:^", + "@deepseek-ai/dsh-tools": "workspace:^", "@deepseek-ai/dsh-web": "workspace:^", "@deepseek-ai/dsh-web-fetch-local": "workspace:^", "@deepseek-ai/dsh-web-search-exa": "workspace:^", diff --git a/packages/web/tool-web/src/invariant.ts b/packages/web/tool-web/src/invariant.ts new file mode 100644 index 0000000000..008fe2f5e1 --- /dev/null +++ b/packages/web/tool-web/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-tool-web' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/web/tool-web/tsconfig.json b/packages/web/tool-web/tsconfig.json index 5226425ec6..f6684272ce 100644 --- a/packages/web/tool-web/tsconfig.json +++ b/packages/web/tool-web/tsconfig.json @@ -6,13 +6,32 @@ }, "include": ["src"], "references": [ - { "path": "../../../vendor/cosmokit" }, - { "path": "../../../vendor/cordis" }, - { "path": "../../../vendor/schemastery" }, - { "path": "../../llm/llm" }, - { "path": "../../core/tools" }, - { "path": "../../core/system-prompt" }, - { "path": "../../timeout/timeout-policy" }, - { "path": "../web" } + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../../vendor/schemastery" + }, + { + "path": "../../llm/llm" + }, + { + "path": "../../core/tools" + }, + { + "path": "../../core/system-prompt" + }, + { + "path": "../../timeout/timeout-policy" + }, + { + "path": "../web" + }, + { + "path": "../../support/invariants" + } ] } diff --git a/packages/web/web-fetch-local/package.json b/packages/web/web-fetch-local/package.json index 1e1d7ea71b..5c42bfa09c 100644 --- a/packages/web/web-fetch-local/package.json +++ b/packages/web/web-fetch-local/package.json @@ -11,17 +11,23 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-timeout": "^0.0.1", "@deepseek-ai/dsh-web": "^0.0.1", "cordis": "^4.0.0-rc.7" @@ -30,6 +36,7 @@ "schemastery": "^3.18.0" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-timeout": "workspace:^", "@deepseek-ai/dsh-web": "workspace:^", "cordis": "^4.0.0-rc.7" diff --git a/packages/web/web-fetch-local/src/invariant.ts b/packages/web/web-fetch-local/src/invariant.ts new file mode 100644 index 0000000000..ef61e2a611 --- /dev/null +++ b/packages/web/web-fetch-local/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-web-fetch-local' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/web/web-fetch-local/tsconfig.json b/packages/web/web-fetch-local/tsconfig.json index c6fb75a5c1..29e7c5078c 100644 --- a/packages/web/web-fetch-local/tsconfig.json +++ b/packages/web/web-fetch-local/tsconfig.json @@ -22,6 +22,9 @@ }, { "path": "../web" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/web/web-search-deepseek/package.json b/packages/web/web-search-deepseek/package.json index 42471006c0..b38f552957 100644 --- a/packages/web/web-search-deepseek/package.json +++ b/packages/web/web-search-deepseek/package.json @@ -11,17 +11,23 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-web": "^0.0.1", "cordis": "^4.0.0-rc.7" }, @@ -29,6 +35,7 @@ "schemastery": "^3.18.0" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-web": "workspace:^", "cordis": "^4.0.0-rc.7" } diff --git a/packages/web/web-search-deepseek/src/invariant.ts b/packages/web/web-search-deepseek/src/invariant.ts new file mode 100644 index 0000000000..8781949be9 --- /dev/null +++ b/packages/web/web-search-deepseek/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + * @module @deepseek-ai/dsh-web-search-deepseek/invariant + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-web-search-deepseek' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/web/web-search-deepseek/tsconfig.json b/packages/web/web-search-deepseek/tsconfig.json index aa7c949fec..e9610ea5c9 100644 --- a/packages/web/web-search-deepseek/tsconfig.json +++ b/packages/web/web-search-deepseek/tsconfig.json @@ -19,6 +19,9 @@ }, { "path": "../web" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/web/web-search-exa/package.json b/packages/web/web-search-exa/package.json index 240909e24a..7d6b802d2e 100644 --- a/packages/web/web-search-exa/package.json +++ b/packages/web/web-search-exa/package.json @@ -11,17 +11,23 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-web": "^0.0.1", "cordis": "^4.0.0-rc.7" }, @@ -29,6 +35,7 @@ "schemastery": "^3.18.0" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-web": "workspace:^", "cordis": "^4.0.0-rc.7" } diff --git a/packages/web/web-search-exa/src/invariant.ts b/packages/web/web-search-exa/src/invariant.ts new file mode 100644 index 0000000000..a2dd956625 --- /dev/null +++ b/packages/web/web-search-exa/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-web-search-exa' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/web/web-search-exa/tsconfig.json b/packages/web/web-search-exa/tsconfig.json index aa7c949fec..e9610ea5c9 100644 --- a/packages/web/web-search-exa/tsconfig.json +++ b/packages/web/web-search-exa/tsconfig.json @@ -19,6 +19,9 @@ }, { "path": "../web" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/web/web-search-perplexity/package.json b/packages/web/web-search-perplexity/package.json index 26d077fada..9aa7080431 100644 --- a/packages/web/web-search-perplexity/package.json +++ b/packages/web/web-search-perplexity/package.json @@ -11,17 +11,23 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-web": "^0.0.1", "cordis": "^4.0.0-rc.7" }, @@ -29,6 +35,7 @@ "schemastery": "^3.18.0" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-web": "workspace:^", "cordis": "^4.0.0-rc.7" } diff --git a/packages/web/web-search-perplexity/src/invariant.ts b/packages/web/web-search-perplexity/src/invariant.ts new file mode 100644 index 0000000000..fe82c79dae --- /dev/null +++ b/packages/web/web-search-perplexity/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + * @module @deepseek-ai/dsh-web-search-perplexity/invariant + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-web-search-perplexity' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/web/web-search-perplexity/tsconfig.json b/packages/web/web-search-perplexity/tsconfig.json index aa7c949fec..e9610ea5c9 100644 --- a/packages/web/web-search-perplexity/tsconfig.json +++ b/packages/web/web-search-perplexity/tsconfig.json @@ -19,6 +19,9 @@ }, { "path": "../web" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/web/web/package.json b/packages/web/web/package.json index b94f7ba685..7b732ec819 100644 --- a/packages/web/web/package.json +++ b/packages/web/web/package.json @@ -11,17 +11,23 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" ], "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "cordis": "^4.0.0-rc.7" }, @@ -29,6 +35,7 @@ "schemastery": "^3.18.0" }, "devDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "cordis": "^4.0.0-rc.7" } diff --git a/packages/web/web/src/invariant.ts b/packages/web/web/src/invariant.ts new file mode 100644 index 0000000000..b9b1b0d45d --- /dev/null +++ b/packages/web/web/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-web' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/web/web/tsconfig.json b/packages/web/web/tsconfig.json index e9de391ba1..d145ddb6ee 100644 --- a/packages/web/web/tsconfig.json +++ b/packages/web/web/tsconfig.json @@ -19,6 +19,9 @@ }, { "path": "../../llm/llm" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/workflow/tool-workflow/package.json b/packages/workflow/tool-workflow/package.json index 327e6ec3a9..dd055ad9ec 100644 --- a/packages/workflow/tool-workflow/package.json +++ b/packages/workflow/tool-workflow/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -23,6 +28,7 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-system-prompt": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", @@ -34,6 +40,7 @@ }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-subagent": "workspace:^", diff --git a/packages/workflow/tool-workflow/src/invariant.ts b/packages/workflow/tool-workflow/src/invariant.ts new file mode 100644 index 0000000000..cd1f0e475b --- /dev/null +++ b/packages/workflow/tool-workflow/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-tool-workflow' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/workflow/tool-workflow/tsconfig.json b/packages/workflow/tool-workflow/tsconfig.json index f66eda75a7..c08ae597f2 100644 --- a/packages/workflow/tool-workflow/tsconfig.json +++ b/packages/workflow/tool-workflow/tsconfig.json @@ -31,6 +31,9 @@ }, { "path": "../workflow" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/workflow/workflow-workerthread/package.json b/packages/workflow/workflow-workerthread/package.json index 1cd6e07e17..a915229449 100644 --- a/packages/workflow/workflow-workerthread/package.json +++ b/packages/workflow/workflow-workerthread/package.json @@ -11,6 +11,10 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./worker": { "types": "./lib/types/worker.d.ts", "default": "./lib/worker.cjs" @@ -20,6 +24,7 @@ }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/worker.cjs", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", @@ -29,6 +34,7 @@ "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-brand": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-subagent": "^0.0.1", diff --git a/packages/workflow/workflow-workerthread/src/invariant.ts b/packages/workflow/workflow-workerthread/src/invariant.ts new file mode 100644 index 0000000000..6bcc40862c --- /dev/null +++ b/packages/workflow/workflow-workerthread/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + * @module @deepseek-ai/dsh-workflow-workerthread/invariant + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-workflow-workerthread' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/workflow/workflow-workerthread/tsconfig.json b/packages/workflow/workflow-workerthread/tsconfig.json index 730a3e61d9..37d90aaea1 100644 --- a/packages/workflow/workflow-workerthread/tsconfig.json +++ b/packages/workflow/workflow-workerthread/tsconfig.json @@ -37,6 +37,9 @@ }, { "path": "../workflow" + }, + { + "path": "../../support/invariants" } ] } diff --git a/packages/workflow/workflow-workerthread/tsdown.config.ts b/packages/workflow/workflow-workerthread/tsdown.config.ts index 8ebd93d89f..962a3d9078 100644 --- a/packages/workflow/workflow-workerthread/tsdown.config.ts +++ b/packages/workflow/workflow-workerthread/tsdown.config.ts @@ -7,7 +7,7 @@ import { defineConfig } from 'tsdown' */ export default defineConfig([ { - entry: ['lib/types/index.js'], + entry: ['lib/types/index.js', 'lib/types/invariant.js'], outDir: 'lib', format: ['esm'], platform: 'node', diff --git a/packages/workflow/workflow/package.json b/packages/workflow/workflow/package.json index 476866382a..b8ce1ebc7f 100644 --- a/packages/workflow/workflow/package.json +++ b/packages/workflow/workflow/package.json @@ -11,11 +11,16 @@ "types": "./lib/types/index.d.ts", "default": "./lib/index.js" }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, "./src/*": "./src/*", "./package.json": "./package.json" }, "files": [ "lib/index.js", + "lib/invariant.js", "lib/types/**/*.d.ts", "lib/types/**/*.d.ts.map", "src" @@ -24,6 +29,7 @@ "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-brand": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "cordis": "^4.0.0-rc.7" @@ -31,6 +37,7 @@ "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-brand": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "cordis": "^4.0.0-rc.7" diff --git a/packages/workflow/workflow/src/invariant.ts b/packages/workflow/workflow/src/invariant.ts new file mode 100644 index 0000000000..b552021ca4 --- /dev/null +++ b/packages/workflow/workflow/src/invariant.ts @@ -0,0 +1,30 @@ +/** + * 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 + */ + +/* jscpd:ignore-start */ +import type { Context } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-workflow' + +/** Cordis companion plugin name. */ +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 = () => {} + +/** + * 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 */ diff --git a/packages/workflow/workflow/tsconfig.json b/packages/workflow/workflow/tsconfig.json index 6ec42e0bfe..76ad9f725a 100644 --- a/packages/workflow/workflow/tsconfig.json +++ b/packages/workflow/workflow/tsconfig.json @@ -22,6 +22,9 @@ }, { "path": "../../llm/llm" + }, + { + "path": "../../support/invariants" } ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43aae285c7..b13f346cd5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -209,6 +209,9 @@ importers: packages/bash/bash: devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-sandbox': specifier: workspace:^ version: link:../../sandbox/sandbox @@ -228,6 +231,9 @@ importers: '@deepseek-ai/dsh-bash': specifier: workspace:^ version: link:../bash + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-timeout': specifier: workspace:^ version: link:../../util/timeout @@ -247,6 +253,9 @@ importers: '@deepseek-ai/dsh-bash-local': specifier: workspace:^ version: link:../bash-local + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-sandbox': specifier: workspace:^ version: link:../../sandbox/sandbox @@ -284,6 +293,9 @@ importers: '@deepseek-ai/dsh-home': specifier: workspace:^ version: link:../../util/home + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -320,6 +332,9 @@ importers: packages/code-runtime/code-runtime: devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants cordis: specifier: ^4.0.0-rc.7 version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) @@ -333,12 +348,18 @@ importers: '@deepseek-ai/dsh-code-runtime': specifier: workspace:^ version: link:../code-runtime + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants cordis: specifier: ^4.0.0-rc.7 version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/compact/compact: devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -407,6 +428,9 @@ importers: '@deepseek-ai/dsh-agent-loop-testkit': specifier: workspace:^ version: link:../../support/agent-loop-testkit + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -447,6 +471,9 @@ importers: '@deepseek-ai/dsh-fs-local': specifier: workspace:^ version: link:../../fs/fs-local + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -493,6 +520,9 @@ importers: '@deepseek-ai/dsh-agent-loop-testkit': specifier: workspace:^ version: link:../../support/agent-loop-testkit + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -606,6 +636,9 @@ importers: specifier: ^3.18.0 version: 3.18.0 devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -628,6 +661,9 @@ importers: '@deepseek-ai/dsh-code-runtime': specifier: workspace:^ version: link:../../code-runtime/code-runtime + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -667,6 +703,9 @@ importers: '@deepseek-ai/dsh-app-boot': specifier: workspace:^ version: link:../../ui/app-boot + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:^ version: link:../../session-persistence/session-persistence-jsonl @@ -770,6 +809,9 @@ importers: '@deepseek-ai/dsh-app-boot': specifier: workspace:^ version: link:../../ui/app-boot + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -801,6 +843,9 @@ importers: specifier: workspace:^ version: link:../../ui/app-boot devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants cordis: specifier: ^4.0.0-rc.7 version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) @@ -828,6 +873,9 @@ importers: '@deepseek-ai/dsh-app-boot': specifier: workspace:^ version: link:../../ui/app-boot + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -870,6 +918,9 @@ importers: '@deepseek-ai/dsh-brand': specifier: workspace:^ version: link:../../util/brand + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -886,6 +937,9 @@ importers: '@deepseek-ai/dsh-fs': specifier: workspace:^ version: link:../fs + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -898,6 +952,9 @@ importers: '@deepseek-ai/dsh-fs': specifier: workspace:^ version: link:../fs + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -932,6 +989,9 @@ importers: '@deepseek-ai/dsh-fs-policy': specifier: workspace:^ version: link:../fs-policy + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -966,6 +1026,9 @@ importers: '@deepseek-ai/dsh-bash-local': specifier: workspace:^ version: link:../../bash/bash-local + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -1003,6 +1066,9 @@ importers: '@deepseek-ai/dsh-agent-loop-testkit': specifier: workspace:^ version: link:../../support/agent-loop-testkit + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -1021,6 +1087,9 @@ importers: '@deepseek-ai/dsh-bash': specifier: workspace:^ version: link:../../bash/bash + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session @@ -1052,6 +1121,9 @@ importers: '@deepseek-ai/dsh-hook-protocol': specifier: workspace:^ version: link:../hook-protocol + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -1098,6 +1170,9 @@ importers: '@deepseek-ai/dsh-hook-protocol': specifier: workspace:^ version: link:../hook-protocol + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -1122,6 +1197,9 @@ importers: '@deepseek-ai/dsh-brand': specifier: workspace:^ version: link:../../util/brand + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants cordis: specifier: ^4.0.0-rc.7 version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) @@ -1132,6 +1210,9 @@ importers: specifier: ^3.18.0 version: 3.18.0 devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../llm @@ -1148,6 +1229,9 @@ importers: specifier: ^3.18.0 version: 3.18.0 devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../llm @@ -1164,6 +1248,9 @@ importers: specifier: ^3.18.0 version: 3.18.0 devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../llm @@ -1183,6 +1270,9 @@ importers: specifier: ^3.18.0 version: 3.18.0 devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -1204,6 +1294,9 @@ importers: packages/sandbox/sandbox: devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -1220,6 +1313,9 @@ importers: specifier: ^3.18.0 version: 3.18.0 devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -1239,6 +1335,9 @@ importers: specifier: ^15.0.0 version: 15.0.0 devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants cordis: specifier: ^4.0.0-rc.7 version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) @@ -1270,6 +1369,9 @@ importers: '@deepseek-ai/dsh-hooks-codex': specifier: workspace:^ version: link:../../hooks/hooks-codex + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:^ version: link:../../session-persistence/session-persistence-jsonl @@ -1301,6 +1403,9 @@ importers: '@deepseek-ai/dsh-app-boot': specifier: workspace:^ version: link:../../ui/app-boot + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants cordis: specifier: ^4.0.0-rc.7 version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) @@ -1313,6 +1418,12 @@ importers: packages/session-persistence/session-persistence: devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants + '@deepseek-ai/dsh-scope': + specifier: workspace:^ + version: link:../../core/scope '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session @@ -1326,6 +1437,9 @@ importers: specifier: ^3.18.0 version: 3.18.0 devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session @@ -1342,6 +1456,9 @@ importers: specifier: ^3.18.0 version: 3.18.0 devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session @@ -1358,6 +1475,9 @@ importers: specifier: ^3.18.0 version: 3.18.0 devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -1377,6 +1497,9 @@ importers: specifier: ^3.18.0 version: 3.18.0 devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants cordis: specifier: ^4.0.0-rc.7 version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) @@ -1396,6 +1519,9 @@ importers: '@deepseek-ai/dsh-home': specifier: workspace:^ version: link:../../util/home + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-skill': specifier: workspace:^ version: link:../skill @@ -1412,6 +1538,9 @@ importers: '@deepseek-ai/dsh-agent': specifier: workspace:^ version: link:../../core/agent + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -1436,6 +1565,9 @@ importers: '@deepseek-ai/dsh-brand': specifier: workspace:^ version: link:../../util/brand + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -1455,6 +1587,9 @@ importers: '@deepseek-ai/dsh-brand': specifier: workspace:^ version: link:../../util/brand + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -1477,6 +1612,9 @@ importers: '@deepseek-ai/dsh-agent': specifier: workspace:^ version: link:../../core/agent + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -1504,6 +1642,9 @@ importers: '@deepseek-ai/dsh-brand': specifier: workspace:^ version: link:../../util/brand + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -1535,6 +1676,9 @@ importers: '@deepseek-ai/dsh-agent': specifier: workspace:^ version: link:../../core/agent + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -1678,6 +1822,9 @@ importers: packages/subagent/subagent-subprocess: devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants cordis: specifier: ^4.0.0-rc.7 version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) @@ -1694,6 +1841,9 @@ importers: '@deepseek-ai/dsh-agent': specifier: workspace:^ version: link:../../core/agent + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -1728,6 +1878,9 @@ importers: specifier: ^4.1.8 version: 4.1.8(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../invariants cordis: specifier: ^4.0.0-rc.7 version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) @@ -1740,6 +1893,9 @@ importers: '@deepseek-ai/dsh-agent-loop': specifier: workspace:^ version: link:../../core/agent-loop + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -1768,6 +1924,9 @@ importers: packages/support/llm-replay: devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -1784,6 +1943,9 @@ importers: specifier: ^4.22.4 version: 4.22.4 devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../invariants cordis: specifier: ^4.0.0-rc.6 version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) @@ -1796,6 +1958,9 @@ importers: '@deepseek-ai/dsh-brand': specifier: workspace:^ version: link:../../util/brand + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session @@ -1815,6 +1980,9 @@ importers: '@deepseek-ai/dsh-agent': specifier: workspace:^ version: link:../../core/agent + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -1836,6 +2004,9 @@ importers: packages/timeout/timeout-policy: devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -1860,6 +2031,9 @@ importers: '@deepseek-ai/dsh-agent-loop-testkit': specifier: workspace:^ version: link:../../support/agent-loop-testkit + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -1966,6 +2140,9 @@ importers: '@cordisjs/plugin-loader': specifier: workspace:^ version: link:../../../vendor/loader + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants cordis: specifier: ^4.0.0-rc.7 version: 4.0.0-rc.7(@cordisjs/plugin-include@vendor+include)(@cordisjs/plugin-loader@vendor+loader) @@ -1985,6 +2162,9 @@ importers: '@deepseek-ai/dsh-agent-spine-demo': specifier: workspace:^ version: link:../../examples/agent-spine-demo + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -2016,6 +2196,9 @@ importers: '@deepseek-ai/dsh-bash': specifier: workspace:^ version: link:../../bash/bash + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-sandbox': specifier: workspace:^ version: link:../../sandbox/sandbox @@ -2044,6 +2227,9 @@ importers: '@deepseek-ai/dsh-agent-loop': specifier: workspace:^ version: link:../../core/agent-loop + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -2062,6 +2248,9 @@ importers: '@deepseek-ai/dsh-agent': specifier: workspace:^ version: link:../../core/agent + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -2096,6 +2285,9 @@ importers: '@deepseek-ai/dsh-agent-loop': specifier: workspace:^ version: link:../../core/agent-loop + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -2139,6 +2331,9 @@ importers: '@deepseek-ai/dsh-brand': specifier: workspace:^ version: link:../../util/brand + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -2160,6 +2355,9 @@ importers: '@deepseek-ai/dsh-agent': specifier: workspace:^ version: link:../../core/agent + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -2169,30 +2367,45 @@ importers: packages/util/brand: devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants cordis: specifier: ^4.0.0-rc.7 version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/util/home: devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants cordis: specifier: ^4.0.0-rc.6 version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/util/paths: devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants cordis: specifier: ^4.0.0-rc.6 version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/util/retention: devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants cordis: specifier: ^4.0.0-rc.6 version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/util/timeout: devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants cordis: specifier: ^4.0.0-rc.7 version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) @@ -2206,6 +2419,9 @@ importers: '@deepseek-ai/dsh-agent': specifier: workspace:^ version: link:../../core/agent + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -2246,6 +2462,9 @@ importers: specifier: ^3.18.0 version: 3.18.0 devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -2259,6 +2478,9 @@ importers: specifier: ^3.18.0 version: 3.18.0 devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-timeout': specifier: workspace:^ version: link:../../util/timeout @@ -2275,6 +2497,9 @@ importers: specifier: ^3.18.0 version: 3.18.0 devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-web': specifier: workspace:^ version: link:../web @@ -2288,6 +2513,9 @@ importers: specifier: ^3.18.0 version: 3.18.0 devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-web': specifier: workspace:^ version: link:../web @@ -2301,6 +2529,9 @@ importers: specifier: ^3.18.0 version: 3.18.0 devDependencies: + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-web': specifier: workspace:^ version: link:../web @@ -2317,6 +2548,9 @@ importers: '@deepseek-ai/dsh-agent': specifier: workspace:^ version: link:../../core/agent + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -2350,6 +2584,9 @@ importers: '@deepseek-ai/dsh-brand': specifier: workspace:^ version: link:../../util/brand + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm diff --git a/scripts/check-workspace-constraints.ts b/scripts/check-workspace-constraints.ts index 224f8540b8..2a9cf01bdb 100644 --- a/scripts/check-workspace-constraints.ts +++ b/scripts/check-workspace-constraints.ts @@ -93,37 +93,6 @@ function workspaceManifests(): WorkspaceManifest[] { return manifests } -const dshPackageFiles = [ - 'lib/index.js', - 'lib/types/**/*.d.ts', - 'lib/types/**/*.d.ts.map', - 'src', -] as const - -const dshBinPackageFiles = [ - 'lib/index.js', - 'lib/bin.js', - 'lib/types/**/*.d.ts', - 'lib/types/**/*.d.ts.map', - 'src', -] as const - -const dshWorkerPackageFiles = [ - 'lib/index.js', - 'lib/worker.cjs', - 'lib/types/**/*.d.ts', - 'lib/types/**/*.d.ts.map', - 'src', -] as const - -const dshInvariantPackageFiles = [ - 'lib/index.js', - 'lib/invariant.js', - 'lib/types/**/*.d.ts', - 'lib/types/**/*.d.ts.map', - 'src', -] as const - const packageFileExtras: Readonly> = { '@deepseek-ai/dsh-helper': ['lib/assets'], '@deepseek-ai/dsh-scripts': [ @@ -139,25 +108,18 @@ function sameStringList(actual: readonly string[] | undefined, expected: readonl function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] { const extras = manifest.name ? packageFileExtras[manifest.name] ?? [] : [] - if (extras.length > 0) { - return [ - 'lib/index.js', - ...manifest.bin ? ['lib/bin.js'] : [], - ...extras, - 'lib/types/**/*.d.ts', - 'lib/types/**/*.d.ts.map', - 'src', - ] - } - if (manifest.bin) return dshBinPackageFiles - // Package-owned diagnostic companions are separately bundled optional - // entries; their source stays in the owner package without bloating root. - if (manifest.exports?.['./invariant']) return dshInvariantPackageFiles - // A declared "./worker" subpath export sanctions the one extra runtime - // bundle a worker-thread entry needs (and NodeNext/publint then validate - // that subpath's targets like any other export). - if (manifest.exports?.['./worker']) return dshWorkerPackageFiles - return dshPackageFiles + return [ + 'lib/index.js', + // Every package publishes its invariant ownership companion as a separate + // bundle; the package-invariant gate validates the companion itself. + 'lib/invariant.js', + ...manifest.bin ? ['lib/bin.js'] : [], + ...manifest.exports?.['./worker'] ? ['lib/worker.cjs'] : [], + ...extras, + 'lib/types/**/*.d.ts', + 'lib/types/**/*.d.ts.map', + 'src', + ] } function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] { diff --git a/scripts/gen-package-invariants.ts b/scripts/gen-package-invariants.ts new file mode 100644 index 0000000000..d30ce5a3eb --- /dev/null +++ b/scripts/gen-package-invariants.ts @@ -0,0 +1,44 @@ +/** 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 new file mode 100644 index 0000000000..698969b3d7 --- /dev/null +++ b/scripts/package-invariants.spec.ts @@ -0,0 +1,104 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { + collectPackageInvariantViolations, + packageInvariantOwners, + renderBaselineInvariant, +} from './package-invariants.ts' + +const roots: string[] = [] + +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) +}) + +function fixture(options: { + packageName?: string + source?: string + invariantExport?: boolean + invariantDependency?: boolean + invariantReference?: boolean + buildEntry?: boolean +} = {}): string { + const root = mkdtempSync(join(tmpdir(), 'dsh-package-invariants-')) + roots.push(root) + const dir = join(root, 'packages/core/probe') + mkdirSync(join(dir, 'src'), { recursive: true }) + const packageName = options.packageName ?? '@deepseek-ai/dsh-probe' + const manifest = { + name: packageName, + exports: options.invariantExport === false ? {} : { + './invariant': { + types: './lib/types/invariant.d.ts', + default: './lib/invariant.js', + }, + }, + files: ['lib/index.js', 'lib/invariant.js', 'src'], + peerDependencies: options.invariantDependency === false ? {} : { + '@deepseek-ai/dsh-invariants': '^0.0.1', + }, + devDependencies: options.invariantDependency === false ? {} : { + '@deepseek-ai/dsh-invariants': 'workspace:^', + }, + } + writeFileSync(join(dir, 'package.json'), `${JSON.stringify(manifest, null, 2)}\n`) + 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, '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", + ) + return root +} + +describe('package invariant gate', () => { + it('accepts a generated owner companion with publication metadata', () => { + expect(collectPackageInvariantViolations(fixture())).toEqual([]) + }) + + it('rejects missing publication metadata and build output', () => { + const violations = collectPackageInvariantViolations(fixture({ + invariantExport: false, + invariantDependency: false, + invariantReference: false, + buildEntry: false, + })) + expect(violations.map(violation => violation.message)).toEqual(expect.arrayContaining([ + expect.stringContaining('exports["./invariant"]'), + expect.stringContaining('peerDependency'), + expect.stringContaining('devDependency'), + expect.stringContaining('TypeScript project references'), + expect.stringContaining('must bundle lib/types/invariant.js'), + ])) + }) + + it('rejects foreign, duplicate, and unresolved registrations', () => { + const source = ` +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 violations = collectPackageInvariantViolations(fixture({ source })) + expect(violations.map(violation => violation.message)).toEqual(expect.arrayContaining([ + expect.stringContaining('must resolve to a local string constant'), + expect.stringContaining('must register exactly its own package name'), + ])) + }) + + 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`) + expect(collectPackageInvariantViolations(root).map(violation => violation.message)) + .toContain('generated baseline is stale; run pnpm run gen-package-invariants') + }) +}) diff --git a/scripts/package-invariants.ts b/scripts/package-invariants.ts new file mode 100644 index 0000000000..45fb64829f --- /dev/null +++ b/scripts/package-invariants.ts @@ -0,0 +1,283 @@ +/** + * Package-invariant companion discovery, generation, 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 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 + files?: string[] + peerDependencies?: Record + devDependencies?: Record +} + +/** One package and the files participating in its invariant publication contract. */ +export interface PackageInvariantOwner { + readonly dir: string + readonly manifestPath: string + readonly sourcePath: string + readonly packageName: string +} + +/** One gate violation with a repo-relative owner path. */ +export interface PackageInvariantViolation { + readonly path: string + readonly message: string +} + +/** Discover every package under the repository package tree. */ +export function packageInvariantOwners(root: string): PackageInvariantOwner[] { + return globSync('packages/*/*/package.json', { cwd: root }) + .map(path => path.split(sep).join('/')) + .sort() + .map((manifestPath) => { + const manifest = readManifest(resolve(root, manifestPath)) + if (manifest.name === undefined || manifest.name === '') { + throw new Error(`${manifestPath}: package invariant owner must declare a package name`) + } + const dir = dirname(manifestPath) + return { + dir, + manifestPath, + sourcePath: `${dir}/src/invariant.ts`, + packageName: manifest.name, + } + }) +} + +/** 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[] = [] + 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) + } + return violations +} + +function readManifest(path: string): PackageManifest { + return JSON.parse(readFileSync(path, 'utf8')) as PackageManifest +} + +function addViolation( + violations: PackageInvariantViolation[], + path: string, + message: string, +): void { + violations.push({ path, message }) +} + +function checkManifest( + owner: PackageInvariantOwner, + manifest: PackageManifest, + violations: PackageInvariantViolation[], +): void { + const invariantExport = manifest.exports?.['./invariant'] + if (typeof invariantExport !== 'object' + || invariantExport.types !== './lib/types/invariant.d.ts' + || invariantExport.default !== './lib/invariant.js') { + addViolation( + violations, + owner.manifestPath, + 'exports["./invariant"] must target ./lib/types/invariant.d.ts and ./lib/invariant.js', + ) + } + if (!manifest.files?.includes('lib/invariant.js')) { + addViolation(violations, owner.manifestPath, 'files must publish lib/invariant.js') + } + if (owner.packageName === '@deepseek-ai/dsh-invariants') return + if (manifest.peerDependencies?.['@deepseek-ai/dsh-invariants'] !== '^0.0.1') { + addViolation( + violations, + owner.manifestPath, + '@deepseek-ai/dsh-invariants must be a ^0.0.1 peerDependency', + ) + } + if (manifest.devDependencies?.['@deepseek-ai/dsh-invariants'] !== 'workspace:^') { + addViolation( + violations, + owner.manifestPath, + '@deepseek-ai/dsh-invariants must also be a workspace:^ devDependency', + ) + } +} + +function checkBuild( + owner: PackageInvariantOwner, + root: string, + violations: PackageInvariantViolation[], +): void { + const tsconfigPath = `${owner.dir}/tsconfig.json` + const tsconfig = JSON.parse(readFileSync(resolve(root, tsconfigPath), 'utf8')) as { + references?: Array<{ path?: string }> + } + if (owner.packageName !== '@deepseek-ai/dsh-invariants' + && !tsconfig.references?.some(reference => reference.path === '../../support/invariants')) { + addViolation( + violations, + tsconfigPath, + 'TypeScript project references must include ../../support/invariants', + ) + } + + const configPath = `${owner.dir}/tsdown.config.ts` + if (!existsSync(resolve(root, configPath))) return + const source = readFileSync(resolve(root, configPath), 'utf8') + if (!source.includes('lib/types/invariant.js')) { + addViolation(violations, configPath, 'package build override must bundle lib/types/invariant.js') + } +} + +function checkSource( + owner: PackageInvariantOwner, + root: string, + violations: PackageInvariantViolation[], +): void { + const absolutePath = resolve(root, owner.sourcePath) + if (!existsSync(absolutePath)) { + addViolation(violations, owner.sourcePath, 'missing package-owned invariant companion') + return + } + const sourceText = readFileSync(absolutePath, 'utf8') + if (sourceText.includes(GENERATED_INVARIANT_MARKER) + && sourceText !== renderBaselineInvariant(owner)) { + addViolation( + violations, + owner.sourcePath, + 'generated baseline is stale; run pnpm run gen-package-invariants', + ) + } + + const sourceFile = ts.createSourceFile( + absolutePath, + sourceText, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TS, + ) + const constants = topLevelStringConstants(sourceFile) + const registrations: string[] = [] + const unresolved: number[] = [] + const visit = (node: ts.Node): void => { + if (ts.isCallExpression(node) && isInvariantRegistration(node.expression)) { + const argument = node.arguments[0] + const packageName = argument === undefined ? undefined : stringValue(argument, constants) + if (packageName === undefined) unresolved.push(sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1) + else registrations.push(packageName) + } + ts.forEachChild(node, visit) + } + visit(sourceFile) + + for (const line of unresolved) { + addViolation( + violations, + owner.sourcePath, + `line ${line}: ctx.invariants.register package name must resolve to a local string constant`, + ) + } + if (registrations.length !== 1 || registrations[0] !== owner.packageName) { + addViolation( + violations, + owner.sourcePath, + `must register exactly its own package name ${JSON.stringify(owner.packageName)}; saw ${JSON.stringify(registrations)}`, + ) + } + for (const exportedName of ['name', 'inject', 'apply']) { + if (!hasNamedExport(sourceFile, exportedName)) { + addViolation(violations, owner.sourcePath, `must named-export ${exportedName}`) + } + } +} + +function topLevelStringConstants(sourceFile: ts.SourceFile): ReadonlyMap { + const constants = new Map() + for (const statement of sourceFile.statements) { + if (!ts.isVariableStatement(statement)) continue + for (const declaration of statement.declarationList.declarations) { + if (!ts.isIdentifier(declaration.name) || declaration.initializer === undefined) continue + const value = stringValue(declaration.initializer, constants) + if (value !== undefined) constants.set(declaration.name.text, value) + } + } + return constants +} + +function stringValue(node: ts.Expression, constants: ReadonlyMap): string | undefined { + if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text + if (ts.isIdentifier(node)) return constants.get(node.text) + return undefined +} + +function isInvariantRegistration(expression: ts.LeftHandSideExpression): boolean { + return ts.isPropertyAccessExpression(expression) + && expression.name.text === 'register' + && ts.isPropertyAccessExpression(expression.expression) + && expression.expression.name.text === 'invariants' +} + +function hasNamedExport(sourceFile: ts.SourceFile, name: string): boolean { + return sourceFile.statements.some((statement) => { + if (!ts.isVariableStatement(statement) + || !statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) return false + return statement.declarationList.declarations.some(declaration => ts.isIdentifier(declaration.name) && declaration.name.text === name) + }) +} + +/** Format violations for the command-line gate. */ +export function formatPackageInvariantViolation( + root: string, + violation: PackageInvariantViolation, +): string { + const path = resolve(root, violation.path) + return `${relative(root, path)}: ${violation.message}` +} diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index f46de4475a..8e2a7f961c 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -204,6 +204,7 @@ function ciPrimaryGates(): Gate[] { return [ pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }), pnpmScript('constraints', 'constraints'), + pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }), pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }), pnpmScript('typecheck', 'typecheck'), lintGate(), @@ -229,6 +230,7 @@ function ciStaticGates(): Gate[] { return [ pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }), pnpmScript('constraints', 'constraints'), + pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }), pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }), ...staticDemoSmokeGates(), ...docSyncLeafGates(), @@ -314,6 +316,7 @@ function hygieneLeafGates(options: { artifactNeeds?: string[] } = {}): Gate[] { pnpmScript('knip', 'knip'), pnpmScript('publint', 'publint', artifactOptions), pnpmScript('constraints', 'constraints'), + pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }), pnpmScript('node-next-types', 'verify-node-next-types', { label: 'node-next types', ...artifactOptions, diff --git a/scripts/test-invariants.spec.ts b/scripts/test-invariants.spec.ts new file mode 100644 index 0000000000..77150abb74 --- /dev/null +++ b/scripts/test-invariants.spec.ts @@ -0,0 +1,70 @@ +import { describe, expect, it, vi } from 'vitest' +import { Context, Service } from 'cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { packageInvariantOwners } from './package-invariants.ts' +import { MANUAL_INVARIANT_TESTS, testInvariantCompanions } from './test-invariants.ts' + +declare module 'cordis' { + interface Context { + testInvariantProbe: TestInvariantProbe + } +} + +class TestInvariantProbe extends Service { + constructor(ctx: Context) { + super(ctx, 'testInvariantProbe') + } +} + +describe('global test invariant host', () => { + it('loads every companion and reserves every package name with enabled checks', async () => { + const ctx = new Context() + await ctx.plugin(TestInvariantProbe) + + const owners = packageInvariantOwners(process.cwd()) + expect(Object.keys(testInvariantCompanions)).toHaveLength(owners.length) + const unreserved: string[] = [] + for (const owner of owners) { + try { + const dispose = ctx.invariants.register(owner.packageName, () => {}) + unreserved.push(owner.packageName) + dispose() + } catch (error) { + expect(error).toHaveProperty( + 'message', + `invariants: package "${owner.packageName}" is already registered`, + ) + } + } + expect(unreserved).toEqual([]) + }) + + it('executes each companion registration with its owning package name', async () => { + const owners = new Map(packageInvariantOwners(process.cwd()).map(owner => [owner.sourcePath, owner.packageName])) + const registrations = new Map() + const register = vi.fn((_packageName: string, installer: InvariantInstaller) => { + expect(typeof installer).toBe('function') + return () => {} + }) + const fakeContext = { invariants: { register } } as unknown as Context + for (const [rawPath, companion] of Object.entries(testInvariantCompanions)) { + const path = rawPath.replace(/^\.\.\//, '') + await companion.apply(fakeContext) + const call = register.mock.calls.at(-1) + if (call === undefined) throw new Error(`${path}: companion did not register`) + registrations.set(path, call[0]) + } + expect(registrations).toEqual(owners) + }) + + it('limits manual composition to focused invariant topology tests', () => { + expect(MANUAL_INVARIANT_TESTS).toEqual([ + '/packages/support/invariants/tests/service.spec.ts', + '/packages/core/session/tests/invariant.spec.ts', + '/packages/core/agent/tests/invariant.spec.ts', + '/packages/core/scope/tests/invariant.spec.ts', + '/packages/core/agent-loop/tests/invariant.spec.ts', + '/packages/examples/agent-spine-demo/tests/agent-core.spec.ts', + ]) + }) +}) diff --git a/scripts/test-invariants.ts b/scripts/test-invariants.ts new file mode 100644 index 0000000000..584db3d5d7 --- /dev/null +++ b/scripts/test-invariants.ts @@ -0,0 +1,104 @@ +/** + * Vitest-wide invariant host. Ordinary Cordis roots receive the invariant + * service with global enablement and every package companion before their first + * plugin starts. Focused invariant tests own their service topology explicitly. + */ + +import { expect } from 'vitest' +import { RegistryService } from 'cordis' +import type { Context, Plugin } from 'cordis' +import InvariantService from '@deepseek-ai/dsh-invariants' + +declare global { + interface ImportMeta { + /** Eager Vite module-glob expansion used by the Vitest setup file. */ + glob(pattern: string, options: { eager: true }): Record + } +} + +/** Loader-safe shape shared by every package invariant companion. */ +export interface TestInvariantCompanion { + readonly name: string + readonly inject: readonly string[] + apply(ctx: Context): Promise<() => void> +} + +/** Every package companion, discovered eagerly so coverage observes each registration. */ +export const testInvariantCompanions: Readonly> = + import.meta.glob('../packages/*/*/src/invariant.ts', { eager: true }) + +/** Tests that exercise selection or companion lifecycle with a deliberately hand-built service tree. */ +export const MANUAL_INVARIANT_TESTS = [ + '/packages/support/invariants/tests/service.spec.ts', + '/packages/core/session/tests/invariant.spec.ts', + '/packages/core/agent/tests/invariant.spec.ts', + '/packages/core/scope/tests/invariant.spec.ts', + '/packages/core/agent-loop/tests/invariant.spec.ts', + '/packages/examples/agent-spine-demo/tests/agent-core.spec.ts', +] as const + +interface InvariantHost { + readonly fibers: readonly PluginFiber[] + readonly byCallback: ReadonlyMap +} + +type PluginFiber = ReturnType + +const hosts = new WeakMap() +// eslint-disable-next-line @typescript-eslint/unbound-method -- every call below supplies its RegistryService receiver explicitly. +const originalPlugin = RegistryService.prototype.plugin + +RegistryService.prototype.plugin = function(plugin: Plugin, config?: unknown, getOuterStack?: () => string[]) { + if (usesManualInvariantTree()) return originalPlugin.call(this, plugin, config, getOuterStack) + + const root = this.ctx.root + const host = hosts.get(root) ?? startInvariantHost(root) + const callback = this.resolve(plugin) + const existing = callback === undefined ? undefined : host.byCallback.get(callback) + if (existing !== undefined) return existing + + const fiber = originalPlugin.call(this, plugin, config, getOuterStack) + // A root-level await is the test's composition boundary. Nested plugin + // fibers must not await their own companion parent through the global host. + if (this.ctx !== root) return fiber + return joinInvariantStartup(fiber, host.fibers) +} + +function usesManualInvariantTree(): boolean { + const testPath = expect.getState().testPath?.replaceAll('\\', '/') ?? '' + return MANUAL_INVARIANT_TESTS.some(path => testPath.endsWith(path)) +} + +function startInvariantHost(root: Context): InvariantHost { + const fibers: PluginFiber[] = [] + const byCallback = new Map() + const mount = (plugin: Plugin, config?: unknown): void => { + const fiber = originalPlugin.call(root.registry, plugin, config) + const callback = root.registry.resolve(plugin) + if (callback === undefined) throw new Error('test invariants: companion is not a valid Cordis plugin') + fibers.push(fiber) + byCallback.set(callback, fiber) + } + + mount(InvariantService, { enabled: true }) + for (const [path, companion] of Object.entries(testInvariantCompanions).sort(([left], [right]) => left.localeCompare(right))) { + if (!companion.inject.includes('invariants')) { + throw new Error(`test invariants: ${path} must inject the invariant service`) + } + mount(companion) + } + + const host = { fibers, byCallback } + hosts.set(root, host) + return host +} + +function joinInvariantStartup(fiber: PluginFiber, invariantFibers: readonly PluginFiber[]): PluginFiber { + const readiness = fiber.await().then(async (loaded) => { + await Promise.all(invariantFibers.map(invariant => invariant.await())) + return loaded + }) + const joined = Object.create(fiber) as PluginFiber + joined.then = readiness.then.bind(readiness) + return joined +} diff --git a/tsdown.config.ts b/tsdown.config.ts index 5efebe5b0b..31fa53c675 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -3,8 +3,9 @@ import { defineConfig } from 'tsdown' /** * JS bundling for vendored Cordis and Harness TypeScript packages. * TypeScript source is compiled first by `tsc -b tsconfig.build.json`; tsdown - * reads only the emitted JS under lib/types and writes lib/index.* runtime - * bundles. Declarations are NOT produced here, hence `dts: false`. + * reads only the emitted JS under lib/types and writes the package root and + * invariant companion runtime bundles. Declarations are NOT produced here, + * hence `dts: false`. * * Per-package shape overrides live in `/tsdown.config.ts` * (schemastery: dual ESM+CJS; logger-console: extra browser entry). @@ -14,7 +15,9 @@ export default defineConfig({ // `workspace: true` would discover package manifests outside that bundle set. Landlock // platform packages contain only a prebuilt native binary, so they have no JS entry. workspace: ['vendor/*', 'packages/*/*'], - entry: ['lib/types/index.js'], + // The brace glob admits the package companion when present while retaining the + // index-only build for vendored Cordis packages outside the Harness package tree. + entry: ['lib/types/{index,invariant}.js'], outDir: 'lib', format: ['esm'], platform: 'node', diff --git a/vitest.config.ts b/vitest.config.ts index c0946e2e10..4fdcf012f0 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -7,6 +7,7 @@ export default defineConfig({ // to source; native resolution would fall through to absent `lib/` outputs. plugins: [tsconfigPaths({ projects: ['./tsconfig.json'] })], test: { + setupFiles: ['./scripts/test-invariants.ts'], include: ['packages/*/*/tests/**/*.spec.ts', 'examples/*/tests/**/*.spec.ts', 'scripts/**/*.spec.ts'], coverage: { provider: 'v8', diff --git a/vitest.e2e.config.ts b/vitest.e2e.config.ts index adc8b3f3b2..ab22640c79 100644 --- a/vitest.e2e.config.ts +++ b/vitest.e2e.config.ts @@ -31,6 +31,7 @@ export default defineConfig({ // through the root tsconfig paths map; the native option cannot do this. plugins: [tsconfigPaths({ projects: ['./tsconfig.json'] })], test: { + setupFiles: ['./scripts/test-invariants.ts'], include: ['packages/*/*/tests/**/*.e2e.ts', 'examples/*/tests/**/*.e2e.ts'], // Real model calls: generous timeouts, and retries for transient flakes // (the shared internal key hits concurrency quotas). No coverage — the diff --git a/vitest.snapshot.config.ts b/vitest.snapshot.config.ts index fb2e890eb7..368f04f423 100644 --- a/vitest.snapshot.config.ts +++ b/vitest.snapshot.config.ts @@ -39,6 +39,7 @@ export default defineConfig({ // through the root tsconfig paths map; the native option cannot do this. plugins: [tsconfigPaths({ projects: ['./tsconfig.json'] })], test: { + setupFiles: ['./scripts/test-invariants.ts'], include: [ 'examples/*/tests/**/*.snapshot.ts', 'packages/sdk/*/tests/**/*.snapshot.ts', diff --git a/website/zh-CN/api/harness/invariants.md b/website/zh-CN/api/harness/invariants.md index b87ea93aa7..e55dd664c7 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#L94) +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/support/invariants/src/index.ts#L95) ### 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#L136) +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/support/invariants/src/index.ts#L137) From 36e99e737b8dfa50fb62bdc9f15dd38bb4da36a2 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 19 Jul 2026 22:24:32 +0800 Subject: [PATCH 02/12] test(sandbox): pack invariant service peer --- packages/sandbox/sandbox-local/tests/packed-install.e2e.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/sandbox/sandbox-local/tests/packed-install.e2e.ts b/packages/sandbox/sandbox-local/tests/packed-install.e2e.ts index 8519357c1b..9fcfe23de8 100644 --- a/packages/sandbox/sandbox-local/tests/packed-install.e2e.ts +++ b/packages/sandbox/sandbox-local/tests/packed-install.e2e.ts @@ -26,6 +26,7 @@ const WORKSPACE_CLOSURE = [ 'packages/sandbox/sandbox', 'packages/llm/llm', 'packages/util/brand', + 'packages/support/invariants', ] /** ELF `e_machine` (offset 18, LE) for this host: x86-64 = 62, AArch64 = 183. */ 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 03/12] 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) From caaa1364ec1b65fd39ebbdf2b184dcba3fb38f6a Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 20 Jul 2026 01:51:28 +0800 Subject: [PATCH 04/12] test(invariants): bound global companion topology --- ...-package-owned-invariant-service.i18n.yaml | 4 +- ...6-07-19-package-owned-invariant-service.md | 4 +- ...7-19-package-owned-invariant-service.zh.md | 4 +- docs/testing.md | 2 +- packages/support/invariants/README.md | 2 +- scripts/test-invariants.spec.ts | 16 ++++- scripts/test-invariants.ts | 58 ++++++++++++++++--- 7 files changed, 72 insertions(+), 18 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.i18n.yaml index 7bb90a9308..ef4090b3d6 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.i18n.yaml +++ b/.agents/notes/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: d73cb196820369e41a748ad7a1c3c50b889d636f -2026-07-19-package-owned-invariant-service.zh.md: 644fd1f0668a097f028ec805c5cb425e536e277a +2026-07-19-package-owned-invariant-service.md: d7b77439c8cfdfdf2fc4f01e779a6d3c7f345457 +2026-07-19-package-owned-invariant-service.zh.md: b553d137f9f06b57f24672bf6984d087ca99ceb2 diff --git a/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.md b/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.md index d73cb19682..d7b77439c8 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.md +++ b/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.md @@ -84,7 +84,7 @@ Service tests cover defaults, global disablement, allow/block selection, blockli Composition tests cover standard-spine forwarding and generated SDK entries. Loader tests preserve each companion namespace, while built plain-Node smokes exercise the compiled subpath exports. The scoped-event freshness gate reruns its semantic Program analysis. -Every Vitest configuration loads a test host that mounts an explicitly enabled service and all package companions before an ordinary Cordis root's first plugin. Focused service and owner tests construct their own invariant topology so they can exercise disablement, filtering, rollback, and reload without duplicate ownership. Gate tests also execute every companion's `apply` function and verify that it calls `register` with its manifest name, rather than accepting source text alone. +Every Vitest configuration loads a test host that mounts an explicitly enabled service before an ordinary Cordis root's first plugin. Package tests add their owner's companion, one exhaustive topology mounts every companion, and focused service and owner tests construct their own invariant topology so they can exercise disablement, filtering, rollback, and reload without duplicate ownership. Gate tests also execute every companion's `apply` function and verify that it calls `register` with its manifest name, rather than accepting source text alone. ## Alternatives considered @@ -101,5 +101,5 @@ Every Vitest configuration loads a test host that mounts an explicitly enabled s - Explicit companion entries make diagnostic cost and ownership visible in Cordis config and package exports. - One selected contribution adds one child fiber and its listener/state cost; filtered registrations retain only name ownership. - Regex sources are deployment configuration and remain fixed until the service reloads. -- Ordinary Vitest roots install every selected companion, trading extra child fibers during tests for repository-wide invariant coverage and immediate fixture failures. +- Ordinary Vitest roots install the current test package's companion; one exhaustive topology retains repository-wide registration coverage without multiplying every child fiber across every test root. - Session storage validation, snapshotting, freezing, provenance, and surface acceptance remain always on and are not affected by invariant selection. diff --git a/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.zh.md b/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.zh.md index 644fd1f066..b553d137f9 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.zh.md @@ -84,7 +84,7 @@ Workspace 约束识别独立的不变式 bundle;包 exports、项目引用、 组合测试覆盖标准 spine 转发和生成的 SDK 条目。Loader 测试固定每个伴随命名空间,构建后的纯 Node smoke 覆盖编译子路径 export。scoped event 新鲜度门禁会重新执行语义 Program 分析。 -每个 Vitest 配置都会加载测试宿主;在普通 Cordis 根上下文启动第一个插件之前,宿主会挂载显式启用的服务以及所有包的伴随插件。服务与所有者的聚焦测试自行构建不变式拓扑,从而在不发生重复所有权冲突的前提下覆盖关闭、过滤、回滚与重载。门禁测试还会执行每个伴随插件的 `apply` 函数,并验证它调用 `register` 时使用包清单中的包名,而不是只检查源码文本。 +每个 Vitest 配置都会加载测试宿主;在普通 Cordis 根上下文启动第一个插件之前,宿主会挂载显式启用的服务。包测试会添加其所有者的伴随插件,一个完整拓扑会挂载所有伴随插件;服务与所有者的聚焦测试自行构建不变式拓扑,从而在不发生重复所有权冲突的前提下覆盖关闭、过滤、回滚与重载。门禁测试还会执行每个伴随插件的 `apply` 函数,并验证它调用 `register` 时使用包清单中的包名,而不是只检查源码文本。 ## 考虑过的替代方案 @@ -101,5 +101,5 @@ Workspace 约束识别独立的不变式 bundle;包 exports、项目引用、 - 显式伴随条目让诊断成本和所有权在 Cordis 配置与包 export 中可见。 - 每个选中贡献增加一个子 fiber 及其监听器和状态成本;被过滤注册只保留包名占用。 - 正则表达式源属于部署配置,在服务重载前保持固定。 -- 普通 Vitest 根上下文会安装每个被选中的伴随插件,以增加测试期间的子 fiber 为代价,换取覆盖整个仓库的不变式检查和对 fixture(测试前置数据)错误的即时反馈。 +- 普通 Vitest 根上下文会安装当前测试包的伴随插件;一个完整拓扑保留全仓库注册覆盖,而不会在每个测试根上下文中重复创建所有子 fiber。 - 会话存储验证、快照、冻结、provenance 与 surface 接受规则始终启用,不受不变式选择影响。 diff --git a/docs/testing.md b/docs/testing.md index 3bf37fe79e..1fcefb4bc3 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -9,7 +9,7 @@ How this repo tests, tier by tier, and the rules that keep a green suite meaning - **Real-API e2e** (`pnpm run test:e2e`): with-key tests against live provider APIs — the DeepSeek model plus provider-specific smokes that gate on their own keys (`EXA_API_KEY`, `PERPLEXITY_API_KEY`, …); each suite self-skips without its key so keyless CI stays green ([real-API e2e Agent Note](../.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md)). - **Snapshot** (`pnpm run test:snapshot`): transport-specific keyless expected outputs cover external presentation. ACP suites boot the real example subprocess, replay a recorded session, and diff normalized JSON-RPC plus the re-persisted log ([ACP snapshot Agent Note](../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md)); the headless suite independently pins `stream-json` through its real one-shot subprocess. TUI completed journeys replay recorded primary/child JSONL through the real agent loop and tools before projecting ANSI into semantic terminal-state expected outputs; package-local snapshots retain transient renderer states, and a real PTY conversation covers the process boundary ([TUI snapshot Agent Note](../.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md)). Use `pnpm run test:snapshot:record` when a model transcript must change and `pnpm run test:snapshot:refresh` when committed replay input remains correct; review every JSONL and expected-output diff. System-prompt/tool-schema content is pinned by one ACP scenario (`text-turn`) and tokenized in every other fixture, so a prompt or schema edit churns one committed line ([pinned-header Agent Note](../.agents/notes/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)). -All Vitest configurations mount enabled `ctx.invariants` and every package companion before ordinary Cordis roots start. Focused invariant topology tests compose enabled or deliberately disabled services explicitly, without competing global registrations. +Every Vitest configuration mounts enabled `ctx.invariants` before ordinary roots start. Package tests add their owner's companion; one exhaustive topology mounts them all. Focused invariant topology tests compose enabled or deliberately disabled services without competing global registrations. ## The with-key policy: inference is cheap here diff --git a/packages/support/invariants/README.md b/packages/support/invariants/README.md index 11e3b2b5c6..7a5d0748e4 100644 --- a/packages/support/invariants/README.md +++ b/packages/support/invariants/README.md @@ -54,7 +54,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 choose the companions they want and may disable or filter them without changing package entrypoints. Vitest gives every ordinary root an explicitly enabled service and mounts the current test package's companion; one exhaustive topology mounts all companions once, while focused invariant-service tests construct their own topology to exercise filtering and lifecycle behavior. ## Model Experience diff --git a/scripts/test-invariants.spec.ts b/scripts/test-invariants.spec.ts index 77150abb74..5c3b9120bc 100644 --- a/scripts/test-invariants.spec.ts +++ b/scripts/test-invariants.spec.ts @@ -2,7 +2,11 @@ import { describe, expect, it, vi } from 'vitest' import { Context, Service } from 'cordis' import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' import { packageInvariantOwners } from './package-invariants.ts' -import { MANUAL_INVARIANT_TESTS, testInvariantCompanions } from './test-invariants.ts' +import { + MANUAL_INVARIANT_TESTS, + testInvariantCompanionPaths, + testInvariantCompanions, +} from './test-invariants.ts' declare module 'cordis' { interface Context { @@ -17,7 +21,7 @@ class TestInvariantProbe extends Service { } describe('global test invariant host', () => { - it('loads every companion and reserves every package name with enabled checks', async () => { + it('uses one exhaustive topology to reserve every package name with enabled checks', async () => { const ctx = new Context() await ctx.plugin(TestInvariantProbe) @@ -39,6 +43,14 @@ describe('global test invariant host', () => { expect(unreserved).toEqual([]) }) + it('mounts the owning package companion while leaving non-package roots service-only', () => { + expect(testInvariantCompanionPaths('/repo/packages/core/tools/tests/tools.spec.ts')) + .toEqual(['../packages/core/tools/src/invariant.ts']) + expect(testInvariantCompanionPaths('/repo/examples/echo-agent/tests/echo.spec.ts')).toEqual([]) + expect(testInvariantCompanionPaths('/repo/scripts/test-invariants.spec.ts')) + .toEqual(Object.keys(testInvariantCompanions).sort()) + }) + it('executes each companion registration with its owning package name', async () => { const owners = new Map(packageInvariantOwners(process.cwd()).map(owner => [owner.sourcePath, owner.packageName])) const registrations = new Map() diff --git a/scripts/test-invariants.ts b/scripts/test-invariants.ts index 584db3d5d7..b91fdda462 100644 --- a/scripts/test-invariants.ts +++ b/scripts/test-invariants.ts @@ -1,7 +1,8 @@ /** * Vitest-wide invariant host. Ordinary Cordis roots receive the invariant - * service with global enablement and every package companion before their first - * plugin starts. Focused invariant tests own their service topology explicitly. + * service with global enablement plus the current test package's companion. + * One topology test mounts every companion; focused invariant tests own their + * service topology explicitly. */ import { expect } from 'vitest' @@ -40,6 +41,7 @@ export const MANUAL_INVARIANT_TESTS = [ interface InvariantHost { readonly fibers: readonly PluginFiber[] readonly byCallback: ReadonlyMap + readonly ready: Promise } type PluginFiber = ReturnType @@ -55,13 +57,15 @@ RegistryService.prototype.plugin = function(plugin: Plugin, config?: unknown, ge const host = hosts.get(root) ?? startInvariantHost(root) const callback = this.resolve(plugin) const existing = callback === undefined ? undefined : host.byCallback.get(callback) - if (existing !== undefined) return existing + if (existing !== undefined) { + return this.ctx === root ? joinInvariantStartup(existing, host.ready) : existing + } const fiber = originalPlugin.call(this, plugin, config, getOuterStack) // A root-level await is the test's composition boundary. Nested plugin // fibers must not await their own companion parent through the global host. if (this.ctx !== root) return fiber - return joinInvariantStartup(fiber, host.fibers) + return joinInvariantStartup(fiber, host.ready) } function usesManualInvariantTree(): boolean { @@ -69,6 +73,30 @@ function usesManualInvariantTree(): boolean { return MANUAL_INVARIANT_TESTS.some(path => testPath.endsWith(path)) } +const ALL_COMPANION_TESTS = ['/scripts/test-invariants.spec.ts'] as const + +/** + * Select the package companions that an ordinary test root must register. + * Package tests receive their owner's checks; the dedicated topology test + * receives every owner so coverage and exhaustive runtime registration remain + * independently enforced. + * @param testPath - absolute or repo-relative normalized Vitest file path. + * @returns sorted `import.meta.glob` keys for companions to mount. + */ +export function testInvariantCompanionPaths(testPath: string): string[] { + const normalized = testPath.replaceAll('\\', '/') + const allPaths = Object.keys(testInvariantCompanions).sort() + if (ALL_COMPANION_TESTS.some(path => normalized.endsWith(path))) return allPaths + + const owner = normalized.match(/\/packages\/([^/]+)\/([^/]+)\/tests\//) + if (owner === null) return [] + const companionPath = `../packages/${owner[1]}/${owner[2]}/src/invariant.ts` + if (testInvariantCompanions[companionPath] === undefined) { + throw new Error(`test invariants: package test has no companion at ${companionPath}`) + } + return [companionPath] +} + function startInvariantHost(root: Context): InvariantHost { const fibers: PluginFiber[] = [] const byCallback = new Map() @@ -81,21 +109,35 @@ function startInvariantHost(root: Context): InvariantHost { } mount(InvariantService, { enabled: true }) - for (const [path, companion] of Object.entries(testInvariantCompanions).sort(([left], [right]) => left.localeCompare(right))) { + const testPath = expect.getState().testPath ?? '' + const companionPaths = testInvariantCompanionPaths(testPath) + for (const path of companionPaths) { + const companion = testInvariantCompanions[path] + if (companion === undefined) { + throw new Error(`test invariants: selected companion vanished at ${path}`) + } if (!companion.inject.includes('invariants')) { throw new Error(`test invariants: ${path} must inject the invariant service`) } mount(companion) } - const host = { fibers, byCallback } + const [serviceFiber, ...companionFibers] = fibers + if (serviceFiber === undefined) throw new Error('test invariants: service fiber was not mounted') + // A companion is initially PENDING on the invariant service, and Cordis + // Fiber.await() only joins work already in flight. Wait for the service to + // activate its dependants before joining their startup and failures. + const ready = serviceFiber.await() + .then(() => Promise.all(companionFibers.map(fiber => fiber.await()))) + .then(() => undefined) + const host = { fibers, byCallback, ready } hosts.set(root, host) return host } -function joinInvariantStartup(fiber: PluginFiber, invariantFibers: readonly PluginFiber[]): PluginFiber { +function joinInvariantStartup(fiber: PluginFiber, invariantReady: Promise): PluginFiber { const readiness = fiber.await().then(async (loaded) => { - await Promise.all(invariantFibers.map(invariant => invariant.await())) + await invariantReady return loaded }) const joined = Object.create(fiber) as PluginFiber From e80fc3e61b64bd06e22f38a4d35691fa50dc6489 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 20 Jul 2026 01:53:09 +0800 Subject: [PATCH 05/12] fix(invariants): join package checks at startup --- ...kage-invariant-runtime-contracts.i18n.yaml | 4 +- ...-19-package-invariant-runtime-contracts.md | 14 +- ...-package-invariant-runtime-contracts.zh.md | 14 +- ...-package-owned-invariant-service.i18n.yaml | 4 +- ...6-07-19-package-owned-invariant-service.md | 6 +- ...7-19-package-owned-invariant-service.zh.md | 6 +- docs/testing.md | 2 +- .../code-runtime/tests/service.spec.ts | 10 +- .../examples/jsonrpc-demo/src/invariant.ts | 11 +- packages/hooks/hook-protocol/src/invariant.ts | 29 ++- .../sandbox/sandbox/tests/invariant.spec.ts | 43 ++++ packages/sdk/create-sdk/src/invariant.ts | 35 ++- packages/sdk/helper/src/invariant.ts | 25 +- packages/sdk/scripts/src/invariant.ts | 29 +-- packages/sdk/telemetry/src/invariant.ts | 23 +- .../subagent-inprocess/src/invariant.ts | 15 +- .../subagent-subprocess/src/invariant.ts | 29 ++- .../support/acp-snapshot/src/invariant.ts | 35 ++- .../agent-loop-testkit/src/invariant.ts | 17 +- packages/support/invariants/README.md | 6 +- packages/support/invariants/src/index.ts | 229 ++++++++++++++---- .../support/invariants/tests/service.spec.ts | 93 ++++++- .../support/loader-smoke/src/invariant.ts | 31 ++- packages/ui/app-boot/src/invariant.ts | 21 +- packages/util/brand/src/invariant.ts | 11 +- packages/util/home/src/invariant.ts | 19 +- packages/util/paths/src/invariant.ts | 19 +- packages/util/retention/src/invariant.ts | 33 ++- packages/util/timeout/src/invariant.ts | 23 +- scripts/test-invariants.spec.ts | 16 +- scripts/test-invariants.ts | 58 ++++- 31 files changed, 596 insertions(+), 314 deletions(-) create mode 100644 packages/sandbox/sandbox/tests/invariant.spec.ts diff --git a/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.i18n.yaml index 06d1f9e07e..48407e2272 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.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-invariant-runtime-contracts.md: d3b327694b0d6779409582a7bb199e9cd48a1e4c -2026-07-19-package-invariant-runtime-contracts.zh.md: 07c15e80587a292fc94e3537980ee26cbae15115 +2026-07-19-package-invariant-runtime-contracts.md: 65986fc0b3aab695d8512d9e221052e1db83445f +2026-07-19-package-invariant-runtime-contracts.zh.md: 0ae0bd305692ee71359c5500e477dee22575a9ef diff --git a/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md b/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md index d3b327694b..65986fc0b3 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md +++ b/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md @@ -10,7 +10,7 @@ The package-owned invariant seam made registration and publication exhaustive, b Every package shape cannot use the same invariant. Cordis plugins own fibers, injections, effects, and services; service seams admit structural third-party implementations; stateful domains need event relations; pure libraries and bin packages expose algebra, parsing, normalization, or entrypoint constraints. The repository needs one enforceable obligation without moving those contracts back into a central product-aware package. -Vitest also mounts every companion globally. Companion modules therefore cannot eagerly import every product entrypoint before a test module establishes its hoisted mocks, and a name-based observer cannot mistake an anonymous child fiber that inherits its parent's display name for the package plugin itself. +Vitest mounts each package test's owning companion globally, and one exhaustive topology mounts every companion. Companion modules therefore cannot eagerly import every product entrypoint before a test module establishes its hoisted mocks, and a name-based observer cannot mistake an anonymous child fiber that inherits its parent's display name for the package plugin itself. ## Decision @@ -31,17 +31,17 @@ At implementation time this covers all 91 workspace packages: four stateful comp ### Product-independent observers -`observePluginInvariant` checks existing fibers immediately and future active fibers through global Cordis lifecycle events. A contract may supply an exact callback when that import is safe. Otherwise it matches `fiber.runtime.name`, the name declared by that fiber's own plugin runtime, rather than the inherited `fiber.name`; anonymous `ctx.inject()` children are therefore not misidentified as their parent package. The observer checks required injection keys, recursively collected effect labels, services provided by that exact fiber, and an optional owner validator. Config-dependent packages encode symmetric relations, such as automatic compaction owning both listeners or neither when disabled. +`observePluginInvariant` checks existing fibers immediately and future active fibers through a callback/name index behind one root-shared Cordis lifecycle listener pair. A contract may supply an exact callback when that import is safe. Otherwise it matches `fiber.runtime.name`, the name declared by that fiber's own plugin runtime, rather than the inherited `fiber.name`; anonymous `ctx.inject()` children are therefore not misidentified as their parent package. The observer checks required injection keys, recursively collected effect labels, services provided by that exact fiber, and an optional owner validator. Config-dependent packages encode symmetric relations, such as automatic compaction owning both listeners or neither when disabled. `observeServiceInvariant` checks the current service and every later binding. `serviceShapeViolation` validates callable members and non-empty string descriptors structurally instead of using `instanceof`, so conforming third-party backends and complete test doubles remain valid while incomplete stand-ins fail. -`assertInvariant` handles synchronous package algebra. Pure-package companions register an asynchronous child effect and dynamically import their owner inside that effect. This preserves atomic service-owned rollback while allowing the test module, Loader, or deployment to establish mocks and module resolution before the invariant samples the owner. +`assertInvariant` handles package algebra. Pure-package companions return an asynchronous installer promise and dynamically import their owner during child startup. The service joins that promise for atomic rollback while allowing the test module, Loader, or deployment to establish mocks and module resolution before the invariant samples the owner. ### Gate and test execution `verify-package-invariants` discovers every workspace package and retains the publication checks for the exact registration name, `./invariant` export, published files, invariant peer and development dependencies, TypeScript reference, and bundle entry. Its source check additionally parses the local `install` function, rejects a generated marker or empty body, requires a second failure-reporter parameter and its use, and rejects duplicate name-based plugin observers across packages. These AST checks are a minimum acceptance rule, not a claim that source shape proves semantic quality. -The Vitest setup host mounts `InvariantService` with `{ enabled: true }` and all 91 companions before an ordinary Cordis root's first plugin. The host joins companion startup to the test's root-level composition boundary, so asynchronous pure checks and plugin-observer setup fail the test rather than becoming background diagnostics. Focused selection, lifecycle, and owner suites build their own enabled topology to avoid duplicate registrations while still testing invariants. +The Vitest setup host mounts `InvariantService` with `{ enabled: true }` before an ordinary Cordis root's first plugin and adds the current test package's companion. The host joins companion startup to the test's root-level composition boundary, so asynchronous pure checks and plugin-observer setup fail the test rather than becoming background diagnostics. One exhaustive topology mounts all 91 companions once to prove runtime registration and coverage; focused selection, lifecycle, and owner suites build their own enabled topology to avoid duplicate registrations while still testing invariants. Helper tests reject invalid plugin names, missing injections, effects, services, custom relations, malformed service shapes, and failed assertions. Package suites then activate real plugins across their existing config and HMR paths. Test-only service stand-ins must implement the complete checked seam rather than bypass global invariants. @@ -50,7 +50,7 @@ Helper tests reject invalid plugin names, missing injections, effects, services, - **Keep generated ownership-only companions.** Rejected because registration without an executable assertion cannot reject a broken package and makes the exhaustive gate misleading. - **Generate one synthetic assertion into every package.** Rejected because a universal assertion would again optimize for satisfying the gate instead of protecting an owner-specific contract. - **Move the per-package contract matrix into `dsh-invariants`.** Rejected because product imports, vocabulary, and change ownership would return to the central service. -- **Import every owner entrypoint statically from its companion.** Rejected because the global test host would preload packages before hoisted mocks and shipped compositions would pay unrelated module initialization costs. +- **Import every owner entrypoint statically from its companion.** Rejected because owning and exhaustive test hosts would preload packages before hoisted mocks and shipped compositions would pay unrelated module initialization costs. - **Require first-party service-class identity.** Rejected because service seams are structural extension boundaries; `instanceof` would reject valid external implementations and test doubles. - **Register invariants implicitly from package root entrypoints.** Rejected for the composition-order and hidden-effect reasons in the package-owned service RFC. @@ -58,8 +58,8 @@ Helper tests reject invalid plugin names, missing injections, effects, services, - Every package contributes an executable check; adding a package without one fails the top-level gate. - The invariant service remains product-independent while providing reusable lifecycle and shape observers. -- Ordinary unit, snapshot, and e2e tests run with global invariant enablement and every companion registered. +- Ordinary unit, snapshot, and e2e roots run with global invariant enablement and the test package's companion; one exhaustive topology registers every companion. - Plugin names used for name-based observation must be unique within one Cordis root; packages may opt into exact callback identity when safe. - Pure-package checks sample stable startup contracts. Mutable behavior must use an event, service, or plugin-fiber observer. -- More companion work runs during tests and selected deployments, trading small startup cost for immediate package-attributed failures. +- Relevant companion work runs during package tests and selected deployments, trading bounded startup cost for immediate package-attributed failures. - The original regex selection, blocklist precedence, registration uniqueness, rollback, disposal, and HMR contracts remain unchanged. diff --git a/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.zh.md b/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.zh.md index 07c15e8058..0ae0bd3056 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.zh.md @@ -10,7 +10,7 @@ Status: implemented 不同包形态不能使用同一种不变式。Cordis 插件拥有 fiber、注入、effect 与服务;服务接缝允许结构兼容的第三方实现;有状态领域需要事件关系;纯库和 bin 包暴露代数、解析、规范化或入口约束。仓库需要一个可执行的统一义务,同时不能把这些契约重新移回了解产品语义的中央包。 -Vitest 还会全局挂载每个伴随插件。因此伴随模块不能在测试模块建立 hoisted mock 之前急切导入所有产品入口;按名称观察时,也不能把继承父级显示名的匿名子 fiber 误认为包插件本身。 +Vitest 会为每个包测试全局挂载其所有者伴随插件,并由一个完整拓扑挂载全部伴随插件。因此伴随模块不能在测试模块建立 hoisted mock 之前急切导入所有产品入口;按名称观察时,也不能把继承父级显示名的匿名子 fiber 误认为包插件本身。 ## 决策 @@ -31,17 +31,17 @@ Vitest 还会全局挂载每个伴随插件。因此伴随模块不能在测试 ### 与产品无关的观察器 -`observePluginInvariant` 会立即检查已有 fiber,并通过全局 Cordis 生命周期事件检查未来进入活跃状态的 fiber。安全导入时,契约可以提供准确 callback;否则匹配 `fiber.runtime.name`,即该 fiber 自身插件运行时声明的名称,而不是继承而来的 `fiber.name`,因此匿名 `ctx.inject()` 子级不会被误认成父包。观察器检查必要注入键、递归收集的 effect 标签、由该 fiber 准确提供的服务,以及可选的所有者验证器。依赖配置的包使用对称关系,例如自动压缩要么同时拥有两个监听器,要么在关闭时两个都没有。 +`observePluginInvariant` 会立即检查已有 fiber,并通过根上下文共享的一对 Cordis 生命周期监听器背后的 callback/名称索引检查未来进入活跃状态的 fiber。安全导入时,契约可以提供准确 callback;否则匹配 `fiber.runtime.name`,即该 fiber 自身插件运行时声明的名称,而不是继承而来的 `fiber.name`,因此匿名 `ctx.inject()` 子级不会被误认成父包。观察器检查必要注入键、递归收集的 effect 标签、由该 fiber 准确提供的服务,以及可选的所有者验证器。依赖配置的包使用对称关系,例如自动压缩要么同时拥有两个监听器,要么在关闭时两个都没有。 `observeServiceInvariant` 检查当前服务及之后的每次绑定。`serviceShapeViolation` 以结构方式验证可调用成员和非空字符串描述字段,而不使用 `instanceof`;因此符合契约的第三方后端和完整测试替身有效,不完整替身会失败。 -`assertInvariant` 处理同步包代数。纯包伴随插件注册异步子 effect,并在该 effect 内动态导入所有者。这样既保留服务拥有的原子回滚,又允许测试模块、Loader 或部署先建立 mock 和模块解析,再由不变式采样所有者。 +`assertInvariant` 处理包代数。纯包伴随插件返回异步 installer promise,并在子 fiber 启动期间动态导入所有者。服务会等待该 promise 以保证原子回滚,同时允许测试模块、Loader 或部署先建立 mock 和模块解析,再由不变式采样所有者。 ### 门禁与测试执行 `verify-package-invariants` 发现每个工作区包,并保留准确注册名、`./invariant` export、发布文件、不变式 peer 与开发依赖、TypeScript 引用和 bundle 入口的发布检查。源码检查还会解析本地 `install` 函数,拒绝生成标记或空函数体,要求第二个失败报告器参数及其使用,并拒绝跨包重复的按名称插件观察器。这些 AST 检查只是最低接收规则,并不宣称源码形状足以证明语义质量。 -Vitest setup host 使用 `{ enabled: true }` 挂载 `InvariantService` 和全部 91 个伴随插件,然后才启动普通 Cordis 根上下文的第一个插件。host 会把伴随插件启动加入测试的根级组合边界,因此异步纯检查和插件观察器安装会让测试失败,而不会变成后台诊断。选择、生命周期和所有者聚焦套件自行构建启用的不变式拓扑,在避免重复注册的同时继续测试不变式。 +Vitest setup host 会在普通 Cordis 根上下文启动第一个插件前,以 `{ enabled: true }` 挂载 `InvariantService`,并添加当前测试包的伴随插件。host 会把伴随插件启动加入测试的根级组合边界,因此异步纯检查和插件观察器安装会让测试失败,而不会变成后台诊断。一个完整拓扑会一次挂载全部 91 个伴随插件,以证明运行时注册与覆盖率;选择、生命周期和所有者聚焦套件自行构建启用的不变式拓扑,在避免重复注册的同时继续测试不变式。 辅助测试会拒绝错误插件名、缺失注入、effect、服务或自定义关系、错误服务形状和失败断言。随后,包套件在已有配置与 HMR 路径上激活真实插件。测试专用服务替身必须实现完整的已检查接缝,不能绕过全局不变式。 @@ -50,7 +50,7 @@ Vitest setup host 使用 `{ enabled: true }` 挂载 `InvariantService` 和全部 - **保留生成的仅声明所有权伴随插件。** 不予采纳,因为没有可执行断言的注册无法拒绝损坏的包,也会让完整门禁产生误导。 - **为每个包生成一个合成断言。** 不予采纳,因为通用断言仍是在优化如何通过门禁,而不是保护所有者专属契约。 - **把逐包契约矩阵移入 `dsh-invariants`。** 不予采纳,因为产品导入、词汇和变更所有权会重新回到中央服务。 -- **从伴随插件静态导入每个所有者入口。** 不予采纳,因为全局测试 host 会在 hoisted mock 之前预加载包,发布组合也会支付无关模块初始化成本。 +- **从伴随插件静态导入每个所有者入口。** 不予采纳,因为所有者测试 host 与完整测试 host 会在 hoisted mock 之前预加载包,发布组合也会支付无关模块初始化成本。 - **要求第一方服务类身份。** 不予采纳,因为服务接缝是结构化扩展边界;`instanceof` 会拒绝有效的外部实现和测试替身。 - **从包根入口隐式注册不变式。** 因包拥有服务 RFC 中的组合顺序与隐藏 effect 问题而不予采纳。 @@ -58,8 +58,8 @@ Vitest setup host 使用 `{ enabled: true }` 挂载 `InvariantService` 和全部 - 每个包都贡献可执行检查;新增包若没有检查,会在顶层门禁失败。 - 不变式服务保持与产品无关,同时提供可复用的生命周期与形状观察器。 -- 普通单元、snapshot 与 e2e 测试均全局启用不变式并注册每个伴随插件。 +- 普通单元、snapshot 与 e2e 根上下文均全局启用不变式并注册当前测试包的伴随插件;一个完整拓扑注册全部伴随插件。 - 用于按名称观察的插件名在一个 Cordis 根上下文内必须唯一;安全时包可以选择准确 callback 身份。 - 纯包检查对稳定启动契约采样;可变行为必须使用事件、服务或插件 fiber 观察器。 -- 测试和被选部署会执行更多伴随工作,以少量启动成本换取即时且带包归属的失败。 +- 包测试和被选部署会执行相关伴随工作,以有界启动成本换取即时且带包归属的失败。 - 原有正则选择、blocklist 优先级、注册唯一性、回滚、dispose 与 HMR 契约保持不变。 diff --git a/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.i18n.yaml index 8e83577949..9ab8642f4b 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.i18n.yaml +++ b/.agents/notes/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: 3ffba4ff849b6e4be731586dc7d1b37f4c1a76eb -2026-07-19-package-owned-invariant-service.zh.md: d8be195eccb1ab53439b6cdb90b725ae0889c8a6 +2026-07-19-package-owned-invariant-service.md: 552b088c1cafc2fa762487f57fa1d4ad64390f7e +2026-07-19-package-owned-invariant-service.zh.md: 84bdc6a6a3baefe95713f8ce5b8a4e2af49b64eb diff --git a/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.md b/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.md index 3ffba4ff84..552b088c1c 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.md +++ b/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.md @@ -49,7 +49,7 @@ Blocklist matches override allowlist matches. Each list entry is a case-sensitiv The public registration boundary is `ctx.invariants.register(packageName, installer)`. It reserves one active registration per full npm package name even when filters disable installation, and returns the effect disposer. Disposing the companion or service releases the reservation and all contribution state. -An enabled installer runs in a dedicated child Cordis fiber owned by the service. `InvariantInstaller.inject` declares the child fiber's service surface explicitly; the registry carries no product-specific dependency metadata. The installer receives a bound `fail(message)` reporter. Calling it throws an `Error` subclass named `InvariantError` with stable code `INVARIANT` and the registering `packageName`; it does not extend a product-package error base. +An enabled installer runs in a dedicated child Cordis fiber owned by the service. `InvariantInstaller.inject` declares the child fiber's service surface explicitly; the registry carries no product-specific dependency metadata. The service joins a returned installer promise before registration succeeds, so asynchronous startup checks remain transactional. The installer receives a bound `fail(message)` reporter. Calling it throws an `Error` subclass named `InvariantError` with stable code `INVARIANT` and the registering `packageName`; it does not extend a product-package error base. Registration setup is transactional. If an installer fails after registering listeners, the child fiber is disposed completely and the name reservation is released before the failure escapes. Filtered registrations create no child but retain their reservation until disposal. Reloading a companion therefore begins with one clean installer state; stateful contributions rebuild baselines from their owning services. @@ -84,7 +84,7 @@ Service tests cover defaults, global disablement, allow/block selection, blockli Composition tests cover standard-spine forwarding and generated SDK entries. Loader tests preserve each companion namespace, while built plain-Node smokes exercise the compiled subpath exports. The scoped-event freshness gate reruns its semantic Program analysis. -Every Vitest configuration loads a test host that mounts an explicitly enabled service and all package companions before an ordinary Cordis root's first plugin. Focused service and owner tests construct their own invariant topology so they can exercise disablement, filtering, rollback, and reload without duplicate ownership. Gate tests also execute every companion's `apply` function and verify that it calls `register` with its manifest name, rather than accepting source text alone. +Every Vitest configuration loads a test host that mounts an explicitly enabled service before an ordinary Cordis root's first plugin and adds the current test package's companion. One exhaustive topology mounts all package companions once; focused service and owner tests construct their own invariant topology so they can exercise disablement, filtering, rollback, and reload without duplicate ownership. Gate tests also execute every companion's `apply` function and verify that it calls `register` with its manifest name, rather than accepting source text alone. ## Alternatives considered @@ -101,5 +101,5 @@ Every Vitest configuration loads a test host that mounts an explicitly enabled s - Explicit companion entries make diagnostic cost and ownership visible in Cordis config and package exports. - One selected contribution adds one child fiber and its listener/state cost; filtered registrations retain only name ownership. - Regex sources are deployment configuration and remain fixed until the service reloads. -- Ordinary Vitest roots install every selected companion, trading extra child fibers during tests for repository-wide invariant coverage and immediate fixture failures. +- Ordinary Vitest roots install the owning test package's selected companion; one exhaustive topology pays the full child-fiber cost once for repository-wide registration coverage. - Session storage validation, snapshotting, freezing, provenance, and surface acceptance remain always on and are not affected by invariant selection. diff --git a/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.zh.md b/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.zh.md index d8be195ecc..84bdc6a6a3 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.zh.md @@ -49,7 +49,7 @@ blocklist 匹配优先于 allowlist 匹配。每个条目都是区分大小写 公开注册边界是 `ctx.invariants.register(packageName, installer)`。即使过滤器禁止安装,它也会为每个完整 npm 包名保留唯一的活跃注册,并返回 effect disposer。卸载伴随插件或服务都会释放注册名及全部贡献状态。 -启用的 installer 在服务拥有的独立 Cordis 子 fiber 中运行。`InvariantInstaller.inject` 显式声明该子 fiber 的服务表面;注册服务不携带产品专用依赖元数据。installer 接收绑定后的 `fail(message)` 报告器。调用它会抛出名为 `InvariantError` 的 `Error` 子类,保留稳定代码 `INVARIANT` 并记录注册方 `packageName`;该错误不继承产品包中的错误基类。 +启用的 installer 在服务拥有的独立 Cordis 子 fiber 中运行。`InvariantInstaller.inject` 显式声明该子 fiber 的服务表面;注册服务不携带产品专用依赖元数据。服务会在注册成功前等待 installer 返回的 promise,因此异步启动检查仍具有事务性。installer 接收绑定后的 `fail(message)` 报告器。调用它会抛出名为 `InvariantError` 的 `Error` 子类,保留稳定代码 `INVARIANT` 并记录注册方 `packageName`;该错误不继承产品包中的错误基类。 注册启动是事务性的。如果 installer 在注册监听器后失败,子 fiber 会完整释放,并在失败向外传播前解除包名占用。被过滤的注册不创建子 fiber,但会保留占用直到 dispose。伴随插件重载时总会从干净的 installer 状态开始;有状态贡献从其所属服务重建基线。 @@ -84,7 +84,7 @@ Workspace 约束识别独立的不变式 bundle;包 exports、项目引用、 组合测试覆盖标准 spine 转发和生成的 SDK 条目。Loader 测试固定每个伴随命名空间,构建后的纯 Node smoke 覆盖编译子路径 export。scoped event 新鲜度门禁会重新执行语义 Program 分析。 -每个 Vitest 配置都会加载测试宿主;在普通 Cordis 根上下文启动第一个插件之前,宿主会挂载显式启用的服务以及所有包的伴随插件。服务与所有者的聚焦测试自行构建不变式拓扑,从而在不发生重复所有权冲突的前提下覆盖关闭、过滤、回滚与重载。门禁测试还会执行每个伴随插件的 `apply` 函数,并验证它调用 `register` 时使用包清单中的包名,而不是只检查源码文本。 +每个 Vitest 配置都会加载测试宿主;在普通 Cordis 根上下文启动第一个插件之前,宿主会挂载显式启用的服务,并添加当前测试包的伴随插件。一个完整拓扑会一次挂载所有包的伴随插件;服务与所有者的聚焦测试自行构建不变式拓扑,从而在不发生重复所有权冲突的前提下覆盖关闭、过滤、回滚与重载。门禁测试还会执行每个伴随插件的 `apply` 函数,并验证它调用 `register` 时使用包清单中的包名,而不是只检查源码文本。 ## 考虑过的替代方案 @@ -101,5 +101,5 @@ Workspace 约束识别独立的不变式 bundle;包 exports、项目引用、 - 显式伴随条目让诊断成本和所有权在 Cordis 配置与包 export 中可见。 - 每个选中贡献增加一个子 fiber 及其监听器和状态成本;被过滤注册只保留包名占用。 - 正则表达式源属于部署配置,在服务重载前保持固定。 -- 普通 Vitest 根上下文会安装每个被选中的伴随插件,以增加测试期间的子 fiber 为代价,换取覆盖整个仓库的不变式检查和对 fixture(测试前置数据)错误的即时反馈。 +- 普通 Vitest 根上下文会安装当前测试包中被选中的伴随插件;一个完整拓扑只支付一次全部子 fiber 成本,用于覆盖整个仓库的注册。 - 会话存储验证、快照、冻结、provenance 与 surface 接受规则始终启用,不受不变式选择影响。 diff --git a/docs/testing.md b/docs/testing.md index 3bf37fe79e..1fcefb4bc3 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -9,7 +9,7 @@ How this repo tests, tier by tier, and the rules that keep a green suite meaning - **Real-API e2e** (`pnpm run test:e2e`): with-key tests against live provider APIs — the DeepSeek model plus provider-specific smokes that gate on their own keys (`EXA_API_KEY`, `PERPLEXITY_API_KEY`, …); each suite self-skips without its key so keyless CI stays green ([real-API e2e Agent Note](../.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md)). - **Snapshot** (`pnpm run test:snapshot`): transport-specific keyless expected outputs cover external presentation. ACP suites boot the real example subprocess, replay a recorded session, and diff normalized JSON-RPC plus the re-persisted log ([ACP snapshot Agent Note](../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md)); the headless suite independently pins `stream-json` through its real one-shot subprocess. TUI completed journeys replay recorded primary/child JSONL through the real agent loop and tools before projecting ANSI into semantic terminal-state expected outputs; package-local snapshots retain transient renderer states, and a real PTY conversation covers the process boundary ([TUI snapshot Agent Note](../.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md)). Use `pnpm run test:snapshot:record` when a model transcript must change and `pnpm run test:snapshot:refresh` when committed replay input remains correct; review every JSONL and expected-output diff. System-prompt/tool-schema content is pinned by one ACP scenario (`text-turn`) and tokenized in every other fixture, so a prompt or schema edit churns one committed line ([pinned-header Agent Note](../.agents/notes/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)). -All Vitest configurations mount enabled `ctx.invariants` and every package companion before ordinary Cordis roots start. Focused invariant topology tests compose enabled or deliberately disabled services explicitly, without competing global registrations. +Every Vitest configuration mounts enabled `ctx.invariants` before ordinary roots start. Package tests add their owner's companion; one exhaustive topology mounts them all. Focused invariant topology tests compose enabled or deliberately disabled services without competing global registrations. ## The with-key policy: inference is cheap here diff --git a/packages/code-runtime/code-runtime/tests/service.spec.ts b/packages/code-runtime/code-runtime/tests/service.spec.ts index 01736ea0ab..68bc809de7 100644 --- a/packages/code-runtime/code-runtime/tests/service.spec.ts +++ b/packages/code-runtime/code-runtime/tests/service.spec.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import { Context } from 'cordis' import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime' import type { CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime' +import { InvariantError } from '@deepseek-ai/dsh-invariants' /** * Minimal concrete runtime: records requests, "executes" by invoking every @@ -96,6 +97,13 @@ describe('CodeRuntime service seam', () => { child.provide('codeRuntime', value as unknown as CodeRuntime) }, } - await expect(ctx.plugin(invalidRuntime)).rejects.toThrow(message) + let caught: unknown + try { + await ctx.plugin(invalidRuntime) + } catch (error) { + caught = error + } + expect(caught).toBeInstanceOf(InvariantError) + expect((caught as Error).message).toMatch(message) }) }) diff --git a/packages/examples/jsonrpc-demo/src/invariant.ts b/packages/examples/jsonrpc-demo/src/invariant.ts index f65eee8165..a9352d02f8 100644 --- a/packages/examples/jsonrpc-demo/src/invariant.ts +++ b/packages/examples/jsonrpc-demo/src/invariant.ts @@ -12,13 +12,10 @@ export const name = 'jsonrpc-demo-invariant' export const inject = ['invariants'] /** 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') +const install: InvariantInstaller = async (_ctx, fail) => { + 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') } /** diff --git a/packages/hooks/hook-protocol/src/invariant.ts b/packages/hooks/hook-protocol/src/invariant.ts index 6503791f1c..997dd75b30 100644 --- a/packages/hooks/hook-protocol/src/invariant.ts +++ b/packages/hooks/hook-protocol/src/invariant.ts @@ -12,22 +12,21 @@ export const name = 'hook-protocol-invariant' export const inject = ['invariants'] /** 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 install: InvariantInstaller = async (_ctx, fail) => { + const [{ parseHookOutput }, { mergeHookOutputs }] = await Promise.all([ + import('./codec.ts'), + 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') + 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') } /** diff --git a/packages/sandbox/sandbox/tests/invariant.spec.ts b/packages/sandbox/sandbox/tests/invariant.spec.ts new file mode 100644 index 0000000000..24854b56ae --- /dev/null +++ b/packages/sandbox/sandbox/tests/invariant.spec.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import { InvariantError } from '@deepseek-ai/dsh-invariants' +import { SandboxProvider } from '@deepseek-ai/dsh-sandbox' +import type { ConfinedArgv, SandboxPolicy } from '@deepseek-ai/dsh-sandbox' + +class StubSandboxProvider extends SandboxProvider { + confine(argv: readonly string[], _policy: SandboxPolicy): ConfinedArgv { + return { + argv: [...argv], + enforcement: 'full', + denialSignatures: [], + runnerFailureSignatures: [], + } + } +} + +describe('sandbox package invariant', () => { + it('accepts a provider that exposes the confinement seam', async () => { + const ctx = new Context() + await ctx.plugin(StubSandboxProvider) + expect(ctx.sandbox).toBeInstanceOf(StubSandboxProvider) + }) + + it('rejects a service binding without confine()', async () => { + const ctx = new Context() + const invalidSandbox = { + name: 'invalid-sandbox', + apply(child: Context) { + child.provide('sandbox', {} as SandboxProvider) + }, + } + let caught: unknown + try { + await ctx.plugin(invalidSandbox) + } catch (error) { + caught = error + } + expect(caught).toBeInstanceOf(InvariantError) + expect(caught).toHaveProperty('packageName', '@deepseek-ai/dsh-sandbox') + expect((caught as Error).message).toMatch(/must expose method "confine"/) + }) +}) diff --git a/packages/sdk/create-sdk/src/invariant.ts b/packages/sdk/create-sdk/src/invariant.ts index 66a95e6a38..417d936128 100644 --- a/packages/sdk/create-sdk/src/invariant.ts +++ b/packages/sdk/create-sdk/src/invariant.ts @@ -12,24 +12,23 @@ export const name = 'create-sdk-invariant' export const inject = ['invariants'] /** 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') +const install: InvariantInstaller = async (_ctx, fail) => { + const [{ parseCreateArgs }, packageEntry] = await Promise.all([ + import('./args.ts'), + 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') } /** diff --git a/packages/sdk/helper/src/invariant.ts b/packages/sdk/helper/src/invariant.ts index e91fb012d5..5faf1ea49c 100644 --- a/packages/sdk/helper/src/invariant.ts +++ b/packages/sdk/helper/src/invariant.ts @@ -12,20 +12,17 @@ export const name = 'helper-invariant' export const inject = ['invariants'] /** 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') +const install: InvariantInstaller = async (_ctx, fail) => { + 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') } /** diff --git a/packages/sdk/scripts/src/invariant.ts b/packages/sdk/scripts/src/invariant.ts index 316e7ebed1..522791fe3b 100644 --- a/packages/sdk/scripts/src/invariant.ts +++ b/packages/sdk/scripts/src/invariant.ts @@ -12,22 +12,19 @@ export const name = 'scripts-invariant' export const inject = ['invariants'] /** 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') +const install: InvariantInstaller = async (_ctx, fail) => { + 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') } /** diff --git a/packages/sdk/telemetry/src/invariant.ts b/packages/sdk/telemetry/src/invariant.ts index 8333b1ff71..9ec704b672 100644 --- a/packages/sdk/telemetry/src/invariant.ts +++ b/packages/sdk/telemetry/src/invariant.ts @@ -12,19 +12,16 @@ export const name = 'telemetry-invariant' export const inject = ['invariants'] /** Assert the final telemetry redaction boundary removes secrets without corrupting ordinary package metadata. */ -const install: InvariantInstaller = (ctx, fail) => { - ctx.effect(async () => { - const [{ DEFAULT_REDACTION_PLACEHOLDER, SecretRedactor }, { telemetryRedactionViolation }] = await Promise.all([ - import('./secret-redactor.ts'), - import('./redaction-contract.ts'), - ]) - const redactor = new SecretRedactor() - const violation = telemetryRedactionViolation(redactor, DEFAULT_REDACTION_PLACEHOLDER, PACKAGE_NAME) - assertInvariant(fail, - violation === undefined, - violation ?? 'telemetry redaction contract failed without a diagnostic') - return () => {} - }, 'telemetry: validate secret-redaction boundary') +const install: InvariantInstaller = async (_ctx, fail) => { + const [{ telemetryRedactionViolation }, { DEFAULT_REDACTION_PLACEHOLDER, SecretRedactor }] = await Promise.all([ + import('./redaction-contract.ts'), + import('./secret-redactor.ts'), + ]) + const redactor = new SecretRedactor() + const violation = telemetryRedactionViolation(redactor, DEFAULT_REDACTION_PLACEHOLDER, PACKAGE_NAME) + assertInvariant(fail, + violation === undefined, + violation ?? 'telemetry redaction contract failed without a diagnostic') } /** diff --git a/packages/subagent/subagent-inprocess/src/invariant.ts b/packages/subagent/subagent-inprocess/src/invariant.ts index a007200d73..caaf598f55 100644 --- a/packages/subagent/subagent-inprocess/src/invariant.ts +++ b/packages/subagent/subagent-inprocess/src/invariant.ts @@ -12,15 +12,12 @@ export const name = 'subagent-inprocess-invariant' export const inject = ['invariants'] /** 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') +const install: InvariantInstaller = async (_ctx, fail) => { + 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') } /** diff --git a/packages/subagent/subagent-subprocess/src/invariant.ts b/packages/subagent/subagent-subprocess/src/invariant.ts index ed29ecc855..9eed9ae9e3 100644 --- a/packages/subagent/subagent-subprocess/src/invariant.ts +++ b/packages/subagent/subagent-subprocess/src/invariant.ts @@ -13,19 +13,24 @@ export const name = 'subagent-subprocess-invariant' export const inject = ['invariants'] /** 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 install: InvariantInstaller = async (_ctx, fail) => { + const { buildChildEnv } = await import('./index.ts') + const ambientProbe = `DSH_INVARIANT_AMBIENT_TOKEN_${process.pid}` + assertInvariant(fail, SENSITIVE_ENV_PATTERN.test(ambientProbe), + 'the invariant ambient probe must remain credential-shaped') + process.env[ambientProbe] = 'must-not-reach-child' + let scrubbed: NodeJS.ProcessEnv + try { + scrubbed = buildChildEnv({}) + } finally { + Reflect.deleteProperty(process.env, ambientProbe) + } + assertInvariant(fail, !Object.hasOwn(scrubbed, ambientProbe), + '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') + 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') } /** diff --git a/packages/support/acp-snapshot/src/invariant.ts b/packages/support/acp-snapshot/src/invariant.ts index fc568d7b37..ac29b83cff 100644 --- a/packages/support/acp-snapshot/src/invariant.ts +++ b/packages/support/acp-snapshot/src/invariant.ts @@ -12,25 +12,22 @@ export const name = 'acp-snapshot-invariant' export const inject = ['invariants'] /** 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') +const install: InvariantInstaller = async (_ctx, fail) => { + 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') } /** diff --git a/packages/support/agent-loop-testkit/src/invariant.ts b/packages/support/agent-loop-testkit/src/invariant.ts index fd72038d3f..69792b77ea 100644 --- a/packages/support/agent-loop-testkit/src/invariant.ts +++ b/packages/support/agent-loop-testkit/src/invariant.ts @@ -12,16 +12,13 @@ export const name = 'agent-loop-testkit-invariant' export const inject = ['invariants'] /** 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') +const install: InvariantInstaller = async (_ctx, fail) => { + 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') } /** diff --git a/packages/support/invariants/README.md b/packages/support/invariants/README.md index eb26f57e43..7b922bcda9 100644 --- a/packages/support/invariants/README.md +++ b/packages/support/invariants/README.md @@ -16,7 +16,7 @@ Defaults are `enabled: true`, `package_allowlist: []`, and `package_blocklist: [ Each entry is a case-sensitive JavaScript regular-expression source compiled with `new RegExp(pattern)`. Matching is unanchored unless the source supplies `^` and `$`; `/pattern/flags` syntax is not parsed. Blank, whitespace-padded, invalid, or duplicate entries within one list fail service startup. A valid pattern may match no currently loaded package so later loading and HMR remain deterministic. -`ctx.invariants.register(packageName, installer)` reserves one active registration for the full npm package name, including when filters keep its installer inactive, and returns its disposer. An enabled contribution runs in a dedicated child Cordis fiber. The installer can declare its required service surface through `installer.inject` and receives `fail(message)`, which throws an `InvariantError` bound to the registering package. Installer failure disposes the child and releases ownership atomically. +`ctx.invariants.register(packageName, installer)` reserves one active registration for the full npm package name, including when filters keep its installer inactive, and returns its disposer. An enabled contribution runs in a dedicated child Cordis fiber. The installer can declare its required service surface through `installer.inject` and receives `fail(message)`, which throws an `InvariantError` bound to the registering package. Synchronous or asynchronous installer completion is joined before registration succeeds; failure disposes the child and releases ownership atomically. The service owns every registration fiber, while the returned disposer also belongs to the companion fiber. Unloading either side removes the listeners and reservation completely. A companion can therefore reload and register the same package name without retaining trace state or duplicate listeners; packages that need an existing baseline rebuild it during installation. @@ -32,7 +32,7 @@ Packages select the narrowest runtime form that protects their public contract: |---|---| | 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. | +| Pure library, bin, or support package | `assertInvariant` checks stable protocol algebra, parser mapping, path/timeout/retention rules, normalization, or entrypoint shape during child startup. | Four companions additionally install stateful event and request checks: @@ -62,7 +62,7 @@ ctx.plugin(InvariantService, { ctx.plugin(SessionInvariant) ``` -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. +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. Plugin and service helpers multiplex package contracts through indexed lifecycle listeners shared by the Cordis root, while contribution disposal removes only that owner's contract. Vitest gives every ordinary root an explicitly enabled service and mounts the current test package's companion; one exhaustive topology test mounts all companions once, and focused invariant-service tests construct their own topology to exercise filtering and lifecycle behavior. ## Model Experience diff --git a/packages/support/invariants/src/index.ts b/packages/support/invariants/src/index.ts index 374012fdbd..265596b677 100644 --- a/packages/support/invariants/src/index.ts +++ b/packages/support/invariants/src/index.ts @@ -28,15 +28,15 @@ export interface Config { */ export type InvariantFailure = (message: string) => never -/** Install one package's listeners into the registration's child context. */ +/** Install one package's checks into the registration's child context. */ export interface InvariantInstaller { /** * Install the package contribution. * @param ctx - child context owned by this invariant registration. * @param fail - reporter bound to the registering package name. - * @returns nothing after synchronous listener installation completes. + * @returns nothing, or a promise settling after asynchronous checks finish. */ - (ctx: Context, fail: InvariantFailure): void + (ctx: Context, fail: InvariantFailure): void | Promise /** Services the child installer fiber may access. */ readonly inject?: Inject } @@ -70,11 +70,111 @@ function collectEffectLabels(fiber: Fiber): ReadonlySet { return labels } +/** One package check routed by a root-shared plugin lifecycle dispatcher. */ +interface PluginObservation { + readonly callback: globalThis.Function | undefined + readonly contract: PluginInvariantContract + readonly fail: InvariantFailure +} + +/** Indexed plugin checks and the two lifecycle listeners shared by one root. */ +interface PluginObservationHub { + readonly byCallback: Map> + readonly byName: Map> +} + +const pluginObservationHubs = new WeakMap() + +/** Check one already-matched active plugin fiber. */ +function inspectPluginObservation(observation: PluginObservation, fiber: Fiber): void { + if (fiber.state !== FiberState.ACTIVE || fiber.uid === null) return + const { callback, contract, fail } = observation + 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) +} + +/** Route one lifecycle notification only to checks that can match its runtime. */ +function inspectObservedPlugin(hub: PluginObservationHub, fiber: Fiber): void { + const callback = fiber.runtime?.callback + if (callback !== undefined) { + for (const observation of hub.byCallback.get(callback) ?? []) { + inspectPluginObservation(observation, fiber) + } + } + const runtimeName = fiber.runtime?.name + if (runtimeName !== undefined) { + for (const observation of hub.byName.get(runtimeName) ?? []) { + inspectPluginObservation(observation, fiber) + } + } +} + +/** Return the root's shared plugin dispatcher, creating its two listeners once. */ +function pluginObservationHub(ctx: Context): PluginObservationHub { + const root = ctx.root + const existing = pluginObservationHubs.get(root) + if (existing !== undefined) return existing + + const hub: PluginObservationHub = { + byCallback: new Map(), + byName: new Map(), + } + pluginObservationHubs.set(root, hub) + root.on('internal/plugin', (fiber) => { inspectObservedPlugin(hub, fiber) }, { global: true }) + root.on('internal/status', (fiber) => { inspectObservedPlugin(hub, fiber) }, { global: true }) + return hub +} + +/** Add one plugin observation to a typed exact-key index. */ +function addIndexedPluginObservation( + index: Map>, + key: Key, + observation: PluginObservation, +): () => void { + const observations = index.get(key) ?? new Set() + index.set(key, observations) + observations.add(observation) + return () => { + observations.delete(observation) + if (observations.size === 0) index.delete(key) + } +} + +/** Add one observation to its exact callback or runtime-name index. */ +function addPluginObservation(hub: PluginObservationHub, observation: PluginObservation): () => void { + if (observation.callback === undefined) { + return addIndexedPluginObservation(hub.byName, observation.contract.name, observation) + } + return addIndexedPluginObservation(hub.byCallback, observation.callback, observation) +} + /** * 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. + * are checked through two indexed lifecycle listeners shared by the root. * @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. @@ -90,50 +190,79 @@ export function observePluginInvariant( 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) - } + const observation: PluginObservation = { callback, contract, fail } if (contract.plugin === undefined) { for (const runtime of ctx.registry.values()) { - for (const fiber of runtime.fibers) inspect(fiber) + if (runtime.name !== contract.name) continue + for (const fiber of runtime.fibers) inspectPluginObservation(observation, fiber) } } else { - for (const fiber of ctx.registry.get(contract.plugin)?.fibers ?? []) inspect(fiber) + for (const fiber of ctx.registry.get(contract.plugin)?.fibers ?? []) { + inspectPluginObservation(observation, fiber) + } + } + const hub = pluginObservationHub(ctx) + ctx.effect( + () => addPluginObservation(hub, observation), + `invariants.observePlugin(${JSON.stringify(contract.name)})`, + ) +} + +/** One structural check routed by a root-shared service lifecycle dispatcher. */ +interface ServiceObservation { + readonly fail: InvariantFailure + readonly validate: (value: unknown) => string | undefined +} + +/** Service checks and the single service listener shared by one root. */ +interface ServiceObservationHub { + readonly byName: Map> +} + +const serviceObservationHubs = new WeakMap() + +/** Check one present service implementation. */ +function inspectServiceObservation(observation: ServiceObservation, value: unknown): void { + if (value === undefined) return + const message = observation.validate(value) + if (message !== undefined) observation.fail(message) +} + +/** Return the root's shared service dispatcher, creating its listener once. */ +function serviceObservationHub(ctx: Context): ServiceObservationHub { + const root = ctx.root + const existing = serviceObservationHubs.get(root) + if (existing !== undefined) return existing + + const hub: ServiceObservationHub = { byName: new Map() } + serviceObservationHubs.set(root, hub) + root.on('internal/service', (name, value: unknown) => { + for (const observation of hub.byName.get(name) ?? []) { + inspectServiceObservation(observation, value) + } + }, { global: true }) + return hub +} + +/** Add one service observation to its exact service-name index. */ +function addServiceObservation( + hub: ServiceObservationHub, + serviceName: string, + observation: ServiceObservation, +): () => void { + const observations = hub.byName.get(serviceName) ?? new Set() + hub.byName.set(serviceName, observations) + observations.add(observation) + return () => { + observations.delete(observation) + if (observations.size === 0) hub.byName.delete(serviceName) } - 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. + * Validate every current and future implementation bound to one Cordis + * service through the root's indexed shared service listener. * @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. @@ -146,16 +275,14 @@ export function observeServiceInvariant( 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 observation: ServiceObservation = { fail, validate } const current: unknown = ctx.get(serviceName) - inspect(current) - ctx.on('internal/service', (name, value: unknown) => { - if (name === serviceName) inspect(value) - }, { global: true }) + inspectServiceObservation(observation, current) + const hub = serviceObservationHub(ctx) + ctx.effect( + () => addServiceObservation(hub, serviceName, observation), + `invariants.observeService(${JSON.stringify(serviceName)})`, + ) } /** Structural runtime surface required from a Cordis service implementation. */ @@ -297,7 +424,7 @@ export class InvariantService extends Service { * even when filtering disables its checks. Enabled installers run in a child * fiber; failure disposes that fiber and releases the reservation. * @param packageName - full npm package name that owns the contribution. - * @param installer - synchronous listener installer for the child context. + * @param installer - listener or startup-check installer for the child context. * @returns an effect-scoped disposer for the registration. */ register(packageName: string, installer: InvariantInstaller): () => void { @@ -324,11 +451,11 @@ export class InvariantService extends Service { } } - const installInvariant = (childCtx: Context) => { + const installInvariant = (childCtx: Context) => ( installer(childCtx, (message): never => { throw new InvariantError(packageName, message) }) - } + ) const child = ctx.plugin(installer.inject === undefined ? installInvariant : Object.assign(installInvariant, { inject: installer.inject })) diff --git a/packages/support/invariants/tests/service.spec.ts b/packages/support/invariants/tests/service.spec.ts index 44f8d61139..76fda543ad 100644 --- a/packages/support/invariants/tests/service.spec.ts +++ b/packages/support/invariants/tests/service.spec.ts @@ -273,6 +273,25 @@ describe('InvariantService lifecycle', () => { expect(retry).toHaveBeenCalledOnce() }) + it('joins asynchronous checks and rolls back their effects on failure', async () => { + const { ctx } = await setup() + const leaked = vi.fn() + const failed = runtimeRegistration(ctx.invariants.register('@deepseek-ai/dsh-async-probe', async (child, fail) => { + child.on('invariants-test/ping', leaked, { global: true }) + await Promise.resolve() + fail('asynchronous check failed') + })) + await expect(Promise.resolve(failed)).rejects.toThrow(/asynchronous check failed/) + ctx.emit('invariants-test/ping') + expect(leaked).not.toHaveBeenCalled() + + const retry = runtimeRegistration(ctx.invariants.register('@deepseek-ai/dsh-async-probe', async () => { + await Promise.resolve() + })) + await retry + await retry() + }) + it('releases a synchronous reservation if the service fiber is already inactive', async () => { const { ctx, fiber } = await setup() const service = ctx.invariants @@ -282,11 +301,15 @@ describe('InvariantService lifecycle', () => { }) describe('package-owned invariant helpers', () => { + interface InvariantDisposer { + (): void | Promise + } + async function registerInstaller( ctx: Context, packageName: string, installer: InvariantInstaller, - ): Promise<() => void> { + ): Promise { const registration = runtimeRegistration(ctx.invariants.register(packageName, installer)) const dispose = await Promise.resolve(registration) return dispose @@ -376,6 +399,38 @@ describe('package-owned invariant helpers', () => { await ctx.plugin(plugin) }) + it('multiplexes same-runtime plugin checks through one root listener pair and disposes each owner', async () => { + const { ctx } = await setup() + const firstValidation = vi.fn(() => undefined) + const secondValidation = vi.fn(() => undefined) + const first = await registerInstaller(ctx, '@deepseek-ai/dsh-shared-plugin-first', (child, fail) => { + observePluginInvariant(child, fail, { name: 'shared-plugin-probe', validate: firstValidation }) + }) + const second = await registerInstaller(ctx, '@deepseek-ai/dsh-shared-plugin-second', (child, fail) => { + observePluginInvariant(child, fail, { name: 'shared-plugin-probe', validate: secondValidation }) + }) + const rootEffectLabels = ctx.fiber.getEffects().map(effect => effect.label) + expect(rootEffectLabels.filter(label => label === 'ctx.on("internal/plugin")')).toHaveLength(1) + expect(rootEffectLabels.filter(label => label === 'ctx.on("internal/status")')).toHaveLength(1) + + const plugin = effectPlugin({ name: 'shared-plugin-probe' }) + const firstFiber = await ctx.plugin(plugin) + expect(firstValidation).toHaveBeenCalledOnce() + expect(secondValidation).toHaveBeenCalledOnce() + + await first() + await firstFiber.dispose() + const secondFiber = await ctx.plugin(plugin) + expect(firstValidation).toHaveBeenCalledOnce() + expect(secondValidation).toHaveBeenCalledTimes(2) + + await second() + await secondFiber.dispose() + await ctx.plugin(plugin) + expect(firstValidation).toHaveBeenCalledOnce() + expect(secondValidation).toHaveBeenCalledTimes(2) + }) + 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) => { @@ -449,12 +504,46 @@ describe('package-owned invariant helpers', () => { .rejects.toThrow(/wrong watched service/) }) + it('multiplexes same-name service checks through one root listener and disposes each owner', async () => { + const { ctx } = await setup() + const firstValidation = vi.fn(() => undefined) + const secondValidation = vi.fn(() => undefined) + const first = await registerInstaller(ctx, '@deepseek-ai/dsh-shared-service-first', (child, fail) => { + observeServiceInvariant(child, fail, 'watchedInvariantProbe', firstValidation) + }) + const second = await registerInstaller(ctx, '@deepseek-ai/dsh-shared-service-second', (child, fail) => { + observeServiceInvariant(child, fail, 'watchedInvariantProbe', secondValidation) + }) + const rootEffectLabels = ctx.fiber.getEffects().map(effect => effect.label) + expect(rootEffectLabels.filter(label => label === 'ctx.on("internal/service")')).toHaveLength(1) + + const firstFiber = await ctx.plugin(WatchedInvariantProbeService) + expect(firstValidation).toHaveBeenCalledOnce() + expect(secondValidation).toHaveBeenCalledOnce() + + await first() + const firstCallsAfterDisposal = firstValidation.mock.calls.length + const secondCallsBeforeRemount = secondValidation.mock.calls.length + await firstFiber.dispose() + const secondFiber = await ctx.plugin(WatchedInvariantProbeService) + expect(firstValidation).toHaveBeenCalledTimes(firstCallsAfterDisposal) + expect(secondValidation.mock.calls.length).toBeGreaterThan(secondCallsBeforeRemount) + + await second() + const firstCallsAfterBothDisposals = firstValidation.mock.calls.length + const secondCallsAfterBothDisposals = secondValidation.mock.calls.length + await secondFiber.dispose() + await ctx.plugin(WatchedInvariantProbeService) + expect(firstValidation).toHaveBeenCalledTimes(firstCallsAfterBothDisposals) + expect(secondValidation).toHaveBeenCalledTimes(secondCallsAfterBothDisposals) + }) + 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() + await valid() const invalid = runtimeRegistration(ctx.invariants.register('@deepseek-ai/dsh-invalid-assertion', (_child, fail) => { assertInvariant(fail, false, 'must stay true') diff --git a/packages/support/loader-smoke/src/invariant.ts b/packages/support/loader-smoke/src/invariant.ts index 826d33dd30..6fbb41a123 100644 --- a/packages/support/loader-smoke/src/invariant.ts +++ b/packages/support/loader-smoke/src/invariant.ts @@ -12,23 +12,20 @@ export const name = 'loader-smoke-invariant' export const inject = ['invariants'] /** 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') +const install: InvariantInstaller = async (_ctx, fail) => { + 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') } /** diff --git a/packages/ui/app-boot/src/invariant.ts b/packages/ui/app-boot/src/invariant.ts index 30e2cfea69..8591c60dd1 100644 --- a/packages/ui/app-boot/src/invariant.ts +++ b/packages/ui/app-boot/src/invariant.ts @@ -13,18 +13,15 @@ export const name = 'app-boot-invariant' export const inject = ['invariants'] /** 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') +const install: InvariantInstaller = async (_ctx, fail) => { + 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') } /** diff --git a/packages/util/brand/src/invariant.ts b/packages/util/brand/src/invariant.ts index 5ac98489cb..d632921d35 100644 --- a/packages/util/brand/src/invariant.ts +++ b/packages/util/brand/src/invariant.ts @@ -12,13 +12,10 @@ export const name = 'brand-invariant' export const inject = ['invariants'] /** 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') +const install: InvariantInstaller = async (_ctx, fail) => { + 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') } /** diff --git a/packages/util/home/src/invariant.ts b/packages/util/home/src/invariant.ts index 651d877e00..fc7c9c3129 100644 --- a/packages/util/home/src/invariant.ts +++ b/packages/util/home/src/invariant.ts @@ -13,17 +13,14 @@ export const name = 'home-invariant' export const inject = ['invariants'] /** 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') +const install: InvariantInstaller = async (_ctx, fail) => { + 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') } /** diff --git a/packages/util/paths/src/invariant.ts b/packages/util/paths/src/invariant.ts index ce0ddb653c..20a3962ff4 100644 --- a/packages/util/paths/src/invariant.ts +++ b/packages/util/paths/src/invariant.ts @@ -14,17 +14,14 @@ export const name = 'paths-invariant' export const inject = ['invariants'] /** 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') +const install: InvariantInstaller = async (_ctx, fail) => { + 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') } /** diff --git a/packages/util/retention/src/invariant.ts b/packages/util/retention/src/invariant.ts index 380e4e3d4b..fa8ab7d36a 100644 --- a/packages/util/retention/src/invariant.ts +++ b/packages/util/retention/src/invariant.ts @@ -12,24 +12,21 @@ export const name = 'retention-invariant' export const inject = ['invariants'] /** 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') +const install: InvariantInstaller = async (_ctx, fail) => { + 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') } /** diff --git a/packages/util/timeout/src/invariant.ts b/packages/util/timeout/src/invariant.ts index 8302380eaa..2d9d22f5d6 100644 --- a/packages/util/timeout/src/invariant.ts +++ b/packages/util/timeout/src/invariant.ts @@ -12,19 +12,16 @@ export const name = 'timeout-invariant' export const inject = ['invariants'] /** 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') +const install: InvariantInstaller = async (_ctx, fail) => { + 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') } /** diff --git a/scripts/test-invariants.spec.ts b/scripts/test-invariants.spec.ts index 77150abb74..5c3b9120bc 100644 --- a/scripts/test-invariants.spec.ts +++ b/scripts/test-invariants.spec.ts @@ -2,7 +2,11 @@ import { describe, expect, it, vi } from 'vitest' import { Context, Service } from 'cordis' import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' import { packageInvariantOwners } from './package-invariants.ts' -import { MANUAL_INVARIANT_TESTS, testInvariantCompanions } from './test-invariants.ts' +import { + MANUAL_INVARIANT_TESTS, + testInvariantCompanionPaths, + testInvariantCompanions, +} from './test-invariants.ts' declare module 'cordis' { interface Context { @@ -17,7 +21,7 @@ class TestInvariantProbe extends Service { } describe('global test invariant host', () => { - it('loads every companion and reserves every package name with enabled checks', async () => { + it('uses one exhaustive topology to reserve every package name with enabled checks', async () => { const ctx = new Context() await ctx.plugin(TestInvariantProbe) @@ -39,6 +43,14 @@ describe('global test invariant host', () => { expect(unreserved).toEqual([]) }) + it('mounts the owning package companion while leaving non-package roots service-only', () => { + expect(testInvariantCompanionPaths('/repo/packages/core/tools/tests/tools.spec.ts')) + .toEqual(['../packages/core/tools/src/invariant.ts']) + expect(testInvariantCompanionPaths('/repo/examples/echo-agent/tests/echo.spec.ts')).toEqual([]) + expect(testInvariantCompanionPaths('/repo/scripts/test-invariants.spec.ts')) + .toEqual(Object.keys(testInvariantCompanions).sort()) + }) + it('executes each companion registration with its owning package name', async () => { const owners = new Map(packageInvariantOwners(process.cwd()).map(owner => [owner.sourcePath, owner.packageName])) const registrations = new Map() diff --git a/scripts/test-invariants.ts b/scripts/test-invariants.ts index 584db3d5d7..b91fdda462 100644 --- a/scripts/test-invariants.ts +++ b/scripts/test-invariants.ts @@ -1,7 +1,8 @@ /** * Vitest-wide invariant host. Ordinary Cordis roots receive the invariant - * service with global enablement and every package companion before their first - * plugin starts. Focused invariant tests own their service topology explicitly. + * service with global enablement plus the current test package's companion. + * One topology test mounts every companion; focused invariant tests own their + * service topology explicitly. */ import { expect } from 'vitest' @@ -40,6 +41,7 @@ export const MANUAL_INVARIANT_TESTS = [ interface InvariantHost { readonly fibers: readonly PluginFiber[] readonly byCallback: ReadonlyMap + readonly ready: Promise } type PluginFiber = ReturnType @@ -55,13 +57,15 @@ RegistryService.prototype.plugin = function(plugin: Plugin, config?: unknown, ge const host = hosts.get(root) ?? startInvariantHost(root) const callback = this.resolve(plugin) const existing = callback === undefined ? undefined : host.byCallback.get(callback) - if (existing !== undefined) return existing + if (existing !== undefined) { + return this.ctx === root ? joinInvariantStartup(existing, host.ready) : existing + } const fiber = originalPlugin.call(this, plugin, config, getOuterStack) // A root-level await is the test's composition boundary. Nested plugin // fibers must not await their own companion parent through the global host. if (this.ctx !== root) return fiber - return joinInvariantStartup(fiber, host.fibers) + return joinInvariantStartup(fiber, host.ready) } function usesManualInvariantTree(): boolean { @@ -69,6 +73,30 @@ function usesManualInvariantTree(): boolean { return MANUAL_INVARIANT_TESTS.some(path => testPath.endsWith(path)) } +const ALL_COMPANION_TESTS = ['/scripts/test-invariants.spec.ts'] as const + +/** + * Select the package companions that an ordinary test root must register. + * Package tests receive their owner's checks; the dedicated topology test + * receives every owner so coverage and exhaustive runtime registration remain + * independently enforced. + * @param testPath - absolute or repo-relative normalized Vitest file path. + * @returns sorted `import.meta.glob` keys for companions to mount. + */ +export function testInvariantCompanionPaths(testPath: string): string[] { + const normalized = testPath.replaceAll('\\', '/') + const allPaths = Object.keys(testInvariantCompanions).sort() + if (ALL_COMPANION_TESTS.some(path => normalized.endsWith(path))) return allPaths + + const owner = normalized.match(/\/packages\/([^/]+)\/([^/]+)\/tests\//) + if (owner === null) return [] + const companionPath = `../packages/${owner[1]}/${owner[2]}/src/invariant.ts` + if (testInvariantCompanions[companionPath] === undefined) { + throw new Error(`test invariants: package test has no companion at ${companionPath}`) + } + return [companionPath] +} + function startInvariantHost(root: Context): InvariantHost { const fibers: PluginFiber[] = [] const byCallback = new Map() @@ -81,21 +109,35 @@ function startInvariantHost(root: Context): InvariantHost { } mount(InvariantService, { enabled: true }) - for (const [path, companion] of Object.entries(testInvariantCompanions).sort(([left], [right]) => left.localeCompare(right))) { + const testPath = expect.getState().testPath ?? '' + const companionPaths = testInvariantCompanionPaths(testPath) + for (const path of companionPaths) { + const companion = testInvariantCompanions[path] + if (companion === undefined) { + throw new Error(`test invariants: selected companion vanished at ${path}`) + } if (!companion.inject.includes('invariants')) { throw new Error(`test invariants: ${path} must inject the invariant service`) } mount(companion) } - const host = { fibers, byCallback } + const [serviceFiber, ...companionFibers] = fibers + if (serviceFiber === undefined) throw new Error('test invariants: service fiber was not mounted') + // A companion is initially PENDING on the invariant service, and Cordis + // Fiber.await() only joins work already in flight. Wait for the service to + // activate its dependants before joining their startup and failures. + const ready = serviceFiber.await() + .then(() => Promise.all(companionFibers.map(fiber => fiber.await()))) + .then(() => undefined) + const host = { fibers, byCallback, ready } hosts.set(root, host) return host } -function joinInvariantStartup(fiber: PluginFiber, invariantFibers: readonly PluginFiber[]): PluginFiber { +function joinInvariantStartup(fiber: PluginFiber, invariantReady: Promise): PluginFiber { const readiness = fiber.await().then(async (loaded) => { - await Promise.all(invariantFibers.map(invariant => invariant.await())) + await invariantReady return loaded }) const joined = Object.create(fiber) as PluginFiber From 512ef2a18e73f930077f91800d0b1e6beaeedd3b Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 20 Jul 2026 02:00:40 +0800 Subject: [PATCH 06/12] docs(invariants): refresh installer API catalogs --- docs/cordis-catalog/services.md | 4 ++-- packages/cordis/tool-cordis/src/api-catalog.ts | 4 ++-- website/zh-CN/api/harness/invariants.md | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 7da704df75..79e540e7bd 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -489,13 +489,13 @@ Package-owned invariant registry with global and regex-based selection. * even when filtering disables its checks. Enabled installers run in a child * fiber; failure disposes that fiber and releases the reservation. * @param packageName - full npm package name that owns the contribution. - * @param installer - synchronous listener installer for the child context. + * @param installer - listener or startup-check installer for the child context. * @returns an effect-scoped disposer for the registration. */ register(packageName: string, installer: InvariantInstaller): () => void ``` -Source: [`packages/support/invariants/src/index.ts:261`](../../packages/support/invariants/src/index.ts) +Source: [`packages/support/invariants/src/index.ts:388`](../../packages/support/invariants/src/index.ts) ## `ctx.llm` — `LlmService` diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index 40b8f38471..bceeed2ad9 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -256,7 +256,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ methods: [ { signature: 'register(packageName: string, installer: InvariantInstaller): () => void', - jsDoc: '/**\n * Register one package\'s invariant installer. The package name is reserved\n * even when filtering disables its checks. Enabled installers run in a child\n * fiber; failure disposes that fiber and releases the reservation.\n * @param packageName - full npm package name that owns the contribution.\n * @param installer - synchronous listener installer for the child context.\n * @returns an effect-scoped disposer for the registration.\n */', + jsDoc: '/**\n * Register one package\'s invariant installer. The package name is reserved\n * even when filtering disables its checks. Enabled installers run in a child\n * fiber; failure disposes that fiber and releases the reservation.\n * @param packageName - full npm package name that owns the contribution.\n * @param installer - listener or startup-check installer for the child context.\n * @returns an effect-scoped disposer for the registration.\n */', }, ], }, @@ -1192,7 +1192,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'InvariantInstaller', - declaration: 'export interface InvariantInstaller {\n (ctx: Context, fail: InvariantFailure): void;\n readonly inject?: Inject;\n}', + declaration: 'export interface InvariantInstaller {\n (ctx: Context, fail: InvariantFailure): void | Promise;\n readonly inject?: Inject;\n}', }, { name: 'JsonValue', diff --git a/website/zh-CN/api/harness/invariants.md b/website/zh-CN/api/harness/invariants.md index 61589489f7..e2f582a566 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#L261) +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/support/invariants/src/index.ts#L388) ### ctx.invariants.register(packageName, installer) @@ -16,7 +16,7 @@ Package-owned invariant registry with global and regex-based selection. * even when filtering disables its checks. Enabled installers run in a child * fiber; failure disposes that fiber and releases the reservation. * @param packageName - full npm package name that owns the contribution. - * @param installer - synchronous listener installer for the child context. + * @param installer - listener or startup-check installer for the child context. * @returns an effect-scoped disposer for the registration. */ register(packageName: string, installer: InvariantInstaller): () => void @@ -25,8 +25,8 @@ register(packageName: string, installer: InvariantInstaller): () => void Register one package's invariant installer. The package name is reserved even when filtering disables its checks. Enabled installers run in a child fiber; failure disposes that fiber and releases the reservation. - `packageName` — full npm package name that owns the contribution. -- `installer` — synchronous listener installer for the child context. +- `installer` — listener or startup-check installer for the child context. **Returns** an effect-scoped disposer for the registration. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/support/invariants/src/index.ts#L303) +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/support/invariants/src/index.ts#L430) From 3b7ce2e6827056763d6ddd272c09f90670646955 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 20 Jul 2026 18:39:37 +0800 Subject: [PATCH 07/12] fix(invariants): require justified empty companions --- ...-package-owned-invariant-service.i18n.yaml | 4 +- ...6-07-19-package-owned-invariant-service.md | 9 ++-- ...7-19-package-owned-invariant-service.zh.md | 9 ++-- packages/AGENTS.md | 2 +- packages/bash/bash-local/src/invariant.ts | 2 +- packages/bash/bash-sandbox/src/invariant.ts | 2 +- packages/bash/bash/src/invariant.ts | 2 +- packages/bash/tool-bash/src/invariant.ts | 2 +- .../code-runtime-worker/src/invariant.ts | 2 +- .../code-runtime/src/invariant.ts | 2 +- .../compact/compact-basic/src/invariant.ts | 2 +- packages/compact/compact/src/invariant.ts | 2 +- .../context/time-context/src/invariant.ts | 2 +- .../workspace-context/src/invariant.ts | 2 +- packages/cordis/tool-cordis/src/invariant.ts | 2 +- packages/core/system-prompt/src/invariant.ts | 2 +- packages/core/tools/src/invariant.ts | 2 +- packages/examples/acp-demo/src/invariant.ts | 2 +- .../agent-spine-demo/src/invariant.ts | 2 +- packages/examples/cli-demo/src/invariant.ts | 2 +- .../examples/jsonrpc-demo/src/invariant.ts | 2 +- packages/examples/stdio-demo/src/invariant.ts | 2 +- packages/fs/fs-local/src/invariant.ts | 2 +- packages/fs/fs-policy/src/invariant.ts | 2 +- packages/fs/fs/src/invariant.ts | 2 +- packages/fs/tool-fs-search/src/invariant.ts | 2 +- packages/fs/tool-fs/src/invariant.ts | 2 +- .../guard/repeat-tool-guard/src/invariant.ts | 2 +- packages/hooks/hook-protocol/src/invariant.ts | 2 +- packages/hooks/hooks-claude/src/invariant.ts | 2 +- packages/hooks/hooks-codex/src/invariant.ts | 2 +- packages/llm/llm-deepseek/src/invariant.ts | 2 +- packages/llm/llm-pi-ai/src/invariant.ts | 2 +- packages/llm/llm/src/invariant.ts | 2 +- packages/llm/token-meter/src/invariant.ts | 2 +- packages/mcp/mcp-client/src/invariant.ts | 2 +- .../sandbox/sandbox-local/src/invariant.ts | 2 +- packages/sandbox/sandbox/src/invariant.ts | 2 +- packages/sdk/create-sdk/src/invariant.ts | 2 +- packages/sdk/helper/src/invariant.ts | 2 +- packages/sdk/scripts/src/invariant.ts | 2 +- packages/sdk/telemetry/src/invariant.ts | 2 +- .../src/invariant.ts | 2 +- .../src/invariant.ts | 2 +- .../session-persistence/src/invariant.ts | 2 +- .../session-query/src/invariant.ts | 2 +- packages/skill/skill-local/src/invariant.ts | 2 +- packages/skill/skill/src/invariant.ts | 2 +- packages/skill/tool-skill/src/invariant.ts | 2 +- packages/spill/spill-local/src/invariant.ts | 2 +- packages/spill/spill-policy/src/invariant.ts | 2 +- packages/spill/spill/src/invariant.ts | 2 +- .../subagent/subagent-acp/src/invariant.ts | 2 +- .../subagent/subagent-fork/src/invariant.ts | 2 +- .../subagent-inprocess/src/invariant.ts | 2 +- .../subagent/subagent-spawn/src/invariant.ts | 2 +- .../subagent-subprocess/src/invariant.ts | 2 +- packages/subagent/subagent/src/invariant.ts | 2 +- .../subagent/tool-subagent/src/invariant.ts | 2 +- .../support/acp-snapshot/src/invariant.ts | 2 +- .../agent-loop-testkit/src/invariant.ts | 2 +- packages/support/invariants/src/invariant.ts | 2 +- packages/support/llm-replay/src/invariant.ts | 2 +- .../support/loader-smoke/src/invariant.ts | 2 +- packages/tasks/tasks/src/invariant.ts | 2 +- packages/tasks/tool-tasks/src/invariant.ts | 2 +- .../timeout/timeout-policy/src/invariant.ts | 2 +- packages/todo/tool-todo/src/invariant.ts | 2 +- packages/ui/acp/src/invariant.ts | 2 +- packages/ui/app-boot/src/invariant.ts | 2 +- packages/ui/jsonrpc/src/invariant.ts | 2 +- packages/ui/permission/src/invariant.ts | 2 +- packages/ui/stdio/src/invariant.ts | 2 +- packages/ui/tool-ask-user/src/invariant.ts | 2 +- packages/ui/tui/src/invariant.ts | 2 +- packages/ui/user-approval/src/invariant.ts | 2 +- packages/ui/user-interaction/src/invariant.ts | 2 +- packages/util/brand/src/invariant.ts | 2 +- packages/util/home/src/invariant.ts | 2 +- packages/util/paths/src/invariant.ts | 2 +- packages/util/retention/src/invariant.ts | 2 +- packages/util/timeout/src/invariant.ts | 2 +- packages/web/tool-web/src/invariant.ts | 2 +- packages/web/web-fetch-local/src/invariant.ts | 2 +- .../web/web-search-deepseek/src/invariant.ts | 2 +- packages/web/web-search-exa/src/invariant.ts | 2 +- .../web-search-perplexity/src/invariant.ts | 2 +- packages/web/web/src/invariant.ts | 2 +- .../workflow/tool-workflow/src/invariant.ts | 2 +- .../workflow-workerthread/src/invariant.ts | 2 +- packages/workflow/workflow/src/invariant.ts | 2 +- scripts/package-invariants.spec.ts | 13 +++++ scripts/package-invariants.ts | 50 ++++++++++++++++++- 93 files changed, 162 insertions(+), 99 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.i18n.yaml index ef4090b3d6..4665f0ede8 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.i18n.yaml +++ b/.agents/notes/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: d7b77439c8cfdfdf2fc4f01e779a6d3c7f345457 -2026-07-19-package-owned-invariant-service.zh.md: b553d137f9f06b57f24672bf6984d087ca99ceb2 +2026-07-19-package-owned-invariant-service.md: 19512580db7b228fbf4c44109ccb07eeaca80b54 +2026-07-19-package-owned-invariant-service.zh.md: f7c4bc4352e3ab1cd3a5aa7d2d0a64734bc22cb2 diff --git a/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.md b/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.md index d7b77439c8..19512580db 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.md +++ b/.agents/notes/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. Checks protect observable relations in event streams or mutable runtime data; service method presence and plugin wiring are type, load, and repository-gate contracts rather than runtime invariants. A package with no plausible runtime relation uses an empty installer whose `No runtime invariant:` comment explains the absence instead of inventing a synthetic assertion. 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. Every other package carries a generated baseline companion until it gains a relational assertion or records why no runtime relation exists. 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 or stale companion source, foreign or unresolved registration names, unexplained empty installers, 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. ### Scoped-event semantic map @@ -91,12 +91,13 @@ Every Vitest configuration loads a test host that mounts an explicitly enabled s - **Keep all checks in `dsh-invariants`.** Rejected because the registry would continue importing every checked product domain, owner changes would require central edits, and package tests would remain detached from the contracts they protect. - **Let root package entrypoints register checks implicitly when `ctx.invariants` happens to exist.** Rejected because root behavior would depend on composition order and optional service presence, diagnostics could not be selected independently, and package loading would hide a registration effect outside an explicit companion. - **Discover every `invariant.ts` file automatically at runtime.** Rejected because filesystem/package discovery is not a runtime ownership contract, makes bundled publication ambiguous, and cannot express explicit Cordis load order or dependency installation. Build-time generation, verification, and the test host may enumerate the source tree because they validate repository completeness rather than composing a shipped deployment. +- **Require a synthetic assertion from every package.** Rejected because checking method presence, plugin names, or fixed examples only turns repository wiring and unit-test facts into startup work. Packages without an observable event or mutable-data relation state that fact locally and keep an empty installer. - **Validate allow/block entries against the currently loaded package set.** Rejected because a zero-match pattern can intentionally target a later or HMR-loaded contribution; current load order must not determine config validity. ## 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 small publication and dependency cost of an invariant companion, including packages whose justified empty installer has no listener. - 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/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.zh.md b/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.zh.md index b553d137f9..f7c4bc4352 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.zh.md +++ b/.agents/notes/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 包名。检查保护事件流或可变运行时数据中可观察的关系;服务方法是否存在以及插件接线是否正确,属于类型、加载和仓库门禁契约,不是运行时不变式。没有合理运行时关系可检查的包使用空 installer,并通过 `No runtime invariant:` 注释解释原因,而不是编造断言。包的根入口不会隐式导入或注册诊断,因此加载根包不会改变运行时检查,也不要求不变式服务存在。 ### 配置与选择 @@ -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 安全的命名空间插件形态;服务包自身的伴随插件导入本地服务类型,避免形成自依赖。 +这四个所有者保存有状态检查与聚焦测试。其他每个包在获得关系断言或记录没有运行时关系的原因之前,都带有生成的基线伴随插件。每个伴随入口都是单独打包的 `./invariant` export,具有独立声明和对 Loader 安全的命名空间插件形态;服务包自身的伴随插件导入本地服务类型,避免形成自依赖。 -`verify-package-invariants` 会发现每个工作区包,并拒绝缺失或陈旧的伴随插件源码、外部或无法解析的注册名、缺失的 `./invariant` export 或发布文件、缺失的不变式对等依赖(peer dependency)、开发依赖及项目引用,以及遗漏伴随入口的自定义构建配置。生成器只写入缺失或带生成标记的所有权基线,因此绝不会替换包自行维护的实现。 +`verify-package-invariants` 会发现每个工作区包,并拒绝缺失或陈旧的伴随插件源码、外部或无法解析的注册名、没有解释的空 installer、缺失的 `./invariant` export 或发布文件、缺失的不变式对等依赖(peer dependency)、开发依赖及项目引用,以及遗漏伴随入口的自定义构建配置。生成器只写入缺失或带生成标记的所有权基线,因此绝不会替换包自行维护的实现。 ### Scoped event 语义映射 @@ -91,12 +91,13 @@ Workspace 约束识别独立的不变式 bundle;包 exports、项目引用、 - **把所有检查保留在 `dsh-invariants`。** 不予采纳,因为注册包仍要导入所有被检查的产品领域,所有者变更仍需中央编辑,测试也继续远离被保护的契约。 - **当 `ctx.invariants` 恰好存在时,让根包入口隐式注册检查。** 不予采纳,因为根入口行为会依赖组合顺序与可选服务是否存在,诊断无法独立选择,而且包加载会隐藏一个不在显式伴随插件中的注册 effect。 - **在运行时自动发现所有 `invariant.ts` 文件。** 不予采纳,因为文件系统或包发现不是运行时所有权契约,会让 bundle 发布含义不清,也无法表达显式 Cordis 加载顺序或依赖安装。构建期生成与校验以及测试 host 可以枚举源码树,因为它们验证的是仓库完整性,而不是组合已发布的部署。 +- **要求每个包提供合成断言。** 不予采纳,因为检查方法是否存在、插件名或固定样例,只会把仓库接线与单元测试事实变成启动时工作。没有可观察事件或可变数据关系的包会在本地说明原因,并保留空 installer。 - **根据当前已加载包集合验证 allow/block 条目。** 不予采纳,因为零匹配模式可能有意指向稍后加载或 HMR 加载的贡献;当前加载顺序不能决定配置有效性。 ## 后果 - 产品包拥有并测试自己的关系断言,服务保持与产品无关。 -- 每个包都要承担不变式伴随插件带来的少量发布与依赖成本,包括目前只安装生成基线、不添加监听器的包。 +- 每个包都要承担不变式伴随插件带来的少量发布与依赖成本,包括使用有理由空 installer、不添加监听器的包。 - 标准组合无需改变插件树即可关闭全部检查或按包名选择。 - 显式伴随条目让诊断成本和所有权在 Cordis 配置与包 export 中可见。 - 每个选中贡献增加一个子 fiber 及其监听器和状态成本;被过滤注册只保留包名占用。 diff --git a/packages/AGENTS.md b/packages/AGENTS.md index adf1ba1cd0..f1403bb973 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 its manifest name, and retain the generated baseline until relational checks exist. `verify-package-invariants` gates source and publication wiring ([rationale](../.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.md)). +- **Every package owns an invariant companion.** Publish `./invariant` and register its manifest name. Check observable event or mutable-data relations; when none exists, keep an empty installer with a `No runtime invariant:` comment explaining why instead of inventing a shape or presence assertion. `verify-package-invariants` gates source and publication wiring ([rationale](../.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.md)). Naming notes: diff --git a/packages/bash/bash-local/src/invariant.ts b/packages/bash/bash-local/src/invariant.ts index 8af5f91b22..0fe56f5929 100644 --- a/packages/bash/bash-local/src/invariant.ts +++ b/packages/bash/bash-local/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/bash/bash-sandbox/src/invariant.ts b/packages/bash/bash-sandbox/src/invariant.ts index e74190fa82..26e0bb769e 100644 --- a/packages/bash/bash-sandbox/src/invariant.ts +++ b/packages/bash/bash-sandbox/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/bash/bash/src/invariant.ts b/packages/bash/bash/src/invariant.ts index 350f68bfc7..9f9d4cf164 100644 --- a/packages/bash/bash/src/invariant.ts +++ b/packages/bash/bash/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/bash/tool-bash/src/invariant.ts b/packages/bash/tool-bash/src/invariant.ts index 286089c7ff..65c124801d 100644 --- a/packages/bash/tool-bash/src/invariant.ts +++ b/packages/bash/tool-bash/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/code-runtime/code-runtime-worker/src/invariant.ts b/packages/code-runtime/code-runtime-worker/src/invariant.ts index 41b3eab511..443fa67a1d 100644 --- a/packages/code-runtime/code-runtime-worker/src/invariant.ts +++ b/packages/code-runtime/code-runtime-worker/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/code-runtime/code-runtime/src/invariant.ts b/packages/code-runtime/code-runtime/src/invariant.ts index 6102927d77..1e5b5e4bb9 100644 --- a/packages/code-runtime/code-runtime/src/invariant.ts +++ b/packages/code-runtime/code-runtime/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/compact/compact-basic/src/invariant.ts b/packages/compact/compact-basic/src/invariant.ts index f72c0e5898..5afc935223 100644 --- a/packages/compact/compact-basic/src/invariant.ts +++ b/packages/compact/compact-basic/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/compact/compact/src/invariant.ts b/packages/compact/compact/src/invariant.ts index f7a2cbffd1..40547461dc 100644 --- a/packages/compact/compact/src/invariant.ts +++ b/packages/compact/compact/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/context/time-context/src/invariant.ts b/packages/context/time-context/src/invariant.ts index 64fb98ac81..c452699b59 100644 --- a/packages/context/time-context/src/invariant.ts +++ b/packages/context/time-context/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/context/workspace-context/src/invariant.ts b/packages/context/workspace-context/src/invariant.ts index f3e76225c0..6a49e1bd15 100644 --- a/packages/context/workspace-context/src/invariant.ts +++ b/packages/context/workspace-context/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/cordis/tool-cordis/src/invariant.ts b/packages/cordis/tool-cordis/src/invariant.ts index a3f375b69f..194ac33b1c 100644 --- a/packages/cordis/tool-cordis/src/invariant.ts +++ b/packages/cordis/tool-cordis/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/core/system-prompt/src/invariant.ts b/packages/core/system-prompt/src/invariant.ts index 5117f93ce0..d281d29f2b 100644 --- a/packages/core/system-prompt/src/invariant.ts +++ b/packages/core/system-prompt/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/core/tools/src/invariant.ts b/packages/core/tools/src/invariant.ts index 7c5743e1d4..a296415939 100644 --- a/packages/core/tools/src/invariant.ts +++ b/packages/core/tools/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/examples/acp-demo/src/invariant.ts b/packages/examples/acp-demo/src/invariant.ts index d8f2ff17dc..4050fa90d7 100644 --- a/packages/examples/acp-demo/src/invariant.ts +++ b/packages/examples/acp-demo/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/examples/agent-spine-demo/src/invariant.ts b/packages/examples/agent-spine-demo/src/invariant.ts index 913b5ca2ab..134c5d7319 100644 --- a/packages/examples/agent-spine-demo/src/invariant.ts +++ b/packages/examples/agent-spine-demo/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/examples/cli-demo/src/invariant.ts b/packages/examples/cli-demo/src/invariant.ts index 5681362dcc..35077586c4 100644 --- a/packages/examples/cli-demo/src/invariant.ts +++ b/packages/examples/cli-demo/src/invariant.ts @@ -17,7 +17,7 @@ export const name = 'cli-demo-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/examples/jsonrpc-demo/src/invariant.ts b/packages/examples/jsonrpc-demo/src/invariant.ts index 21f19faf0f..b59fcfd9ba 100644 --- a/packages/examples/jsonrpc-demo/src/invariant.ts +++ b/packages/examples/jsonrpc-demo/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/examples/stdio-demo/src/invariant.ts b/packages/examples/stdio-demo/src/invariant.ts index 5d51c3cd9b..93d5a73df7 100644 --- a/packages/examples/stdio-demo/src/invariant.ts +++ b/packages/examples/stdio-demo/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/fs/fs-local/src/invariant.ts b/packages/fs/fs-local/src/invariant.ts index 0c89f2299b..ed93f88c72 100644 --- a/packages/fs/fs-local/src/invariant.ts +++ b/packages/fs/fs-local/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/fs/fs-policy/src/invariant.ts b/packages/fs/fs-policy/src/invariant.ts index 11f155d2ec..60b40e074d 100644 --- a/packages/fs/fs-policy/src/invariant.ts +++ b/packages/fs/fs-policy/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/fs/fs/src/invariant.ts b/packages/fs/fs/src/invariant.ts index 55895b4b24..79dcb9b3ee 100644 --- a/packages/fs/fs/src/invariant.ts +++ b/packages/fs/fs/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/fs/tool-fs-search/src/invariant.ts b/packages/fs/tool-fs-search/src/invariant.ts index f0055b611a..b010f15d6c 100644 --- a/packages/fs/tool-fs-search/src/invariant.ts +++ b/packages/fs/tool-fs-search/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/fs/tool-fs/src/invariant.ts b/packages/fs/tool-fs/src/invariant.ts index 7a683d978f..ecdf7237ee 100644 --- a/packages/fs/tool-fs/src/invariant.ts +++ b/packages/fs/tool-fs/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/guard/repeat-tool-guard/src/invariant.ts b/packages/guard/repeat-tool-guard/src/invariant.ts index 05c4df1a66..1bf0818b72 100644 --- a/packages/guard/repeat-tool-guard/src/invariant.ts +++ b/packages/guard/repeat-tool-guard/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/hooks/hook-protocol/src/invariant.ts b/packages/hooks/hook-protocol/src/invariant.ts index 9893493644..c2dac9910d 100644 --- a/packages/hooks/hook-protocol/src/invariant.ts +++ b/packages/hooks/hook-protocol/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/hooks/hooks-claude/src/invariant.ts b/packages/hooks/hooks-claude/src/invariant.ts index 5c6002f7f5..f712873f5f 100644 --- a/packages/hooks/hooks-claude/src/invariant.ts +++ b/packages/hooks/hooks-claude/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/hooks/hooks-codex/src/invariant.ts b/packages/hooks/hooks-codex/src/invariant.ts index 1b8f03a057..e81eb33340 100644 --- a/packages/hooks/hooks-codex/src/invariant.ts +++ b/packages/hooks/hooks-codex/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/llm/llm-deepseek/src/invariant.ts b/packages/llm/llm-deepseek/src/invariant.ts index c3e6b5e153..8a00b0b498 100644 --- a/packages/llm/llm-deepseek/src/invariant.ts +++ b/packages/llm/llm-deepseek/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/llm/llm-pi-ai/src/invariant.ts b/packages/llm/llm-pi-ai/src/invariant.ts index 4ada7a1a51..a49ebab07e 100644 --- a/packages/llm/llm-pi-ai/src/invariant.ts +++ b/packages/llm/llm-pi-ai/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/llm/llm/src/invariant.ts b/packages/llm/llm/src/invariant.ts index a8a56ce245..3911551916 100644 --- a/packages/llm/llm/src/invariant.ts +++ b/packages/llm/llm/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/llm/token-meter/src/invariant.ts b/packages/llm/token-meter/src/invariant.ts index 00ceb567be..8fd25f6998 100644 --- a/packages/llm/token-meter/src/invariant.ts +++ b/packages/llm/token-meter/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/mcp/mcp-client/src/invariant.ts b/packages/mcp/mcp-client/src/invariant.ts index d9e75e9955..92e58ec0e4 100644 --- a/packages/mcp/mcp-client/src/invariant.ts +++ b/packages/mcp/mcp-client/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/sandbox/sandbox-local/src/invariant.ts b/packages/sandbox/sandbox-local/src/invariant.ts index 3582962f94..e16cc06ead 100644 --- a/packages/sandbox/sandbox-local/src/invariant.ts +++ b/packages/sandbox/sandbox-local/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/sandbox/sandbox/src/invariant.ts b/packages/sandbox/sandbox/src/invariant.ts index b220f11717..ffe9cb9db5 100644 --- a/packages/sandbox/sandbox/src/invariant.ts +++ b/packages/sandbox/sandbox/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/sdk/create-sdk/src/invariant.ts b/packages/sdk/create-sdk/src/invariant.ts index 87a697e438..2f995b9029 100644 --- a/packages/sdk/create-sdk/src/invariant.ts +++ b/packages/sdk/create-sdk/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/sdk/helper/src/invariant.ts b/packages/sdk/helper/src/invariant.ts index 63a6fc2055..873281d567 100644 --- a/packages/sdk/helper/src/invariant.ts +++ b/packages/sdk/helper/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/sdk/scripts/src/invariant.ts b/packages/sdk/scripts/src/invariant.ts index fd0a0ef55b..7a637eecab 100644 --- a/packages/sdk/scripts/src/invariant.ts +++ b/packages/sdk/scripts/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/sdk/telemetry/src/invariant.ts b/packages/sdk/telemetry/src/invariant.ts index 945b9e50e7..8af7848563 100644 --- a/packages/sdk/telemetry/src/invariant.ts +++ b/packages/sdk/telemetry/src/invariant.ts @@ -17,7 +17,7 @@ export const name = 'telemetry-invariant' /** Services required before the companion can register. */ export const inject = ['invariants'] -/** Reserve this package's invariant ownership until it adds relational checks. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/session-persistence/session-persistence-jsonl/src/invariant.ts b/packages/session-persistence/session-persistence-jsonl/src/invariant.ts index 12c65db1c4..d7e8c4bb07 100644 --- a/packages/session-persistence/session-persistence-jsonl/src/invariant.ts +++ b/packages/session-persistence/session-persistence-jsonl/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/session-persistence/session-persistence-sqlite/src/invariant.ts b/packages/session-persistence/session-persistence-sqlite/src/invariant.ts index a9b04cf5f5..c995336c2b 100644 --- a/packages/session-persistence/session-persistence-sqlite/src/invariant.ts +++ b/packages/session-persistence/session-persistence-sqlite/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/session-persistence/session-persistence/src/invariant.ts b/packages/session-persistence/session-persistence/src/invariant.ts index cc7cc8fa2c..bbbe4cb523 100644 --- a/packages/session-persistence/session-persistence/src/invariant.ts +++ b/packages/session-persistence/session-persistence/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/session-query/session-query/src/invariant.ts b/packages/session-query/session-query/src/invariant.ts index 91bcee721e..d65f690cbc 100644 --- a/packages/session-query/session-query/src/invariant.ts +++ b/packages/session-query/session-query/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/skill/skill-local/src/invariant.ts b/packages/skill/skill-local/src/invariant.ts index 475d02bb8f..1230edb9a1 100644 --- a/packages/skill/skill-local/src/invariant.ts +++ b/packages/skill/skill-local/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/skill/skill/src/invariant.ts b/packages/skill/skill/src/invariant.ts index c1d4091bb2..43abd10c43 100644 --- a/packages/skill/skill/src/invariant.ts +++ b/packages/skill/skill/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/skill/tool-skill/src/invariant.ts b/packages/skill/tool-skill/src/invariant.ts index abf4ba3961..97a5e04558 100644 --- a/packages/skill/tool-skill/src/invariant.ts +++ b/packages/skill/tool-skill/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/spill/spill-local/src/invariant.ts b/packages/spill/spill-local/src/invariant.ts index d638ffa2a9..41b4c82d1c 100644 --- a/packages/spill/spill-local/src/invariant.ts +++ b/packages/spill/spill-local/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/spill/spill-policy/src/invariant.ts b/packages/spill/spill-policy/src/invariant.ts index d4aa544ecd..aeae1b4583 100644 --- a/packages/spill/spill-policy/src/invariant.ts +++ b/packages/spill/spill-policy/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/spill/spill/src/invariant.ts b/packages/spill/spill/src/invariant.ts index 714e43f3a6..0b23ef8633 100644 --- a/packages/spill/spill/src/invariant.ts +++ b/packages/spill/spill/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/subagent/subagent-acp/src/invariant.ts b/packages/subagent/subagent-acp/src/invariant.ts index a5828fd4c1..5cda5764a3 100644 --- a/packages/subagent/subagent-acp/src/invariant.ts +++ b/packages/subagent/subagent-acp/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/subagent/subagent-fork/src/invariant.ts b/packages/subagent/subagent-fork/src/invariant.ts index c6903fd82d..4a40e34fa4 100644 --- a/packages/subagent/subagent-fork/src/invariant.ts +++ b/packages/subagent/subagent-fork/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/subagent/subagent-inprocess/src/invariant.ts b/packages/subagent/subagent-inprocess/src/invariant.ts index 0eac204104..5ece1a32c3 100644 --- a/packages/subagent/subagent-inprocess/src/invariant.ts +++ b/packages/subagent/subagent-inprocess/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/subagent/subagent-spawn/src/invariant.ts b/packages/subagent/subagent-spawn/src/invariant.ts index d179a2b72a..913b2e136c 100644 --- a/packages/subagent/subagent-spawn/src/invariant.ts +++ b/packages/subagent/subagent-spawn/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/subagent/subagent-subprocess/src/invariant.ts b/packages/subagent/subagent-subprocess/src/invariant.ts index 22dac32814..ee0047854f 100644 --- a/packages/subagent/subagent-subprocess/src/invariant.ts +++ b/packages/subagent/subagent-subprocess/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/subagent/subagent/src/invariant.ts b/packages/subagent/subagent/src/invariant.ts index 3a79592ee1..ab491bfb4d 100644 --- a/packages/subagent/subagent/src/invariant.ts +++ b/packages/subagent/subagent/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/subagent/tool-subagent/src/invariant.ts b/packages/subagent/tool-subagent/src/invariant.ts index 08881e4b2b..9b38442897 100644 --- a/packages/subagent/tool-subagent/src/invariant.ts +++ b/packages/subagent/tool-subagent/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/support/acp-snapshot/src/invariant.ts b/packages/support/acp-snapshot/src/invariant.ts index 3579b96cf5..02a7cf0b68 100644 --- a/packages/support/acp-snapshot/src/invariant.ts +++ b/packages/support/acp-snapshot/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/support/agent-loop-testkit/src/invariant.ts b/packages/support/agent-loop-testkit/src/invariant.ts index fc2554aa77..f299eb08cd 100644 --- a/packages/support/agent-loop-testkit/src/invariant.ts +++ b/packages/support/agent-loop-testkit/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/support/invariants/src/invariant.ts b/packages/support/invariants/src/invariant.ts index b6b4d8ae48..d55b2b73b1 100644 --- a/packages/support/invariants/src/invariant.ts +++ b/packages/support/invariants/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/support/llm-replay/src/invariant.ts b/packages/support/llm-replay/src/invariant.ts index 50295cbedf..c6349a23b0 100644 --- a/packages/support/llm-replay/src/invariant.ts +++ b/packages/support/llm-replay/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/support/loader-smoke/src/invariant.ts b/packages/support/loader-smoke/src/invariant.ts index 9265a5f8b4..6728f4ab5b 100644 --- a/packages/support/loader-smoke/src/invariant.ts +++ b/packages/support/loader-smoke/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/tasks/tasks/src/invariant.ts b/packages/tasks/tasks/src/invariant.ts index 468fe664b9..afc5901d92 100644 --- a/packages/tasks/tasks/src/invariant.ts +++ b/packages/tasks/tasks/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/tasks/tool-tasks/src/invariant.ts b/packages/tasks/tool-tasks/src/invariant.ts index fded38c895..dbb01600be 100644 --- a/packages/tasks/tool-tasks/src/invariant.ts +++ b/packages/tasks/tool-tasks/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/timeout/timeout-policy/src/invariant.ts b/packages/timeout/timeout-policy/src/invariant.ts index 9e7b5b7d4b..4a6933e8f8 100644 --- a/packages/timeout/timeout-policy/src/invariant.ts +++ b/packages/timeout/timeout-policy/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/todo/tool-todo/src/invariant.ts b/packages/todo/tool-todo/src/invariant.ts index a5980342f3..f925a1ad5b 100644 --- a/packages/todo/tool-todo/src/invariant.ts +++ b/packages/todo/tool-todo/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/ui/acp/src/invariant.ts b/packages/ui/acp/src/invariant.ts index 2c081fc48c..e6ccda5342 100644 --- a/packages/ui/acp/src/invariant.ts +++ b/packages/ui/acp/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/ui/app-boot/src/invariant.ts b/packages/ui/app-boot/src/invariant.ts index 498d967799..de383a5eb0 100644 --- a/packages/ui/app-boot/src/invariant.ts +++ b/packages/ui/app-boot/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/ui/jsonrpc/src/invariant.ts b/packages/ui/jsonrpc/src/invariant.ts index 552c312481..f3ef43b461 100644 --- a/packages/ui/jsonrpc/src/invariant.ts +++ b/packages/ui/jsonrpc/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/ui/permission/src/invariant.ts b/packages/ui/permission/src/invariant.ts index 1a774e2e4c..1c9129c916 100644 --- a/packages/ui/permission/src/invariant.ts +++ b/packages/ui/permission/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/ui/stdio/src/invariant.ts b/packages/ui/stdio/src/invariant.ts index 443440a215..37176f11c6 100644 --- a/packages/ui/stdio/src/invariant.ts +++ b/packages/ui/stdio/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/ui/tool-ask-user/src/invariant.ts b/packages/ui/tool-ask-user/src/invariant.ts index eebb1ced42..44c57bae86 100644 --- a/packages/ui/tool-ask-user/src/invariant.ts +++ b/packages/ui/tool-ask-user/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/ui/tui/src/invariant.ts b/packages/ui/tui/src/invariant.ts index cba5fd9d2e..1f89affab4 100644 --- a/packages/ui/tui/src/invariant.ts +++ b/packages/ui/tui/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/ui/user-approval/src/invariant.ts b/packages/ui/user-approval/src/invariant.ts index 73dee4f9f1..b1b90d5509 100644 --- a/packages/ui/user-approval/src/invariant.ts +++ b/packages/ui/user-approval/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/ui/user-interaction/src/invariant.ts b/packages/ui/user-interaction/src/invariant.ts index 262681fc06..bbf1b18041 100644 --- a/packages/ui/user-interaction/src/invariant.ts +++ b/packages/ui/user-interaction/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/util/brand/src/invariant.ts b/packages/util/brand/src/invariant.ts index e932e7e35e..b10caf9080 100644 --- a/packages/util/brand/src/invariant.ts +++ b/packages/util/brand/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/util/home/src/invariant.ts b/packages/util/home/src/invariant.ts index 5874a57c1c..f72c8ec6ae 100644 --- a/packages/util/home/src/invariant.ts +++ b/packages/util/home/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/util/paths/src/invariant.ts b/packages/util/paths/src/invariant.ts index c2cedfbb0d..6424d64726 100644 --- a/packages/util/paths/src/invariant.ts +++ b/packages/util/paths/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/util/retention/src/invariant.ts b/packages/util/retention/src/invariant.ts index 7516f9e5ed..2d43081596 100644 --- a/packages/util/retention/src/invariant.ts +++ b/packages/util/retention/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/util/timeout/src/invariant.ts b/packages/util/timeout/src/invariant.ts index 15140bb880..0a0ddb50c2 100644 --- a/packages/util/timeout/src/invariant.ts +++ b/packages/util/timeout/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/web/tool-web/src/invariant.ts b/packages/web/tool-web/src/invariant.ts index 008fe2f5e1..f790931b3f 100644 --- a/packages/web/tool-web/src/invariant.ts +++ b/packages/web/tool-web/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/web/web-fetch-local/src/invariant.ts b/packages/web/web-fetch-local/src/invariant.ts index ef61e2a611..f7dc4579fb 100644 --- a/packages/web/web-fetch-local/src/invariant.ts +++ b/packages/web/web-fetch-local/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/web/web-search-deepseek/src/invariant.ts b/packages/web/web-search-deepseek/src/invariant.ts index 8781949be9..ac2b4e3a85 100644 --- a/packages/web/web-search-deepseek/src/invariant.ts +++ b/packages/web/web-search-deepseek/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/web/web-search-exa/src/invariant.ts b/packages/web/web-search-exa/src/invariant.ts index a2dd956625..e66d9f6847 100644 --- a/packages/web/web-search-exa/src/invariant.ts +++ b/packages/web/web-search-exa/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/web/web-search-perplexity/src/invariant.ts b/packages/web/web-search-perplexity/src/invariant.ts index fe82c79dae..a4d370d677 100644 --- a/packages/web/web-search-perplexity/src/invariant.ts +++ b/packages/web/web-search-perplexity/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/web/web/src/invariant.ts b/packages/web/web/src/invariant.ts index b9b1b0d45d..af5862ab5f 100644 --- a/packages/web/web/src/invariant.ts +++ b/packages/web/web/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/workflow/tool-workflow/src/invariant.ts b/packages/workflow/tool-workflow/src/invariant.ts index cd1f0e475b..e3f46f0a73 100644 --- a/packages/workflow/tool-workflow/src/invariant.ts +++ b/packages/workflow/tool-workflow/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/workflow/workflow-workerthread/src/invariant.ts b/packages/workflow/workflow-workerthread/src/invariant.ts index 6bcc40862c..ac47988dcc 100644 --- a/packages/workflow/workflow-workerthread/src/invariant.ts +++ b/packages/workflow/workflow-workerthread/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/packages/workflow/workflow/src/invariant.ts b/packages/workflow/workflow/src/invariant.ts index b552021ca4..19e5c9e3c4 100644 --- a/packages/workflow/workflow/src/invariant.ts +++ b/packages/workflow/workflow/src/invariant.ts @@ -17,7 +17,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** diff --git a/scripts/package-invariants.spec.ts b/scripts/package-invariants.spec.ts index 698969b3d7..57395cbf82 100644 --- a/scripts/package-invariants.spec.ts +++ b/scripts/package-invariants.spec.ts @@ -101,4 +101,17 @@ export const apply = (ctx: { invariants: { register(name: string, install: () => expect(collectPackageInvariantViolations(root).map(violation => violation.message)) .toContain('generated baseline is stale; run pnpm run gen-package-invariants') }) + + it('rejects an unexplained empty package installer', () => { + const source = ` +export const name = 'probe-invariant' +export const inject = ['invariants'] +const PACKAGE_NAME = '@deepseek-ai/dsh-probe' +const install = () => {} +export const apply = (ctx: { invariants: { register(name: string, install: () => void): () => void } }) => + ctx.invariants.register(PACKAGE_NAME, install) +` + expect(collectPackageInvariantViolations(fixture({ source })).map(violation => violation.message)) + .toContain('empty install function must explain why with a "No runtime invariant:" comment') + }) }) diff --git a/scripts/package-invariants.ts b/scripts/package-invariants.ts index 45fb64829f..0078a6a4c4 100644 --- a/scripts/package-invariants.ts +++ b/scripts/package-invariants.ts @@ -11,6 +11,9 @@ import ts from 'typescript' /** Marker identifying baseline companions owned by this generator. */ export const GENERATED_INVARIANT_MARKER = '@generated scripts/gen-package-invariants.ts' +/** Required explanation marker for an intentionally empty installer. */ +export const NO_RUNTIME_INVARIANT_MARKER = 'No runtime invariant:' + interface PackageManifest { name?: string exports?: Record @@ -78,7 +81,7 @@ 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. */ +/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ const install: InvariantInstaller = () => {} /** @@ -237,6 +240,51 @@ function checkSource( addViolation(violations, owner.sourcePath, `must named-export ${exportedName}`) } } + checkEmptyInstallerReason(owner, sourceFile, sourceText, violations) +} + +function checkEmptyInstallerReason( + owner: PackageInvariantOwner, + sourceFile: ts.SourceFile, + sourceText: string, + violations: PackageInvariantViolation[], +): void { + 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) continue + const installer = installerFunction(declaration.initializer) + if (installer === undefined + || !ts.isBlock(installer.body) + || installer.body.statements.length > 0) return + const declarationText = sourceText.slice(statement.getFullStart(), statement.getEnd()) + if (!declarationText.includes(NO_RUNTIME_INVARIANT_MARKER)) { + addViolation( + violations, + owner.sourcePath, + `empty install function must explain why with a "${NO_RUNTIME_INVARIANT_MARKER}" comment`, + ) + } + return + } + } +} + +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 { From 1145ee5fc378e1d2158d8d19719d42507558d7bc Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 20 Jul 2026 19:34:19 +0800 Subject: [PATCH 08/12] fix(invariants): assert runtime relationships, not API shapes --- ...kage-invariant-runtime-contracts.i18n.yaml | 4 +- ...-19-package-invariant-runtime-contracts.md | 88 +++--- ...-package-invariant-runtime-contracts.zh.md | 90 +++--- ...-package-owned-invariant-service.i18n.yaml | 4 +- ...6-07-19-package-owned-invariant-service.md | 14 +- ...7-19-package-owned-invariant-service.zh.md | 14 +- packages/AGENTS.md | 2 +- packages/bash/bash-local/src/invariant.ts | 29 +- packages/bash/bash-sandbox/src/invariant.ts | 31 +- packages/bash/bash/src/invariant.ts | 22 +- packages/bash/bash/tests/invariant.spec.ts | 33 ++ packages/bash/tool-bash/src/invariant.ts | 35 +- .../code-runtime-worker/src/invariant.ts | 26 +- .../code-runtime/src/invariant.ts | 30 +- .../code-runtime/tests/service.spec.ts | 21 -- .../compact/compact-basic/src/invariant.ts | 47 +-- .../compact-basic/tests/compact-basic.spec.ts | 24 +- packages/compact/compact/src/invariant.ts | 100 +++++- .../compact/compact/tests/compact.spec.ts | 4 +- .../compact/compact/tests/invariant.spec.ts | 90 ++++++ .../context/time-context/src/invariant.ts | 62 +++- .../time-context/tests/invariant.spec.ts | 83 +++++ .../workspace-context/src/invariant.ts | 28 +- packages/cordis/tool-cordis/src/invariant.ts | 29 +- packages/core/system-prompt/src/invariant.ts | 49 ++- .../system-prompt/tests/invariant.spec.ts | 44 +++ packages/core/tools/src/invariant.ts | 69 ++-- packages/core/tools/tests/invariant.spec.ts | 84 +++++ packages/examples/acp-demo/src/invariant.ts | 25 +- .../agent-spine-demo/src/invariant.ts | 25 +- packages/examples/cli-demo/src/invariant.ts | 25 +- .../examples/jsonrpc-demo/src/invariant.ts | 20 +- packages/examples/stdio-demo/src/invariant.ts | 25 +- packages/fs/fs-local/src/invariant.ts | 28 +- packages/fs/fs-policy/src/invariant.ts | 27 +- packages/fs/fs/src/invariant.ts | 29 +- packages/fs/fs/tests/invariant.spec.ts | 44 +++ packages/fs/tool-fs-search/src/invariant.ts | 30 +- packages/fs/tool-fs/src/invariant.ts | 30 +- .../guard/repeat-tool-guard/src/invariant.ts | 26 +- packages/hooks/hook-protocol/src/invariant.ts | 103 ++++-- .../hook-protocol/tests/invariant.spec.ts | 86 +++++ packages/hooks/hooks-claude/src/invariant.ts | 41 +-- .../hooks-claude/tests/invariant.spec.ts | 26 -- packages/hooks/hooks-codex/src/invariant.ts | 39 +-- .../hooks/hooks-codex/tests/invariant.spec.ts | 26 -- packages/llm/llm-deepseek/src/invariant.ts | 28 +- packages/llm/llm-pi-ai/src/invariant.ts | 28 +- packages/llm/llm/src/invariant.ts | 91 +++++- packages/llm/llm/tests/invariant.spec.ts | 86 +++++ packages/llm/llm/tests/service.spec.ts | 4 +- packages/llm/token-meter/src/invariant.ts | 29 +- packages/mcp/mcp-client/src/invariant.ts | 29 +- .../sandbox/sandbox-local/src/invariant.ts | 28 +- packages/sandbox/sandbox/src/invariant.ts | 22 +- .../sandbox/sandbox/tests/invariant.spec.ts | 43 --- packages/sdk/create-sdk/src/invariant.ts | 33 +- packages/sdk/helper/src/invariant.ts | 27 +- packages/sdk/scripts/src/invariant.ts | 29 +- packages/sdk/telemetry/src/invariant.ts | 26 +- .../src/invariant.ts | 28 +- .../src/invariant.ts | 28 +- .../session-persistence/src/invariant.ts | 19 +- .../session-query/src/invariant.ts | 31 +- packages/skill/skill-local/src/invariant.ts | 28 +- packages/skill/skill/src/invariant.ts | 28 +- packages/skill/tool-skill/src/invariant.ts | 30 +- packages/spill/spill-local/src/invariant.ts | 28 +- packages/spill/spill-policy/src/invariant.ts | 32 +- packages/spill/spill/src/invariant.ts | 22 +- .../subagent/subagent-acp/src/invariant.ts | 28 +- .../subagent/subagent-fork/src/invariant.ts | 28 +- .../subagent-inprocess/src/invariant.ts | 22 +- .../subagent/subagent-spawn/src/invariant.ts | 28 +- .../subagent-subprocess/src/invariant.ts | 35 +- packages/subagent/subagent/src/invariant.ts | 89 +++++- .../subagent/subagent/tests/invariant.spec.ts | 82 +++++ .../subagent/tool-subagent/src/invariant.ts | 30 +- .../support/acp-snapshot/src/invariant.ts | 32 +- .../agent-loop-testkit/src/invariant.ts | 23 +- packages/support/invariants/README.md | 46 +-- packages/support/invariants/src/index.ts | 298 +----------------- packages/support/invariants/src/invariant.ts | 29 +- .../support/invariants/tests/service.spec.ts | 287 ----------------- packages/support/llm-replay/src/invariant.ts | 31 +- .../support/loader-smoke/src/invariant.ts | 30 +- packages/tasks/tasks/src/invariant.ts | 56 +++- packages/tasks/tasks/tests/invariant.spec.ts | 87 +++++ packages/tasks/tool-tasks/src/invariant.ts | 30 +- .../timeout/timeout-policy/src/invariant.ts | 28 +- packages/todo/tool-todo/src/invariant.ts | 47 ++- .../todo/tool-todo/tests/invariant.spec.ts | 53 ++++ packages/ui/acp/src/invariant.ts | 35 +- packages/ui/app-boot/src/invariant.ts | 26 +- packages/ui/jsonrpc/src/invariant.ts | 28 +- packages/ui/permission/src/invariant.ts | 35 +- .../ui/permission/tests/invariant.spec.ts | 42 +++ packages/ui/stdio/src/invariant.ts | 30 +- packages/ui/tool-ask-user/src/invariant.ts | 29 +- packages/ui/tui/src/invariant.ts | 31 +- packages/ui/user-approval/src/invariant.ts | 89 +++++- .../ui/user-approval/tests/invariant.spec.ts | 72 +++++ packages/ui/user-interaction/src/invariant.ts | 28 +- packages/util/brand/src/invariant.ts | 20 +- packages/util/home/src/invariant.ts | 25 +- packages/util/paths/src/invariant.ts | 26 +- packages/util/retention/src/invariant.ts | 31 +- packages/util/timeout/src/invariant.ts | 26 +- packages/web/tool-web/src/invariant.ts | 30 +- packages/web/web-fetch-local/src/invariant.ts | 28 +- .../web/web-search-deepseek/src/invariant.ts | 25 +- packages/web/web-search-exa/src/invariant.ts | 28 +- .../web-search-perplexity/src/invariant.ts | 25 +- packages/web/web/src/invariant.ts | 28 +- .../workflow/tool-workflow/src/invariant.ts | 30 +- .../workflow-workerthread/src/invariant.ts | 28 +- packages/workflow/workflow/src/invariant.ts | 126 +++++++- .../workflow/workflow/tests/invariant.spec.ts | 108 +++++++ .../workflow/workflow/tests/workflow.spec.ts | 10 + scripts/package-invariants.spec.ts | 83 ++--- scripts/package-invariants.ts | 70 ++-- scripts/test-invariants.spec.ts | 14 + scripts/test-invariants.ts | 14 + tsconfig.base.json | 30 ++ 124 files changed, 2923 insertions(+), 2334 deletions(-) create mode 100644 packages/bash/bash/tests/invariant.spec.ts create mode 100644 packages/compact/compact/tests/invariant.spec.ts create mode 100644 packages/context/time-context/tests/invariant.spec.ts create mode 100644 packages/core/system-prompt/tests/invariant.spec.ts create mode 100644 packages/core/tools/tests/invariant.spec.ts create mode 100644 packages/fs/fs/tests/invariant.spec.ts create mode 100644 packages/hooks/hook-protocol/tests/invariant.spec.ts delete mode 100644 packages/hooks/hooks-claude/tests/invariant.spec.ts delete mode 100644 packages/hooks/hooks-codex/tests/invariant.spec.ts create mode 100644 packages/llm/llm/tests/invariant.spec.ts delete mode 100644 packages/sandbox/sandbox/tests/invariant.spec.ts create mode 100644 packages/subagent/subagent/tests/invariant.spec.ts create mode 100644 packages/tasks/tasks/tests/invariant.spec.ts create mode 100644 packages/todo/tool-todo/tests/invariant.spec.ts create mode 100644 packages/ui/permission/tests/invariant.spec.ts create mode 100644 packages/ui/user-approval/tests/invariant.spec.ts create mode 100644 packages/workflow/workflow/tests/invariant.spec.ts diff --git a/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.i18n.yaml index 48407e2272..a02e27ef7d 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.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-invariant-runtime-contracts.md: 65986fc0b3aab695d8512d9e221052e1db83445f -2026-07-19-package-invariant-runtime-contracts.zh.md: 0ae0bd305692ee71359c5500e477dee22575a9ef +2026-07-19-package-invariant-runtime-contracts.md: 4e15842d1228d82621f91caa18cbe02fd5407dd8 +2026-07-19-package-invariant-runtime-contracts.zh.md: 88b1eb1a79d88865515617d6737fbded10a13b6e diff --git a/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md b/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md index 65986fc0b3..4e15842d12 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md +++ b/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md @@ -1,4 +1,4 @@ -# Agent Note: Executable package invariant contracts +# Agent Note: Meaningful package invariant contracts Status: implemented @@ -6,60 +6,70 @@ 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. +The package-owned invariant seam made publication and registration exhaustive, but its first generated baseline accepted empty installers. A follow-up then replaced those empties with generic assertions about plugin names, injections, effects, service methods, and fixed pure-library examples. Those assertions made every companion executable without making the system safer: TypeScript, Cordis startup, package tests, and module-load tests already enforce those shapes, while the invariant service should detect impossible runtime state. -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. +A useful runtime invariant relates observations over time or across a mutable data structure. Examples include a terminal event without its start, an LLM delta for a block that is not open, or a durable result whose identity differs from its request. Merely confirming that a declared method exists, that a plugin has its expected name, or that a constant example still returns a known value is not such a relation. -Vitest mounts each package test's owning companion globally, and one exhaustive topology mounts every companion. Companion modules therefore cannot eagerly import every product entrypoint before a test module establishes its hoisted mocks, and a name-based observer cannot mistake an anonymous child fiber that inherits its parent's display name for the package plugin itself. +Some packages genuinely own no continuously observable relation. Pure utilities, composition-only packages, thin adapters, binaries, and test-support packages may have important contracts, but those contracts are better enforced by types, load checks, focused unit tests, or integration tests. Requiring a synthetic runtime assertion for those packages would optimize for satisfying a gate instead of detecting corruption. ## Decision -### Every companion executes a package contract +### Registration is exhaustive; assertions must be meaningful -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. +Every workspace package publishes a separately built `./invariant` companion and registers its exact npm package name. A companion does one of two things: -The implemented contracts use four forms: +- installs a package-owned check over an event stream or relevant mutable data structure and reports violations through its bound `fail(message)` reporter; or +- uses an empty installer whose declaration has an owner-specific `No runtime invariant:` comment explaining why the package has no plausible runtime relation to observe. -| Owner shape | Runtime contract | +The empty form is an explicit architectural conclusion, not a generated placeholder. A future package change that introduces mutable state or an event protocol must replace the explanation with the corresponding check. + +The central `dsh-invariants` service owns only configuration, registration uniqueness, child-fiber lifecycle, rollback, disposal, and package-attributed failure. It exposes no generic plugin-shape, service-shape, or startup-assertion helpers and imports no product package. + +### Implemented checks + +The current 91-package workspace has 18 executable companions and 73 justified empty companions. + +| Owner | Runtime relationship | |---|---| -| 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. | +| `dsh-session` | Strict sequence growth, turn/step enclosure, and same-step tool call/result pairing. | +| `dsh-agent` | Non-repeating agent status and terminal disposal transitions. | +| `dsh-scope` | Scoped-event carrier presence and routed-subject consistency. | +| `dsh-agent-loop` | Frozen loop request reconstruction from the session event log. | +| `dsh-llm` | Stream block grammar, delta type/index matching, single usage, closed blocks, and terminal finish. | +| `dsh-tools` | Monotonic pre/execute/post stages and immutable final execution/result snapshots. | +| `dsh-system-prompt` | Authoritative assembly section, tool, and variable data constraints. | +| `dsh-compact` | Compaction start/summary/end pairing, range endpoints, token counts, and successful-summary presence. | +| `dsh-hook-protocol` | Hook invocation/result correlation, dialect, identity, and duration constraints. | +| `dsh-bash` | Durable sandbox-mode events use the closed sandbox-mode vocabulary. | +| `dsh-fs` | Filesystem decision/observation events carry usable target and version identities. | +| `dsh-subagent` | Provider add/remove and child start/end events preserve identity and pairing. | +| `dsh-permission` | Durable permission decisions name a preset in the active permission table. | +| `dsh-user-approval` | Approval asked/decided records pair by call and use valid outcomes and policies. | +| `dsh-workflow` | Workflow and child-agent start/end events preserve run metadata, identity, outcome, count, and error relations. | +| `dsh-tasks` | Current and terminal task snapshots preserve id/kind, owner, status, and timestamp relationships. | +| `dsh-tool-todo` | Durable whole-list snapshots use unique trimmed items, closed statuses, and at most one active item. | +| `dsh-time-context` | Plugin-attributed clock readings agree across turn, step, elapsed baseline, rendered time, and event time. | -At implementation time this covers all 91 workspace packages: four stateful companions, 62 plugin-fiber companions, eight service-shape companions, and 17 pure/bin/support companions. +Session-backed companions reconstruct their trace from existing durable events when they load. Other checks observe the authoritative live event boundary or mutable service result. Validation runs before publication where accepting an invalid event would otherwise commit bad state. -### Product-independent observers +### Repository gate and tests -`observePluginInvariant` checks existing fibers immediately and future active fibers through a callback/name index behind one root-shared Cordis lifecycle listener pair. A contract may supply an exact callback when that import is safe. Otherwise it matches `fiber.runtime.name`, the name declared by that fiber's own plugin runtime, rather than the inherited `fiber.name`; anonymous `ctx.inject()` children are therefore not misidentified as their parent package. The observer checks required injection keys, recursively collected effect labels, services provided by that exact fiber, and an optional owner validator. Config-dependent packages encode symmetric relations, such as automatic compaction owning both listeners or neither when disabled. +`verify-package-invariants` discovers every workspace package and enforces companion source, exact-name registration, `./invariant` exports, publication files, dependencies, TypeScript references, and bundle entries. Its AST rule rejects generated markers and unexplained empty installers. A non-empty installer must accept and use the failure reporter. The gate deliberately does not infer semantic quality from method names or helper calls. -`observeServiceInvariant` checks the current service and every later binding. `serviceShapeViolation` validates callable members and non-empty string descriptors structurally instead of using `instanceof`, so conforming third-party backends and complete test doubles remain valid while incomplete stand-ins fail. - -`assertInvariant` handles package algebra. Pure-package companions return an asynchronous installer promise and dynamically import their owner during child startup. The service joins that promise for atomic rollback while allowing the test module, Loader, or deployment to establish mocks and module resolution before the invariant samples the owner. - -### Gate and test execution - -`verify-package-invariants` discovers every workspace package and retains the publication checks for the exact registration name, `./invariant` export, published files, invariant peer and development dependencies, TypeScript reference, and bundle entry. Its source check additionally parses the local `install` function, rejects a generated marker or empty body, requires a second failure-reporter parameter and its use, and rejects duplicate name-based plugin observers across packages. These AST checks are a minimum acceptance rule, not a claim that source shape proves semantic quality. - -The Vitest setup host mounts `InvariantService` with `{ enabled: true }` before an ordinary Cordis root's first plugin and adds the current test package's companion. The host joins companion startup to the test's root-level composition boundary, so asynchronous pure checks and plugin-observer setup fail the test rather than becoming background diagnostics. One exhaustive topology mounts all 91 companions once to prove runtime registration and coverage; focused selection, lifecycle, and owner suites build their own enabled topology to avoid duplicate registrations while still testing invariants. - -Helper tests reject invalid plugin names, missing injections, effects, services, custom relations, malformed service shapes, and failed assertions. Package suites then activate real plugins across their existing config and HMR paths. Test-only service stand-ins must implement the complete checked seam rather than bypass global invariants. +Vitest mounts `InvariantService` with `{ enabled: true }` for every package test topology and loads the owning companion. The invariant subpath path mapping resolves source companions instead of stale built output. Focused suites cover every executable companion's valid and invalid observations, and the exhaustive topology loads all companions to prove registration and disposal wiring. Tests that synthesize event streams must produce a valid surrounding lifecycle unless the test is intentionally asserting a violation. ## Alternatives considered -- **Keep generated ownership-only companions.** Rejected because registration without an executable assertion cannot reject a broken package and makes the exhaustive gate misleading. -- **Generate one synthetic assertion into every package.** Rejected because a universal assertion would again optimize for satisfying the gate instead of protecting an owner-specific contract. -- **Move the per-package contract matrix into `dsh-invariants`.** Rejected because product imports, vocabulary, and change ownership would return to the central service. -- **Import every owner entrypoint statically from its companion.** Rejected because owning and exhaustive test hosts would preload packages before hoisted mocks and shipped compositions would pay unrelated module initialization costs. -- **Require first-party service-class identity.** Rejected because service seams are structural extension boundaries; `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. +- **Keep generated empty companions.** Rejected because an unexplained placeholder can survive after a package gains a meaningful runtime relation. +- **Require an assertion from every package.** Rejected because method-presence, plugin-shape, and fixed-example assertions duplicate stronger type, load, and unit-test contracts without checking runtime consistency. +- **Keep generic shape helpers in the service.** Rejected because they blur compile-time API validation with runtime invariants and encourage centrally defined product assumptions. +- **Move the product checks into the service.** Rejected because product vocabulary, dependencies, tests, and change ownership belong with the package that emits the data. +- **Register companions implicitly from root entrypoints.** Rejected because composition order and optional service presence would create hidden effects. ## Consequences -- Every package contributes an executable check; adding a package without one fails the top-level gate. -- The invariant service remains product-independent while providing reusable lifecycle and shape observers. -- Ordinary unit, snapshot, and e2e roots run with global invariant enablement and the test package's companion; one exhaustive topology registers every companion. -- Plugin names used for name-based observation must be unique within one Cordis root; packages may opt into exact callback identity when safe. -- Pure-package checks sample stable startup contracts. Mutable behavior must use an event, service, or plugin-fiber observer. -- Relevant companion work runs during package tests and selected deployments, trading bounded startup cost for immediate package-attributed failures. -- The original regex selection, blocklist precedence, registration uniqueness, rollback, disposal, and HMR contracts remain unchanged. +- Every package has visible ownership and publication wiring, but only packages with a plausible runtime relation add listeners or trace state. +- Empty companions remain reviewable decisions with package-specific explanations and fail the gate if the explanation is removed. +- Type declarations, Cordis loadability, plugin metadata, service method surfaces, and pure algebra remain covered by their owning compile, load, unit, or integration gates. +- Runtime failures identify the owning npm package and point to an inconsistent observation rather than restating a required API shape. +- The original selection, blocklist precedence, duplicate ownership, rollback, disposal, and HMR service contracts remain unchanged. diff --git a/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.zh.md b/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.zh.md index 0ae0bd3056..88b1eb1a79 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.zh.md @@ -1,65 +1,75 @@ -# Agent Note: 可执行的包不变式契约 +# Agent Note:有意义的包不变量契约 -Status: implemented +状态:已实施 [English](2026-07-19-package-invariant-runtime-contracts.md) | 中文 ## 问题 -包拥有的不变式接缝让注册与发布覆盖完整,但生成的基线把包名所有权视为充分条件。空 installer 可以通过仓库门禁,却不观察任何运行时状态,也不拒绝任何无效状态。这样一来,完整计数只能证明接线存在,不能保护包契约。 +包自有不变量接缝让发布和注册实现了全覆盖,但最初的生成基线允许空安装器。后续方案又用针对插件名称、注入、effect、服务方法和固定纯函数示例的通用断言替代这些空实现。这些断言虽然让每个 companion 都能执行,却没有提高系统安全性:TypeScript、Cordis 启动、包测试和模块加载测试已经约束这些形状,而不变量服务应当发现不可能出现的运行时状态。 -不同包形态不能使用同一种不变式。Cordis 插件拥有 fiber、注入、effect 与服务;服务接缝允许结构兼容的第三方实现;有状态领域需要事件关系;纯库和 bin 包暴露代数、解析、规范化或入口约束。仓库需要一个可执行的统一义务,同时不能把这些契约重新移回了解产品语义的中央包。 +有用的运行时不变量会关联时间上的多个观测,或关联可变数据结构中的多个部分。例如:终止事件没有对应的开始事件、LLM delta 指向未打开的 block,或持久化结果的身份与请求不同。仅确认声明的方法存在、插件名称符合预期,或常量示例仍返回已知值,都不属于这种关系。 -Vitest 会为每个包测试全局挂载其所有者伴随插件,并由一个完整拓扑挂载全部伴随插件。因此伴随模块不能在测试模块建立 hoisted mock 之前急切导入所有产品入口;按名称观察时,也不能把继承父级显示名的匿名子 fiber 误认为包插件本身。 +有些包确实没有可持续观测的关系。纯工具、仅负责组合的包、薄适配器、可执行入口和测试支持包可能仍有重要契约,但类型检查、加载检查、聚焦单元测试或集成测试更适合执行这些契约。强迫这些包添加合成运行时断言,只会让实现围绕通过门禁优化,而不是检测损坏。 ## 决策 -### 每个伴随插件都执行包契约 +### 注册必须全覆盖;断言必须有意义 -每个工作区包保留独立发布的 `./invariant` 伴随插件和准确 npm 包名注册,但 installer 必须通过绑定的 `fail(message)` 报告器执行至少一个包专属检查。删除所有权基线生成器及其根脚本入口;生成标记、空 installer 和从不引用报告器的 installer 都属于仓库错误。 +每个 workspace 包都发布单独构建的 `./invariant` companion,并用完整 npm 包名注册。companion 只能采用以下两种形式之一: -实现后的契约采用四种形态: +- 安装包自有的事件流或相关可变数据结构检查,并通过绑定的 `fail(message)` 报告器报告违规;或 +- 使用空安装器,并在其声明前写一条该包专属的 `No runtime invariant:` 注释,说明为什么该包没有合理的运行时关系可供观测。 -| 所有者形态 | 运行时契约 | +空形式是明确的架构结论,不是生成占位符。如果后续包变更引入可变状态或事件协议,就必须用相应检查替换该说明。 + +中央 `dsh-invariants` 服务只负责配置、注册唯一性、子 fiber 生命周期、回滚、释放和归属到包的失败。它不暴露通用插件形状、服务形状或启动断言 helper,也不导入产品包。 + +### 已实施的检查 + +当前 91 个包的 workspace 包含 18 个可执行 companion 和 73 个有理由的空 companion。 + +| 所有者 | 运行时关系 | |---|---| -| 有状态的 session、agent、scope 与 agent-loop 所有者 | 验证事件顺序、包围关系、状态转换、作用域主体和可重建的模型请求。 | -| Cordis 插件所有者 | 验证插件自身声明的运行时名称、必要注入、拥有的 effect、提供的服务,以及包专属的全有或全无关系或配置依赖关系。 | -| Cordis 服务接缝 | 验证当前和未来实现的结构化方法与描述字段表面。 | -| 纯库、bin 与支持包 | 验证稳定的解析映射、协议优先级、保留与超时代数、路径解析、规范化、环境清理或刻意为空的运行时入口。 | +| `dsh-session` | 序号严格递增、turn/step 包围关系,以及同一 step 内的工具调用/结果配对。 | +| `dsh-agent` | agent 状态不得重复,并且不能离开终态 disposed。 | +| `dsh-scope` | scoped event 必须携带 carrier,且路由 subject 保持一致。 | +| `dsh-agent-loop` | 从 session 事件日志重建冻结的 loop 请求。 | +| `dsh-llm` | stream block 文法、delta 类型/索引匹配、单次 usage、block 闭合和终止 finish。 | +| `dsh-tools` | pre/execute/post 阶段单调推进,以及最终 execution/result 快照不可变。 | +| `dsh-system-prompt` | 权威 assembly 中 section、tool 和 variable 的数据约束。 | +| `dsh-compact` | compaction start/summary/end 配对、范围端点、token 数量和成功时必须存在 summary。 | +| `dsh-hook-protocol` | hook invocation/result 的关联、dialect、身份和 duration 约束。 | +| `dsh-bash` | 持久化 sandbox-mode 事件必须使用封闭的 sandbox-mode 词表。 | +| `dsh-fs` | 文件系统决策/观测事件必须携带可用的 target 和 version 身份。 | +| `dsh-subagent` | provider add/remove 和 child start/end 事件必须保持身份与配对。 | +| `dsh-permission` | 持久化 permission 决策必须引用当前 permission 表中的 preset。 | +| `dsh-user-approval` | approval asked/decided 记录按 call 配对,并使用有效 outcome 和 policy。 | +| `dsh-workflow` | workflow 和 child-agent start/end 事件保持 run metadata、身份、outcome、数量和 error 关系。 | +| `dsh-tasks` | 当前与终态 task snapshot 保持 id/kind、owner、status 和 timestamp 关系。 | +| `dsh-tool-todo` | 持久化全量 snapshot 使用唯一且已 trim 的条目、封闭 status,并且最多有一个活动条目。 | +| `dsh-time-context` | 标注插件来源的时钟 reading 在 turn、step、elapsed baseline、渲染时间和事件时间之间保持一致。 | -实现时覆盖全部 91 个工作区包:四个有状态伴随插件、62 个插件 fiber 伴随插件、八个服务形状伴随插件和 17 个纯库、bin 或支持包伴随插件。 +基于 session 的 companion 在加载时从已有持久化事件重建 trace。其他检查观测权威 live event 边界或可变服务结果。如果接受无效事件会提交错误状态,验证就在发布前执行。 -### 与产品无关的观察器 +### 仓库门禁与测试 -`observePluginInvariant` 会立即检查已有 fiber,并通过根上下文共享的一对 Cordis 生命周期监听器背后的 callback/名称索引检查未来进入活跃状态的 fiber。安全导入时,契约可以提供准确 callback;否则匹配 `fiber.runtime.name`,即该 fiber 自身插件运行时声明的名称,而不是继承而来的 `fiber.name`,因此匿名 `ctx.inject()` 子级不会被误认成父包。观察器检查必要注入键、递归收集的 effect 标签、由该 fiber 准确提供的服务,以及可选的所有者验证器。依赖配置的包使用对称关系,例如自动压缩要么同时拥有两个监听器,要么在关闭时两个都没有。 +`verify-package-invariants` 发现每个 workspace 包,并强制 companion 源文件、完整名称注册、`./invariant` export、发布文件、依赖、TypeScript reference 和 bundle entry 完整。其 AST 规则拒绝生成标记和没有解释的空安装器。非空安装器必须接收并使用失败报告器。门禁不会通过方法名或 helper 调用推断语义质量。 -`observeServiceInvariant` 检查当前服务及之后的每次绑定。`serviceShapeViolation` 以结构方式验证可调用成员和非空字符串描述字段,而不使用 `instanceof`;因此符合契约的第三方后端和完整测试替身有效,不完整替身会失败。 - -`assertInvariant` 处理包代数。纯包伴随插件返回异步 installer promise,并在子 fiber 启动期间动态导入所有者。服务会等待该 promise 以保证原子回滚,同时允许测试模块、Loader 或部署先建立 mock 和模块解析,再由不变式采样所有者。 - -### 门禁与测试执行 - -`verify-package-invariants` 发现每个工作区包,并保留准确注册名、`./invariant` export、发布文件、不变式 peer 与开发依赖、TypeScript 引用和 bundle 入口的发布检查。源码检查还会解析本地 `install` 函数,拒绝生成标记或空函数体,要求第二个失败报告器参数及其使用,并拒绝跨包重复的按名称插件观察器。这些 AST 检查只是最低接收规则,并不宣称源码形状足以证明语义质量。 - -Vitest setup host 会在普通 Cordis 根上下文启动第一个插件前,以 `{ enabled: true }` 挂载 `InvariantService`,并添加当前测试包的伴随插件。host 会把伴随插件启动加入测试的根级组合边界,因此异步纯检查和插件观察器安装会让测试失败,而不会变成后台诊断。一个完整拓扑会一次挂载全部 91 个伴随插件,以证明运行时注册与覆盖率;选择、生命周期和所有者聚焦套件自行构建启用的不变式拓扑,在避免重复注册的同时继续测试不变式。 - -辅助测试会拒绝错误插件名、缺失注入、effect、服务或自定义关系、错误服务形状和失败断言。随后,包套件在已有配置与 HMR 路径上激活真实插件。测试专用服务替身必须实现完整的已检查接缝,不能绕过全局不变式。 +Vitest 为每个包测试拓扑使用 `{ enabled: true }` 挂载 `InvariantService`,并加载所有者 companion。不变量 subpath 的 path mapping 会解析源 companion,而不是陈旧的构建输出。聚焦 suite 覆盖每个可执行 companion 的有效和无效观测;穷举拓扑加载全部 companion,以证明注册和释放 wiring。合成事件流的测试必须构造有效的外围生命周期,除非测试本身就是在断言违规。 ## 考虑过的替代方案 -- **保留生成的仅声明所有权伴随插件。** 不予采纳,因为没有可执行断言的注册无法拒绝损坏的包,也会让完整门禁产生误导。 -- **为每个包生成一个合成断言。** 不予采纳,因为通用断言仍是在优化如何通过门禁,而不是保护所有者专属契约。 -- **把逐包契约矩阵移入 `dsh-invariants`。** 不予采纳,因为产品导入、词汇和变更所有权会重新回到中央服务。 -- **从伴随插件静态导入每个所有者入口。** 不予采纳,因为所有者测试 host 与完整测试 host 会在 hoisted mock 之前预加载包,发布组合也会支付无关模块初始化成本。 -- **要求第一方服务类身份。** 不予采纳,因为服务接缝是结构化扩展边界;`instanceof` 会拒绝有效的外部实现和测试替身。 -- **从包根入口隐式注册不变式。** 因包拥有服务 RFC 中的组合顺序与隐藏 effect 问题而不予采纳。 +- **保留生成的空 companion。** 拒绝,因为包获得有意义的运行时关系后,没有解释的占位符仍可能继续存在。 +- **要求每个包都执行断言。** 拒绝,因为方法存在性、插件形状和固定示例断言会重复更强的类型、加载和单元测试契约,却没有检查运行时一致性。 +- **在服务中保留通用形状 helper。** 拒绝,因为这会混淆编译期 API 验证和运行时不变量,并鼓励在中央定义产品假设。 +- **把产品检查移入服务。** 拒绝,因为产品词汇、依赖、测试和变更所有权应归属于产生这些数据的包。 +- **从根入口隐式注册 companion。** 拒绝,因为组合顺序和可选服务存在性会产生隐藏 effect。 ## 后果 -- 每个包都贡献可执行检查;新增包若没有检查,会在顶层门禁失败。 -- 不变式服务保持与产品无关,同时提供可复用的生命周期与形状观察器。 -- 普通单元、snapshot 与 e2e 根上下文均全局启用不变式并注册当前测试包的伴随插件;一个完整拓扑注册全部伴随插件。 -- 用于按名称观察的插件名在一个 Cordis 根上下文内必须唯一;安全时包可以选择准确 callback 身份。 -- 纯包检查对稳定启动契约采样;可变行为必须使用事件、服务或插件 fiber 观察器。 -- 包测试和被选部署会执行相关伴随工作,以有界启动成本换取即时且带包归属的失败。 -- 原有正则选择、blocklist 优先级、注册唯一性、回滚、dispose 与 HMR 契约保持不变。 +- 每个包都有可见的所有权与发布 wiring,但只有具备合理运行时关系的包才会增加 listener 或 trace 状态。 +- 空 companion 是带包专属说明、可评审的决策;删除说明后门禁会失败。 +- 类型声明、Cordis 可加载性、插件 metadata、服务方法形状和纯代数继续由所属的编译、加载、单元或集成门禁覆盖。 +- 运行时失败会标明所属 npm 包,并指出不一致的观测,而不是复述必要的 API 形状。 +- 原有 selection、blocklist 优先级、重复所有权、回滚、释放和 HMR 服务契约保持不变。 diff --git a/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.i18n.yaml index 9ab8642f4b..c37979ae66 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.i18n.yaml +++ b/.agents/notes/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: 552b088c1cafc2fa762487f57fa1d4ad64390f7e -2026-07-19-package-owned-invariant-service.zh.md: 84bdc6a6a3baefe95713f8ce5b8a4e2af49b64eb +2026-07-19-package-owned-invariant-service.md: bd167f8da1e855b703b75a60e2dbd8959f5bf9b3 +2026-07-19-package-owned-invariant-service.zh.md: 6bd4f30123b7c7ba39266d5d31dd4b378604fd5a diff --git a/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.md b/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.md index 552b088c1c..bd167f8da1 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.md +++ b/.agents/notes/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 and installs an executable package-specific contract. Generated ownership-only installers are forbidden by the follow-up [runtime-contract Agent Note](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. +Every workspace package publishes a `./invariant` companion plugin that registers its exact full npm name. A companion checks a meaningful event or mutable-data relationship when its owner has one; otherwise it carries an owner-specific explanation for its empty installer. Generated ownership placeholders and synthetic API-shape assertions are forbidden by the follow-up [runtime-contract Agent Note](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 @@ -55,7 +55,7 @@ Registration setup is transactional. If an installer fails after registering lis The former functional-plugin entrypoint and one-argument `InvariantError` constructor are not retained as compatibility surfaces. The repository is pre-release and all call sites move to the service and package-attributed error together. -### Stateful companions and exhaustive ownership +### Initial stateful companions and exhaustive ownership | Companion entry | Registration name | Owned checks | |---|---|---| @@ -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. 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. +These four owners supplied the initial stateful checks. The follow-up runtime-contract decision adds checks for fourteen more owners with real event or mutable-data relationships and records justified empty companions for the rest. 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 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. +`verify-package-invariants` discovers every workspace package and rejects missing companion source, generated markers, unexplained empty installers, non-empty installers that omit or ignore the reporter, 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 @@ -80,7 +80,7 @@ Workspace constraints recognize the separate invariant bundle, and package expor ## Testing -Service tests cover defaults, global disablement, allow/block selection, blocklist precedence, anchoring, unanchored matching, case sensitivity, invalid configuration, zero-match patterns, late registration, duplicate ownership, disposal, rollback, and HMR re-registration. Owner tests keep each invariant's positive and negative behavior beside its source. +Service tests cover defaults, global disablement, allow/block selection, blocklist precedence, anchoring, unanchored matching, case sensitivity, invalid configuration, zero-match patterns, late registration, duplicate ownership, disposal, rollback, and HMR re-registration. Owners with executable checks keep positive and negative behavior beside the companion source. Composition tests cover standard-spine forwarding and generated SDK entries. Loader tests preserve each companion namespace, while built plain-Node smokes exercise the compiled subpath exports. The scoped-event freshness gate reruns its semantic Program analysis. @@ -96,10 +96,10 @@ 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 publication, dependency, and runtime-check cost of an executable invariant companion. +- Every package pays the publication and dependency cost of a companion; only owners with a meaningful runtime relationship add listener or trace-state cost. - 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. +- One selected executable contribution adds one child fiber and its listener/state cost; a selected empty contribution has no listener or trace-state cost, while filtered registrations retain only name ownership. - Regex sources are deployment configuration and remain fixed until the service reloads. - Ordinary Vitest roots install the owning test package's selected companion; one exhaustive topology pays the full child-fiber cost once for repository-wide registration coverage. - Session storage validation, snapshotting, freezing, provenance, and surface acceptance remain always on and are not affected by invariant selection. diff --git a/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.zh.md b/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.zh.md index 84bdc6a6a3..6bd4f30123 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.zh.md +++ b/.agents/notes/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 包名,并安装可执行的包专属契约。后续的[运行时契约 Agent Note](2026-07-19-package-invariant-runtime-contracts.md) 禁止生成的仅声明所有权 installer。包的根入口不会隐式导入或注册诊断,因此加载根包不会改变运行时检查,也不要求不变式服务存在。 +工作区内的每个包都发布 `./invariant` 伴随插件,注册自己完整且准确的 npm 包名。如果所有者具备有意义的事件或可变数据关系,companion 就检查该关系;否则空 installer 必须携带该所有者专属的说明。后续的[运行时契约 Agent Note](2026-07-19-package-invariant-runtime-contracts.md) 禁止生成的所有权占位符和合成 API 形状断言。包的根入口不会隐式导入或注册诊断,因此加载根包不会改变运行时检查,也不要求不变式服务存在。 ### 配置与选择 @@ -55,7 +55,7 @@ blocklist 匹配优先于 allowlist 匹配。每个条目都是区分大小写 原有函数式插件入口与单参数 `InvariantError` 构造函数不作为兼容表面保留。仓库尚未发布,所有调用方会一起迁移到服务和带包归属的错误。 -### 有状态伴随插件与完整所有权 +### 首批有状态伴随插件与完整所有权 | 伴随入口 | 注册名 | 所属检查 | |---|---|---| @@ -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` | 模型请求重建 | -这四个所有者保存有状态检查与聚焦测试。其他所有者检查自己的插件 fiber 与 effect、结构化服务实现或稳定的纯库代数。每个伴随入口都是单独打包的 `./invariant` export,具有独立声明和对 Loader 安全的命名空间插件形态;服务包自身的伴随插件导入本地服务类型,避免形成自依赖。 +这四个所有者提供了首批有状态检查。后续运行时契约决策为另外十四个确有事件或可变数据关系的所有者增加检查,并为其余包记录有理由的空 companion。每个伴随入口都是单独打包的 `./invariant` export,具有独立声明和对 Loader 安全的命名空间插件形态;服务包自身的伴随插件导入本地服务类型,避免形成自依赖。 -`verify-package-invariants` 会发现每个工作区包,并拒绝缺失的伴随插件源码、生成标记、空 installer、不使用失败报告器的 installer、外部或无法解析的注册名、缺失的 `./invariant` export 或发布文件、缺失的不变式对等依赖(peer dependency)、开发依赖及项目引用,以及遗漏伴随入口的自定义构建配置。 +`verify-package-invariants` 会发现每个工作区包,并拒绝缺失的伴随插件源码、生成标记、没有解释的空 installer、缺少或不使用失败报告器的非空 installer、外部或无法解析的注册名、缺失的 `./invariant` export 或发布文件、缺失的不变式对等依赖(peer dependency)、开发依赖及项目引用,以及遗漏伴随入口的自定义构建配置。 ### Scoped event 语义映射 @@ -80,7 +80,7 @@ Workspace 约束识别独立的不变式 bundle;包 exports、项目引用、 ## 测试 -服务测试覆盖默认值、全局关闭、allow/block 选择、blocklist 优先级、锚定与非锚定匹配、大小写敏感、无效配置、零匹配模式、延迟注册、重复所有权、dispose、回滚和 HMR 重新注册。所有者测试把各不变式的正向与负向行为保留在其源码旁边。 +服务测试覆盖默认值、全局关闭、allow/block 选择、blocklist 优先级、锚定与非锚定匹配、大小写敏感、无效配置、零匹配模式、延迟注册、重复所有权、dispose、回滚和 HMR 重新注册。具备可执行检查的所有者会把正向与负向行为保留在 companion 源码旁边。 组合测试覆盖标准 spine 转发和生成的 SDK 条目。Loader 测试固定每个伴随命名空间,构建后的纯 Node smoke 覆盖编译子路径 export。scoped event 新鲜度门禁会重新执行语义 Program 分析。 @@ -96,10 +96,10 @@ Workspace 约束识别独立的不变式 bundle;包 exports、项目引用、 ## 后果 - 产品包拥有并测试自己的关系断言,服务保持与产品无关。 -- 每个包都要承担可执行不变式伴随插件带来的发布、依赖与运行时检查成本。 +- 每个包都承担 companion 的发布与依赖成本;只有具备有意义运行时关系的所有者才增加 listener 或 trace 状态成本。 - 标准组合无需改变插件树即可关闭全部检查或按包名选择。 - 显式伴随条目让诊断成本和所有权在 Cordis 配置与包 export 中可见。 -- 每个选中贡献增加一个子 fiber 及其监听器和状态成本;被过滤注册只保留包名占用。 +- 每个选中的可执行贡献增加一个子 fiber 及其 listener/状态成本;选中的空贡献不增加 listener 或 trace 状态成本,被过滤注册则只保留包名占用。 - 正则表达式源属于部署配置,在服务重载前保持固定。 - 普通 Vitest 根上下文会安装当前测试包中被选中的伴随插件;一个完整拓扑只支付一次全部子 fiber 成本,用于覆盖整个仓库的注册。 - 会话存储验证、快照、冻结、provenance 与 surface 接受规则始终启用,不受不变式选择影响。 diff --git a/packages/AGENTS.md b/packages/AGENTS.md index 1219088e70..16ee490395 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 executable invariants.** Publish `./invariant`, register its manifest name, and enforce a runtime contract with the bound reporter; generated, empty, and reporter-free installers fail `verify-package-invariants` ([rationale](../.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md)). +- **Every package owns an explicit invariant companion.** Publish `./invariant` and register its manifest name. Check observable event or mutable-data relationships; when none exists, keep an empty installer with a package-specific `No runtime invariant:` explanation instead of inventing an API-shape assertion. Generated companions, unexplained empties, and non-empty installers that ignore the reporter fail `verify-package-invariants` ([rationale](../.agents/notes/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 397bf0d6dc..3cc7bd62e2 100644 --- a/packages/bash/bash-local/src/invariant.ts +++ b/packages/bash/bash-local/src/invariant.ts @@ -1,28 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-bash-local`. @module @deepseek-ai/dsh-bash-local/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-bash-local`. + * @module @deepseek-ai/dsh-bash-local/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-bash-local' /** Cordis companion plugin name. */ export const name = 'bash-local-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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', - ], - }) -} +/** + * No runtime invariant: this package exposes no independent event sequence or mutable data relation + * beyond contracts enforced at its owning seam. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -31,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 f758c90f6f..b79b626033 100644 --- a/packages/bash/bash-sandbox/src/invariant.ts +++ b/packages/bash/bash-sandbox/src/invariant.ts @@ -1,30 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-bash-sandbox`. @module @deepseek-ai/dsh-bash-sandbox/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-bash-sandbox`. + * @module @deepseek-ai/dsh-bash-sandbox/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-bash-sandbox' /** Cordis companion plugin name. */ export const name = 'bash-sandbox-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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', - ], - }) -} +/** + * No runtime invariant: this package exposes no independent event sequence or mutable data relation + * beyond contracts enforced at its owning seam. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -33,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 91f135b1bb..1229e19e4c 100644 --- a/packages/bash/bash/src/invariant.ts +++ b/packages/bash/bash/src/invariant.ts @@ -1,24 +1,30 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-bash`. @module @deepseek-ai/dsh-bash/invariant */ +/** Package-owned session-event invariants for the bash seam. @module @deepseek-ai/dsh-bash/invariant */ import type { Context } from 'cordis' -import { observeServiceInvariant, serviceShapeViolation, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import { SANDBOX_MODES } from './session-mode.ts' const PACKAGE_NAME = '@deepseek-ai/dsh-bash' /** Cordis companion plugin name. */ export const name = 'bash-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Validate every implementation bound to this package's service seam. */ +/** Install validation for the durable sandbox-mode vocabulary. */ const install: InvariantInstaller = (ctx, fail) => { - observeServiceInvariant(ctx, fail, 'bash', value => serviceShapeViolation(value, { - methods: ['resolve', 'run', 'start'], - })) + ctx.on('internal/dispatch', (_mode, eventName, args) => { + if (eventName !== 'session/event') return + const event = (args as [Session, SessionEvent])[1] + if (event.type === 'bash/sandbox-mode' && !SANDBOX_MODES.includes(event.data.mode)) { + fail(`bash/sandbox-mode carries unknown mode ${JSON.stringify(event.data.mode)}`) + } + }, { global: true }) } /** - * Register this package's invariant companion. + * Register the bash invariant companion. * @param ctx - Cordis context carrying the invariant service. * @returns the installed registration's disposer after setup succeeds. */ diff --git a/packages/bash/bash/tests/invariant.spec.ts b/packages/bash/bash/tests/invariant.spec.ts new file mode 100644 index 0000000000..9d6209042f --- /dev/null +++ b/packages/bash/bash/tests/invariant.spec.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' +import InvariantService from '@deepseek-ai/dsh-invariants' +import * as BashInvariant from '@deepseek-ai/dsh-bash/invariant' + +async function setup(): Promise { + const ctx = new Context() + await ctx.plugin(InvariantService) + await ctx.plugin(BashInvariant) + return ctx +} + +function modeEvent(mode: string): SessionEvent { + return { type: 'bash/sandbox-mode', seq: 0, time: 0, data: { mode } } as SessionEvent +} + +describe('bash invariants', () => { + it('accepts the closed sandbox vocabulary and ignores unrelated events', async () => { + const ctx = await setup() + expect(() => { ctx.emit('session/event', {} as Session, modeEvent('workspace-write')) }).not.toThrow() + expect(() => { ctx.emit('session/event', {} as Session, { + type: 'turn/start', seq: 0, time: 0, data: {}, + } as SessionEvent) }).not.toThrow() + expect(() => { ctx.emit('tools/change') }).not.toThrow() + }) + + it('rejects an unknown durable sandbox mode', async () => { + const ctx = await setup() + expect(() => { ctx.emit('session/event', {} as Session, modeEvent('host-root')) }) + .toThrow(/unknown mode "host-root"/) + }) +}) diff --git a/packages/bash/tool-bash/src/invariant.ts b/packages/bash/tool-bash/src/invariant.ts index ca63f9ff77..0620f0cfa9 100644 --- a/packages/bash/tool-bash/src/invariant.ts +++ b/packages/bash/tool-bash/src/invariant.ts @@ -1,34 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tool-bash`. @module @deepseek-ai/dsh-tool-bash/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-tool-bash`. + * @module @deepseek-ai/dsh-tool-bash/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tool-bash' /** Cordis companion plugin name. */ export const name = 'tool-bash-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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', - ], - }) -} +/** + * No runtime invariant: the environment registry validates ownership and collected values at each + * mutation/read; it publishes no independent snapshot that a companion could cross-check. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -37,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 63daf58ae9..3455104441 100644 --- a/packages/code-runtime/code-runtime-worker/src/invariant.ts +++ b/packages/code-runtime/code-runtime-worker/src/invariant.ts @@ -1,31 +1,24 @@ /** - * Package-owned runtime contract checks for `@deepseek-ai/dsh-code-runtime-worker`. + * Package-owned invariant companion for `@deepseek-ai/dsh-code-runtime-worker`. * @module @deepseek-ai/dsh-code-runtime-worker/invariant */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-code-runtime-worker' /** Cordis companion plugin name. */ export const name = 'code-runtime-worker-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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', - ], - }) -} +/** + * No runtime invariant: this process-boundary implementation exposes no same-process event relation; + * worker protocol and built-worker tests cover it. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -34,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 1ee564c60b..9c4019699b 100644 --- a/packages/code-runtime/code-runtime/src/invariant.ts +++ b/packages/code-runtime/code-runtime/src/invariant.ts @@ -1,29 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-code-runtime`. @module @deepseek-ai/dsh-code-runtime/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-code-runtime`. + * @module @deepseek-ai/dsh-code-runtime/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observeServiceInvariant, serviceShapeViolation, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-code-runtime' /** Cordis companion plugin name. */ export const name = 'code-runtime-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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' - }) -} +/** + * No runtime invariant: this package exposes no independent event sequence or mutable data relation + * beyond contracts enforced at its owning seam. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -32,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 68bc809de7..56a32930c9 100644 --- a/packages/code-runtime/code-runtime/tests/service.spec.ts +++ b/packages/code-runtime/code-runtime/tests/service.spec.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest' import { Context } from 'cordis' import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime' import type { CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime' -import { InvariantError } from '@deepseek-ai/dsh-invariants' /** * Minimal concrete runtime: records requests, "executes" by invoking every @@ -86,24 +85,4 @@ describe('CodeRuntime service seam', () => { 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) - }, - } - let caught: unknown - try { - await ctx.plugin(invalidRuntime) - } catch (error) { - caught = error - } - expect(caught).toBeInstanceOf(InvariantError) - expect((caught as Error).message).toMatch(message) - }) }) diff --git a/packages/compact/compact-basic/src/invariant.ts b/packages/compact/compact-basic/src/invariant.ts index 3e034a2f58..172790d233 100644 --- a/packages/compact/compact-basic/src/invariant.ts +++ b/packages/compact/compact-basic/src/invariant.ts @@ -1,46 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-compact-basic`. @module @deepseek-ai/dsh-compact-basic/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-compact-basic`. + * @module @deepseek-ai/dsh-compact-basic/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-compact-basic' /** Cordis companion plugin name. */ export const name = 'compact-basic-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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 - }, - }) -} +/** + * No runtime invariant: this package exposes no independent event sequence or mutable data relation + * beyond contracts enforced at its owning seam. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -49,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 762289c9d9..0b118ac245 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 { CompactService, CompactionResult } from '@deepseek-ai/dsh-compact' +import type { 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' @@ -199,28 +199,6 @@ describe('compact configuration and defaults', () => { } }) - 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 974f7a29ca..59fdc806b2 100644 --- a/packages/compact/compact/src/invariant.ts +++ b/packages/compact/compact/src/invariant.ts @@ -1,24 +1,106 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-compact`. @module @deepseek-ai/dsh-compact/invariant */ +/** Package-owned compaction log-stream invariants. @module @deepseek-ai/dsh-compact/invariant */ import type { Context } from 'cordis' -import { observeServiceInvariant, serviceShapeViolation, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' +import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type {} from './types.ts' const PACKAGE_NAME = '@deepseek-ai/dsh-compact' /** Cordis companion plugin name. */ export const name = 'compact-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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'], - })) +interface CompactionTrace { + turn: number + summarized: boolean } +type CompactionTransition = + | { kind: 'start'; turn: number } + | { kind: 'summary'; turn: number } + | { kind: 'end' } + +/** Validate one compaction event without advancing committed trace state. */ +function validateCompactionEvent( + open: CompactionTrace | undefined, + event: SessionEvent, + fail: InvariantFailure, +): CompactionTransition | undefined { + if (event.type === 'compact/start') { + if (open !== undefined) fail(`compact/start for turn ${event.data.turn} while turn ${open.turn} is still compacting`) + return { kind: 'start', turn: event.data.turn } + } + if (event.type === 'compact/summary') { + if (open === undefined) fail('compact/summary has no matching compact/start') + if (open.summarized) fail('compact/summary repeated within one compaction') + const seqs = event.data.shadowedSeqs + if (seqs.length === 0) fail('compact/summary shadowedSeqs must be non-empty') + if (seqs[0] !== event.data.shadowedRange.start || seqs.at(-1) !== event.data.shadowedRange.end) { + fail('compact/summary shadowedRange must match the first and last shadowedSeqs') + } + if (!Number.isSafeInteger(event.data.shadowedTokenCount) || event.data.shadowedTokenCount < 0) { + fail('compact/summary shadowedTokenCount must be a non-negative safe integer') + } + return { kind: 'summary', turn: open.turn } + } + if (event.type !== 'compact/end') return undefined + if (open === undefined) fail('compact/end has no matching compact/start') + if (event.data.turn !== open.turn) { + fail(`compact/end turn ${event.data.turn} does not match compact/start turn ${open.turn}`) + } + if (event.data.error === undefined && !open.summarized) { + fail('successful compact/end requires one compact/summary') + } + return { kind: 'end' } +} + +/** Apply one committed compaction transition. */ +function applyCompactionTransition( + transition: CompactionTransition, +): CompactionTrace | undefined { + if (transition.kind === 'start') return { turn: transition.turn, summarized: false } + if (transition.kind === 'summary') return { turn: transition.turn, summarized: true } + return undefined +} + +/** Install compaction start/summary/end checks. */ +const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => { + const traces = new WeakMap() + const staged = new WeakMap() + const seed = (session: Session): void => { + let open: CompactionTrace | undefined + for (const event of session.events) { + const transition = validateCompactionEvent(open, event, fail) + if (transition !== undefined) open = applyCompactionTransition(transition) + } + if (open !== undefined) traces.set(session, open) + } + const traceFor = (session: Session): CompactionTrace | undefined => traces.get(session) + + for (const session of ctx.sessions.list()) seed(session) + ctx.on('session/created', (session) => { seed(session) }, { global: true }) + ctx.on('session/event', (session, event) => { + if (event.type !== 'compact/start' && event.type !== 'compact/summary' && event.type !== 'compact/end') return + const candidate = staged.get(event) + /* v8 ignore next -- internal/dispatch stages every compaction event */ + if (candidate === undefined || candidate.session !== session) return fail('compaction event published without pre-commit validation') + staged.delete(event) + const next = applyCompactionTransition(candidate.transition) + if (next === undefined) traces.delete(session) + else traces.set(session, next) + }, { global: true }) + ctx.on('internal/dispatch', (_mode, eventName, args) => { + if (eventName !== 'session/event') return + const [session, event] = args as [Session, SessionEvent] + const transition = validateCompactionEvent(traceFor(session), event, fail) + if (transition !== undefined) staged.set(event, { session, transition }) + }, { global: true }) +}, { inject: ['sessions'] }) + /** - * Register this package's invariant companion. + * Register the compact invariant companion. * @param ctx - Cordis context carrying the invariant service. * @returns the installed registration's disposer after setup succeeds. */ diff --git a/packages/compact/compact/tests/compact.spec.ts b/packages/compact/compact/tests/compact.spec.ts index 559d46bdc9..1ff03bdfb1 100644 --- a/packages/compact/compact/tests/compact.spec.ts +++ b/packages/compact/compact/tests/compact.spec.ts @@ -38,7 +38,7 @@ class StubCompactService extends CompactService { const summaryEvent = session.append('compact/summary', { summary, shadowedRange: { start, end }, - shadowedSeqs: [], + shadowedSeqs: [start], shadowedTokenCount: 0, provider: 'mock', model: 'stub', @@ -50,7 +50,7 @@ class StubCompactService extends CompactService { endSeq: endEvent.seq, summary, shadowedRange: { start, end }, - shadowedSeqs: [], + shadowedSeqs: [start], shadowedTokenCount: 0, } } diff --git a/packages/compact/compact/tests/invariant.spec.ts b/packages/compact/compact/tests/invariant.spec.ts new file mode 100644 index 0000000000..2f4d10ff71 --- /dev/null +++ b/packages/compact/compact/tests/invariant.spec.ts @@ -0,0 +1,90 @@ +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import SessionStore from '@deepseek-ai/dsh-session' +import * as CompactInvariant from '@deepseek-ai/dsh-compact/invariant' +import InvariantService from '@deepseek-ai/dsh-invariants' + +async function setup(): Promise { + const ctx = new Context() + await ctx.plugin(SessionStore) + await ctx.plugin(InvariantService) + await ctx.plugin(CompactInvariant) + return ctx +} + +const summary = (overrides: Record = {}) => ({ + summary: [{ type: 'text' as const, text: 'short' }], + shadowedRange: { start: 2, end: 4 }, + shadowedSeqs: [2, 3, 4], + shadowedTokenCount: 12, + provider: 'mock', + model: 'mock', + ...overrides, +}) + +describe('compaction invariants', () => { + it('accepts successful and failed compaction lifecycles', async () => { + const ctx = await setup() + const success = ctx.sessions.create() + success.append('compact/start', { turn: 1 }) + success.append('compact/summary', summary()) + success.append('compact/end', { turn: 1 }) + + const failed = ctx.sessions.create() + failed.append('compact/start', { turn: 2 }) + failed.append('compact/end', { turn: 2, error: 'provider failed' }) + }) + + it('rebuilds an open trace when the companion loads after the session', async () => { + const ctx = new Context() + await ctx.plugin(SessionStore) + const session = ctx.sessions.create() + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) + session.append('compact/start', { turn: 3 }) + await ctx.plugin(InvariantService) + await ctx.plugin(CompactInvariant) + expect(() => session.append('compact/end', { turn: 3, error: 'resume failed' })).not.toThrow() + session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + }) + + it.each([ + ['summary without start', (session: ReturnType) => { + session.append('compact/summary', summary()) + }, /no matching compact\/start/], + ['nested start', (session: ReturnType) => { + session.append('compact/start', { turn: 1 }) + session.append('compact/start', { turn: 2 }) + }, /still compacting/], + ['repeated summary', (session: ReturnType) => { + session.append('compact/start', { turn: 1 }) + session.append('compact/summary', summary()) + session.append('compact/summary', summary()) + }, /repeated within one compaction/], + ['empty shadow set', (session: ReturnType) => { + session.append('compact/start', { turn: 1 }) + session.append('compact/summary', summary({ shadowedSeqs: [] })) + }, /shadowedSeqs must be non-empty/], + ['wrong endpoints', (session: ReturnType) => { + session.append('compact/start', { turn: 1 }) + session.append('compact/summary', summary({ shadowedRange: { start: 1, end: 4 } })) + }, /shadowedRange must match/], + ['invalid token count', (session: ReturnType) => { + session.append('compact/start', { turn: 1 }) + session.append('compact/summary', summary({ shadowedTokenCount: -1 })) + }, /non-negative safe integer/], + ['end without start', (session: ReturnType) => { + session.append('compact/end', { turn: 1, error: 'failed' }) + }, /no matching compact\/start/], + ['wrong end turn', (session: ReturnType) => { + session.append('compact/start', { turn: 1 }) + session.append('compact/end', { turn: 2, error: 'failed' }) + }, /does not match/], + ['success without summary', (session: ReturnType) => { + session.append('compact/start', { turn: 1 }) + session.append('compact/end', { turn: 1 }) + }, /requires one compact\/summary/], + ])('rejects %s', async (_name, action, message) => { + const ctx = await setup() + expect(() => { action(ctx.sessions.create()) }).toThrow(message) + }) +}) diff --git a/packages/context/time-context/src/invariant.ts b/packages/context/time-context/src/invariant.ts index f1497f4c4a..804d419627 100644 --- a/packages/context/time-context/src/invariant.ts +++ b/packages/context/time-context/src/invariant.ts @@ -1,30 +1,64 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-time-context`. @module @deepseek-ai/dsh-time-context/invariant */ +/** Package-owned durable clock-context invariants. @module @deepseek-ai/dsh-time-context/invariant */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' +import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-time-context' +const SOURCE_NAME = 'time-context' +const READING = new RegExp( + '^Time sampled while preparing turn (\\d+), step (\\d+): ' + + '(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:Z|[+-]\\d{2}:\\d{2})\\[[^\\]]+\\])\\n' + + 'Elapsed since the preceding (model-visible message|step context): ' + + '(?:unavailable|(?:(?:\\d+d )?(?:\\d+h )?(?:\\d+m )?\\d+s))\\.$', +) /** Cordis companion plugin name. */ export const name = 'time-context-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Install checks for this package's active plugin fibers. */ +/** Validate one plugin-attributed time reading against its durable event timestamp. */ +function validateReading(event: SessionEvent<'context/message'>, fail: InvariantFailure): void { + const [block] = event.data.content + if (event.data.content.length !== 1 || block?.type !== 'text') { + fail('time-context messages must contain exactly one text block') + } + const match = READING.exec(block.text) + if (match === null) fail('time-context message does not match the durable reading format') + const turn = Number(match[1]) + const step = Number(match[2]) + if (!Number.isSafeInteger(turn) || turn < 1 || !Number.isSafeInteger(step) || step < 1) { + fail('time-context turn and step must be positive safe integers') + } + const baseline = match[4] + if ((step === 1) !== (baseline === 'model-visible message')) { + fail(`time-context step ${step} uses the wrong elapsed-time baseline ${JSON.stringify(baseline)}`) + } + const rendered = match[3] + /* v8 ignore next -- the preceding fixed regexp always supplies capture group three. */ + if (rendered === undefined) fail('time-context reading omitted its rendered timestamp') + const renderedTime = Date.parse(rendered.replace(/\[[^\]]+\]$/, '')) + if (!Number.isFinite(renderedTime) || !Number.isSafeInteger(event.time) + || event.time < renderedTime || event.time - renderedTime >= 1_000) { + fail('time-context rendered timestamp must identify the durable event second') + } +} + +/** Install validation for plugin-attributed context readings. */ const install: InvariantInstaller = (ctx, fail) => { - observePluginInvariant(ctx, fail, { - name: 'time-context', - inject: [ - 'agents', - ], - effects: [ - 'ctx.on("agent/pre-step")', - ], - }) + ctx.on('internal/dispatch', (_mode, eventName, args) => { + if (eventName !== 'session/event') return + const event = (args as [Session, SessionEvent])[1] + if (event.type !== 'context/message' + || event.data.source.kind !== 'plugin' + || event.data.source.plugin !== SOURCE_NAME) return + validateReading(event, fail) + }, { global: true }) } /** - * Register this package's invariant companion. + * Register the time-context invariant companion. * @param ctx - Cordis context carrying the invariant service. * @returns the installed registration's disposer after setup succeeds. */ diff --git a/packages/context/time-context/tests/invariant.spec.ts b/packages/context/time-context/tests/invariant.spec.ts new file mode 100644 index 0000000000..eab422e95e --- /dev/null +++ b/packages/context/time-context/tests/invariant.spec.ts @@ -0,0 +1,83 @@ +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import type { ContentBlock } from '@deepseek-ai/dsh-llm' +import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' +import * as TimeInvariant from '@deepseek-ai/dsh-time-context/invariant' +import InvariantService from '@deepseek-ai/dsh-invariants' + +const SECOND = Date.parse('2026-07-14T00:00:00Z') + +async function setup(): Promise { + const ctx = new Context() + await ctx.plugin(InvariantService) + await ctx.plugin(TimeInvariant) + return ctx +} + +function event(text: string, time = SECOND + 456, content?: unknown[]): SessionEvent { + return { + type: 'context/message', + seq: 0, + time, + data: { + content: (content ?? [{ type: 'text', text }]) as ContentBlock[], + source: { kind: 'plugin', plugin: 'time-context' }, + }, + } +} + +function reading( + turn = '1', + step = '1', + baseline = 'model-visible message', + timestamp = '2026-07-14T00:00:00+00:00[UTC]', +): string { + return `Time sampled while preparing turn ${turn}, step ${step}: ${timestamp}\n` + + `Elapsed since the preceding ${baseline}: unavailable.` +} + +describe('time-context invariants', () => { + it('accepts a reading whose turn, step, baseline, and timestamp agree', async () => { + const ctx = await setup() + const text = 'Time sampled while preparing turn 2, step 3: 2026-07-14T00:00:00+00:00[UTC]\n' + + 'Elapsed since the preceding step context: 4m 2s.' + expect(() => { ctx.emit('session/event', {} as Session, event(text)) }).not.toThrow() + }) + + it.each([ + ['not a reading', SECOND, undefined, /durable reading format/], + [reading('0'), SECOND, undefined, /positive safe integers/], + [reading('999999999999999999999'), SECOND, undefined, /positive safe integers/], + [reading('1', '0', 'step context'), SECOND, undefined, /positive safe integers/], + [reading('1', '999999999999999999999', 'step context'), SECOND, undefined, /positive safe integers/], + [reading('1', '1', 'step context'), SECOND, undefined, /wrong elapsed-time baseline/], + [reading('1', '2', 'model-visible message'), SECOND, undefined, /wrong elapsed-time baseline/], + [reading('1', '1', 'model-visible message', '2026-99-99T00:00:00+00:00[UTC]'), SECOND, undefined, /durable event second/], + [reading(), Number.NaN, undefined, /durable event second/], + [reading(), SECOND - 1, undefined, /durable event second/], + [reading(), SECOND + 1_000, undefined, /durable event second/], + ['ignored', SECOND, [], /exactly one text block/], + ['ignored', SECOND, [{ type: 'image', data: 'x', mimeType: 'image/png' }], /exactly one text block/], + ['ignored', SECOND, [{ type: 'text', text: 'one' }, { type: 'text', text: 'two' }], /exactly one text block/], + ] as const)('rejects an incoherent durable reading', async (text, time, content, message) => { + const ctx = await setup() + expect(() => { + ctx.emit('session/event', {} as Session, event(text, time, content === undefined ? undefined : [...content])) + }).toThrow(message) + }) + + it('ignores context messages owned by another package', async () => { + const ctx = await setup() + const other = event('unrelated') as SessionEvent<'context/message'> + other.data.source = { kind: 'plugin', plugin: 'other' } + expect(() => { ctx.emit('session/event', {} as Session, other) }).not.toThrow() + other.data.source = { kind: 'user' } + expect(() => { ctx.emit('session/event', {} as Session, other) }).not.toThrow() + expect(() => { + ctx.emit('session/event', {} as Session, { + type: 'turn/start', seq: 0, time: 0, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }, + }) + ctx.emit('tools/change') + }).not.toThrow() + }) +}) diff --git a/packages/context/workspace-context/src/invariant.ts b/packages/context/workspace-context/src/invariant.ts index 0258acff14..d9f56417b8 100644 --- a/packages/context/workspace-context/src/invariant.ts +++ b/packages/context/workspace-context/src/invariant.ts @@ -1,27 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-workspace-context`. @module @deepseek-ai/dsh-workspace-context/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-workspace-context`. + * @module @deepseek-ai/dsh-workspace-context/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-workspace-context' /** Cordis companion plugin name. */ export const name = 'workspace-context-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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")', - ], - }) -} +/** + * No runtime invariant: replay intentionally tolerates unknown or malformed workspace metadata, + * while focused pipeline tests own its private pending/cache state transitions. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -30,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 8d09b3bb9e..6fd73d0353 100644 --- a/packages/cordis/tool-cordis/src/invariant.ts +++ b/packages/cordis/tool-cordis/src/invariant.ts @@ -1,28 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tool-cordis`. @module @deepseek-ai/dsh-tool-cordis/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-tool-cordis`. + * @module @deepseek-ai/dsh-tool-cordis/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tool-cordis' /** Cordis companion plugin name. */ export const name = 'tool-cordis-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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()', - ], - }) -} +/** + * No runtime invariant: this model-facing adapter has no independent lifecycle stream; execution + * relations are owned by the capability seam it calls. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -31,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 36052bb90a..e199cc98b4 100644 --- a/packages/core/system-prompt/src/invariant.ts +++ b/packages/core/system-prompt/src/invariant.ts @@ -1,31 +1,50 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-system-prompt`. @module @deepseek-ai/dsh-system-prompt/invariant */ +/** Package-owned prompt-assembly invariants. @module @deepseek-ai/dsh-system-prompt/invariant */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { PromptAssembly } from './index.ts' const PACKAGE_NAME = '@deepseek-ai/dsh-system-prompt' +const VARIABLE_NAME = /^[a-z][a-z0-9_]*$/ /** Cordis companion plugin name. */ export const name = 'system-prompt-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Install checks for this package's active plugin fibers. */ +/** Validate the authoritative assembly returned by the waterfall. */ +function validateAssembly(assembly: PromptAssembly, fail: InvariantFailure): void { + const sectionNames = new Set() + for (const section of assembly.sections) { + if (section.name.length === 0) fail('assembled section names must be non-empty') + if (sectionNames.has(section.name)) fail(`assembled section name ${JSON.stringify(section.name)} is duplicated`) + sectionNames.add(section.name) + if (typeof section.text !== 'string') fail(`assembled section ${JSON.stringify(section.name)} text must be a string`) + } + + for (const tool of assembly.tools) { + if (tool.name.length === 0) fail('assembled tool names must be non-empty') + } + + for (const [name, value] of Object.entries(assembly.variables)) { + if (!VARIABLE_NAME.test(name)) fail(`assembled variable name ${JSON.stringify(name)} is invalid`) + if (value !== undefined && typeof value !== 'string') { + fail(`assembled variable ${JSON.stringify(name)} must be a string or undefined`) + } + } +} + +/** Install validation around the authoritative assembly waterfall result. */ const install: InvariantInstaller = (ctx, fail) => { - observePluginInvariant(ctx, fail, { - name: 'SystemPrompt', - effects: [ - 'ctx.provide("systemPrompt")', - 'systemPrompt.section()', - ], - services: [ - 'systemPrompt', - ], - }) + ctx.on('system-prompt/assemble', async (_assembly, _context, next) => { + const assembled = await next() + validateAssembly(assembled, fail) + return assembled + }, { global: true, prepend: true }) } /** - * Register this package's invariant companion. + * Register the system-prompt invariant companion. * @param ctx - Cordis context carrying the invariant service. * @returns the installed registration's disposer after setup succeeds. */ diff --git a/packages/core/system-prompt/tests/invariant.spec.ts b/packages/core/system-prompt/tests/invariant.spec.ts new file mode 100644 index 0000000000..ce03af0b9f --- /dev/null +++ b/packages/core/system-prompt/tests/invariant.spec.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import type { PromptAssembly } from '@deepseek-ai/dsh-system-prompt' +import * as SystemPromptInvariant from '@deepseek-ai/dsh-system-prompt/invariant' +import InvariantService from '@deepseek-ai/dsh-invariants' + +async function setup(): Promise { + const ctx = new Context() + await ctx.plugin(InvariantService) + await ctx.plugin(SystemPromptInvariant) + return ctx +} + +const valid = (): PromptAssembly => ({ + sections: [{ name: 'identity', text: 'prompt' }], + tools: [{ name: 'echo', description: 'Echo', parameters: {} }], + variables: { cwd: '/repo', optional: undefined }, +}) + +async function assemble(ctx: Context, result: PromptAssembly): Promise { + return ctx.waterfall( + ctx as never, 'system-prompt/assemble', valid(), {}, + () => Promise.resolve(result), + ) +} + +describe('system-prompt invariants', () => { + it('accepts a well-formed authoritative assembly', async () => { + const ctx = await setup() + await expect(assemble(ctx, valid())).resolves.toEqual(valid()) + }) + + it.each([ + [{ ...valid(), sections: [{ name: '', text: 'x' }] }, /section names must be non-empty/], + [{ ...valid(), sections: [{ name: 'x', text: 'a' }, { name: 'x', text: 'b' }] }, /section name "x" is duplicated/], + [{ ...valid(), sections: [{ name: 'x', text: 1 as never }] }, /section "x" text must be a string/], + [{ ...valid(), tools: [{ name: '', description: 'x', parameters: {} }] }, /tool names must be non-empty/], + [{ ...valid(), variables: { Bad: 'x' } }, /variable name "Bad" is invalid/], + [{ ...valid(), variables: { value: 1 as never } }, /variable "value" must be a string or undefined/], + ])('rejects malformed authoritative assembly %#', async (assembly, message) => { + const ctx = await setup() + await expect(assemble(ctx, assembly)).rejects.toThrow(message) + }) +}) diff --git a/packages/core/tools/src/invariant.ts b/packages/core/tools/src/invariant.ts index c5b53e67fa..2f5f27a281 100644 --- a/packages/core/tools/src/invariant.ts +++ b/packages/core/tools/src/invariant.ts @@ -1,34 +1,67 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tools`. @module @deepseek-ai/dsh-tools/invariant */ +/** Package-owned tool-pipeline invariants. @module @deepseek-ai/dsh-tools/invariant */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { ToolExecution, ToolExecutionResult } from './index.ts' const PACKAGE_NAME = '@deepseek-ai/dsh-tools' /** Cordis companion plugin name. */ export const name = 'tools-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Install checks for this package's active plugin fibers. */ +type ToolStage = 'pre' | 'execute' | 'post' + +/** Validate the immutable final execution/result snapshot. */ +function validateResult( + exec: Readonly, + result: Readonly, + fail: InvariantFailure, +): void { + if (!Object.isFrozen(exec)) fail('tools/result execution must be frozen before publication') + if (!Object.isFrozen(result) || !Object.isFrozen(result.content)) { + fail('tools/result outcome and content must be frozen before publication') + } + if (exec.name.length === 0 || String(exec.callId).length === 0) { + fail('tools/result execution must carry non-empty name and callId') + } +} + +/** Install monotonic pipeline and final-snapshot checks. */ const install: InvariantInstaller = (ctx, fail) => { - observePluginInvariant(ctx, fail, { - name: 'ToolRegistry', - inject: [ - 'systemPrompt', - ], - effects: [ - 'ctx.provide("tools")', - 'systemPrompt.tools()', - ], - services: [ - 'tools', - ], - }) + const stages = new WeakMap() + ctx.on('internal/dispatch', (_mode, eventName, args) => { + if (eventName === 'tools/pre-execute') { + const exec = args[0] as ToolExecution + if (stages.has(exec)) fail('tools/pre-execute repeated for one execution') + stages.set(exec, 'pre') + return + } + if (eventName === 'tools/execute') { + const exec = args[0] as ToolExecution + if (stages.get(exec) !== 'pre') fail('tools/execute must follow tools/pre-execute') + stages.set(exec, 'execute') + return + } + if (eventName === 'tools/post-execute') { + const exec = args[0] as ToolExecution + const previous = stages.get(exec) + if (previous !== 'pre' && previous !== 'execute') { + fail('tools/post-execute must follow tools/pre-execute or tools/execute') + } + stages.set(exec, 'post') + return + } + if (eventName !== 'tools/result') return + const [exec, result] = args as [Readonly, Readonly] + validateResult(exec, result, fail) + stages.delete(exec) + }, { global: true }) } /** - * Register this package's invariant companion. + * Register the tools invariant companion. * @param ctx - Cordis context carrying the invariant service. * @returns the installed registration's disposer after setup succeeds. */ diff --git a/packages/core/tools/tests/invariant.spec.ts b/packages/core/tools/tests/invariant.spec.ts new file mode 100644 index 0000000000..3db0e961ce --- /dev/null +++ b/packages/core/tools/tests/invariant.spec.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import { scopeTarget } from '@deepseek-ai/dsh-scope' +import { CallId } from '@deepseek-ai/dsh-llm' +import type { ToolExecution, ToolExecutionResult, ToolExecutionToken } from '@deepseek-ai/dsh-tools' +import * as ToolsInvariant from '@deepseek-ai/dsh-tools/invariant' +import InvariantService from '@deepseek-ai/dsh-invariants' + +async function setup(): Promise { + const ctx = new Context() + await ctx.plugin(InvariantService) + await ctx.plugin(ToolsInvariant) + return ctx +} + +const execution = (overrides: Partial = {}): ToolExecution => ({ + token: Symbol('tool') as ToolExecutionToken, + callId: CallId('call-1'), + name: 'echo', + arguments: Object.freeze({ text: 'hi' }), + ...overrides, +}) + +const outcome = (): ToolExecutionResult => Object.freeze({ + content: Object.freeze([{ type: 'text' as const, text: 'ok' }]) as never, + isError: false, +}) + +function emitResult(ctx: Context, exec: ToolExecution, result: ToolExecutionResult): void { + ctx.emit(scopeTarget(ctx as never, undefined), 'tools/result', exec, result) +} + +async function stage(ctx: Context, name: 'tools/pre-execute' | 'tools/execute', exec: ToolExecution): Promise { + if (name === 'tools/pre-execute') { + await ctx.waterfall(ctx as never, name, exec, () => Promise.resolve({ kind: 'allow' as const })) + } else { + await ctx.waterfall(ctx as never, name, exec, () => Promise.resolve(outcome())) + } +} + +describe('tool-pipeline invariants', () => { + it('accepts dispatch and denial stage orders with frozen results', async () => { + const ctx = await setup() + const dispatched = execution() + await stage(ctx, 'tools/pre-execute', dispatched) + await stage(ctx, 'tools/execute', dispatched) + await ctx.waterfall(ctx as never, 'tools/post-execute', dispatched, outcome(), () => Promise.resolve({ kind: 'accept' as const })) + Object.freeze(dispatched) + emitResult(ctx, dispatched, outcome()) + + const denied = execution({ callId: CallId('call-2') }) + await stage(ctx, 'tools/pre-execute', denied) + await ctx.waterfall(ctx as never, 'tools/post-execute', denied, outcome(), () => Promise.resolve({ kind: 'accept' as const })) + Object.freeze(denied) + emitResult(ctx, denied, outcome()) + ctx.emit('tools/change') + }) + + it('rejects repeated and out-of-order pipeline stages', async () => { + const ctx = await setup() + const exec = execution() + await stage(ctx, 'tools/pre-execute', exec) + await expect(stage(ctx, 'tools/pre-execute', exec)).rejects.toThrow(/repeated/) + + const noPre = execution({ callId: CallId('call-2') }) + await expect(stage(ctx, 'tools/execute', noPre)).rejects.toThrow(/must follow tools\/pre-execute/) + expect(() => ctx.waterfall( + ctx as never, 'tools/post-execute', noPre, outcome(), + () => Promise.resolve({ kind: 'accept' as const }), + )).toThrow(/must follow tools\/pre-execute or tools\/execute/) + }) + + it('rejects mutable or anonymous final snapshots', async () => { + const ctx = await setup() + expect(() => { emitResult(ctx, execution(), outcome()) }).toThrow(/execution must be frozen/) + + const exec = Object.freeze(execution()) + expect(() => { emitResult(ctx, exec, { content: [], isError: false }) }) + .toThrow(/outcome and content must be frozen/) + + const anonymous = Object.freeze(execution({ name: '' })) + expect(() => { emitResult(ctx, anonymous, outcome()) }).toThrow(/non-empty name and callId/) + }) +}) diff --git a/packages/examples/acp-demo/src/invariant.ts b/packages/examples/acp-demo/src/invariant.ts index b8801dd71e..95b57b57e1 100644 --- a/packages/examples/acp-demo/src/invariant.ts +++ b/packages/examples/acp-demo/src/invariant.ts @@ -1,24 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-acp-demo`. @module @deepseek-ai/dsh-acp-demo/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-acp-demo`. + * @module @deepseek-ai/dsh-acp-demo/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-acp-demo' /** Cordis companion plugin name. */ export const name = 'acp-demo-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Install checks for this package's active plugin fibers. */ -const install: InvariantInstaller = (ctx, fail) => { - observePluginInvariant(ctx, fail, { - name: 'acp-demo', - effects: [ - 'ctx.plugin()', - ], - }) -} +/** + * No runtime invariant: this composition package owns no independent event stream or mutable data; + * Loader and built-entry tests cover its wiring. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -27,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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/src/invariant.ts b/packages/examples/agent-spine-demo/src/invariant.ts index 75ab162567..fada985329 100644 --- a/packages/examples/agent-spine-demo/src/invariant.ts +++ b/packages/examples/agent-spine-demo/src/invariant.ts @@ -1,24 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-agent-spine-demo`. @module @deepseek-ai/dsh-agent-spine-demo/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-agent-spine-demo`. + * @module @deepseek-ai/dsh-agent-spine-demo/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-agent-spine-demo' /** Cordis companion plugin name. */ export const name = 'agent-spine-demo-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Install checks for this package's active plugin fibers. */ -const install: InvariantInstaller = (ctx, fail) => { - observePluginInvariant(ctx, fail, { - name: 'agent-spine-demo', - effects: [ - 'ctx.plugin()', - ], - }) -} +/** + * No runtime invariant: this composition package owns no independent event stream or mutable data; + * Loader and built-entry tests cover its wiring. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -27,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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/src/invariant.ts b/packages/examples/cli-demo/src/invariant.ts index 016c0fd18f..8eb40e9268 100644 --- a/packages/examples/cli-demo/src/invariant.ts +++ b/packages/examples/cli-demo/src/invariant.ts @@ -1,24 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-cli-demo`. @module @deepseek-ai/dsh-cli-demo/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-cli-demo`. + * @module @deepseek-ai/dsh-cli-demo/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-cli-demo' /** Cordis companion plugin name. */ export const name = 'cli-demo-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Install checks for this package's active plugin fibers. */ -const install: InvariantInstaller = (ctx, fail) => { - observePluginInvariant(ctx, fail, { - name: 'cli-demo', - effects: [ - 'ctx.plugin()', - ], - }) -} +/** + * No runtime invariant: this composition package owns no independent event stream or mutable data; + * Loader and built-entry tests cover its wiring. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -27,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) +/* jscpd:ignore-end */ diff --git a/packages/examples/jsonrpc-demo/src/invariant.ts b/packages/examples/jsonrpc-demo/src/invariant.ts index a9352d02f8..dd093a5418 100644 --- a/packages/examples/jsonrpc-demo/src/invariant.ts +++ b/packages/examples/jsonrpc-demo/src/invariant.ts @@ -1,22 +1,24 @@ -/** Package-owned runtime contract for @deepseek-ai/dsh-jsonrpc-demo. @module @deepseek-ai/dsh-jsonrpc-demo/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-jsonrpc-demo`. + * @module @deepseek-ai/dsh-jsonrpc-demo/invariant + */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-jsonrpc-demo' /** Cordis companion plugin name. */ export const name = 'jsonrpc-demo-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Assert that Loader configuration, rather than a hidden root plugin, owns composition. */ -const install: InvariantInstaller = async (_ctx, fail) => { - 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') -} +/** + * No runtime invariant: this composition package owns no independent event stream or mutable data; + * Loader and built-entry tests cover its wiring. + */ +const install: InvariantInstaller = () => {} /** * 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 8e24a3b94b..125d01347c 100644 --- a/packages/examples/stdio-demo/src/invariant.ts +++ b/packages/examples/stdio-demo/src/invariant.ts @@ -1,24 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-stdio-demo`. @module @deepseek-ai/dsh-stdio-demo/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-stdio-demo`. + * @module @deepseek-ai/dsh-stdio-demo/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-stdio-demo' /** Cordis companion plugin name. */ export const name = 'stdio-demo-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Install checks for this package's active plugin fibers. */ -const install: InvariantInstaller = (ctx, fail) => { - observePluginInvariant(ctx, fail, { - name: 'stdio-demo', - effects: [ - 'ctx.plugin()', - ], - }) -} +/** + * No runtime invariant: this composition package owns no independent event stream or mutable data; + * Loader and built-entry tests cover its wiring. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -27,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) +/* jscpd:ignore-end */ diff --git a/packages/fs/fs-local/src/invariant.ts b/packages/fs/fs-local/src/invariant.ts index 6469bffae6..3e38550065 100644 --- a/packages/fs/fs-local/src/invariant.ts +++ b/packages/fs/fs-local/src/invariant.ts @@ -1,27 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-fs-local`. @module @deepseek-ai/dsh-fs-local/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-fs-local`. + * @module @deepseek-ai/dsh-fs-local/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-fs-local' /** Cordis companion plugin name. */ export const name = 'fs-local-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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', - ], - }) -} +/** + * No runtime invariant: this package exposes no independent event sequence or mutable data relation + * beyond contracts enforced at its owning seam. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -30,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 de77ac303d..369fa5ea84 100644 --- a/packages/fs/fs-policy/src/invariant.ts +++ b/packages/fs/fs-policy/src/invariant.ts @@ -1,26 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-fs-policy`. @module @deepseek-ai/dsh-fs-policy/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-fs-policy`. + * @module @deepseek-ai/dsh-fs-policy/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-fs-policy' /** Cordis companion plugin name. */ export const name = 'fs-policy-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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")', - ], - }) -} +/** + * No runtime invariant: this package exposes no independent event sequence or mutable data relation + * beyond contracts enforced at its owning seam. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -29,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 586f962247..429c1cec41 100644 --- a/packages/fs/fs/src/invariant.ts +++ b/packages/fs/fs/src/invariant.ts @@ -1,24 +1,37 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-fs`. @module @deepseek-ai/dsh-fs/invariant */ +/** Package-owned filesystem event-data invariants. @module @deepseek-ai/dsh-fs/invariant */ import type { Context } from 'cordis' -import { observeServiceInvariant, serviceShapeViolation, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { FsTarget, FsVersion } from './types.ts' const PACKAGE_NAME = '@deepseek-ai/dsh-fs' /** Cordis companion plugin name. */ export const name = 'fs-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Validate every implementation bound to this package's service seam. */ +/** Assert that an event carries a usable opaque target identity. */ +function validateTarget(target: FsTarget, fail: (message: string) => never): void { + if (target.targetKey.length === 0) fail('filesystem event targetKey must be non-empty') + if (target.displayPath.length === 0) fail('filesystem event displayPath must be non-empty') +} + +/** Install checks over the filesystem decision and observation event stream. */ const install: InvariantInstaller = (ctx, fail) => { - observeServiceInvariant(ctx, fail, 'fs', value => serviceShapeViolation(value, { - methods: ['resolve', 'stat', 'lstat', 'readText', 'streamText', 'listDir', 'writeText', 'editText'], - })) + ctx.on('internal/dispatch', (_mode, eventName, args) => { + if (eventName !== 'fs/write-intent' + && eventName !== 'fs/edit-intent' + && eventName !== 'fs/observed') return + validateTarget(args[0] as FsTarget, fail) + if (eventName === 'fs/observed' && (args[1] as FsVersion).length === 0) { + fail('fs/observed version must be non-empty') + } + }, { global: true }) } /** - * Register this package's invariant companion. + * Register the filesystem invariant companion. * @param ctx - Cordis context carrying the invariant service. * @returns the installed registration's disposer after setup succeeds. */ diff --git a/packages/fs/fs/tests/invariant.spec.ts b/packages/fs/fs/tests/invariant.spec.ts new file mode 100644 index 0000000000..c160e3d238 --- /dev/null +++ b/packages/fs/fs/tests/invariant.spec.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import { FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs' +import type { FsTarget } from '@deepseek-ai/dsh-fs' +import * as FsInvariant from '@deepseek-ai/dsh-fs/invariant' +import InvariantService from '@deepseek-ai/dsh-invariants' + +async function setup(): Promise { + const ctx = new Context() + await ctx.plugin(InvariantService) + await ctx.plugin(FsInvariant) + return ctx +} + +const target = (key = 'file:1', displayPath = 'file.txt'): FsTarget => ({ + targetKey: FsTargetKey(key), + displayPath, +}) + +describe('filesystem invariants', () => { + it('accepts decision and observation events with usable identities', async () => { + const ctx = await setup() + await expect(ctx.waterfall( + ctx as never, 'fs/write-intent', target(), undefined, + () => Promise.resolve(undefined), + )).resolves.toBeUndefined() + await expect(ctx.waterfall( + ctx as never, 'fs/edit-intent', target(), undefined, + () => Promise.resolve(undefined), + )).resolves.toBeUndefined() + expect(() => { ctx.emit('fs/observed', target(), FsVersion('v1'), undefined) }).not.toThrow() + expect(() => { ctx.emit('tools/change') }).not.toThrow() + }) + + it('rejects empty target and version identities', async () => { + const ctx = await setup() + expect(() => { ctx.emit('fs/observed', target(''), FsVersion('v1'), undefined) }) + .toThrow(/targetKey must be non-empty/) + expect(() => { ctx.emit('fs/observed', target('file:1', ''), FsVersion('v1'), undefined) }) + .toThrow(/displayPath must be non-empty/) + expect(() => { ctx.emit('fs/observed', target(), FsVersion(''), undefined) }) + .toThrow(/version must be non-empty/) + }) +}) diff --git a/packages/fs/tool-fs-search/src/invariant.ts b/packages/fs/tool-fs-search/src/invariant.ts index 65da611560..f7f206896d 100644 --- a/packages/fs/tool-fs-search/src/invariant.ts +++ b/packages/fs/tool-fs-search/src/invariant.ts @@ -1,29 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tool-fs-search`. @module @deepseek-ai/dsh-tool-fs-search/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-tool-fs-search`. + * @module @deepseek-ai/dsh-tool-fs-search/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tool-fs-search' /** Cordis companion plugin name. */ export const name = 'tool-fs-search-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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()', - ], - }) -} +/** + * No runtime invariant: this model-facing adapter has no independent lifecycle stream; execution + * relations are owned by the capability seam it calls. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -32,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 caa31a13b5..eaa2485c06 100644 --- a/packages/fs/tool-fs/src/invariant.ts +++ b/packages/fs/tool-fs/src/invariant.ts @@ -1,29 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tool-fs`. @module @deepseek-ai/dsh-tool-fs/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-tool-fs`. + * @module @deepseek-ai/dsh-tool-fs/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tool-fs' /** Cordis companion plugin name. */ export const name = 'tool-fs-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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()', - ], - }) -} +/** + * No runtime invariant: this model-facing adapter has no independent lifecycle stream; execution + * relations are owned by the capability seam it calls. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -32,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 6bbdd1b539..5d8544b9aa 100644 --- a/packages/guard/repeat-tool-guard/src/invariant.ts +++ b/packages/guard/repeat-tool-guard/src/invariant.ts @@ -1,25 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-repeat-tool-guard`. @module @deepseek-ai/dsh-repeat-tool-guard/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-repeat-tool-guard`. + * @module @deepseek-ai/dsh-repeat-tool-guard/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-repeat-tool-guard' /** Cordis companion plugin name. */ export const name = 'repeat-tool-guard-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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")', - ], - }) -} +/** + * No runtime invariant: the repeat chain is private to one post-execute listener and exposes no + * package-owned event or snapshot that an independent companion can observe. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -28,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 997dd75b30..73d6f08e3b 100644 --- a/packages/hooks/hook-protocol/src/invariant.ts +++ b/packages/hooks/hook-protocol/src/invariant.ts @@ -1,39 +1,98 @@ -/** Package-owned runtime contracts for @deepseek-ai/dsh-hook-protocol. @module @deepseek-ai/dsh-hook-protocol/invariant */ +/** Package-owned hook provenance-stream invariants. @module @deepseek-ai/dsh-hook-protocol/invariant */ -/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' +import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type {} from './types.ts' const PACKAGE_NAME = '@deepseek-ai/dsh-hook-protocol' /** Cordis companion plugin name. */ export const name = 'hook-protocol-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Assert blocking-exit decoding and restrictive merge precedence. */ -const install: InvariantInstaller = async (_ctx, fail) => { - const [{ parseHookOutput }, { mergeHookOutputs }] = await Promise.all([ - import('./codec.ts'), - 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') +interface HookTransition { + key: string + delta: 1 | -1 } +/** Correlation key shared by an invoked/result pair. */ +function hookKey(data: { turn: number; point: string; handlerId: string }): string { + return `${data.turn}\0${data.point}\0${data.handlerId}` +} + +/** Validate one hook event against committed pending invocations. */ +function validateHookEvent( + pending: ReadonlyMap, + event: SessionEvent, + fail: InvariantFailure, +): HookTransition | undefined { + if (event.type === 'hook/invoked') { + if (event.data.point.length === 0 || event.data.handlerId.length === 0) { + fail('hook/invoked point and handlerId must be non-empty') + } + const dialect: string = event.data.dialect + if (dialect !== 'claude' && dialect !== 'codex') { + fail(`hook/invoked carries unknown dialect ${JSON.stringify(dialect)}`) + } + return { key: hookKey(event.data), delta: 1 } + } + if (event.type !== 'hook/result') return undefined + const key = hookKey(event.data) + if ((pending.get(key) ?? 0) === 0) { + fail(`hook/result has no matching hook/invoked for ${JSON.stringify(event.data.handlerId)}`) + } + if (!Number.isFinite(event.data.durationMs) || event.data.durationMs < 0) { + fail('hook/result durationMs must be a non-negative finite number') + } + return { key, delta: -1 } +} + +/** Apply one committed hook-pair transition. */ +function applyHookTransition(pending: Map, transition: HookTransition): void { + const next = (pending.get(transition.key) ?? 0) + transition.delta + if (next === 0) pending.delete(transition.key) + else pending.set(transition.key, next) +} + +/** Install hook invoked/result pairing checks. */ +const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => { + const traces = new WeakMap>() + const staged = new WeakMap() + const seed = (session: Session): Map => { + const pending = new Map() + traces.set(session, pending) + for (const event of session.events) { + const transition = validateHookEvent(pending, event, fail) + if (transition !== undefined) applyHookTransition(pending, transition) + } + return pending + } + const traceFor = (session: Session): Map => traces.get(session) ?? seed(session) + + for (const session of ctx.sessions.list()) seed(session) + ctx.on('session/created', (session) => { seed(session) }, { global: true }) + ctx.on('session/event', (session, event) => { + if (event.type !== 'hook/invoked' && event.type !== 'hook/result') return + const candidate = staged.get(event) + /* v8 ignore next -- internal/dispatch stages every hook provenance event */ + if (candidate === undefined || candidate.session !== session) return fail('hook event published without pre-commit validation') + staged.delete(event) + applyHookTransition(traceFor(session), candidate.transition) + }, { global: true }) + ctx.on('internal/dispatch', (_mode, eventName, args) => { + if (eventName !== 'session/event') return + const [session, event] = args as [Session, SessionEvent] + const transition = validateHookEvent(traceFor(session), event, fail) + if (transition !== undefined) staged.set(event, { session, transition }) + }, { global: true }) +}, { inject: ['sessions'] }) + /** - * Register this package's invariant companion. + * Register the hook-protocol 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 */ diff --git a/packages/hooks/hook-protocol/tests/invariant.spec.ts b/packages/hooks/hook-protocol/tests/invariant.spec.ts new file mode 100644 index 0000000000..dc7b1d38bb --- /dev/null +++ b/packages/hooks/hook-protocol/tests/invariant.spec.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session' +import * as HookInvariant from '@deepseek-ai/dsh-hook-protocol/invariant' +import InvariantService from '@deepseek-ai/dsh-invariants' + +async function setup(): Promise { + const ctx = new Context() + await ctx.plugin(SessionStore) + await ctx.plugin(InvariantService) + await ctx.plugin(HookInvariant) + return ctx +} + +const invoked = (overrides: Record = {}) => ({ + turn: 1, + point: 'PreToolUse', + dialect: 'claude' as const, + handlerId: 'hook-1', + ...overrides, +}) + +const result = (overrides: Record = {}) => ({ + turn: 1, + point: 'PreToolUse', + handlerId: 'hook-1', + decision: 'pass', + durationMs: 3, + ...overrides, +}) + +describe('hook-protocol invariants', () => { + it('pairs serial and repeated handler invocations', async () => { + const ctx = await setup() + const session = ctx.sessions.create() + session.append('hook/invoked', invoked()) + session.append('hook/invoked', invoked()) + session.append('hook/result', result()) + session.append('hook/result', result()) + }) + + it('rebuilds pending hook provenance from an existing session', async () => { + const ctx = new Context() + await ctx.plugin(SessionStore) + const session = ctx.sessions.create() + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) + session.append('hook/invoked', invoked()) + await ctx.plugin(InvariantService) + await ctx.plugin(HookInvariant) + expect(() => session.append('hook/result', result())).not.toThrow() + session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + }) + + it('adopts a bare session first observed through publication', async () => { + const ctx = await setup() + const session = new Session(SessionId('bare-hook-session')) + expect(() => { + ctx.emit('session/event', session, { + type: 'hook/invoked', seq: 0, time: 0, data: invoked(), + }) + ctx.emit('session/event', session, { + type: 'hook/result', seq: 1, time: 1, data: result(), + }) + }).not.toThrow() + }) + + it.each([ + [invoked({ point: '' }), /point and handlerId must be non-empty/], + [invoked({ handlerId: '' }), /point and handlerId must be non-empty/], + [invoked({ dialect: 'other' }), /unknown dialect/], + ])('rejects malformed hook invocation %#', async (data, message) => { + const ctx = await setup() + expect(() => ctx.sessions.create().append('hook/invoked', data as never)).toThrow(message) + }) + + it('rejects unmatched and malformed results', async () => { + const ctx = await setup() + const session = ctx.sessions.create() + expect(() => session.append('hook/result', result())).toThrow(/no matching hook\/invoked/) + session.append('hook/invoked', invoked()) + expect(() => session.append('hook/result', result({ durationMs: -1 }))) + .toThrow(/durationMs must be a non-negative finite number/) + expect(() => session.append('hook/result', result({ point: 'Stop' }))) + .toThrow(/no matching hook\/invoked/) + }) +}) diff --git a/packages/hooks/hooks-claude/src/invariant.ts b/packages/hooks/hooks-claude/src/invariant.ts index d092d78a00..18bc942e9f 100644 --- a/packages/hooks/hooks-claude/src/invariant.ts +++ b/packages/hooks/hooks-claude/src/invariant.ts @@ -1,40 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-hooks-claude`. @module @deepseek-ai/dsh-hooks-claude/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-hooks-claude`. + * @module @deepseek-ai/dsh-hooks-claude/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-hooks-claude' /** Cordis companion plugin name. */ export const name = 'hooks-claude-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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' - }, - }) -} +/** + * No runtime invariant: this bridge publishes hook-protocol session events, whose companion owns + * their cross-event provenance relation. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -43,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 deleted file mode 100644 index 1f7513cc64..0000000000 --- a/packages/hooks/hooks-claude/tests/invariant.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -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 4fba6cea2d..5f0f6ed173 100644 --- a/packages/hooks/hooks-codex/src/invariant.ts +++ b/packages/hooks/hooks-codex/src/invariant.ts @@ -1,38 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-hooks-codex`. @module @deepseek-ai/dsh-hooks-codex/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-hooks-codex`. + * @module @deepseek-ai/dsh-hooks-codex/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-hooks-codex' /** Cordis companion plugin name. */ export const name = 'hooks-codex-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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' - }, - }) -} +/** + * No runtime invariant: this bridge publishes hook-protocol session events, whose companion owns + * their cross-event provenance relation. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -41,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 deleted file mode 100644 index bd6ee595f9..0000000000 --- a/packages/hooks/hooks-codex/tests/invariant.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -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 3327cec448..dd2df6e99c 100644 --- a/packages/llm/llm-deepseek/src/invariant.ts +++ b/packages/llm/llm-deepseek/src/invariant.ts @@ -1,27 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-llm-deepseek`. @module @deepseek-ai/dsh-llm-deepseek/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-llm-deepseek`. + * @module @deepseek-ai/dsh-llm-deepseek/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-llm-deepseek' /** Cordis companion plugin name. */ export const name = 'llm-deepseek-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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()', - ], - }) -} +/** + * No runtime invariant: this package exposes no independent event sequence or mutable data relation + * beyond contracts enforced at its owning seam. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -30,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 ba546fb902..a096804fd2 100644 --- a/packages/llm/llm-pi-ai/src/invariant.ts +++ b/packages/llm/llm-pi-ai/src/invariant.ts @@ -1,27 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-llm-pi-ai`. @module @deepseek-ai/dsh-llm-pi-ai/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-llm-pi-ai`. + * @module @deepseek-ai/dsh-llm-pi-ai/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-llm-pi-ai' /** Cordis companion plugin name. */ export const name = 'llm-pi-ai-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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()', - ], - }) -} +/** + * No runtime invariant: this package exposes no independent event sequence or mutable data relation + * beyond contracts enforced at its owning seam. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -30,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 a4b8b50a3f..76d55509cb 100644 --- a/packages/llm/llm/src/invariant.ts +++ b/packages/llm/llm/src/invariant.ts @@ -1,30 +1,93 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-llm`. @module @deepseek-ai/dsh-llm/invariant */ +/** Package-owned LLM stream-protocol invariants. @module @deepseek-ai/dsh-llm/invariant */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { ContentBlockType, StreamChunk } from './types.ts' const PACKAGE_NAME = '@deepseek-ai/dsh-llm' /** Cordis companion plugin name. */ export const name = 'llm-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Install checks for this package's active plugin fibers. */ +/** Require one chunk index to be a non-negative safe integer. */ +function validateIndex(index: number, fail: InvariantFailure): void { + if (!Number.isSafeInteger(index) || index < 0) { + fail(`LLM stream block index must be a non-negative safe integer, got ${index}`) + } +} + +/** Require a delta to address an open block of its matching type. */ +function validateDelta( + open: ReadonlyMap, + index: number, + expected: ContentBlockType, + fail: InvariantFailure, +): void { + validateIndex(index, fail) + const actual = open.get(index) + if (actual !== expected) { + fail(`${expected} delta at index ${index} requires an open ${expected} block, got ${String(actual)}`) + } +} + +/** Wrap one provider stream and enforce its grammar as chunks are consumed. */ +async function* validateStream( + source: AsyncIterable, + fail: InvariantFailure, +): AsyncIterable { + const open = new Map() + let usageSeen = false + let finished = false + for await (const chunk of source) { + if (finished) fail(`LLM stream emitted ${chunk.type} after terminal finish`) + switch (chunk.type) { + case 'block-start': + validateIndex(chunk.index, fail) + if (open.has(chunk.index)) fail(`LLM stream repeated block-start index ${chunk.index}`) + open.set(chunk.index, chunk.blockType) + break + case 'text-delta': + validateDelta(open, chunk.index, 'text', fail) + break + case 'reasoning-delta': + validateDelta(open, chunk.index, 'reasoning', fail) + break + case 'tool-call-delta': + validateDelta(open, chunk.index, 'tool-call', fail) + break + case 'block-end': { + validateIndex(chunk.index, fail) + const blockType = open.get(chunk.index) + if (blockType === undefined) fail(`LLM stream block-end index ${chunk.index} has no open block`) + if (chunk.block.type !== blockType) { + fail(`LLM stream block-end index ${chunk.index} closes ${chunk.block.type}, expected ${blockType}`) + } + open.delete(chunk.index) + break + } + case 'usage': + if (usageSeen) fail('LLM stream emitted usage more than once') + usageSeen = true + break + case 'finish': + if (open.size > 0) fail(`LLM stream finished with ${open.size} open block(s)`) + finished = true + break + } + yield chunk + } + if (!finished) fail('LLM stream ended without a terminal finish chunk') +} + +/** Install validation around every provider stream. */ const install: InvariantInstaller = (ctx, fail) => { - observePluginInvariant(ctx, fail, { - name: 'LlmService', - effects: [ - 'ctx.provide("llm")', - ], - services: [ - 'llm', - ], - }) + ctx.on('llm/stream', (_options, next) => validateStream(next(), fail), { global: true, prepend: true }) } /** - * Register this package's invariant companion. + * Register the LLM invariant companion. * @param ctx - Cordis context carrying the invariant service. * @returns the installed registration's disposer after setup succeeds. */ diff --git a/packages/llm/llm/tests/invariant.spec.ts b/packages/llm/llm/tests/invariant.spec.ts new file mode 100644 index 0000000000..9eb868df1c --- /dev/null +++ b/packages/llm/llm/tests/invariant.spec.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import { CallId } from '@deepseek-ai/dsh-llm' +import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' +import * as LlmInvariant from '@deepseek-ai/dsh-llm/invariant' +import InvariantService from '@deepseek-ai/dsh-invariants' + +async function setup(): Promise { + const ctx = new Context() + await ctx.plugin(InvariantService) + await ctx.plugin(LlmInvariant) + return ctx +} + +const options: GenerateOptions = { provider: 'mock', model: 'mock', messages: [] } + +async function* source(chunks: readonly StreamChunk[]): AsyncIterable { + yield* chunks +} + +async function consume(ctx: Context, chunks: readonly StreamChunk[]): Promise { + const stream = ctx.waterfall(ctx as never, 'llm/stream', options, () => source(chunks)) + const consumed: StreamChunk[] = [] + for await (const chunk of stream) consumed.push(chunk) + return consumed +} + +const finish: StreamChunk = { type: 'finish', reason: { kind: 'stop' } } + +describe('LLM stream invariants', () => { + it('accepts a complete interleaved stream grammar', async () => { + const ctx = await setup() + const chunks: StreamChunk[] = [ + { type: 'block-start', index: 0, blockType: 'text' }, + { type: 'text-delta', index: 0, text: 'a' }, + { type: 'block-start', index: 1, blockType: 'reasoning' }, + { type: 'reasoning-delta', index: 1, text: 'b' }, + { type: 'block-end', index: 1, block: { type: 'reasoning', text: 'b' } }, + { type: 'block-end', index: 0, block: { type: 'text', text: 'a' } }, + { type: 'block-start', index: 2, blockType: 'tool-call' }, + { type: 'tool-call-delta', index: 2, id: CallId('c1'), name: 'echo', argumentsDelta: '{}' }, + { type: 'block-end', index: 2, block: { type: 'tool-call', id: CallId('c1'), name: 'echo', arguments: '{}' } }, + { type: 'usage', usage: { inputTokens: 1, outputTokens: 1 } }, + finish, + ] + await expect(consume(ctx, chunks)).resolves.toEqual(chunks) + }) + + it.each([ + [[{ type: 'block-start', index: -1, blockType: 'text' }, finish], /non-negative safe integer/], + [[ + { type: 'block-start', index: 0, blockType: 'text' }, + { type: 'block-start', index: 0, blockType: 'text' }, + ], /repeated block-start/], + [[{ type: 'text-delta', index: 0, text: 'x' }], /requires an open text block/], + [[ + { type: 'block-start', index: 0, blockType: 'reasoning' }, + { type: 'text-delta', index: 0, text: 'x' }, + ], /got reasoning/], + [[{ type: 'block-end', index: 0, block: { type: 'text', text: '' } }], /has no open block/], + [[ + { type: 'block-start', index: 0, blockType: 'text' }, + { type: 'block-end', index: 0, block: { type: 'reasoning', text: '' } }, + ], /closes reasoning, expected text/], + [[ + { type: 'usage', usage: { inputTokens: 1, outputTokens: 1 } }, + { type: 'usage', usage: { inputTokens: 1, outputTokens: 1 } }, + ], /usage more than once/], + [[{ type: 'block-start', index: 0, blockType: 'text' }, finish], /finished with 1 open block/], + [[finish, { type: 'usage', usage: { inputTokens: 1, outputTokens: 1 } }], /usage after terminal finish/], + [[], /ended without a terminal finish/], + ] as Array<[StreamChunk[], RegExp]>)('rejects malformed stream %#', async (chunks, message) => { + const ctx = await setup() + await expect(consume(ctx, chunks)).rejects.toThrow(message) + }) + + it('preserves provider exceptions without inventing a missing-finish failure', async () => { + const ctx = await setup() + const stream = ctx.waterfall(ctx as never, 'llm/stream', options, async function* () { + throw new Error('provider failed') + }) + await expect((async () => { + for await (const _chunk of stream) { /* consume */ } + })()).rejects.toThrow('provider failed') + }) +}) diff --git a/packages/llm/llm/tests/service.spec.ts b/packages/llm/llm/tests/service.spec.ts index 6e14d749ba..9a4345a4ad 100644 --- a/packages/llm/llm/tests/service.spec.ts +++ b/packages/llm/llm/tests/service.spec.ts @@ -60,6 +60,7 @@ class CatalogAdapter extends ScriptedAdapter { const SCRIPT: StreamChunk[] = [ { type: 'block-start', index: 0, blockType: 'text' }, { type: 'text-delta', index: 0, text: 'hi' }, + { type: 'block-end', index: 0, block: { type: 'text', text: 'hi' } }, { type: 'finish', reason: { kind: 'stop' } }, ] @@ -489,13 +490,14 @@ describe('LlmService', () => { const inner = next() return (async function * () { yield { type: 'block-start', index: 99, blockType: 'text' } satisfies StreamChunk + yield { type: 'block-end', index: 99, block: { type: 'text', text: '' } } satisfies StreamChunk yield * inner })() }) const chunks: StreamChunk[] = [] for await (const chunk of ctx.llm.stream({ provider: 'test-model', model: 'dynamic-model', messages: [] })) chunks.push(chunk) - expect(chunks).toHaveLength(4) + expect(chunks).toHaveLength(6) expect(chunks[0]).toMatchObject({ index: 99 }) }) diff --git a/packages/llm/token-meter/src/invariant.ts b/packages/llm/token-meter/src/invariant.ts index 838a9409e8..b8f0cc385a 100644 --- a/packages/llm/token-meter/src/invariant.ts +++ b/packages/llm/token-meter/src/invariant.ts @@ -1,28 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-token-meter`. @module @deepseek-ai/dsh-token-meter/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-token-meter`. + * @module @deepseek-ai/dsh-token-meter/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-token-meter' /** Cordis companion plugin name. */ export const name = 'token-meter-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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', - ], - }) -} +/** + * No runtime invariant: token estimates are per-call outputs and the private session cache is + * invalidated at its event mutation boundary; neither exposes an independent observation stream. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -31,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 3cb9dd6005..e2d8ac22cb 100644 --- a/packages/mcp/mcp-client/src/invariant.ts +++ b/packages/mcp/mcp-client/src/invariant.ts @@ -1,28 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-mcp-client`. @module @deepseek-ai/dsh-mcp-client/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-mcp-client`. + * @module @deepseek-ai/dsh-mcp-client/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-mcp-client' /** Cordis companion plugin name. */ export const name = 'mcp-client-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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', - ], - }) -} +/** + * No runtime invariant: MCP generations contribute through the tool registry, but the bridge + * exposes no independent server-to-tool snapshot after an asynchronous resync. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -31,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 da6897f755..e990d46acc 100644 --- a/packages/sandbox/sandbox-local/src/invariant.ts +++ b/packages/sandbox/sandbox-local/src/invariant.ts @@ -1,27 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-sandbox-local`. @module @deepseek-ai/dsh-sandbox-local/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-sandbox-local`. + * @module @deepseek-ai/dsh-sandbox-local/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-sandbox-local' /** Cordis companion plugin name. */ export const name = 'sandbox-local-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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', - ], - }) -} +/** + * No runtime invariant: this package exposes no independent event sequence or mutable data relation + * beyond contracts enforced at its owning seam. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -30,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 cd1f767df0..7ee5be733f 100644 --- a/packages/sandbox/sandbox/src/invariant.ts +++ b/packages/sandbox/sandbox/src/invariant.ts @@ -1,21 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-sandbox`. @module @deepseek-ai/dsh-sandbox/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-sandbox`. + * @module @deepseek-ai/dsh-sandbox/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observeServiceInvariant, serviceShapeViolation, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-sandbox' /** Cordis companion plugin name. */ export const name = 'sandbox-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Validate every implementation bound to this package's service seam. */ -const install: InvariantInstaller = (ctx, fail) => { - observeServiceInvariant(ctx, fail, 'sandbox', value => serviceShapeViolation(value, { - methods: ['confine'], - })) -} +/** + * No runtime invariant: this package exposes no independent event sequence or mutable data relation + * beyond contracts enforced at its owning seam. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -24,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) +/* jscpd:ignore-end */ diff --git a/packages/sandbox/sandbox/tests/invariant.spec.ts b/packages/sandbox/sandbox/tests/invariant.spec.ts deleted file mode 100644 index 24854b56ae..0000000000 --- a/packages/sandbox/sandbox/tests/invariant.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { Context } from 'cordis' -import { InvariantError } from '@deepseek-ai/dsh-invariants' -import { SandboxProvider } from '@deepseek-ai/dsh-sandbox' -import type { ConfinedArgv, SandboxPolicy } from '@deepseek-ai/dsh-sandbox' - -class StubSandboxProvider extends SandboxProvider { - confine(argv: readonly string[], _policy: SandboxPolicy): ConfinedArgv { - return { - argv: [...argv], - enforcement: 'full', - denialSignatures: [], - runnerFailureSignatures: [], - } - } -} - -describe('sandbox package invariant', () => { - it('accepts a provider that exposes the confinement seam', async () => { - const ctx = new Context() - await ctx.plugin(StubSandboxProvider) - expect(ctx.sandbox).toBeInstanceOf(StubSandboxProvider) - }) - - it('rejects a service binding without confine()', async () => { - const ctx = new Context() - const invalidSandbox = { - name: 'invalid-sandbox', - apply(child: Context) { - child.provide('sandbox', {} as SandboxProvider) - }, - } - let caught: unknown - try { - await ctx.plugin(invalidSandbox) - } catch (error) { - caught = error - } - expect(caught).toBeInstanceOf(InvariantError) - expect(caught).toHaveProperty('packageName', '@deepseek-ai/dsh-sandbox') - expect((caught as Error).message).toMatch(/must expose method "confine"/) - }) -}) diff --git a/packages/sdk/create-sdk/src/invariant.ts b/packages/sdk/create-sdk/src/invariant.ts index 417d936128..368b46fc70 100644 --- a/packages/sdk/create-sdk/src/invariant.ts +++ b/packages/sdk/create-sdk/src/invariant.ts @@ -1,35 +1,24 @@ -/** Package-owned runtime contracts for @deepseek-ai/create-sdk. @module @deepseek-ai/create-sdk/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/create-sdk`. + * @module @deepseek-ai/create-sdk/invariant + */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/create-sdk' /** Cordis companion plugin name. */ export const name = 'create-sdk-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Assert the bin-only entrypoint and its core argument mapping. */ -const install: InvariantInstaller = async (_ctx, fail) => { - const [{ parseCreateArgs }, packageEntry] = await Promise.all([ - import('./args.ts'), - 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') -} +/** + * No runtime invariant: this SDK build-time package owns no live event stream or mutable data; + * generated output and consumer tests cover its contract. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. diff --git a/packages/sdk/helper/src/invariant.ts b/packages/sdk/helper/src/invariant.ts index 5faf1ea49c..9185ac8867 100644 --- a/packages/sdk/helper/src/invariant.ts +++ b/packages/sdk/helper/src/invariant.ts @@ -1,29 +1,24 @@ -/** Package-owned runtime contracts for @deepseek-ai/dsh-helper. @module @deepseek-ai/dsh-helper/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-helper`. + * @module @deepseek-ai/dsh-helper/invariant + */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-helper' /** Cordis companion plugin name. */ export const name = 'helper-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Assert FeatureId's zero-cost representation and boundary validation. */ -const install: InvariantInstaller = async (_ctx, fail) => { - 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') -} +/** + * No runtime invariant: this SDK build-time package owns no live event stream or mutable data; + * generated output and consumer tests cover its contract. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. diff --git a/packages/sdk/scripts/src/invariant.ts b/packages/sdk/scripts/src/invariant.ts index 522791fe3b..72e97f3628 100644 --- a/packages/sdk/scripts/src/invariant.ts +++ b/packages/sdk/scripts/src/invariant.ts @@ -1,31 +1,24 @@ -/** Package-owned runtime contracts for @deepseek-ai/dsh-scripts. @module @deepseek-ai/dsh-scripts/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-scripts`. + * @module @deepseek-ai/dsh-scripts/invariant + */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-scripts' /** Cordis companion plugin name. */ export const name = 'scripts-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Assert the launcher's opaque post-separator forwarding boundary. */ -const install: InvariantInstaller = async (_ctx, fail) => { - 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') -} +/** + * No runtime invariant: this SDK build-time package owns no live event stream or mutable data; + * generated output and consumer tests cover its contract. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. diff --git a/packages/sdk/telemetry/src/invariant.ts b/packages/sdk/telemetry/src/invariant.ts index 9ec704b672..c3676a1384 100644 --- a/packages/sdk/telemetry/src/invariant.ts +++ b/packages/sdk/telemetry/src/invariant.ts @@ -1,28 +1,24 @@ -/** Package-owned runtime contracts for @deepseek-ai/dsh-telemetry. @module @deepseek-ai/dsh-telemetry/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-telemetry`. + * @module @deepseek-ai/dsh-telemetry/invariant + */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-telemetry' /** Cordis companion plugin name. */ export const name = 'telemetry-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Assert the final telemetry redaction boundary removes secrets without corrupting ordinary package metadata. */ -const install: InvariantInstaller = async (_ctx, fail) => { - const [{ telemetryRedactionViolation }, { DEFAULT_REDACTION_PLACEHOLDER, SecretRedactor }] = await Promise.all([ - import('./redaction-contract.ts'), - import('./secret-redactor.ts'), - ]) - const redactor = new SecretRedactor() - const violation = telemetryRedactionViolation(redactor, DEFAULT_REDACTION_PLACEHOLDER, PACKAGE_NAME) - assertInvariant(fail, - violation === undefined, - violation ?? 'telemetry redaction contract failed without a diagnostic') -} +/** + * No runtime invariant: this SDK build-time package owns no live event stream or mutable data; + * generated output and consumer tests cover its contract. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. diff --git a/packages/session-persistence/session-persistence-jsonl/src/invariant.ts b/packages/session-persistence/session-persistence-jsonl/src/invariant.ts index a3f3854cff..94d7c2b494 100644 --- a/packages/session-persistence/session-persistence-jsonl/src/invariant.ts +++ b/packages/session-persistence/session-persistence-jsonl/src/invariant.ts @@ -1,33 +1,24 @@ /** - * Package-owned runtime contract checks for `@deepseek-ai/dsh-session-persistence-jsonl`. + * Package-owned invariant companion for `@deepseek-ai/dsh-session-persistence-jsonl`. * @module @deepseek-ai/dsh-session-persistence-jsonl/invariant */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-session-persistence-jsonl' /** Cordis companion plugin name. */ export const name = 'session-persistence-jsonl-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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', - ], - }) -} +/** + * No runtime invariant: persistence correctness requires backend round-trip and crash-tail tests; + * this package exposes no continuously observable in-process relation. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -36,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 8eb41c061b..9d841a053d 100644 --- a/packages/session-persistence/session-persistence-sqlite/src/invariant.ts +++ b/packages/session-persistence/session-persistence-sqlite/src/invariant.ts @@ -1,33 +1,24 @@ /** - * Package-owned runtime contract checks for `@deepseek-ai/dsh-session-persistence-sqlite`. + * Package-owned invariant companion for `@deepseek-ai/dsh-session-persistence-sqlite`. * @module @deepseek-ai/dsh-session-persistence-sqlite/invariant */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-session-persistence-sqlite' /** Cordis companion plugin name. */ export const name = 'session-persistence-sqlite-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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', - ], - }) -} +/** + * No runtime invariant: persistence correctness requires backend round-trip and crash-tail tests; + * this package exposes no continuously observable in-process relation. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -36,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 c62805d300..316774f3fd 100644 --- a/packages/session-persistence/session-persistence/src/invariant.ts +++ b/packages/session-persistence/session-persistence/src/invariant.ts @@ -1,24 +1,24 @@ /** - * Package-owned runtime contract checks for `@deepseek-ai/dsh-session-persistence`. + * Package-owned invariant companion for `@deepseek-ai/dsh-session-persistence`. * @module @deepseek-ai/dsh-session-persistence/invariant */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observeServiceInvariant, serviceShapeViolation, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-session-persistence' /** Cordis companion plugin name. */ export const name = 'session-persistence-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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'], - })) -} +/** + * No runtime invariant: persistence correctness requires backend round-trip and crash-tail tests; + * this package exposes no continuously observable in-process relation. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -27,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 0f7a93acde..d087dd2378 100644 --- a/packages/session-query/session-query/src/invariant.ts +++ b/packages/session-query/session-query/src/invariant.ts @@ -1,30 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-session-query`. @module @deepseek-ai/dsh-session-query/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-session-query`. + * @module @deepseek-ai/dsh-session-query/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-session-query' /** Cordis companion plugin name. */ export const name = 'session-query-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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', - ], - }) -} +/** + * No runtime invariant: query results are immutable per-call projections whose lineage and event + * relations are validated while they are built; the service retains no observable result state. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -33,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 7afa8c5023..6d4917a4d9 100644 --- a/packages/skill/skill-local/src/invariant.ts +++ b/packages/skill/skill-local/src/invariant.ts @@ -1,27 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-skill-local`. @module @deepseek-ai/dsh-skill-local/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-skill-local`. + * @module @deepseek-ai/dsh-skill-local/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-skill-local' /** Cordis companion plugin name. */ export const name = 'skill-local-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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()', - ], - }) -} +/** + * No runtime invariant: this package exposes no independent event sequence or mutable data relation + * beyond contracts enforced at its owning seam. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -30,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 6a673db92a..5145dee6da 100644 --- a/packages/skill/skill/src/invariant.ts +++ b/packages/skill/skill/src/invariant.ts @@ -1,27 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-skill`. @module @deepseek-ai/dsh-skill/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-skill`. + * @module @deepseek-ai/dsh-skill/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-skill' /** Cordis companion plugin name. */ export const name = 'skill-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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', - ], - }) -} +/** + * No runtime invariant: provider/runtime maps and revisioned caches mutate atomically inside the + * registry, which exposes no independent change event or snapshot for cross-checking them. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -30,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 8e6d6bbc3c..68d70fa2d2 100644 --- a/packages/skill/tool-skill/src/invariant.ts +++ b/packages/skill/tool-skill/src/invariant.ts @@ -1,29 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tool-skill`. @module @deepseek-ai/dsh-tool-skill/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-tool-skill`. + * @module @deepseek-ai/dsh-tool-skill/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tool-skill' /** Cordis companion plugin name. */ export const name = 'tool-skill-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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")', - ], - }) -} +/** + * No runtime invariant: this model-facing adapter has no independent lifecycle stream; execution + * relations are owned by the capability seam it calls. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -32,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 b769007d7d..4b44ddbebf 100644 --- a/packages/spill/spill-local/src/invariant.ts +++ b/packages/spill/spill-local/src/invariant.ts @@ -1,27 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-spill-local`. @module @deepseek-ai/dsh-spill-local/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-spill-local`. + * @module @deepseek-ai/dsh-spill-local/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-spill-local' /** Cordis companion plugin name. */ export const name = 'spill-local-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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', - ], - }) -} +/** + * No runtime invariant: this package exposes no independent event sequence or mutable data relation + * beyond contracts enforced at its owning seam. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -30,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 4af3a81f70..82a4bee211 100644 --- a/packages/spill/spill-policy/src/invariant.ts +++ b/packages/spill/spill-policy/src/invariant.ts @@ -1,31 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-spill-policy`. @module @deepseek-ai/dsh-spill-policy/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-spill-policy`. + * @module @deepseek-ai/dsh-spill-policy/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-spill-policy' /** Cordis companion plugin name. */ export const name = 'spill-policy-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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' - }, - }) -} +/** + * No runtime invariant: this package exposes no independent event sequence or mutable data relation + * beyond contracts enforced at its owning seam. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -34,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) +/* jscpd:ignore-end */ diff --git a/packages/spill/spill/src/invariant.ts b/packages/spill/spill/src/invariant.ts index ba971b8bc2..5011ac1d52 100644 --- a/packages/spill/spill/src/invariant.ts +++ b/packages/spill/spill/src/invariant.ts @@ -1,21 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-spill`. @module @deepseek-ai/dsh-spill/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-spill`. + * @module @deepseek-ai/dsh-spill/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observeServiceInvariant, serviceShapeViolation, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-spill' /** Cordis companion plugin name. */ export const name = 'spill-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Validate every implementation bound to this package's service seam. */ -const install: InvariantInstaller = (ctx, fail) => { - observeServiceInvariant(ctx, fail, 'spillStore', value => serviceShapeViolation(value, { - methods: ['saveText'], - })) -} +/** + * No runtime invariant: this package exposes no independent event sequence or mutable data relation + * beyond contracts enforced at its owning seam. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -24,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 0c17843ff7..85c1601348 100644 --- a/packages/subagent/subagent-acp/src/invariant.ts +++ b/packages/subagent/subagent-acp/src/invariant.ts @@ -1,27 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-subagent-acp`. @module @deepseek-ai/dsh-subagent-acp/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-subagent-acp`. + * @module @deepseek-ai/dsh-subagent-acp/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-acp' /** Cordis companion plugin name. */ export const name = 'subagent-acp-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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()', - ], - }) -} +/** + * No runtime invariant: this package exposes no independent event sequence or mutable data relation + * beyond contracts enforced at its owning seam. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -30,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 2fc8a0f696..e3d65701b1 100644 --- a/packages/subagent/subagent-fork/src/invariant.ts +++ b/packages/subagent/subagent-fork/src/invariant.ts @@ -1,27 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-subagent-fork`. @module @deepseek-ai/dsh-subagent-fork/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-subagent-fork`. + * @module @deepseek-ai/dsh-subagent-fork/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-fork' /** Cordis companion plugin name. */ export const name = 'subagent-fork-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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()', - ], - }) -} +/** + * No runtime invariant: this package exposes no independent event sequence or mutable data relation + * beyond contracts enforced at its owning seam. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -30,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 caaf598f55..7b8bfc36e2 100644 --- a/packages/subagent/subagent-inprocess/src/invariant.ts +++ b/packages/subagent/subagent-inprocess/src/invariant.ts @@ -1,24 +1,24 @@ -/** Package-owned runtime contracts for @deepseek-ai/dsh-subagent-inprocess. @module @deepseek-ai/dsh-subagent-inprocess/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-subagent-inprocess`. + * @module @deepseek-ai/dsh-subagent-inprocess/invariant + */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-inprocess' /** Cordis companion plugin name. */ export const name = 'subagent-inprocess-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Assert that structured-output guidance names the tool it actually installs. */ -const install: InvariantInstaller = async (_ctx, fail) => { - 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') -} +/** + * No runtime invariant: this package exposes no independent event sequence or mutable data relation + * beyond contracts enforced at its owning seam. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. diff --git a/packages/subagent/subagent-spawn/src/invariant.ts b/packages/subagent/subagent-spawn/src/invariant.ts index 28201593e4..0ba0182f9f 100644 --- a/packages/subagent/subagent-spawn/src/invariant.ts +++ b/packages/subagent/subagent-spawn/src/invariant.ts @@ -1,27 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-subagent-spawn`. @module @deepseek-ai/dsh-subagent-spawn/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-subagent-spawn`. + * @module @deepseek-ai/dsh-subagent-spawn/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-spawn' /** Cordis companion plugin name. */ export const name = 'subagent-spawn-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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()', - ], - }) -} +/** + * No runtime invariant: this package exposes no independent event sequence or mutable data relation + * beyond contracts enforced at its owning seam. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -30,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 9eed9ae9e3..c273ce5209 100644 --- a/packages/subagent/subagent-subprocess/src/invariant.ts +++ b/packages/subagent/subagent-subprocess/src/invariant.ts @@ -1,37 +1,24 @@ -/** Package-owned runtime contracts for @deepseek-ai/dsh-subagent-subprocess. @module @deepseek-ai/dsh-subagent-subprocess/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-subagent-subprocess`. + * @module @deepseek-ai/dsh-subagent-subprocess/invariant + */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import 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. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Assert ambient credential scrubbing and explicit credential precedence. */ -const install: InvariantInstaller = async (_ctx, fail) => { - const { buildChildEnv } = await import('./index.ts') - const ambientProbe = `DSH_INVARIANT_AMBIENT_TOKEN_${process.pid}` - assertInvariant(fail, SENSITIVE_ENV_PATTERN.test(ambientProbe), - 'the invariant ambient probe must remain credential-shaped') - process.env[ambientProbe] = 'must-not-reach-child' - let scrubbed: NodeJS.ProcessEnv - try { - scrubbed = buildChildEnv({}) - } finally { - Reflect.deleteProperty(process.env, ambientProbe) - } - assertInvariant(fail, !Object.hasOwn(scrubbed, ambientProbe), - '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') -} +/** + * No runtime invariant: this package exposes no independent event sequence or mutable data relation + * beyond contracts enforced at its owning seam. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. diff --git a/packages/subagent/subagent/src/invariant.ts b/packages/subagent/subagent/src/invariant.ts index 3217a3dd9a..3c350c13a1 100644 --- a/packages/subagent/subagent/src/invariant.ts +++ b/packages/subagent/subagent/src/invariant.ts @@ -1,30 +1,89 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-subagent`. @module @deepseek-ai/dsh-subagent/invariant */ +/** Package-owned subagent registry and lifecycle invariants. @module @deepseek-ai/dsh-subagent/invariant */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { SubagentProvider } from './types.ts' +import type { SubagentRunEndInfo, SubagentRunInfo } from './index.ts' const PACKAGE_NAME = '@deepseek-ai/dsh-subagent' /** Cordis companion plugin name. */ export const name = 'subagent-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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', - ], - }) +/** Assert that a terminal lifecycle payload matches its start identity. */ +function validateRunEnd(start: SubagentRunInfo, end: SubagentRunEndInfo, fail: InvariantFailure): void { + if (start.provider !== end.provider || start.id !== end.id || start.local !== end.local) { + fail(`subagent/end identity diverges from subagent/start for run ${JSON.stringify(end.runId)}`) + } } +/** Install provider-registry and start/end pairing checks. */ +const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => { + const providers = new Set(ctx.subagents.list()) + const runs = new Map() + const stagedProviders = new WeakSet() + const stagedRemovals = new Set() + const stagedStarts = new WeakSet() + const stagedEnds = new WeakSet() + + ctx.on('internal/dispatch', (_mode, eventName, args) => { + if (eventName === 'subagent/provider-added') { + const provider = args[0] as SubagentProvider + if (provider.name.length === 0) fail('subagent provider names must be non-empty') + if (providers.has(provider.name)) fail(`subagent/provider-added repeated ${JSON.stringify(provider.name)}`) + stagedProviders.add(provider) + return + } + if (eventName === 'subagent/provider-removed') { + const providerName = args[0] as string + if (!providers.has(providerName)) fail(`subagent/provider-removed names unknown provider ${JSON.stringify(providerName)}`) + stagedRemovals.add(providerName) + return + } + if (eventName === 'subagent/start') { + const info = args[0] as SubagentRunInfo + if (!providers.has(info.provider)) fail(`subagent/start names inactive provider ${JSON.stringify(info.provider)}`) + if (String(info.runId).length === 0 || String(info.id).length === 0) { + fail('subagent/start runId and child id must be non-empty') + } + if (runs.has(info.runId)) fail(`subagent/start repeated run id ${JSON.stringify(info.runId)}`) + stagedStarts.add(info) + return + } + if (eventName !== 'subagent/end') return + const info = args[0] as SubagentRunEndInfo + const start = runs.get(info.runId) + if (start === undefined) fail(`subagent/end has no matching subagent/start for run ${JSON.stringify(info.runId)}`) + validateRunEnd(start, info, fail) + stagedEnds.add(info) + }, { global: true }) + + ctx.on('subagent/provider-added', (provider) => { + /* v8 ignore next -- internal/dispatch stages the same provider object */ + if (!stagedProviders.delete(provider)) return + providers.add(provider.name) + }, { global: true }) + ctx.on('subagent/provider-removed', (providerName) => { + /* v8 ignore next -- internal/dispatch stages the same provider name */ + if (!stagedRemovals.delete(providerName)) return + providers.delete(providerName) + }, { global: true }) + ctx.on('subagent/start', (info) => { + /* v8 ignore next -- internal/dispatch stages the same lifecycle object */ + if (!stagedStarts.delete(info)) return + runs.set(info.runId, info) + }, { global: true }) + ctx.on('subagent/end', (info) => { + /* v8 ignore next -- internal/dispatch stages the same lifecycle object */ + if (!stagedEnds.delete(info)) return + runs.delete(info.runId) + }, { global: true }) +}, { inject: ['subagents'] }) + /** - * Register this package's invariant companion. + * Register the subagent invariant companion. * @param ctx - Cordis context carrying the invariant service. * @returns the installed registration's disposer after setup succeeds. */ diff --git a/packages/subagent/subagent/tests/invariant.spec.ts b/packages/subagent/subagent/tests/invariant.spec.ts new file mode 100644 index 0000000000..ac3a919862 --- /dev/null +++ b/packages/subagent/subagent/tests/invariant.spec.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import { scopeTarget } from '@deepseek-ai/dsh-scope' +import { SessionId } from '@deepseek-ai/dsh-session' +import SubagentService, { SubagentRunId } from '@deepseek-ai/dsh-subagent' +import type { + SubagentProvider, + SubagentRunEndInfo, + SubagentRunInfo, +} from '@deepseek-ai/dsh-subagent' +import * as SubagentInvariant from '@deepseek-ai/dsh-subagent/invariant' +import InvariantService from '@deepseek-ai/dsh-invariants' + +async function setup(): Promise { + const ctx = new Context() + await ctx.plugin(SubagentService) + await ctx.plugin(InvariantService) + await ctx.plugin(SubagentInvariant) + return ctx +} + +const provider = (name: string): SubagentProvider => ({ + name, + capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + inheritsParentContext: false, + start: async () => { throw new Error('not used') }, +}) + +const start = (overrides: Partial = {}): SubagentRunInfo => ({ + runId: SubagentRunId('run-1'), + provider: 'mock', + id: SessionId('child-1'), + local: false, + ...overrides, +}) + +const end = (overrides: Partial = {}): SubagentRunEndInfo => ({ + ...start(), + stopReason: 'completed', + ...overrides, +}) + +function emitRun(ctx: Context, name: 'subagent/start', info: SubagentRunInfo): void +function emitRun(ctx: Context, name: 'subagent/end', info: SubagentRunEndInfo): void +function emitRun(ctx: Context, name: 'subagent/start' | 'subagent/end', info: SubagentRunInfo | SubagentRunEndInfo): void { + ctx.emit(scopeTarget(ctx.subagents, {}), name as 'subagent/start', info) +} + +describe('subagent invariants', () => { + it('accepts provider and run lifecycle pairs', async () => { + const ctx = await setup() + const mock = provider('mock') + ctx.emit('subagent/provider-added', mock) + emitRun(ctx, 'subagent/start', start()) + emitRun(ctx, 'subagent/end', end()) + ctx.emit('subagent/provider-removed', 'mock') + ctx.emit('tools/change') + }) + + it('rejects malformed provider transitions', async () => { + const ctx = await setup() + expect(() => { ctx.emit('subagent/provider-added', provider('')) }).toThrow(/names must be non-empty/) + const mock = provider('mock') + ctx.emit('subagent/provider-added', mock) + expect(() => { ctx.emit('subagent/provider-added', mock) }).toThrow(/repeated "mock"/) + expect(() => { ctx.emit('subagent/provider-removed', 'missing') }).toThrow(/unknown provider/) + }) + + it('rejects malformed and unpaired run transitions', async () => { + const ctx = await setup() + expect(() => { emitRun(ctx, 'subagent/start', start()) }).toThrow(/inactive provider/) + ctx.emit('subagent/provider-added', provider('mock')) + expect(() => { emitRun(ctx, 'subagent/start', start({ runId: SubagentRunId('') })) }) + .toThrow(/runId and child id must be non-empty/) + emitRun(ctx, 'subagent/start', start()) + expect(() => { emitRun(ctx, 'subagent/start', start()) }).toThrow(/repeated run id/) + expect(() => { emitRun(ctx, 'subagent/end', end({ runId: SubagentRunId('missing') })) }) + .toThrow(/no matching subagent\/start/) + expect(() => { emitRun(ctx, 'subagent/end', end({ id: SessionId('other') })) }) + .toThrow(/identity diverges/) + }) +}) diff --git a/packages/subagent/tool-subagent/src/invariant.ts b/packages/subagent/tool-subagent/src/invariant.ts index b1bfa209a1..bd30f4c563 100644 --- a/packages/subagent/tool-subagent/src/invariant.ts +++ b/packages/subagent/tool-subagent/src/invariant.ts @@ -1,29 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tool-subagent`. @module @deepseek-ai/dsh-tool-subagent/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-tool-subagent`. + * @module @deepseek-ai/dsh-tool-subagent/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tool-subagent' /** Cordis companion plugin name. */ export const name = 'tool-subagent-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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")', - ], - }) -} +/** + * No runtime invariant: this model-facing adapter has no independent lifecycle stream; execution + * relations are owned by the capability seam it calls. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -32,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 ac29b83cff..e94876100e 100644 --- a/packages/support/acp-snapshot/src/invariant.ts +++ b/packages/support/acp-snapshot/src/invariant.ts @@ -1,34 +1,24 @@ -/** Package-owned runtime contracts for @deepseek-ai/dsh-acp-snapshot. @module @deepseek-ai/dsh-acp-snapshot/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-acp-snapshot`. + * @module @deepseek-ai/dsh-acp-snapshot/invariant + */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-acp-snapshot' /** Cordis companion plugin name. */ export const name = 'acp-snapshot-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Assert stable JSON-RPC correlation and volatile-value tokenization. */ -const install: InvariantInstaller = async (_ctx, fail) => { - 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') -} +/** + * No runtime invariant: this test-support package owns no production event stream or mutable data; + * consuming test suites exercise its behavior. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. diff --git a/packages/support/agent-loop-testkit/src/invariant.ts b/packages/support/agent-loop-testkit/src/invariant.ts index 69792b77ea..33ee4474f9 100644 --- a/packages/support/agent-loop-testkit/src/invariant.ts +++ b/packages/support/agent-loop-testkit/src/invariant.ts @@ -1,25 +1,24 @@ -/** Package-owned runtime contracts for @deepseek-ai/dsh-agent-loop-testkit. @module @deepseek-ai/dsh-agent-loop-testkit/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-agent-loop-testkit`. + * @module @deepseek-ai/dsh-agent-loop-testkit/invariant + */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-agent-loop-testkit' /** Cordis companion plugin name. */ export const name = 'agent-loop-testkit-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Assert the awaitable helper shape and optional-options call boundary. */ -const install: InvariantInstaller = async (_ctx, fail) => { - 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') -} +/** + * No runtime invariant: this test-support package owns no production event stream or mutable data; + * consuming test suites exercise its behavior. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. diff --git a/packages/support/invariants/README.md b/packages/support/invariants/README.md index 7b922bcda9..1d5a377936 100644 --- a/packages/support/invariants/README.md +++ b/packages/support/invariants/README.md @@ -12,38 +12,37 @@ interface Config { } ``` -Defaults are `enabled: true`, `package_allowlist: []`, and `package_blocklist: []`. A package is selected only when the service is enabled, the empty allowlist or at least one allowlist pattern matches its full npm name, and no blocklist pattern matches. Blocklist matches therefore override allowlist matches. +Defaults are `enabled: true`, `package_allowlist: []`, and `package_blocklist: []`. A package is selected only when the service is enabled, the allowlist is empty or at least one allowlist pattern matches its full npm name, and no blocklist pattern matches. Blocklist matches therefore override allowlist matches. Each entry is a case-sensitive JavaScript regular-expression source compiled with `new RegExp(pattern)`. Matching is unanchored unless the source supplies `^` and `$`; `/pattern/flags` syntax is not parsed. Blank, whitespace-padded, invalid, or duplicate entries within one list fail service startup. A valid pattern may match no currently loaded package so later loading and HMR remain deterministic. `ctx.invariants.register(packageName, installer)` reserves one active registration for the full npm package name, including when filters keep its installer inactive, and returns its disposer. An enabled contribution runs in a dedicated child Cordis fiber. The installer can declare its required service surface through `installer.inject` and receives `fail(message)`, which throws an `InvariantError` bound to the registering package. Synchronous or asynchronous installer completion is joined before registration succeeds; failure disposes the child and releases ownership atomically. -The service owns every registration fiber, while the returned disposer also belongs to the companion fiber. Unloading either side removes the listeners and reservation completely. A companion can therefore reload and register the same package name without retaining trace state or duplicate listeners; packages that need an existing baseline rebuild it during installation. +The service owns every registration fiber, while the returned disposer also belongs to the companion fiber. Unloading either side removes listeners, trace state, and the reservation. A companion can therefore reload and register the same package name without retaining its previous state. Session-backed companions rebuild their baseline from durable events; live-only companions observe operations that begin after reload. -`InvariantError` extends `Error`, carries stable `code: 'INVARIANT'`, and exposes the owning `packageName` without adding a product-package dependency to the service. +`InvariantError` extends `Error`, carries stable `code: 'INVARIANT'`, and exposes the owning `packageName` without adding a product dependency to the service. ## Package companions -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. +Publication and registration are exhaustive; runtime assertions are deliberately not synthetic. A companion installs a check only when its package owns an observable event relationship or relevant mutable-data relationship. Confirming a required method, plugin name, injection, effect, or fixed pure-function result is a type, load, or unit-test concern rather than a runtime invariant. -Packages select the narrowest runtime form that protects their public contract: +When no plausible runtime relationship exists, the companion uses an empty installer with a package-specific leading `No runtime invariant:` comment explaining why. This is common for pure utilities, thin implementations whose behavior is already observed through their seam, composition-only packages, binaries, persistence adapters whose contracts require crash/round-trip tests, and test-support packages. The explanation must be revisited when the owner gains mutable state or an event protocol. -| Package shape | Companion check | +The current executable companions protect these relationships: + +| Companion | Checks | |---|---| -| 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 during child startup. | +| `dsh-session`, `dsh-agent`, `dsh-scope`, `dsh-agent-loop` | Session enclosure and call/result trace, agent-status transitions, scoped subjects, and model-request reconstruction. | +| `dsh-llm`, `dsh-tools`, `dsh-system-prompt` | Stream grammar, tool-pipeline stages and frozen results, and authoritative prompt-assembly data. | +| `dsh-compact`, `dsh-hook-protocol`, `dsh-bash` | Durable compaction and hook pairing, compaction metadata, and sandbox-mode vocabulary. | +| `dsh-fs`, `dsh-subagent`, `dsh-workflow` | Filesystem event identity, provider/child pairing, and workflow/agent lifecycle identity. | +| `dsh-permission`, `dsh-user-approval` | Active-preset references and approval asked/decided audit pairing. | +| `dsh-tasks`, `dsh-tool-todo` | Task snapshot lifecycle/ownership fields and durable whole-list todo structure. | +| `dsh-time-context` | Durable clock readings agree with their turn, step, elapsed baseline, and event timestamp. | -Four companions additionally install stateful event and request checks: +The root entrypoint of each owner remains independent of diagnostics. Loading the service alone installs no product checks, and loading a companion without the service waits on its declared `invariants` injection. -| Companion | Registration | Checks | -|---|---|---| -| `@deepseek-ai/dsh-session/invariant` | `@deepseek-ai/dsh-session` | sequence, turn/step enclosure, and same-step tool call/result trace | -| `@deepseek-ai/dsh-agent/invariant` | `@deepseek-ai/dsh-agent` | agent-status transitions | -| `@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. 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. +`pnpm run verify-package-invariants` discovers all workspace packages. It rejects generated markers, unexplained empty installers, non-empty installers that omit or ignore the reporter, incorrect registration names, and incomplete export, publication, dependency, TypeScript-reference, or bundle wiring. This source rule is a minimum ownership check; focused tests prove each executable companion's semantics. ## Composition @@ -62,11 +61,13 @@ ctx.plugin(InvariantService, { ctx.plugin(SessionInvariant) ``` -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. Plugin and service helpers multiplex package contracts through indexed lifecycle listeners shared by the Cordis root, while contribution disposal removes only that owner's contract. Vitest gives every ordinary root an explicitly enabled service and mounts the current test package's companion; one exhaustive topology test mounts all companions once, and focused invariant-service tests construct their own topology to exercise filtering and lifecycle behavior. +The standard agent spine mounts the service and its four core stateful companions. Custom compositions explicitly add companions for other loaded packages whose contracts they want checked; filters can disable or select registrations without changing package entrypoints. + +Every ordinary Vitest topology mounts an explicitly enabled service and the current test package's companion. Focused suites cover valid and invalid observations for executable companions, while one exhaustive topology mounts all companions to prove registration and disposal wiring. ## Model Experience -None, as the service and companions observe runtime events and requests but never alter prompts, messages, schemas, streams, or tool results. +None. The service and companions observe runtime events, mutable snapshots, and requests but never alter prompts, messages, schemas, streams, or tool results. #### KV Cache effect @@ -74,7 +75,6 @@ None; invariant checks do not assemble or send provider requests. ## Known Limitations and Deferred Work -- 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. +- Request reconstruction covers frozen loop-built requests with a live session id; direct one-shot LLM calls remain outside that marker contract. +- Live-only lifecycle companions cannot reconstruct operations that began before their own reload. Standard and test compositions mount them before the corresponding operations begin. - 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 265596b677..43216baa1e 100644 --- a/packages/support/invariants/src/index.ts +++ b/packages/support/invariants/src/index.ts @@ -6,8 +6,8 @@ * @module @deepseek-ai/dsh-invariants */ -import { Context, FiberState, Service } from 'cordis' -import type { Fiber, Inject, Plugin } from 'cordis' +import { Context, Service } from 'cordis' +import type { Inject } from 'cordis' import z from 'schemastery' import type Schema from 'schemastery' @@ -41,300 +41,6 @@ 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 -} - -/** One package check routed by a root-shared plugin lifecycle dispatcher. */ -interface PluginObservation { - readonly callback: globalThis.Function | undefined - readonly contract: PluginInvariantContract - readonly fail: InvariantFailure -} - -/** Indexed plugin checks and the two lifecycle listeners shared by one root. */ -interface PluginObservationHub { - readonly byCallback: Map> - readonly byName: Map> -} - -const pluginObservationHubs = new WeakMap() - -/** Check one already-matched active plugin fiber. */ -function inspectPluginObservation(observation: PluginObservation, fiber: Fiber): void { - if (fiber.state !== FiberState.ACTIVE || fiber.uid === null) return - const { callback, contract, fail } = observation - 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) -} - -/** Route one lifecycle notification only to checks that can match its runtime. */ -function inspectObservedPlugin(hub: PluginObservationHub, fiber: Fiber): void { - const callback = fiber.runtime?.callback - if (callback !== undefined) { - for (const observation of hub.byCallback.get(callback) ?? []) { - inspectPluginObservation(observation, fiber) - } - } - const runtimeName = fiber.runtime?.name - if (runtimeName !== undefined) { - for (const observation of hub.byName.get(runtimeName) ?? []) { - inspectPluginObservation(observation, fiber) - } - } -} - -/** Return the root's shared plugin dispatcher, creating its two listeners once. */ -function pluginObservationHub(ctx: Context): PluginObservationHub { - const root = ctx.root - const existing = pluginObservationHubs.get(root) - if (existing !== undefined) return existing - - const hub: PluginObservationHub = { - byCallback: new Map(), - byName: new Map(), - } - pluginObservationHubs.set(root, hub) - root.on('internal/plugin', (fiber) => { inspectObservedPlugin(hub, fiber) }, { global: true }) - root.on('internal/status', (fiber) => { inspectObservedPlugin(hub, fiber) }, { global: true }) - return hub -} - -/** Add one plugin observation to a typed exact-key index. */ -function addIndexedPluginObservation( - index: Map>, - key: Key, - observation: PluginObservation, -): () => void { - const observations = index.get(key) ?? new Set() - index.set(key, observations) - observations.add(observation) - return () => { - observations.delete(observation) - if (observations.size === 0) index.delete(key) - } -} - -/** Add one observation to its exact callback or runtime-name index. */ -function addPluginObservation(hub: PluginObservationHub, observation: PluginObservation): () => void { - if (observation.callback === undefined) { - return addIndexedPluginObservation(hub.byName, observation.contract.name, observation) - } - return addIndexedPluginObservation(hub.byCallback, observation.callback, observation) -} - -/** - * 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 two indexed lifecycle listeners shared by the root. - * @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 observation: PluginObservation = { callback, contract, fail } - - if (contract.plugin === undefined) { - for (const runtime of ctx.registry.values()) { - if (runtime.name !== contract.name) continue - for (const fiber of runtime.fibers) inspectPluginObservation(observation, fiber) - } - } else { - for (const fiber of ctx.registry.get(contract.plugin)?.fibers ?? []) { - inspectPluginObservation(observation, fiber) - } - } - const hub = pluginObservationHub(ctx) - ctx.effect( - () => addPluginObservation(hub, observation), - `invariants.observePlugin(${JSON.stringify(contract.name)})`, - ) -} - -/** One structural check routed by a root-shared service lifecycle dispatcher. */ -interface ServiceObservation { - readonly fail: InvariantFailure - readonly validate: (value: unknown) => string | undefined -} - -/** Service checks and the single service listener shared by one root. */ -interface ServiceObservationHub { - readonly byName: Map> -} - -const serviceObservationHubs = new WeakMap() - -/** Check one present service implementation. */ -function inspectServiceObservation(observation: ServiceObservation, value: unknown): void { - if (value === undefined) return - const message = observation.validate(value) - if (message !== undefined) observation.fail(message) -} - -/** Return the root's shared service dispatcher, creating its listener once. */ -function serviceObservationHub(ctx: Context): ServiceObservationHub { - const root = ctx.root - const existing = serviceObservationHubs.get(root) - if (existing !== undefined) return existing - - const hub: ServiceObservationHub = { byName: new Map() } - serviceObservationHubs.set(root, hub) - root.on('internal/service', (name, value: unknown) => { - for (const observation of hub.byName.get(name) ?? []) { - inspectServiceObservation(observation, value) - } - }, { global: true }) - return hub -} - -/** Add one service observation to its exact service-name index. */ -function addServiceObservation( - hub: ServiceObservationHub, - serviceName: string, - observation: ServiceObservation, -): () => void { - const observations = hub.byName.get(serviceName) ?? new Set() - hub.byName.set(serviceName, observations) - observations.add(observation) - return () => { - observations.delete(observation) - if (observations.size === 0) hub.byName.delete(serviceName) - } -} - -/** - * Validate every current and future implementation bound to one Cordis - * service through the root's indexed shared service listener. - * @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 observation: ServiceObservation = { fail, validate } - const current: unknown = ctx.get(serviceName) - inspectServiceObservation(observation, current) - const hub = serviceObservationHub(ctx) - ctx.effect( - () => addServiceObservation(hub, serviceName, observation), - `invariants.observeService(${JSON.stringify(serviceName)})`, - ) -} - -/** 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 bd9efcb74c..7780e987f5 100644 --- a/packages/support/invariants/src/invariant.ts +++ b/packages/support/invariants/src/invariant.ts @@ -1,28 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-invariants`. @module @deepseek-ai/dsh-invariants/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-invariants`. + * @module @deepseek-ai/dsh-invariants/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import InvariantService, { observePluginInvariant, type InvariantInstaller } from './index.ts' +import type { InvariantInstaller } from './index.ts' const PACKAGE_NAME = '@deepseek-ai/dsh-invariants' /** Cordis companion plugin name. */ export const name = 'invariants-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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', - ], - }) -} +/** + * No runtime invariant: registration ownership and child lifecycle are the service's mutation + * boundary itself; observing them from the same registry would only duplicate its implementation. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -31,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 76fda543ad..b190379302 100644 --- a/packages/support/invariants/tests/service.spec.ts +++ b/packages/support/invariants/tests/service.spec.ts @@ -2,19 +2,12 @@ import { describe, expect, it, vi } from 'vitest' import { Context, Service } from 'cordis' 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 { @@ -28,12 +21,6 @@ class InvariantProbeService extends Service { } } -class WatchedInvariantProbeService extends Service { - constructor(ctx: Context) { - super(ctx, 'watchedInvariantProbe') - } -} - interface RuntimeRegistration extends PromiseLike<() => void> { (): void | Promise } @@ -299,277 +286,3 @@ describe('InvariantService lifecycle', () => { expect(() => service.register('@deepseek-ai/dsh-session', () => {})).toThrow(/inactive/i) }) }) - -describe('package-owned invariant helpers', () => { - interface InvariantDisposer { - (): void | Promise - } - - async function registerInstaller( - ctx: Context, - packageName: string, - installer: InvariantInstaller, - ): Promise { - 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('multiplexes same-runtime plugin checks through one root listener pair and disposes each owner', async () => { - const { ctx } = await setup() - const firstValidation = vi.fn(() => undefined) - const secondValidation = vi.fn(() => undefined) - const first = await registerInstaller(ctx, '@deepseek-ai/dsh-shared-plugin-first', (child, fail) => { - observePluginInvariant(child, fail, { name: 'shared-plugin-probe', validate: firstValidation }) - }) - const second = await registerInstaller(ctx, '@deepseek-ai/dsh-shared-plugin-second', (child, fail) => { - observePluginInvariant(child, fail, { name: 'shared-plugin-probe', validate: secondValidation }) - }) - const rootEffectLabels = ctx.fiber.getEffects().map(effect => effect.label) - expect(rootEffectLabels.filter(label => label === 'ctx.on("internal/plugin")')).toHaveLength(1) - expect(rootEffectLabels.filter(label => label === 'ctx.on("internal/status")')).toHaveLength(1) - - const plugin = effectPlugin({ name: 'shared-plugin-probe' }) - const firstFiber = await ctx.plugin(plugin) - expect(firstValidation).toHaveBeenCalledOnce() - expect(secondValidation).toHaveBeenCalledOnce() - - await first() - await firstFiber.dispose() - const secondFiber = await ctx.plugin(plugin) - expect(firstValidation).toHaveBeenCalledOnce() - expect(secondValidation).toHaveBeenCalledTimes(2) - - await second() - await secondFiber.dispose() - await ctx.plugin(plugin) - expect(firstValidation).toHaveBeenCalledOnce() - expect(secondValidation).toHaveBeenCalledTimes(2) - }) - - 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('multiplexes same-name service checks through one root listener and disposes each owner', async () => { - const { ctx } = await setup() - const firstValidation = vi.fn(() => undefined) - const secondValidation = vi.fn(() => undefined) - const first = await registerInstaller(ctx, '@deepseek-ai/dsh-shared-service-first', (child, fail) => { - observeServiceInvariant(child, fail, 'watchedInvariantProbe', firstValidation) - }) - const second = await registerInstaller(ctx, '@deepseek-ai/dsh-shared-service-second', (child, fail) => { - observeServiceInvariant(child, fail, 'watchedInvariantProbe', secondValidation) - }) - const rootEffectLabels = ctx.fiber.getEffects().map(effect => effect.label) - expect(rootEffectLabels.filter(label => label === 'ctx.on("internal/service")')).toHaveLength(1) - - const firstFiber = await ctx.plugin(WatchedInvariantProbeService) - expect(firstValidation).toHaveBeenCalledOnce() - expect(secondValidation).toHaveBeenCalledOnce() - - await first() - const firstCallsAfterDisposal = firstValidation.mock.calls.length - const secondCallsBeforeRemount = secondValidation.mock.calls.length - await firstFiber.dispose() - const secondFiber = await ctx.plugin(WatchedInvariantProbeService) - expect(firstValidation).toHaveBeenCalledTimes(firstCallsAfterDisposal) - expect(secondValidation.mock.calls.length).toBeGreaterThan(secondCallsBeforeRemount) - - await second() - const firstCallsAfterBothDisposals = firstValidation.mock.calls.length - const secondCallsAfterBothDisposals = secondValidation.mock.calls.length - await secondFiber.dispose() - await ctx.plugin(WatchedInvariantProbeService) - expect(firstValidation).toHaveBeenCalledTimes(firstCallsAfterBothDisposals) - expect(secondValidation).toHaveBeenCalledTimes(secondCallsAfterBothDisposals) - }) - - 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') - }) - await 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 9c82c33686..36a3f8eeca 100644 --- a/packages/support/llm-replay/src/invariant.ts +++ b/packages/support/llm-replay/src/invariant.ts @@ -1,30 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-llm-replay`. @module @deepseek-ai/dsh-llm-replay/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-llm-replay`. + * @module @deepseek-ai/dsh-llm-replay/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-llm-replay' /** Cordis companion plugin name. */ export const name = 'llm-replay-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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")', - ], - ], - }) -} +/** + * No runtime invariant: this test-only adapter consumes a fixed replay script; its stream grammar + * is checked by the LLM companion and fixture derivation tests. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -33,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 6fbb41a123..1e3cc54b81 100644 --- a/packages/support/loader-smoke/src/invariant.ts +++ b/packages/support/loader-smoke/src/invariant.ts @@ -1,32 +1,24 @@ -/** Package-owned runtime contracts for @deepseek-ai/dsh-loader-smoke. @module @deepseek-ai/dsh-loader-smoke/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-loader-smoke`. + * @module @deepseek-ai/dsh-loader-smoke/invariant + */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-loader-smoke' /** Cordis companion plugin name. */ export const name = 'loader-smoke-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Assert default source mode and plain-Node built-artifact launch resolution. */ -const install: InvariantInstaller = async (_ctx, fail) => { - 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') -} +/** + * No runtime invariant: this test-support package owns no production event stream or mutable data; + * consuming test suites exercise its behavior. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. diff --git a/packages/tasks/tasks/src/invariant.ts b/packages/tasks/tasks/src/invariant.ts index ce3bf24175..a633213607 100644 --- a/packages/tasks/tasks/src/invariant.ts +++ b/packages/tasks/tasks/src/invariant.ts @@ -1,31 +1,55 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tasks`. @module @deepseek-ai/dsh-tasks/invariant */ +/** Package-owned background-task snapshot invariants. @module @deepseek-ai/dsh-tasks/invariant */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { Agent } from '@deepseek-ai/dsh-agent' +import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { TaskSnapshot } from './types.ts' const PACKAGE_NAME = '@deepseek-ai/dsh-tasks' +const TERMINAL_STATUSES = new Set(['completed', 'killed', 'failed']) /** Cordis companion plugin name. */ export const name = 'tasks-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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', - ], - }) +/** Validate the cross-field relationships in one registry snapshot. */ +function validateSnapshot(snapshot: TaskSnapshot, owner: Agent | undefined, fail: InvariantFailure): void { + const id = String(snapshot.id) + const prefix = `${snapshot.kind}-` + const ordinal = Number(id.slice(prefix.length)) + if (snapshot.kind.length === 0 || !id.startsWith(prefix) + || !Number.isSafeInteger(ordinal) || ordinal < 1) { + fail(`task snapshot id ${JSON.stringify(id)} must be ${JSON.stringify(prefix)} followed by a positive ordinal`) + } + if (snapshot.label.length === 0) fail(`task ${JSON.stringify(id)} label must be non-empty`) + if (!Number.isSafeInteger(snapshot.startedAt) || snapshot.startedAt < 0) { + fail(`task ${JSON.stringify(id)} startedAt must be a non-negative epoch integer`) + } + + const terminal = TERMINAL_STATUSES.has(snapshot.status) + if (terminal !== (snapshot.finishedAt !== undefined)) { + fail(`task ${JSON.stringify(id)} finishedAt must be present exactly for a terminal status`) + } + if (snapshot.finishedAt !== undefined + && (!Number.isSafeInteger(snapshot.finishedAt) || snapshot.finishedAt < snapshot.startedAt)) { + fail(`task ${JSON.stringify(id)} finishedAt must be an epoch integer no earlier than startedAt`) + } + + const expectedOwner = owner?.id + if (snapshot.ownerSession !== expectedOwner) { + fail(`task ${JSON.stringify(id)} ownerSession does not match its completion owner`) + } } +/** Install checks over current unowned records and every terminal snapshot. */ +const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => { + for (const snapshot of ctx.tasks.list()) validateSnapshot(snapshot, undefined, fail) + ctx.tasks.onTaskDone((snapshot, owner) => { validateSnapshot(snapshot, owner, fail) }) +}, { inject: ['tasks'] }) + /** - * Register this package's invariant companion. + * Register the task-registry invariant companion. * @param ctx - Cordis context carrying the invariant service. * @returns the installed registration's disposer after setup succeeds. */ diff --git a/packages/tasks/tasks/tests/invariant.spec.ts b/packages/tasks/tasks/tests/invariant.spec.ts new file mode 100644 index 0000000000..e23609df5d --- /dev/null +++ b/packages/tasks/tasks/tests/invariant.spec.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import type { Agent } from '@deepseek-ai/dsh-agent' +import { SessionId } from '@deepseek-ai/dsh-session' +import TaskService, { TaskId } from '@deepseek-ai/dsh-tasks' +import type { TaskDoneListener, TaskSnapshot } from '@deepseek-ai/dsh-tasks' +import * as TasksInvariant from '@deepseek-ai/dsh-tasks/invariant' +import InvariantService from '@deepseek-ai/dsh-invariants' + +const BASE: TaskSnapshot = { + id: TaskId('bash-1'), + kind: 'bash', + label: 'compile', + status: 'completed', + startedAt: 10, + finishedAt: 20, + reported: false, +} + +const RUNNING: TaskSnapshot = { + id: TaskId('bash-1'), + kind: 'bash', + label: 'compile', + status: 'running', + startedAt: 10, + reported: false, +} + +const TERMINAL_WITHOUT_FINISH: TaskSnapshot = { + id: TaskId('bash-1'), + kind: 'bash', + label: 'compile', + status: 'completed', + startedAt: 10, + reported: false, +} + +async function setup(seed: TaskSnapshot[] = []): Promise<(snapshot: unknown, owner?: Agent) => void> { + const ctx = new Context() + let listener: TaskDoneListener | undefined + const probe = { + list: () => seed, + onTaskDone(value: TaskDoneListener) { + listener = value + return () => { listener = undefined } + }, + } as unknown as TaskService + await ctx.plugin(InvariantService) + await ctx.plugin({ + name: 'task-invariant-probe', + apply(child: Context) { child.provide('tasks', probe) }, + }) + await ctx.plugin(TasksInvariant) + if (listener === undefined) throw new Error('task invariant did not subscribe to terminal snapshots') + return (snapshot, owner) => { listener!(snapshot as TaskSnapshot, owner) } +} + +describe('task-registry invariants', () => { + it('accepts coherent current and terminal snapshots', async () => { + const notify = await setup([RUNNING]) + expect(() => { notify(BASE) }).not.toThrow() + const owner = { id: SessionId('owner') } as Agent + expect(() => { notify({ ...BASE, id: TaskId('subagent-2'), kind: 'subagent', ownerSession: owner.id }, owner) }) + .not.toThrow() + }) + + it.each([ + [{ ...BASE, id: TaskId('-1'), kind: '' }, undefined, /positive ordinal/], + [{ ...BASE, id: TaskId('other-1') }, undefined, /must be "bash-" followed by a positive ordinal/], + [{ ...BASE, id: TaskId('bash-x') }, undefined, /positive ordinal/], + [{ ...BASE, id: TaskId('bash-0') }, undefined, /positive ordinal/], + [{ ...BASE, startedAt: -1 }, undefined, /startedAt must be a non-negative epoch integer/], + [{ ...BASE, startedAt: 0.5 }, undefined, /startedAt must be a non-negative epoch integer/], + [{ ...BASE, status: 'running' }, undefined, /finishedAt must be present exactly for a terminal status/], + [TERMINAL_WITHOUT_FINISH, undefined, /finishedAt must be present exactly for a terminal status/], + [{ ...BASE, finishedAt: 9 }, undefined, /no earlier than startedAt/], + [{ ...BASE, finishedAt: 20.5 }, undefined, /no earlier than startedAt/], + [{ ...BASE, ownerSession: SessionId('recorded') }, { id: SessionId('actual') } as Agent, /does not match its completion owner/], + ] as const)('rejects an incoherent registry snapshot', async (snapshot, owner, message) => { + const notify = await setup() + expect(() => { notify(snapshot, owner) }).toThrow(message) + }) + + it('rejects an incoherent record already present at installation', async () => { + await expect(setup([{ ...BASE, label: '' }])).rejects.toThrow(/label must be non-empty/) + }) +}) diff --git a/packages/tasks/tool-tasks/src/invariant.ts b/packages/tasks/tool-tasks/src/invariant.ts index e4af0931d2..cedad9dc1c 100644 --- a/packages/tasks/tool-tasks/src/invariant.ts +++ b/packages/tasks/tool-tasks/src/invariant.ts @@ -1,29 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tool-tasks`. @module @deepseek-ai/dsh-tool-tasks/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-tool-tasks`. + * @module @deepseek-ai/dsh-tool-tasks/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tool-tasks' /** Cordis companion plugin name. */ export const name = 'tool-tasks-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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()', - ], - }) -} +/** + * No runtime invariant: this model-facing adapter has no independent lifecycle stream; execution + * relations are owned by the capability seam it calls. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -32,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 b0e564dbbd..ddc3b3966e 100644 --- a/packages/timeout/timeout-policy/src/invariant.ts +++ b/packages/timeout/timeout-policy/src/invariant.ts @@ -1,27 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-timeout-policy`. @module @deepseek-ai/dsh-timeout-policy/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-timeout-policy`. + * @module @deepseek-ai/dsh-timeout-policy/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-timeout-policy' /** Cordis companion plugin name. */ export const name = 'timeout-policy-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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")', - ], - }) -} +/** + * No runtime invariant: this stateless policy plugin owns no package-local event history or mutable + * data relation beyond the seam it intercepts. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -30,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 9ad57156f4..0b3a3739e4 100644 --- a/packages/todo/tool-todo/src/invariant.ts +++ b/packages/todo/tool-todo/src/invariant.ts @@ -1,30 +1,49 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tool-todo`. @module @deepseek-ai/dsh-tool-todo/invariant */ +/** Package-owned durable todo-snapshot invariants. @module @deepseek-ai/dsh-tool-todo/invariant */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' +import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tool-todo' +const TODO_STATUSES = new Set(['pending', 'in_progress', 'completed']) /** Cordis companion plugin name. */ export const name = 'tool-todo-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Install checks for this package's active plugin fibers. */ +/** Validate one whole-list todo snapshot before it reaches the durable log. */ +function validateTodos(value: unknown, fail: InvariantFailure): void { + if (!Array.isArray(value)) fail('todo/write todos must be an array') + const seen = new Set() + let active = 0 + for (const item of value) { + if (typeof item !== 'object' || item === null) fail('todo/write entries must be objects') + const { content, status } = item as Record + if (typeof content !== 'string' || content.length === 0 || content.trim() !== content) { + fail('todo/write content must be non-empty and already trimmed') + } + if (seen.has(content)) fail(`todo/write repeats content ${JSON.stringify(content)}`) + seen.add(content) + if (typeof status !== 'string' || !TODO_STATUSES.has(status)) { + fail(`todo/write carries unknown status ${JSON.stringify(status)}`) + } + if (status === 'in_progress') active += 1 + } + if (active > 1) fail(`todo/write contains ${active} in-progress entries; at most one is allowed`) +} + +/** Install validation for durable whole-list todo snapshots. */ const install: InvariantInstaller = (ctx, fail) => { - observePluginInvariant(ctx, fail, { - name: 'tool-todo', - inject: [ - 'tools', - ], - effects: [ - 'tools.register()', - ], - }) + ctx.on('internal/dispatch', (_mode, eventName, args) => { + if (eventName !== 'session/event') return + const event = (args as [Session, SessionEvent])[1] + if (event.type === 'todo/write') validateTodos(event.data.todos, fail) + }, { global: true }) } /** - * Register this package's invariant companion. + * Register the todo invariant companion. * @param ctx - Cordis context carrying the invariant service. * @returns the installed registration's disposer after setup succeeds. */ diff --git a/packages/todo/tool-todo/tests/invariant.spec.ts b/packages/todo/tool-todo/tests/invariant.spec.ts new file mode 100644 index 0000000000..8593f60299 --- /dev/null +++ b/packages/todo/tool-todo/tests/invariant.spec.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' +import * as TodoInvariant from '@deepseek-ai/dsh-tool-todo/invariant' +import InvariantService from '@deepseek-ai/dsh-invariants' + +async function setup(): Promise { + const ctx = new Context() + await ctx.plugin(InvariantService) + await ctx.plugin(TodoInvariant) + return ctx +} + +function event(todos: unknown): SessionEvent { + return { type: 'todo/write', seq: 0, time: 0, data: { todos } } as SessionEvent +} + +describe('todo snapshot invariants', () => { + it('accepts a unique whole-list snapshot with one active item', async () => { + const ctx = await setup() + expect(() => { ctx.emit('session/event', {} as Session, event([ + { content: 'Inspect state', status: 'completed' }, + { content: 'Apply fix', status: 'in_progress' }, + { content: 'Run checks', status: 'pending' }, + ])) }).not.toThrow() + }) + + it.each([ + ['not-an-array', /must be an array/], + [[null], /entries must be objects/], + [[42], /entries must be objects/], + [[{ content: 42, status: 'pending' }], /content must be non-empty/], + [[{ content: '', status: 'pending' }], /content must be non-empty/], + [[{ content: ' padded ', status: 'pending' }], /already trimmed/], + [[{ content: 'same', status: 'pending' }, { content: 'same', status: 'completed' }], /repeats content/], + [[{ content: 'task', status: 42 }], /unknown status/], + [[{ content: 'task', status: 'paused' }], /unknown status/], + [[{ content: 'one', status: 'in_progress' }, { content: 'two', status: 'in_progress' }], /at most one/], + ])('rejects an incoherent durable todo snapshot', async (todos, message) => { + const ctx = await setup() + expect(() => { ctx.emit('session/event', {} as Session, event(todos)) }).toThrow(message) + }) + + it('ignores unrelated dispatches and session events', async () => { + const ctx = await setup() + expect(() => { + ctx.emit('tools/change') + ctx.emit('session/event', {} as Session, { + type: 'turn/start', seq: 0, time: 0, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }, + }) + }).not.toThrow() + }) +}) diff --git a/packages/ui/acp/src/invariant.ts b/packages/ui/acp/src/invariant.ts index 1b98d6630c..fdefcf291e 100644 --- a/packages/ui/acp/src/invariant.ts +++ b/packages/ui/acp/src/invariant.ts @@ -1,34 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-acp`. @module @deepseek-ai/dsh-acp/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-acp`. + * @module @deepseek-ai/dsh-acp/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-acp' /** Cordis companion plugin name. */ export const name = 'acp-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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', - ], - }) -} +/** + * No runtime invariant: this presentation adapter owns no durable package-local event stream; + * boundary and replay tests cover its protocol mapping. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -37,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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/invariant.ts b/packages/ui/app-boot/src/invariant.ts index 8591c60dd1..0dacba6e40 100644 --- a/packages/ui/app-boot/src/invariant.ts +++ b/packages/ui/app-boot/src/invariant.ts @@ -1,28 +1,24 @@ -/** Package-owned runtime contracts for @deepseek-ai/dsh-app-boot. @module @deepseek-ai/dsh-app-boot/invariant */ +/** + * Package-owned invariant companion 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 { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-app-boot' /** Cordis companion plugin name. */ export const name = 'app-boot-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Assert ordinary and replay config-path selection. */ -const install: InvariantInstaller = async (_ctx, fail) => { - 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') -} +/** + * No runtime invariant: this presentation adapter owns no durable package-local event stream; + * boundary and replay tests cover its protocol mapping. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. diff --git a/packages/ui/jsonrpc/src/invariant.ts b/packages/ui/jsonrpc/src/invariant.ts index aad2ac9f41..1a3c9b053b 100644 --- a/packages/ui/jsonrpc/src/invariant.ts +++ b/packages/ui/jsonrpc/src/invariant.ts @@ -1,27 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-jsonrpc`. @module @deepseek-ai/dsh-jsonrpc/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-jsonrpc`. + * @module @deepseek-ai/dsh-jsonrpc/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-jsonrpc' /** Cordis companion plugin name. */ export const name = 'jsonrpc-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Install checks for this package's active plugin fibers. */ -const install: InvariantInstaller = (ctx, fail) => { - observePluginInvariant(ctx, fail, { - name: 'jsonrpc', - inject: [ - 'agents', - ], - effects: [ - 'jsonrpc.serve', - ], - }) -} +/** + * No runtime invariant: this presentation adapter owns no durable package-local event stream; + * boundary and replay tests cover its protocol mapping. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -30,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 24191712bf..0be5942d24 100644 --- a/packages/ui/permission/src/invariant.ts +++ b/packages/ui/permission/src/invariant.ts @@ -1,34 +1,29 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-permission`. @module @deepseek-ai/dsh-permission/invariant */ +/** Package-owned permission-preset event invariants. @module @deepseek-ai/dsh-permission/invariant */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-permission' /** Cordis companion plugin name. */ export const name = 'permission-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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', - ], - }) -} +/** Install validation that durable preset events remain resolvable. */ +const install: InvariantInstaller = Object.assign((ctx: Context, fail: (message: string) => never) => { + ctx.on('internal/dispatch', (_mode, eventName, args) => { + if (eventName !== 'session/event') return + const event = (args as [Session, SessionEvent])[1] + if (event.type === 'permission/preset' && !ctx.permission.names.includes(event.data.preset)) { + fail(`permission/preset names unknown preset ${JSON.stringify(event.data.preset)}`) + } + }, { global: true }) +}, { inject: ['permission'] }) /** - * Register this package's invariant companion. + * Register the permission invariant companion. * @param ctx - Cordis context carrying the invariant service. * @returns the installed registration's disposer after setup succeeds. */ diff --git a/packages/ui/permission/tests/invariant.spec.ts b/packages/ui/permission/tests/invariant.spec.ts new file mode 100644 index 0000000000..6488239d38 --- /dev/null +++ b/packages/ui/permission/tests/invariant.spec.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest' +import { Context, Service } from 'cordis' +import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' +import * as PermissionInvariant from '@deepseek-ai/dsh-permission/invariant' +import InvariantService from '@deepseek-ai/dsh-invariants' + +class PermissionProbe extends Service { + readonly names = ['safe', 'trusted'] + + constructor(ctx: Context) { + super(ctx, 'permission') + } +} + +async function setup(): Promise { + const ctx = new Context() + await ctx.plugin(PermissionProbe) + await ctx.plugin(InvariantService) + await ctx.plugin(PermissionInvariant) + return ctx +} + +function presetEvent(preset: string): SessionEvent { + return { type: 'permission/preset', seq: 0, time: 0, data: { preset } } +} + +describe('permission invariants', () => { + it('accepts configured preset events and ignores other session data', async () => { + const ctx = await setup() + expect(() => { ctx.emit('session/event', {} as Session, presetEvent('safe')) }).not.toThrow() + expect(() => { ctx.emit('session/event', {} as Session, { + type: 'turn/end', seq: 0, time: 0, data: {}, + } as SessionEvent) }).not.toThrow() + expect(() => { ctx.emit('tools/change') }).not.toThrow() + }) + + it('rejects a durable preset that the active table cannot resolve', async () => { + const ctx = await setup() + expect(() => { ctx.emit('session/event', {} as Session, presetEvent('missing')) }) + .toThrow(/unknown preset "missing"/) + }) +}) diff --git a/packages/ui/stdio/src/invariant.ts b/packages/ui/stdio/src/invariant.ts index 56bf12cda3..31c3e78e7d 100644 --- a/packages/ui/stdio/src/invariant.ts +++ b/packages/ui/stdio/src/invariant.ts @@ -1,29 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-stdio`. @module @deepseek-ai/dsh-stdio/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-stdio`. + * @module @deepseek-ai/dsh-stdio/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-stdio' /** Cordis companion plugin name. */ export const name = 'stdio-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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()', - ], - }) -} +/** + * No runtime invariant: this presentation adapter owns no durable package-local event stream; + * boundary and replay tests cover its protocol mapping. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -32,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 2b18cad1f3..140bbd79c5 100644 --- a/packages/ui/tool-ask-user/src/invariant.ts +++ b/packages/ui/tool-ask-user/src/invariant.ts @@ -1,28 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tool-ask-user`. @module @deepseek-ai/dsh-tool-ask-user/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-tool-ask-user`. + * @module @deepseek-ai/dsh-tool-ask-user/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tool-ask-user' /** Cordis companion plugin name. */ export const name = 'tool-ask-user-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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()', - ], - }) -} +/** + * No runtime invariant: this model-facing adapter has no independent lifecycle stream; execution + * relations are owned by the capability seam it calls. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -31,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 d3014146d7..f8072f3968 100644 --- a/packages/ui/tui/src/invariant.ts +++ b/packages/ui/tui/src/invariant.ts @@ -1,30 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tui`. @module @deepseek-ai/dsh-tui/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-tui`. + * @module @deepseek-ai/dsh-tui/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tui' /** Cordis companion plugin name. */ export const name = 'tui-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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()', - ], - }) -} +/** + * No runtime invariant: this presentation adapter owns no durable package-local event stream; + * boundary and replay tests cover its protocol mapping. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -33,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) +/* jscpd:ignore-end */ diff --git a/packages/ui/user-approval/src/invariant.ts b/packages/ui/user-approval/src/invariant.ts index 9a905be06f..9fc42fa908 100644 --- a/packages/ui/user-approval/src/invariant.ts +++ b/packages/ui/user-approval/src/invariant.ts @@ -1,31 +1,88 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-user-approval`. @module @deepseek-ai/dsh-user-approval/invariant */ +/** Package-owned approval audit-stream invariants. @module @deepseek-ai/dsh-user-approval/invariant */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' +import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { ApprovalRequestId } from './index.ts' +import { APPROVAL_POLICIES } from './index.ts' const PACKAGE_NAME = '@deepseek-ai/dsh-user-approval' +const APPROVAL_OUTCOMES = ['allowed-once', 'rejected', 'cancelled', 'unavailable'] as const /** Cordis companion plugin name. */ export const name = 'user-approval-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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', - ], - }) +type ApprovalTransition = + | { kind: 'asked'; id: ApprovalRequestId } + | { kind: 'decided'; id: ApprovalRequestId } + +/** Validate one approval event against committed unmatched questions. */ +function validateApprovalEvent( + pending: ReadonlySet, + event: SessionEvent, + fail: InvariantFailure, +): ApprovalTransition | undefined { + if (event.type === 'approval/asked') { + if (event.data.toolName.length === 0) fail('approval/asked toolName must be non-empty') + if (pending.has(event.data.id)) fail(`approval/asked repeated open id ${JSON.stringify(event.data.id)}`) + return { kind: 'asked', id: event.data.id } + } + if (event.type === 'approval/decided') { + if (!pending.has(event.data.id)) fail(`approval/decided has no matching approval/asked for id ${JSON.stringify(event.data.id)}`) + if (!APPROVAL_OUTCOMES.includes(event.data.outcome)) { + fail(`approval/decided carries unknown outcome ${JSON.stringify(event.data.outcome)}`) + } + return { kind: 'decided', id: event.data.id } + } + if (event.type === 'approval/policy' && !APPROVAL_POLICIES.includes(event.data.policy)) { + fail(`approval/policy carries unknown policy ${JSON.stringify(event.data.policy)}`) + } + return undefined } +/** Apply one accepted approval-pair transition. */ +function applyApprovalTransition(pending: Set, transition: ApprovalTransition): void { + if (transition.kind === 'asked') pending.add(transition.id) + else pending.delete(transition.id) +} + +/** Install audit pairing and closed-vocabulary checks. */ +const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => { + const traces = new WeakMap>() + const staged = new WeakMap() + const seed = (session: Session): Set => { + const pending = new Set() + traces.set(session, pending) + for (const event of session.events) { + const transition = validateApprovalEvent(pending, event, fail) + if (transition !== undefined) applyApprovalTransition(pending, transition) + } + return pending + } + const traceFor = (session: Session): Set => traces.get(session) ?? seed(session) + + for (const session of ctx.sessions.list()) seed(session) + ctx.on('session/created', (session) => { seed(session) }, { global: true }) + ctx.on('session/event', (session, event) => { + if (event.type !== 'approval/asked' && event.type !== 'approval/decided') return + const candidate = staged.get(event) + /* v8 ignore next -- internal/dispatch stages every package-owned pair event */ + if (candidate === undefined || candidate.session !== session) return fail('approval audit event published without pre-commit validation') + staged.delete(event) + applyApprovalTransition(traceFor(session), candidate.transition) + }, { global: true }) + ctx.on('internal/dispatch', (_mode, eventName, args) => { + if (eventName !== 'session/event') return + const [session, event] = args as [Session, SessionEvent] + const transition = validateApprovalEvent(traceFor(session), event, fail) + if (transition !== undefined) staged.set(event, { session, transition }) + }, { global: true }) +}, { inject: ['sessions'] }) + /** - * Register this package's invariant companion. + * Register the approval invariant companion. * @param ctx - Cordis context carrying the invariant service. * @returns the installed registration's disposer after setup succeeds. */ diff --git a/packages/ui/user-approval/tests/invariant.spec.ts b/packages/ui/user-approval/tests/invariant.spec.ts new file mode 100644 index 0000000000..ea9d4472e5 --- /dev/null +++ b/packages/ui/user-approval/tests/invariant.spec.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session' +import { ApprovalRequestId } from '@deepseek-ai/dsh-user-approval' +import * as ApprovalInvariant from '@deepseek-ai/dsh-user-approval/invariant' +import InvariantService from '@deepseek-ai/dsh-invariants' + +async function setup(): Promise { + const ctx = new Context() + await ctx.plugin(SessionStore) + await ctx.plugin(InvariantService) + await ctx.plugin(ApprovalInvariant) + return ctx +} + +describe('approval invariants', () => { + it('accepts paired audit events and closed policy values', async () => { + const ctx = await setup() + const session = ctx.sessions.create() + const id = ApprovalRequestId('ask-1') + session.append('approval/asked', { id, toolName: 'bash' }) + session.append('approval/decided', { id, outcome: 'allowed-once' }) + session.append('approval/policy', { policy: 'never' }) + }) + + it('rebuilds an unmatched question from an existing session', async () => { + const ctx = new Context() + await ctx.plugin(SessionStore) + const session = ctx.sessions.create() + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) + const id = ApprovalRequestId('ask-resume') + session.append('approval/asked', { id, toolName: 'bash' }) + await ctx.plugin(InvariantService) + await ctx.plugin(ApprovalInvariant) + expect(() => session.append('approval/decided', { id, outcome: 'cancelled' })).not.toThrow() + session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + }) + + it('adopts a bare session first observed through publication', async () => { + const ctx = await setup() + const session = new Session(SessionId('bare-approval-session')) + const id = ApprovalRequestId('bare-ask') + const asked = { + type: 'approval/asked', seq: 0, time: 0, data: { id, toolName: 'bash' }, + } as const + const decided = { + type: 'approval/decided', seq: 1, time: 1, data: { id, outcome: 'rejected' as const }, + } as const + expect(() => { + ctx.emit('session/event', session, asked) + ctx.emit('session/event', session, decided) + }).not.toThrow() + }) + + it('rejects malformed and unpaired audit events', async () => { + const ctx = await setup() + const session = ctx.sessions.create() + const id = ApprovalRequestId('ask-1') + expect(() => session.append('approval/asked', { id, toolName: '' })) + .toThrow(/toolName must be non-empty/) + session.append('approval/asked', { id, toolName: 'bash' }) + expect(() => session.append('approval/asked', { id, toolName: 'bash' })) + .toThrow(/repeated open id/) + expect(() => session.append('approval/decided', { + id: ApprovalRequestId('missing'), outcome: 'rejected', + })).toThrow(/no matching approval\/asked/) + expect(() => session.append('approval/decided', { id, outcome: 'maybe' as never })) + .toThrow(/unknown outcome/) + expect(() => session.append('approval/policy', { policy: 'always' as never })) + .toThrow(/unknown policy/) + }) +}) diff --git a/packages/ui/user-interaction/src/invariant.ts b/packages/ui/user-interaction/src/invariant.ts index 85029b692b..f4f2f2f31e 100644 --- a/packages/ui/user-interaction/src/invariant.ts +++ b/packages/ui/user-interaction/src/invariant.ts @@ -1,27 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-user-interaction`. @module @deepseek-ai/dsh-user-interaction/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-user-interaction`. + * @module @deepseek-ai/dsh-user-interaction/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-user-interaction' /** Cordis companion plugin name. */ export const name = 'user-interaction-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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', - ], - }) -} +/** + * No runtime invariant: the single provider slot is validated at registration and asks return + * directly to their caller; the seam publishes no independent request/answer audit stream. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -30,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 d632921d35..bf29a81b4c 100644 --- a/packages/util/brand/src/invariant.ts +++ b/packages/util/brand/src/invariant.ts @@ -1,22 +1,24 @@ -/** Package-owned runtime contract for @deepseek-ai/dsh-brand. @module @deepseek-ai/dsh-brand/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-brand`. + * @module @deepseek-ai/dsh-brand/invariant + */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-brand' /** Cordis companion plugin name. */ export const name = 'brand-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Assert that the nominal-type primitive remains erased at runtime. */ -const install: InvariantInstaller = async (_ctx, fail) => { - 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') -} +/** + * No runtime invariant: this pure utility owns no event stream or mutable runtime data; its value + * algebra is enforced by unit tests. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. diff --git a/packages/util/home/src/invariant.ts b/packages/util/home/src/invariant.ts index fc7c9c3129..dbd8e38f51 100644 --- a/packages/util/home/src/invariant.ts +++ b/packages/util/home/src/invariant.ts @@ -1,27 +1,24 @@ -/** Package-owned runtime contracts for @deepseek-ai/dsh-home. @module @deepseek-ai/dsh-home/invariant */ +/** + * Package-owned invariant companion 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 { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-home' /** Cordis companion plugin name. */ export const name = 'home-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Assert the canonical environment key and configured-path precedence. */ -const install: InvariantInstaller = async (_ctx, fail) => { - 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') -} +/** + * No runtime invariant: this pure utility owns no event stream or mutable runtime data; its value + * algebra is enforced by unit tests. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. diff --git a/packages/util/paths/src/invariant.ts b/packages/util/paths/src/invariant.ts index 20a3962ff4..f1661b7f52 100644 --- a/packages/util/paths/src/invariant.ts +++ b/packages/util/paths/src/invariant.ts @@ -1,28 +1,24 @@ -/** Package-owned runtime contracts for @deepseek-ai/dsh-paths. @module @deepseek-ai/dsh-paths/invariant */ +/** + * Package-owned invariant companion 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 { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-paths' /** Cordis companion plugin name. */ export const name = 'paths-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Assert tilde expansion and explicit-over-environment home precedence. */ -const install: InvariantInstaller = async (_ctx, fail) => { - 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') -} +/** + * No runtime invariant: this pure utility owns no event stream or mutable runtime data; its value + * algebra is enforced by unit tests. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. diff --git a/packages/util/retention/src/invariant.ts b/packages/util/retention/src/invariant.ts index fa8ab7d36a..0365793b03 100644 --- a/packages/util/retention/src/invariant.ts +++ b/packages/util/retention/src/invariant.ts @@ -1,33 +1,24 @@ -/** Package-owned runtime contracts for @deepseek-ai/dsh-retention. @module @deepseek-ai/dsh-retention/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-retention`. + * @module @deepseek-ai/dsh-retention/invariant + */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-retention' /** Cordis companion plugin name. */ export const name = 'retention-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Assert exact head-retention accounting after the budget is exceeded. */ -const install: InvariantInstaller = async (_ctx, fail) => { - 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') -} +/** + * No runtime invariant: this pure utility owns no event stream or mutable runtime data; its value + * algebra is enforced by unit tests. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. diff --git a/packages/util/timeout/src/invariant.ts b/packages/util/timeout/src/invariant.ts index 2d9d22f5d6..bb9604d6b7 100644 --- a/packages/util/timeout/src/invariant.ts +++ b/packages/util/timeout/src/invariant.ts @@ -1,28 +1,24 @@ -/** Package-owned runtime contracts for @deepseek-ai/dsh-timeout. @module @deepseek-ai/dsh-timeout/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-timeout`. + * @module @deepseek-ai/dsh-timeout/invariant + */ /* jscpd:ignore-start */ import type { Context } from 'cordis' -import { assertInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-timeout' /** Cordis companion plugin name. */ export const name = 'timeout-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Assert default-before-cap arithmetic and capability-code classification. */ -const install: InvariantInstaller = async (_ctx, fail) => { - 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') -} +/** + * No runtime invariant: this pure utility owns no event stream or mutable runtime data; its value + * algebra is enforced by unit tests. + */ +const install: InvariantInstaller = () => {} /** * 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 1cceb0f166..435f9ca549 100644 --- a/packages/web/tool-web/src/invariant.ts +++ b/packages/web/tool-web/src/invariant.ts @@ -1,29 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tool-web`. @module @deepseek-ai/dsh-tool-web/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-tool-web`. + * @module @deepseek-ai/dsh-tool-web/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tool-web' /** Cordis companion plugin name. */ export const name = 'tool-web-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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()', - ], - }) -} +/** + * No runtime invariant: this model-facing adapter has no independent lifecycle stream; execution + * relations are owned by the capability seam it calls. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -32,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 5a29395715..053fb12200 100644 --- a/packages/web/web-fetch-local/src/invariant.ts +++ b/packages/web/web-fetch-local/src/invariant.ts @@ -1,27 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-web-fetch-local`. @module @deepseek-ai/dsh-web-fetch-local/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-web-fetch-local`. + * @module @deepseek-ai/dsh-web-fetch-local/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-web-fetch-local' /** Cordis companion plugin name. */ export const name = 'web-fetch-local-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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()', - ], - }) -} +/** + * No runtime invariant: this package exposes no independent event sequence or mutable data relation + * beyond contracts enforced at its owning seam. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -30,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 bfed9260dc..8b79315f7d 100644 --- a/packages/web/web-search-deepseek/src/invariant.ts +++ b/packages/web/web-search-deepseek/src/invariant.ts @@ -1,30 +1,24 @@ /** - * Package-owned runtime contract checks for `@deepseek-ai/dsh-web-search-deepseek`. + * Package-owned invariant companion for `@deepseek-ai/dsh-web-search-deepseek`. * @module @deepseek-ai/dsh-web-search-deepseek/invariant */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-web-search-deepseek' /** Cordis companion plugin name. */ export const name = 'web-search-deepseek-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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()', - ], - }) -} +/** + * No runtime invariant: this package exposes no independent event sequence or mutable data relation + * beyond contracts enforced at its owning seam. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -33,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 8d5da8d433..060ceb78ba 100644 --- a/packages/web/web-search-exa/src/invariant.ts +++ b/packages/web/web-search-exa/src/invariant.ts @@ -1,27 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-web-search-exa`. @module @deepseek-ai/dsh-web-search-exa/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-web-search-exa`. + * @module @deepseek-ai/dsh-web-search-exa/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-web-search-exa' /** Cordis companion plugin name. */ export const name = 'web-search-exa-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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()', - ], - }) -} +/** + * No runtime invariant: this package exposes no independent event sequence or mutable data relation + * beyond contracts enforced at its owning seam. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -30,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 7669f6e53a..cf3e009fed 100644 --- a/packages/web/web-search-perplexity/src/invariant.ts +++ b/packages/web/web-search-perplexity/src/invariant.ts @@ -1,30 +1,24 @@ /** - * Package-owned runtime contract checks for `@deepseek-ai/dsh-web-search-perplexity`. + * Package-owned invariant companion for `@deepseek-ai/dsh-web-search-perplexity`. * @module @deepseek-ai/dsh-web-search-perplexity/invariant */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-web-search-perplexity' /** Cordis companion plugin name. */ export const name = 'web-search-perplexity-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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()', - ], - }) -} +/** + * No runtime invariant: this package exposes no independent event sequence or mutable data relation + * beyond contracts enforced at its owning seam. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -33,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 395679cf36..2ac094b34f 100644 --- a/packages/web/web/src/invariant.ts +++ b/packages/web/web/src/invariant.ts @@ -1,27 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-web`. @module @deepseek-ai/dsh-web/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-web`. + * @module @deepseek-ai/dsh-web/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-web' /** Cordis companion plugin name. */ export const name = 'web-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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', - ], - }) -} +/** + * No runtime invariant: provider maps are private and selection/result caps are enforced on each + * call; the seam publishes no independent registry or request/result observation stream. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -30,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 966e4b3613..5f3ebc68ce 100644 --- a/packages/workflow/tool-workflow/src/invariant.ts +++ b/packages/workflow/tool-workflow/src/invariant.ts @@ -1,29 +1,24 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tool-workflow`. @module @deepseek-ai/dsh-tool-workflow/invariant */ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-tool-workflow`. + * @module @deepseek-ai/dsh-tool-workflow/invariant + */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-tool-workflow' /** Cordis companion plugin name. */ export const name = 'tool-workflow-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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()', - ], - }) -} +/** + * No runtime invariant: this model-facing adapter has no independent lifecycle stream; execution + * relations are owned by the capability seam it calls. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -32,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 27c0865d79..6845aeebff 100644 --- a/packages/workflow/workflow-workerthread/src/invariant.ts +++ b/packages/workflow/workflow-workerthread/src/invariant.ts @@ -1,33 +1,24 @@ /** - * Package-owned runtime contract checks for `@deepseek-ai/dsh-workflow-workerthread`. + * Package-owned invariant companion for `@deepseek-ai/dsh-workflow-workerthread`. * @module @deepseek-ai/dsh-workflow-workerthread/invariant */ +/* jscpd:ignore-start */ import type { Context } from 'cordis' -import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-workflow-workerthread' /** Cordis companion plugin name. */ export const name = 'workflow-workerthread-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** 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', - ], - }) -} +/** + * No runtime invariant: this process-boundary implementation exposes no same-process event relation; + * worker protocol and built-worker tests cover it. + */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -36,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ 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 a37d480e69..f6b8b8ced2 100644 --- a/packages/workflow/workflow/src/invariant.ts +++ b/packages/workflow/workflow/src/invariant.ts @@ -1,24 +1,134 @@ -/** Package-owned runtime contract checks for `@deepseek-ai/dsh-workflow`. @module @deepseek-ai/dsh-workflow/invariant */ +/** Package-owned workflow lifecycle invariants. @module @deepseek-ai/dsh-workflow/invariant */ import type { Context } from 'cordis' -import { observeServiceInvariant, serviceShapeViolation, type InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type { + WorkflowAgentEndInfo, + WorkflowAgentInfo, + WorkflowResultInfo, + WorkflowRunInfo, +} from './types.ts' const PACKAGE_NAME = '@deepseek-ai/dsh-workflow' /** Cordis companion plugin name. */ export const name = 'workflow-invariant' -/** Services required before the companion can register. */ +/** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** Validate every implementation bound to this package's service seam. */ +interface WorkflowTrace { + meta: string + agents: Map + starts: number +} + +/** Require every event for a run to retain its validated identity snapshot. */ +function traceFor( + traces: ReadonlyMap, + info: WorkflowRunInfo, + fail: InvariantFailure, +): WorkflowTrace { + const trace = traces.get(info.id) + if (trace === undefined) fail(`workflow event has no matching workflow/start for run ${JSON.stringify(info.id)}`) + if (trace.meta !== JSON.stringify(info.meta)) { + fail(`workflow event meta diverges from workflow/start for run ${JSON.stringify(info.id)}`) + } + return trace +} + +/** Assert the immutable identity fields shared by an agent pair. */ +function validateAgentEnd(start: WorkflowAgentInfo, end: WorkflowAgentEndInfo, fail: InvariantFailure): void { + if (start.label !== end.label || start.phase !== end.phase || start.childId !== end.childId) { + fail(`workflow/agent-end identity diverges from workflow/agent-start for seq ${end.seq}`) + } + const outcome: string = end.outcome + if (outcome !== 'completed' && outcome !== 'failed' && outcome !== 'cancelled') { + fail(`workflow/agent-end carries unknown outcome ${JSON.stringify(outcome)}`) + } +} + +/** Validate a terminal result against the accumulated run trace. */ +function validateWorkflowEnd(trace: WorkflowTrace, result: WorkflowResultInfo, fail: InvariantFailure): void { + if (trace.agents.size > 0) fail(`workflow/end has ${trace.agents.size} agent call(s) without workflow/agent-end`) + if (!Number.isSafeInteger(result.agentsStarted) || result.agentsStarted < trace.starts) { + fail('workflow/end agentsStarted must be a safe integer covering every observed agent start') + } + if (result.stopReason === 'completed' ? result.error !== undefined : typeof result.error !== 'string') { + fail('workflow/end error must be absent exactly for completed runs') + } +} + +/** Install workflow start/end and child-call pairing checks. */ const install: InvariantInstaller = (ctx, fail) => { - observeServiceInvariant(ctx, fail, 'workflows', value => serviceShapeViolation(value, { - methods: ['start'], - })) + const traces = new Map() + const stagedStarts = new WeakSet() + const stagedAgentStarts = new WeakSet() + const stagedAgentEnds = new WeakSet() + const stagedEnds = new WeakSet() + + ctx.on('internal/dispatch', (_mode, eventName, args) => { + if (eventName === 'workflow/start') { + const info = args[0] as WorkflowRunInfo + if (String(info.id).length === 0 || info.meta.name.length === 0 || info.meta.description.length === 0) { + fail('workflow/start id, meta.name, and meta.description must be non-empty') + } + if (traces.has(info.id)) fail(`workflow/start repeated run id ${JSON.stringify(info.id)}`) + stagedStarts.add(info) + return + } + if (!eventName.startsWith('workflow/')) return + const info = args[0] as WorkflowRunInfo + const trace = traceFor(traces, info, fail) + if (eventName === 'workflow/agent-start') { + const agent = args[1] as WorkflowAgentInfo + if (!Number.isSafeInteger(agent.seq) || agent.seq < 1 || String(agent.childId).length === 0) { + fail('workflow/agent-start seq must be positive and childId must be non-empty') + } + if (trace.agents.has(agent.seq)) fail(`workflow/agent-start repeated seq ${agent.seq}`) + stagedAgentStarts.add(agent) + return + } + if (eventName === 'workflow/agent-end') { + const agent = args[1] as WorkflowAgentEndInfo + const start = trace.agents.get(agent.seq) + if (start === undefined) return fail(`workflow/agent-end has no matching start for seq ${agent.seq}`) + validateAgentEnd(start, agent, fail) + stagedAgentEnds.add(agent) + return + } + if (eventName === 'workflow/end') { + const result = args[1] as WorkflowResultInfo + validateWorkflowEnd(trace, result, fail) + stagedEnds.add(result) + } + }, { global: true }) + + ctx.on('workflow/start', (info) => { + /* v8 ignore next -- internal/dispatch stages the same run-info object */ + if (!stagedStarts.delete(info)) return + traces.set(info.id, { meta: JSON.stringify(info.meta), agents: new Map(), starts: 0 }) + }, { global: true }) + ctx.on('workflow/agent-start', (info, agent) => { + /* v8 ignore next -- internal/dispatch stages the same agent object */ + if (!stagedAgentStarts.delete(agent)) return + const trace = traceFor(traces, info, fail) + trace.agents.set(agent.seq, agent) + trace.starts += 1 + }, { global: true }) + ctx.on('workflow/agent-end', (info, agent) => { + /* v8 ignore next -- internal/dispatch stages the same agent object */ + if (!stagedAgentEnds.delete(agent)) return + traceFor(traces, info, fail).agents.delete(agent.seq) + }, { global: true }) + ctx.on('workflow/end', (info, result) => { + /* v8 ignore next -- internal/dispatch stages the same result object */ + if (!stagedEnds.delete(result)) return + traces.delete(info.id) + }, { global: true }) } /** - * Register this package's invariant companion. + * Register the workflow invariant companion. * @param ctx - Cordis context carrying the invariant service. * @returns the installed registration's disposer after setup succeeds. */ diff --git a/packages/workflow/workflow/tests/invariant.spec.ts b/packages/workflow/workflow/tests/invariant.spec.ts new file mode 100644 index 0000000000..671a7a3e86 --- /dev/null +++ b/packages/workflow/workflow/tests/invariant.spec.ts @@ -0,0 +1,108 @@ +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import { SessionId } from '@deepseek-ai/dsh-session' +import { WorkflowRunId } from '@deepseek-ai/dsh-workflow' +import type { + WorkflowAgentEndInfo, + WorkflowAgentInfo, + WorkflowResultInfo, + WorkflowRunInfo, +} from '@deepseek-ai/dsh-workflow' +import * as WorkflowInvariant from '@deepseek-ai/dsh-workflow/invariant' +import InvariantService from '@deepseek-ai/dsh-invariants' + +async function setup(): Promise { + const ctx = new Context() + await ctx.plugin(InvariantService) + await ctx.plugin(WorkflowInvariant) + return ctx +} + +const info = (overrides: Partial = {}): WorkflowRunInfo => ({ + id: WorkflowRunId('workflow-1'), + meta: { name: 'review', description: 'Review a change' }, + ...overrides, +}) + +const agent = (overrides: Partial = {}): WorkflowAgentInfo => ({ + seq: 1, + label: 'reviewer', + childId: SessionId('child-1'), + ...overrides, +}) + +const agentEnd = (overrides: Partial = {}): WorkflowAgentEndInfo => ({ + ...agent(), + outcome: 'completed', + ...overrides, +}) + +const result = (overrides: Partial = {}): WorkflowResultInfo => ({ + stopReason: 'completed', + agentsStarted: 1, + ...overrides, +}) + +describe('workflow invariants', () => { + it('accepts a complete workflow and child lifecycle', async () => { + const ctx = await setup() + const run = info() + ctx.emit('workflow/start', run) + ctx.emit('workflow/phase', run, 'inspect') + ctx.emit('workflow/log', run, 'working') + ctx.emit('workflow/agent-start', run, agent()) + ctx.emit('workflow/agent-end', run, agentEnd()) + ctx.emit('workflow/end', run, result()) + ctx.emit('tools/change') + }) + + it('rejects invalid run identity and enclosure', async () => { + const ctx = await setup() + expect(() => { ctx.emit('workflow/start', info({ id: WorkflowRunId('') })) }).toThrow(/must be non-empty/) + const run = info() + ctx.emit('workflow/start', run) + expect(() => { ctx.emit('workflow/start', run) }).toThrow(/repeated run id/) + expect(() => { ctx.emit('workflow/log', info({ meta: { name: 'other', description: 'x' } }), 'x') }) + .toThrow(/meta diverges/) + const fresh = await setup() + expect(() => { fresh.emit('workflow/log', info(), 'x') }).toThrow(/no matching workflow\/start/) + }) + + it('rejects malformed and unpaired child lifecycles', async () => { + const ctx = await setup() + const run = info() + ctx.emit('workflow/start', run) + expect(() => { ctx.emit('workflow/agent-start', run, agent({ seq: 0 })) }).toThrow(/seq must be positive/) + ctx.emit('workflow/agent-start', run, agent()) + expect(() => { ctx.emit('workflow/agent-start', run, agent()) }).toThrow(/repeated seq/) + expect(() => { ctx.emit('workflow/agent-end', run, agentEnd({ seq: 2 })) }).toThrow(/no matching start/) + expect(() => { ctx.emit('workflow/agent-end', run, agentEnd({ childId: SessionId('other') })) }) + .toThrow(/identity diverges/) + expect(() => { ctx.emit('workflow/agent-end', run, agentEnd({ outcome: 'unknown' as never })) }) + .toThrow(/unknown outcome/) + }) + + it('rejects inconsistent terminal results', async () => { + const active = await setup() + active.emit('workflow/start', info()) + active.emit('workflow/agent-start', info(), agent()) + expect(() => { active.emit('workflow/end', info(), result()) }).toThrow(/without workflow\/agent-end/) + + const count = await setup() + count.emit('workflow/start', info()) + count.emit('workflow/agent-start', info(), agent()) + count.emit('workflow/agent-end', info(), agentEnd()) + expect(() => { count.emit('workflow/end', info(), result({ agentsStarted: 0 })) }) + .toThrow(/covering every observed agent start/) + + const completed = await setup() + completed.emit('workflow/start', info()) + expect(() => { completed.emit('workflow/end', info(), result({ error: 'unexpected' })) }) + .toThrow(/absent exactly for completed/) + + const failed = await setup() + failed.emit('workflow/start', info()) + expect(() => { failed.emit('workflow/end', info(), result({ stopReason: 'error' })) }) + .toThrow(/absent exactly for completed/) + }) +}) diff --git a/packages/workflow/workflow/tests/workflow.spec.ts b/packages/workflow/workflow/tests/workflow.spec.ts index 0df3824071..01413ab75f 100644 --- a/packages/workflow/workflow/tests/workflow.spec.ts +++ b/packages/workflow/workflow/tests/workflow.spec.ts @@ -58,8 +58,11 @@ describe('dsh-workflow (interface)', () => { ctx.on('workflow/log', (info, message) => { seen.push([info, message]) }) ctx.on('workflow/agent-start', (info, agent) => { seen.push([info, agent]) }) const engine = ctx.workflows as StubEngine + engine.emit('workflow/start', INFO) engine.emit('workflow/log', INFO, 'hello') engine.emit('workflow/agent-start', INFO, { seq: 1, label: 'l', childId: 'c' }) + engine.emit('workflow/agent-end', INFO, { seq: 1, label: 'l', childId: 'c', outcome: 'completed' }) + engine.emit('workflow/end', INFO, { stopReason: 'completed', agentsStarted: 1 }) expect(seen).toEqual([ [INFO, 'hello'], [INFO, { seq: 1, label: 'l', childId: 'c' }], @@ -77,8 +80,11 @@ describe('dsh-workflow (interface)', () => { ctx.on('workflow/agent-start', (_info, agent) => { seen.push(agent.label) }) const engine = ctx.workflows as StubEngine const payload = { seq: 1, label: 'original', childId: 'c' } + engine.emit('workflow/start', INFO) engine.emit('workflow/agent-start', INFO, payload) await Promise.resolve() + engine.emit('workflow/agent-end', INFO, { ...payload, outcome: 'completed' }) + engine.emit('workflow/end', INFO, { stopReason: 'completed', agentsStarted: 1 }) expect(seen).toEqual(['original']) expect(String(warn.mock.calls[0]![0])).toContain('listener rejected') }) @@ -91,7 +97,9 @@ describe('dsh-workflow (interface)', () => { ctx.on('workflow/phase', () => { throw new Error('bad listener') }) ctx.on('workflow/phase', (_info, title) => { reached.push(title) }) const engine = ctx.workflows as StubEngine + engine.emit('workflow/start', INFO) expect(() => { engine.emit('workflow/phase', INFO, 'Scan') }).not.toThrow() + engine.emit('workflow/end', INFO, { stopReason: 'completed', agentsStarted: 0 }) expect(reached).toEqual(['Scan']) expect(warn).toHaveBeenCalledOnce() expect(String(warn.mock.calls[0]![0])).toContain('workflow/phase listener threw') @@ -107,7 +115,9 @@ describe('dsh-workflow (interface)', () => { }) ctx.on('workflow/phase', (_info, title) => { reached.push(title) }) const engine = ctx.workflows as StubEngine + engine.emit('workflow/start', INFO) expect(() => { engine.emit('workflow/phase', INFO, 'Scan') }).not.toThrow() + engine.emit('workflow/end', INFO, { stopReason: 'completed', agentsStarted: 0 }) expect(reached).toEqual(['Scan']) expect(warn).toHaveBeenCalledOnce() expect(String(warn.mock.calls[0]![0])).toContain('[unrenderable thrown value]') diff --git a/scripts/package-invariants.spec.ts b/scripts/package-invariants.spec.ts index 9995d078bf..1014ce5033 100644 --- a/scripts/package-invariants.spec.ts +++ b/scripts/package-invariants.spec.ts @@ -16,8 +16,10 @@ 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') +const install = (ctx: { on(name: string, listener: (value: number) => void): void }, fail: (message: string) => never) => { + ctx.on('probe/value', (value) => { + if (value < 0) fail('observed values must be non-negative') + }) } export const apply = (ctx: { invariants: { register(name: string, install: typeof install): () => void } }) => Promise.resolve(ctx.invariants.register(${JSON.stringify(packageName)}, install)) @@ -65,41 +67,6 @@ 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 hand-owned checking companion with publication metadata', () => { expect(collectPackageInvariantViolations(fixture())).toEqual([]) @@ -139,27 +106,24 @@ export const apply = (ctx: { invariants: { register(name: string, install: typeo ])) }) - it('rejects generated markers and empty or reporter-free installers', () => { + it('rejects generated markers and reporter-free executable installers', () => { const generated = fixture({ source: `/** @generated */\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({ + const reporterFree = fixture({ source: ` export const name = 'probe-invariant' export const inject = ['invariants'] -const install = () => {} +const install = () => { 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(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', - ])) + expect(collectPackageInvariantViolations(reporterFree).map(violation => violation.message)) + .toContain('install function must accept the bound failure reporter as its second parameter') const unused = fixture({ source: ` @@ -174,22 +138,19 @@ export const apply = (ctx: { invariants: { register(name: string, install: typeo .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('name-based plugin invariant "shared-runtime-name" is already owned by "@deepseek-ai/dsh-probe-two"') - }) + it('accepts explained empty installers and rejects unexplained ones', () => { + const explained = ` +export const name = 'probe-invariant' +export const inject = ['invariants'] +const PACKAGE_NAME = '@deepseek-ai/dsh-probe' +/** No runtime invariant: this pure package owns no events or mutable data. */ +const install = () => {} +export const apply = (ctx: { invariants: { register(name: string, install: () => void): () => void } }) => + ctx.invariants.register(PACKAGE_NAME, install) +` + expect(collectPackageInvariantViolations(fixture({ source: explained }))).toEqual([]) - it('rejects an unexplained empty package installer', () => { - const source = ` + const unexplained = ` export const name = 'probe-invariant' export const inject = ['invariants'] const PACKAGE_NAME = '@deepseek-ai/dsh-probe' @@ -197,7 +158,7 @@ const install = () => {} export const apply = (ctx: { invariants: { register(name: string, install: () => void): () => void } }) => ctx.invariants.register(PACKAGE_NAME, install) ` - expect(collectPackageInvariantViolations(fixture({ source })).map(violation => violation.message)) + expect(collectPackageInvariantViolations(fixture({ source: unexplained })).map(violation => violation.message)) .toContain('empty install function must explain why with a "No runtime invariant:" comment') }) }) diff --git a/scripts/package-invariants.ts b/scripts/package-invariants.ts index 6448888680..4600d04ee3 100644 --- a/scripts/package-invariants.ts +++ b/scripts/package-invariants.ts @@ -8,6 +8,9 @@ import { existsSync, globSync, readFileSync } from 'node:fs' import { dirname, relative, resolve, sep } from 'node:path' import ts from 'typescript' +/** Required explanation marker for an intentionally empty installer. */ +export const NO_RUNTIME_INVARIANT_MARKER = 'No runtime invariant:' + interface PackageManifest { name?: string exports?: Record @@ -53,23 +56,11 @@ export function packageInvariantOwners(root: string): PackageInvariantOwner[] { /** 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) - 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)}`, - ) - } - } + checkSource(owner, root, violations) } return violations } @@ -151,11 +142,11 @@ function checkSource( owner: PackageInvariantOwner, root: string, violations: PackageInvariantViolation[], -): string[] { +): void { 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')) { @@ -206,49 +197,26 @@ 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 + checkInstaller(owner, sourceFile, sourceText, violations) } function checkInstaller( owner: PackageInvariantOwner, sourceFile: ts.SourceFile, + sourceText: string, violations: PackageInvariantViolation[], ): void { let initializer: ts.Expression | undefined + let declarationStatement: ts.VariableStatement | 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 + && declaration.initializer !== undefined) { + initializer = declaration.initializer + declarationStatement = statement + } } } const installer = initializer === undefined ? undefined : installerFunction(initializer) @@ -257,7 +225,17 @@ function checkInstaller( 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 declarationText = declarationStatement === undefined + ? '' + : sourceText.slice(declarationStatement.getFullStart(), declarationStatement.getEnd()) + if (!declarationText.includes(NO_RUNTIME_INVARIANT_MARKER)) { + addViolation( + violations, + owner.sourcePath, + `empty install function must explain why with a "${NO_RUNTIME_INVARIANT_MARKER}" comment`, + ) + } + return } const reporter = installer.parameters[1]?.name if (reporter === undefined || !ts.isIdentifier(reporter)) { diff --git a/scripts/test-invariants.spec.ts b/scripts/test-invariants.spec.ts index 5c3b9120bc..56837e92fb 100644 --- a/scripts/test-invariants.spec.ts +++ b/scripts/test-invariants.spec.ts @@ -72,10 +72,24 @@ describe('global test invariant host', () => { it('limits manual composition to focused invariant topology tests', () => { expect(MANUAL_INVARIANT_TESTS).toEqual([ '/packages/support/invariants/tests/service.spec.ts', + '/packages/bash/bash/tests/invariant.spec.ts', + '/packages/compact/compact/tests/invariant.spec.ts', + '/packages/context/time-context/tests/invariant.spec.ts', '/packages/core/session/tests/invariant.spec.ts', '/packages/core/agent/tests/invariant.spec.ts', '/packages/core/scope/tests/invariant.spec.ts', '/packages/core/agent-loop/tests/invariant.spec.ts', + '/packages/core/system-prompt/tests/invariant.spec.ts', + '/packages/core/tools/tests/invariant.spec.ts', + '/packages/fs/fs/tests/invariant.spec.ts', + '/packages/hooks/hook-protocol/tests/invariant.spec.ts', + '/packages/llm/llm/tests/invariant.spec.ts', + '/packages/subagent/subagent/tests/invariant.spec.ts', + '/packages/tasks/tasks/tests/invariant.spec.ts', + '/packages/todo/tool-todo/tests/invariant.spec.ts', + '/packages/ui/permission/tests/invariant.spec.ts', + '/packages/ui/user-approval/tests/invariant.spec.ts', + '/packages/workflow/workflow/tests/invariant.spec.ts', '/packages/examples/agent-spine-demo/tests/agent-core.spec.ts', ]) }) diff --git a/scripts/test-invariants.ts b/scripts/test-invariants.ts index b91fdda462..1c8c1302d6 100644 --- a/scripts/test-invariants.ts +++ b/scripts/test-invariants.ts @@ -31,10 +31,24 @@ export const testInvariantCompanions: Readonly to its source. Package // dir names are unique across groups, so first-on-disk-wins resolution is // unambiguous; adding a package under an existing group needs no edit From b8ad62eb359409dd688ef3c2401fd6af27d2bf7f Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:04:40 +0800 Subject: [PATCH 09/12] fix(invariants): finish current-master ownership migration --- docs/cordis-catalog/services.md | 2 +- packages/AGENTS.md | 4 ++-- packages/compact/compact/src/invariant.ts | 3 +++ packages/hooks/hook-protocol/src/invariant.ts | 3 +++ packages/spill/spill-policy/tests/spill-policy.spec.ts | 10 ---------- packages/support/invariants/README.md | 2 +- packages/ui/user-approval/src/invariant.ts | 3 +++ scripts/package-invariants.ts | 2 +- scripts/test-invariants.spec.ts | 2 +- scripts/test-invariants.ts | 2 +- 10 files changed, 16 insertions(+), 17 deletions(-) diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index d494c5827f..4b0e132da8 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -501,7 +501,7 @@ Package-owned invariant registry with global and regex-based selection. register(packageName: string, installer: InvariantInstaller): () => void ``` -Source: [`packages/support/invariants/src/index.ts:388`](../../packages/support/invariants/src/index.ts) +Source: [`packages/support/invariants/src/index.ts:94`](../../packages/support/invariants/src/index.ts) ## `ctx.llm` — `LlmService` diff --git a/packages/AGENTS.md b/packages/AGENTS.md index 16ee490395..77a4712e04 100644 --- a/packages/AGENTS.md +++ b/packages/AGENTS.md @@ -15,8 +15,8 @@ These package-specific rules supplement the repo-wide [conventions](../AGENTS.md - **Enforce at the operation boundary that owns the decision.** Schema omission, prompt filtering, facades, wrappers, and listener order are not enforcement when direct or alternate callers can bypass them; test denial through the executor. - **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 explicit invariant companion.** Publish `./invariant` and register its manifest name. Check observable event or mutable-data relationships; when none exists, keep an empty installer with a package-specific `No runtime invariant:` explanation instead of inventing an API-shape assertion. Generated companions, unexplained empties, and non-empty installers that ignore the reporter fail `verify-package-invariants` ([rationale](../.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md)). +- **Registry contributions prove disposal** through the HMR-safety test required by [testing policy](../docs/testing.md): dispose the fiber and observe removal. +- **Every package owns `./invariant`.** Register the manifest name; check an event/data relation or give empty installers package-specific `No runtime invariant:` reasons. Generated companions, unexplained empties, and ignored reporters fail [`verify-package-invariants`](../.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md). Naming notes: diff --git a/packages/compact/compact/src/invariant.ts b/packages/compact/compact/src/invariant.ts index 59fdc806b2..da5d5eba5a 100644 --- a/packages/compact/compact/src/invariant.ts +++ b/packages/compact/compact/src/invariant.ts @@ -66,6 +66,8 @@ function applyCompactionTransition( } /** Install compaction start/summary/end checks. */ +// Event owners keep precommit staging local so their vocabularies never move into a central helper. +/* jscpd:ignore-start */ const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => { const traces = new WeakMap() const staged = new WeakMap() @@ -98,6 +100,7 @@ const install: InvariantInstaller = Object.assign((ctx: Context, fail: Invariant if (transition !== undefined) staged.set(event, { session, transition }) }, { global: true }) }, { inject: ['sessions'] }) +/* jscpd:ignore-end */ /** * Register the compact invariant companion. diff --git a/packages/hooks/hook-protocol/src/invariant.ts b/packages/hooks/hook-protocol/src/invariant.ts index 73d6f08e3b..6972d109da 100644 --- a/packages/hooks/hook-protocol/src/invariant.ts +++ b/packages/hooks/hook-protocol/src/invariant.ts @@ -57,6 +57,8 @@ function applyHookTransition(pending: Map, transition: HookTrans } /** Install hook invoked/result pairing checks. */ +// Event owners keep precommit staging local so their vocabularies never move into a central helper. +/* jscpd:ignore-start */ const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => { const traces = new WeakMap>() const staged = new WeakMap() @@ -88,6 +90,7 @@ const install: InvariantInstaller = Object.assign((ctx: Context, fail: Invariant if (transition !== undefined) staged.set(event, { session, transition }) }, { global: true }) }, { inject: ['sessions'] }) +/* jscpd:ignore-end */ /** * Register the hook-protocol invariant companion. diff --git a/packages/spill/spill-policy/tests/spill-policy.spec.ts b/packages/spill/spill-policy/tests/spill-policy.spec.ts index f0cbbbd2a0..d249430ab1 100644 --- a/packages/spill/spill-policy/tests/spill-policy.spec.ts +++ b/packages/spill/spill-policy/tests/spill-policy.spec.ts @@ -110,16 +110,6 @@ describe('config validation', () => { 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/support/invariants/README.md b/packages/support/invariants/README.md index 58cf2e8f2a..4cd2aa1f9b 100644 --- a/packages/support/invariants/README.md +++ b/packages/support/invariants/README.md @@ -69,7 +69,7 @@ Every ordinary Vitest topology mounts an explicitly enabled service and the curr ## Model Experience -None. The service and companions observe runtime events, mutable snapshots, and requests but never alter prompts, messages, schemas, streams, or tool results. +None, as the service and companions observe runtime events and mutable snapshots without altering prompts, messages, schemas, streams, or tool results. #### KV Cache effect diff --git a/packages/ui/user-approval/src/invariant.ts b/packages/ui/user-approval/src/invariant.ts index 9fc42fa908..5643e412c5 100644 --- a/packages/ui/user-approval/src/invariant.ts +++ b/packages/ui/user-approval/src/invariant.ts @@ -49,6 +49,8 @@ function applyApprovalTransition(pending: Set, transition: Ap } /** Install audit pairing and closed-vocabulary checks. */ +// Event owners keep precommit staging local so their vocabularies never move into a central helper. +/* jscpd:ignore-start */ const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => { const traces = new WeakMap>() const staged = new WeakMap() @@ -80,6 +82,7 @@ const install: InvariantInstaller = Object.assign((ctx: Context, fail: Invariant if (transition !== undefined) staged.set(event, { session, transition }) }, { global: true }) }, { inject: ['sessions'] }) +/* jscpd:ignore-end */ /** * Register the approval invariant companion. diff --git a/scripts/package-invariants.ts b/scripts/package-invariants.ts index 4600d04ee3..6b099c084d 100644 --- a/scripts/package-invariants.ts +++ b/scripts/package-invariants.ts @@ -9,7 +9,7 @@ import { dirname, relative, resolve, sep } from 'node:path' import ts from 'typescript' /** Required explanation marker for an intentionally empty installer. */ -export const NO_RUNTIME_INVARIANT_MARKER = 'No runtime invariant:' +const NO_RUNTIME_INVARIANT_MARKER = 'No runtime invariant:' interface PackageManifest { name?: string diff --git a/scripts/test-invariants.spec.ts b/scripts/test-invariants.spec.ts index 56837e92fb..52182cafaf 100644 --- a/scripts/test-invariants.spec.ts +++ b/scripts/test-invariants.spec.ts @@ -72,7 +72,6 @@ describe('global test invariant host', () => { it('limits manual composition to focused invariant topology tests', () => { expect(MANUAL_INVARIANT_TESTS).toEqual([ '/packages/support/invariants/tests/service.spec.ts', - '/packages/bash/bash/tests/invariant.spec.ts', '/packages/compact/compact/tests/invariant.spec.ts', '/packages/context/time-context/tests/invariant.spec.ts', '/packages/core/session/tests/invariant.spec.ts', @@ -84,6 +83,7 @@ describe('global test invariant host', () => { '/packages/fs/fs/tests/invariant.spec.ts', '/packages/hooks/hook-protocol/tests/invariant.spec.ts', '/packages/llm/llm/tests/invariant.spec.ts', + '/packages/sandbox/sandbox-policy/tests/invariant.spec.ts', '/packages/subagent/subagent/tests/invariant.spec.ts', '/packages/tasks/tasks/tests/invariant.spec.ts', '/packages/todo/tool-todo/tests/invariant.spec.ts', diff --git a/scripts/test-invariants.ts b/scripts/test-invariants.ts index 1c8c1302d6..2f5eec6271 100644 --- a/scripts/test-invariants.ts +++ b/scripts/test-invariants.ts @@ -31,7 +31,6 @@ export const testInvariantCompanions: Readonly Date: Mon, 20 Jul 2026 20:09:48 +0800 Subject: [PATCH 10/12] test(session): preserve surface boundary coverage --- packages/core/session/tests/surface.spec.ts | 49 +++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/packages/core/session/tests/surface.spec.ts b/packages/core/session/tests/surface.spec.ts index fc8ebfcd10..5243564b6c 100644 --- a/packages/core/session/tests/surface.spec.ts +++ b/packages/core/session/tests/surface.spec.ts @@ -30,6 +30,28 @@ function provenanceEvent(seq: number, sourceEventSeqs: unknown): SessionEvent { } as unknown as SessionEvent } +function toolResultEvent( + seq: number, + callId: string, + surfaceOp: SurfaceEvent['surfaceOp'] = 'append', + sourceEventSeqs?: number[], +): SessionEvent { + return { + type: 'tool/result', + seq, + time: seq, + data: { + turn: 1, + step: 1, + callId: CallId(callId), + content: [{ type: 'text', text: `result ${seq}` }], + isError: false, + }, + surfaceOp, + ...sourceEventSeqs === undefined ? {} : { sourceEventSeqs }, + } +} + describe('foldSurface provenance', () => { it('accepts absent or valid provenance and complete replacement coverage', () => { const events = [ @@ -94,6 +116,33 @@ describe('foldSurface provenance', () => { ) }) +describe('foldSurface tool-result rewrites', () => { + it('rejects a replacement spanning multiple current nodes', () => { + const events = [ + provenanceEvent(0, undefined), + provenanceEvent(1, undefined), + toolResultEvent(2, 'rewrite', { op: 'replace', start: 0, end: 1 }, [0, 1]), + ] + expect(() => foldSurface(events)).toThrow(/must rewrite exactly one current node/) + }) + + it('rejects a replacement targeting a non-result node', () => { + const events = [ + provenanceEvent(0, undefined), + toolResultEvent(1, 'rewrite', { op: 'replace', start: 0, end: 0 }, [0]), + ] + expect(() => foldSurface(events)).toThrow(/must target a current tool\/result/) + }) + + it('rejects changes outside tool-result content', () => { + const events = [ + toolResultEvent(0, 'original'), + toolResultEvent(1, 'changed', { op: 'replace', start: 0, end: 0 }, [0]), + ] + expect(() => foldSurface(events)).toThrow(/may change only content/) + }) +}) + describe('SurfaceManager', () => { it('shares ordered entries and nested replacement ranges with foldSurface', () => { const s = new Session(SessionId('shared-fold')) From d897adac87d0adf2f38d580466c079f1329dcc64 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:16:22 +0800 Subject: [PATCH 11/12] fix(invariants): keep registration gate final-head clean --- packages/AGENTS.md | 4 ++-- scripts/package-invariants.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/AGENTS.md b/packages/AGENTS.md index f1403bb973..43993b9a63 100644 --- a/packages/AGENTS.md +++ b/packages/AGENTS.md @@ -15,8 +15,8 @@ These package-specific rules supplement the repo-wide [conventions](../AGENTS.md - **Enforce at the operation boundary that owns the decision.** Schema omission, prompt filtering, facades, wrappers, and listener order are not enforcement when direct or alternate callers can bypass them; test denial through the executor. - **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` and register its manifest name. Check observable event or mutable-data relations; when none exists, keep an empty installer with a `No runtime invariant:` comment explaining why instead of inventing a shape or presence assertion. `verify-package-invariants` gates source and publication wiring ([rationale](../.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.md)). +- **Registry contributions prove disposal** through an HMR test: dispose the contributing fiber and observe removal ([testing policy](../docs/testing.md)). +- **Every package owns `./invariant`.** Register the manifest name; check an event/data relation or give an empty installer a package-specific `No runtime invariant:` reason. `verify-package-invariants` gates the contract ([rationale](../.agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.md)). Naming notes: diff --git a/scripts/package-invariants.ts b/scripts/package-invariants.ts index 0078a6a4c4..a635a4e583 100644 --- a/scripts/package-invariants.ts +++ b/scripts/package-invariants.ts @@ -12,7 +12,7 @@ import ts from 'typescript' export const GENERATED_INVARIANT_MARKER = '@generated scripts/gen-package-invariants.ts' /** Required explanation marker for an intentionally empty installer. */ -export const NO_RUNTIME_INVARIANT_MARKER = 'No runtime invariant:' +const NO_RUNTIME_INVARIANT_MARKER = 'No runtime invariant:' interface PackageManifest { name?: string From c25e89cb4ce3990a2ae0bb146c45a85be51cc4f0 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 20 Jul 2026 21:23:57 +0800 Subject: [PATCH 12/12] fix(invariants): reconcile TUI package rename --- packages/examples/tui-demo/package.json | 1 + packages/ui/stdio/src/invariant.ts | 30 ------------------------- pnpm-lock.yaml | 3 +++ 3 files changed, 4 insertions(+), 30 deletions(-) delete mode 100644 packages/ui/stdio/src/invariant.ts diff --git a/packages/examples/tui-demo/package.json b/packages/examples/tui-demo/package.json index c356105964..841505fb00 100644 --- a/packages/examples/tui-demo/package.json +++ b/packages/examples/tui-demo/package.json @@ -64,6 +64,7 @@ "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^", + "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tui": "workspace:^", "@deepseek-ai/dsh-tool-ask-user": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", diff --git a/packages/ui/stdio/src/invariant.ts b/packages/ui/stdio/src/invariant.ts deleted file mode 100644 index 37176f11c6..0000000000 --- a/packages/ui/stdio/src/invariant.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * 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 - */ - -/* jscpd:ignore-start */ -import type { Context } from 'cordis' -import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' - -const PACKAGE_NAME = '@deepseek-ai/dsh-stdio' - -/** Cordis companion plugin name. */ -export const name = 'stdio-invariant' -/** Services required before the companion can register. */ -export const inject = ['invariants'] - -/** No runtime invariant: no package-owned event or mutable-data relation has been identified yet. */ -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 */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9067fc39b4..5bd0959770 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -930,6 +930,9 @@ importers: '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:^ version: link:../../session-persistence/session-persistence-jsonl + '@deepseek-ai/dsh-system-prompt': + specifier: workspace:^ + version: link:../../core/system-prompt '@deepseek-ai/dsh-tool-ask-user': specifier: workspace:^ version: link:../../ui/tool-ask-user