Merge remote-tracking branch 'origin/master' into codex/simp-prune-workflow-worker-surface

This commit is contained in:
Tianyi Cui
2026-07-15 01:49:00 +08:00
24 changed files with 1071 additions and 248 deletions

View File

@@ -110,7 +110,7 @@ Every session event is turn-enclosed. Reloading a crashed session preserves the
### Agent Scope
Every live agent owns a scoped `agent.ctx`. Its registrations shadow same-named globals, receive only that agent's dispatches, and unwind with the agent. `CreateAgentOptions.setup(agentCtx)` composes the scope before publication. See the [agent-scope RFC](rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md); subagent composition controls are documented [separately](rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md).
Every live agent owns a scoped `agent.ctx`. Its registrations shadow same-named globals, receive only that agent's dispatches, and unwind with the agent. `CreateAgentOptions.setup(agentCtx)` composes the scope before publication. The [semantic-gates RFC](rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md) defines typed resolvers that derive carrier checks from merged `Events` signatures and `scopeTarget`, eliminating the handwritten event table. See the [agent-scope RFC](rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md); subagent composition controls are documented [separately](rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md).
## State

View File

@@ -245,7 +245,7 @@ Creation announcement during session publication. A synchronous throw vetoes and
'session/created'(this: Scoped<Session>, session: Session): void
```
Source: [`packages/core/session/src/index.ts:46`](../../packages/core/session/src/index.ts)
Source: [`packages/core/session/src/index.ts:47`](../../packages/core/session/src/index.ts)
### `session/disposed` — emit
@@ -255,7 +255,7 @@ Emitted once when an announced session leaves the store, including publication r
'session/disposed'(this: Scoped<Session>, session: Session): void
```
Source: [`packages/core/session/src/index.ts:55`](../../packages/core/session/src/index.ts)
Source: [`packages/core/session/src/index.ts:57`](../../packages/core/session/src/index.ts)
### `session/event` — emit
@@ -267,7 +267,7 @@ Post-commit, fire-and-forget append feed. The listener snapshot resolves before
Types: [SessionEvent](../core-data-structures/core.md)
Source: [`packages/core/session/src/index.ts:66`](../../packages/core/session/src/index.ts)
Source: [`packages/core/session/src/index.ts:69`](../../packages/core/session/src/index.ts)
### `session/flush` — parallel
@@ -277,7 +277,7 @@ Awaited parallel durability checkpoint: every listener runs and the caller await
'session/flush'(this: Scoped<Session>, session: Session): Promise<void> | void
```
Source: [`packages/core/session/src/index.ts:75`](../../packages/core/session/src/index.ts)
Source: [`packages/core/session/src/index.ts:79`](../../packages/core/session/src/index.ts)
## `skill/*`
@@ -311,7 +311,7 @@ A ready child settled. Scope-filtered dispatch uses the same delegating parent c
'subagent/end'(this: Scoped<SubagentService>, info: SubagentRunEndInfo): void
```
Source: [`packages/subagent/subagent/src/index.ts:90`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:92`](../../packages/subagent/subagent/src/index.ts)
### `subagent/provider-added` — emit
@@ -341,7 +341,7 @@ A provider established a ready child. For in-process providers, `ctx.agents.get(
'subagent/start'(this: Scoped<SubagentService>, info: SubagentRunInfo): void
```
Source: [`packages/subagent/subagent/src/index.ts:82`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:83`](../../packages/subagent/subagent/src/index.ts)
## `system-prompt/*`

View File

@@ -200,7 +200,7 @@ list(): Session[]
fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session
```
Source: [`packages/core/session/src/index.ts:560`](../../packages/core/session/src/index.ts)
Source: [`packages/core/session/src/index.ts:564`](../../packages/core/session/src/index.ts)
## `ctx.skills` — `SkillService`
@@ -226,7 +226,7 @@ list(): string[]
async start(name: string, request: SubagentStartRequest): Promise<SubagentRun>
```
Source: [`packages/subagent/subagent/src/index.ts:123`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:125`](../../packages/subagent/subagent/src/index.ts)
## `ctx.systemPrompt` — `SystemPrompt`

View File

@@ -3,7 +3,7 @@
# Event Producer And Consumer Matrix
This matrix shows which packages dispatch each harness-owned event and which packages listen to it. It is intentionally a table rather than one large graph: events are many-to-many, and dense relation data is easier to review in rows. Dynamic dispatch overrides cover sites that deliberately bypass `ctx.emit`, such as subagent lifecycle containment.
This matrix shows which packages dispatch each harness-owned event and which packages listen to it. It is intentionally a table rather than one large graph: events are many-to-many, and dense relation data is easier to review in rows. Receiver and event-name types also cover contained dispatch sites that deliberately bypass `ctx.emit`, such as subagent lifecycle containment.
| Event | Mode | Declared in | Dispatchers | Listeners |
| --- | --- | --- | --- | --- |
@@ -15,7 +15,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `agent/queued` | `emit` | [`packages/core/agent/src/types.ts:167`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | - |
| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:224`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
| `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:239`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`tool-skill`](../packages/skill/tool-skill) |
| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:180`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`invariants`](../packages/support/invariants) |
| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:180`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:157`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`invariants`](../packages/support/invariants), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`stdio-agent`](../packages/ui/stdio-agent) |
| `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:250`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:260`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
@@ -25,16 +25,16 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:68`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`) | [`fs-policy`](../packages/fs/fs-policy) |
| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:51`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:39`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`invariants`](../packages/support/invariants), [`llm-replay`](../packages/support/llm-replay) |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:46`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:55`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | - |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:66`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/ui/acp), [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence), [`stdio-agent`](../packages/ui/stdio-agent) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:75`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence) |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:47`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:57`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | - |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:69`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/ui/acp), [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence), [`stdio-agent`](../packages/ui/stdio-agent) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:79`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence) |
| `skill/provider-added` | `emit` | [`packages/skill/skill/src/index.ts:131`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`emit`) | - |
| `skill/provider-removed` | `emit` | [`packages/skill/skill/src/index.ts:137`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`emit`) | - |
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:90`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`jsonrpc`](../packages/ui/jsonrpc) |
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:92`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`jsonrpc`](../packages/ui/jsonrpc) |
| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:66`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:72`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) |
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:82`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude) |
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:83`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude) |
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:27`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | - |
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:33`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:116`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - |
@@ -55,4 +55,4 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| --- | --- | --- |
| `internal/dispatch` | - | [`invariants`](../packages/support/invariants) |
Maintenance mode: hybrid generated: Cordis event declarations and most producer/listener edges are AST-scanned; dynamic dispatch sites are classified in `scripts/gen-doc-graphs.ts`.
Maintenance mode: generated: Cordis event declarations and producer/listener edges are resolved from the repository TypeScript Program.

View File

@@ -174,6 +174,7 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand;
| [Parallel pre-push gates](implemented/process/2026-07-06-parallel-pre-push-gates.md) | 2026-07-06 |
| [A gated Known-Limitations section in every package README](implemented/process/2026-07-10-readme-known-limitations-gate.md) | 2026-07-10 |
| [Package Model Experience contract](implemented/process/2026-07-12-package-model-experience-contract.md) | 2026-07-12 |
| [TypeScript Program-backed semantic gates](implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md) | 2026-07-14 |
### Testing

View File

@@ -328,7 +328,7 @@ The plugin does not police trusted setup by scanning registries or reject prompt
### Generated artifacts keep public contracts aligned
The event catalog, service catalog, producer/consumer matrix, configuration catalog, module graph, tool catalog, and type-equivalence blocks are generated or freshness-gated from source. `verify-scoped-dispatch` keeps the declared scoped-event set aligned with runtime invariant coverage.
The event catalog, service catalog, producer/consumer matrix, configuration catalog, module graph, tool catalog, type-equivalence blocks, and scoped-event resolver map are generated or freshness-gated from source. The [TypeScript semantic-gates RFC](../process/2026-07-14-typescript-program-backed-semantic-gates.md) owns Program construction, semantic event discovery, and resolver-generation rules.
Behavioral tests pin scoped routing and disposal, final-entry collision cleanup, publication rollback, ordered quiescence, durable pre/post-commit behavior, live tool filtering across presentation and execution, cooperative prompt assembly, structured-output commit in native and Code Mode, async subagent startup and signal cancellation, worker terminal arbitration, ACP settlement, and process teardown.

View File

@@ -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-14-typescript-program-backed-semantic-gates.md: 3e7a76e86d83080ae1a4f91ca97cc9749c90ef29
2026-07-14-typescript-program-backed-semantic-gates.zh.md: 0a13452012e7f6cbd3ad7994845ba1985355c089

View File

@@ -0,0 +1,63 @@
# RFC: TypeScript Program-backed semantic gates
Status: implemented
English | [中文](2026-07-14-typescript-program-backed-semantic-gates.zh.md)
## Problem
Repository gates sometimes need facts that TypeScript syntax does not carry by itself: whether a receiver is a Cordis `Context`, which concrete event names reach a forwarding helper, and whether declaration merging changed an event signature.
The existing gates use TypeScript's single-file syntax model and maintain these facts through naming conventions, handwritten tables, and JSDoc.
The repository needs one semantic source of truth without introducing runtime package cycles, broad fallback heuristics, or machine-readable annotations that restate information already available to TypeScript.
## Decision
Repository gates can combine project-wide type information through `ts.Program` and use `TypeChecker` to extract **strongly typed** facts, reducing their reliance on naming conventions, handwritten tables, and JSDoc metadata.
The repository applies this model to two gates.
### One project model expands the root solution
[`TypeScriptProject`](../../../../scripts/ts-project.ts) parses the root `tsconfig.json`, recursively expands every project reference, and combines the referenced source roots into one no-emit semantic program. A normal program created from the solution config can redirect referenced projects to built declarations; explicit expansion keeps the package `src` files available for AST traversal and symbol identity.
The wrapper owns config diagnostics, semantic compiler options, repository-relative paths, source lookup, and the shared checker. Individual gates do not glob package sources or construct partial programs independently.
### A. Event relations follow receiver and value types
[`gen-doc-graphs`](../../../../scripts/gen-doc-graphs.ts) classifies calls by assignability to the repository's actual `Context`, `AgentEventDispatch`, and Cordis `EventsService` types. Variable names and property spellings do not determine whether a call is an event operation.
Context and agent-dispatch calls contribute only finite string-literal event sets. Direct `EventsService.dispatch()` calls recover the event slot through array literals, constant aliases, conditional branches, and resolved call sites of non-exported local helpers. Generic forwarding parameters are not concrete producers: attribution stays with the call sites that supply a closed event value.
Every declared harness event must have a discovered producer. A missing producer fails generation as dead vocabulary or an unsupported semantic dispatch shape; listener-free extension points remain valid. `internal/dispatch` instrumentation is not treated as a subscription to every event it observes, so the matrix contains direct product listeners rather than manually asserted indirect relationships.
### B. Scoped-event routing generates one typed resolver map
[`gen-scoped-events`](../../../../scripts/gen-scoped-events.ts) scans real `scopeTarget(base, key)` calls to establish the routing-key type for each scoped base. It then finds Cordis `Events` members with `this: Scoped<Base>` and searches every payload parameter plus one public property level for a type identical to that key after removing `null` and `undefined`.
Exactly one match generates a resolver. Multiple matches are ambiguous and fail. Zero matches require `@dshScopeScan unsupported`, which is reserved for events whose routing key intentionally stays outside the payload, such as owner-keyed session events and parent-keyed subagent lifecycle events. The annotation records an unsupported scan; it does not encode an event name, parameter index, property path, or replacement type.
The committed [`scoped-events.generated.ts`](../../../../packages/support/invariants/src/scoped-events.generated.ts) imports every scoped-event owner for its type-side `Events` contributions. Each generated lambda accepts `Parameters<Events[K]>`, and the complete object satisfies a `Record` over the derived `ScopedEventName` union. Ordinary TypeScript compilation therefore checks event existence, parameter position, property access, and scoped-event completeness. The only cast adapts Cordis's runtime `unknown[]` dispatch boundary to the already type-checked resolver.
The invariants plugin consumes this generated runtime map instead of maintaining its own table. Additional event-owner packages are dev dependencies and project references of `dsh-invariants`, not peer dependencies, so the compile-time aggregation does not expand the plugin's runtime closure.
### Semantic gaps fail explicitly
The generators reject missing declarations, config diagnostics, widened or generic event names, inconsistent routing-key types, ambiguous payload matches, unnecessary unsupported annotations, and stale generated output. Recovery through local helper call sites is deliberately narrow: exported or unresolved dataflow requires a new semantic rule rather than a package-specific override.
## Verification
`verify-doc-graphs` freshness-checks semantic producer/listener discovery, and `verify-scoped-events` freshness-checks the generated resolver map. The root TypeScript build compiles the resolver against merged `Events`; workspace constraints and runtime-closure checks ensure its type-only aggregation does not become a deployment dependency.
## Alternatives considered
- **Keep syntax-only scans with receiver allowlists and manual overrides.** This is simple per exception but makes renames and new helper shapes update a second representation. Completeness can detect a missing producer, but it cannot prove that the override still describes the source.
## Consequences
- Event relation generation follows semantic receiver identity and closed event values instead of local naming conventions.
- Scoped-event membership, subject extraction, and runtime invariant coverage come from event declarations and real dispatch contracts rather than handwritten tables.
- Refactors that change event names, parameter positions, subject properties, or routing-key types fail generation or compilation at the owning contract.
- Building a flattened Program costs more startup time and memory than parsing isolated files, and semantic gates depend on a valid root project graph.
- Generated TypeScript remains committed source: changes to event owners or dispatch shapes must regenerate it and the affected documentation.

View File

@@ -0,0 +1,63 @@
# RFC: 基于 TypeScript Program 的语义门禁
Status: implemented
[English](2026-07-14-typescript-program-backed-semantic-gates.md) | 中文
## 问题
仓库门禁有时需要判断 TypeScript 语法本身不携带的事实:接收者是否为 Cordis `Context`、哪些具体事件名会进入转发辅助函数、声明合并是否改变了事件签名。
当前的门禁基于 TypeScript 单文件语法解析能力使用命名约定、手写的表格、JSDoc 等方式来维护这类信息。
仓库需要一个语义真源同时不能引入运行时包package之间的循环依赖、宽泛的兜底启发式逻辑或重复描述 TypeScript 已有信息的机器可读标注。
## 决策
仓库可以通过项目级类型信息 `ts.Program` 进行跨文件项目类型联合计算,并通过 `TypeChecker` 来提取 **强类型** 信息用以缓解原有命名约定、手写表格、JSDoc 标注等形式。
当前已完成 A / B 两个门禁的语义化改造。
### 一个项目模型展开根项目配置
[`TypeScriptProject`](../../../../scripts/ts-project.ts) 解析根 `tsconfig.json`,递归展开每个项目引用,并将各引用项目的源码根合并为一个不输出文件的语义 Program。直接从根项目配置创建普通 Program 时TypeScript 可能将引用项目重定向到构建后的声明文件;显式展开可以让门禁继续遍历各包的 `src` 文件,并使用真实符号标识。
该封装统一负责配置诊断、语义编译选项、仓库相对路径、源码查找和共享 TypeChecker。各门禁不再自行按文件通配模式扫描包源码也不再分别构建不完整的 Program。
### A. 事件关系由接收者类型和值类型决定
[`gen-doc-graphs`](../../../../scripts/gen-doc-graphs.ts) 根据调用接收者与仓库中真实 `Context``AgentEventDispatch` 和 Cordis `EventsService` 类型之间的可赋值关系进行分类。变量名和属性拼写不再决定某次调用是否属于事件操作。
Context 与 AgentEventDispatch 调用只贡献有限的字符串字面量事件集合。对于直接调用 `EventsService.dispatch()` 的路径,生成器会沿数组字面量、常量别名、条件分支和未导出本地辅助函数的已解析调用点恢复事件槽位。泛型转发参数不算作具体生产方:事件仍归属于传入封闭事件值的调用点。
每个已声明的 harness 事件都必须存在扫描得到的生产方。找不到生产方时,生成过程会将其视为无调用方的事件词汇或尚不支持的语义 dispatch 形态并明确失败;没有监听方的扩展点仍然合法。`internal/dispatch` 插桩不会被当作它所观察的每个事件的订阅,因此关系矩阵只记录直接的产品监听方,不再手工补充间接关系。
### B. 带作用域的事件路由生成一份强类型解析函数表
[`gen-scoped-events`](../../../../scripts/gen-scoped-events.ts) 扫描真实的 `scopeTarget(base, key)` 调用,为每种 scoped 基础对象确定路由键类型。随后,它查找带有 `this: Scoped<Base>` 的 Cordis `Events` 成员,并在每个事件参数及其一层公开属性中搜索类型;移除 `null``undefined` 后,候选类型必须与路由键类型完全相同。
恰好一个匹配项会生成解析函数。存在多个匹配项时,含义不明确,生成器会失败。没有匹配项时,事件必须标记 `@dshScopeScan unsupported`;该标记只用于路由键有意留在事件参数之外的情况,例如按所属 agent智能体路由的会话事件和按父 agent 路由的 subagent 生命周期事件。此标记只表示扫描不受支持,不编码事件名、参数下标、属性路径或替代类型。
仓库提交的 [`scoped-events.generated.ts`](../../../../packages/support/invariants/src/scoped-events.generated.ts) 会导入每个带作用域的事件声明方,使它们从类型侧合并进 `Events`。每个生成函数都接收 `Parameters<Events[K]>`,完整对象则满足基于 `ScopedEventName` 联合类型派生出的 `Record`。因此,常规 TypeScript 编译会检查事件是否存在、参数位置、属性访问和带作用域的事件集合完整性。唯一的类型断言只负责将 Cordis 运行时的 `unknown[]` dispatch 边界适配到已经通过类型检查的解析函数。
不变式插件消费这份生成的运行时表,不再维护自己的事件表。新增的事件声明方包只作为 `dsh-invariants` 的开发依赖和项目引用存在,不进入对等依赖,因此编译期聚合不会扩大插件的运行时依赖闭包。
### 语义缺口必须显式失败
遇到声明缺失、配置诊断、事件名被拓宽或保持泛型、路由键类型不一致、事件参数匹配不唯一、不必要的 unsupported 标记,或生成产物陈旧时,生成器都会拒绝继续。通过本地辅助函数调用点恢复信息的能力被刻意限制在窄范围内:如果数据流经过导出或无法解析的边界,应新增通用语义规则,而不是添加特定包的覆盖项。
## 验证
`verify-doc-graphs` 对语义生产方/监听方扫描执行新鲜度检查,`verify-scoped-events` 对生成的解析函数表执行新鲜度检查。根 TypeScript 构建会将解析函数与合并后的 `Events` 一起编译workspace 约束和运行时依赖闭包检查则确保仅参与类型聚合的依赖不会变成部署依赖。
## 考虑过的替代方案
- **保留语法扫描、接收者白名单和手写覆盖项。** 每个例外都容易单独处理,但重命名和新增辅助函数形态时还必须更新第二份表示。完整性检查能够发现生产方缺失,却无法证明覆盖项仍与源码一致。
## 后果
- 事件关系生成依据语义接收者身份和封闭事件值,不再依赖局部命名约定;
- 带作用域的事件成员关系、主体提取和运行时不变式覆盖来自事件声明与真实 dispatch 契约,不再来自手写表;
- 修改事件名、参数位置、主体属性或路由键类型时,会在其所属契约处触发生成或编译失败;
- 构建扁平化 Program 比解析孤立文件消耗更多启动时间和内存,语义门禁也依赖有效的根项目图;
- 生成的 TypeScript 仍属于提交到仓库的源码:事件声明方或 dispatch 形态发生变化后,必须重新生成该文件和受影响的文档。