From 13255ceb155cc4b3e72c2cf0335fd633ddd110e8 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Wed, 15 Jul 2026 00:42:08 +0800 Subject: [PATCH] docs: add rfc --- docs/architecture.md | 2 +- docs/rfc/INDEX.md | 1 + .../2026-07-12-agent-scope-runtime-design.md | 2 +- ...pt-program-backed-semantic-gates.i18n.yaml | 6 ++ ...ypescript-program-backed-semantic-gates.md | 63 +++++++++++++++++++ ...script-program-backed-semantic-gates.zh.md | 63 +++++++++++++++++++ 6 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.i18n.yaml create mode 100644 docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md create mode 100644 docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.zh.md diff --git a/docs/architecture.md b/docs/architecture.md index 899c48bb8a..b17c3302d8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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. Generated typed resolvers 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). +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 diff --git a/docs/rfc/INDEX.md b/docs/rfc/INDEX.md index b2268571d5..11345799e5 100644 --- a/docs/rfc/INDEX.md +++ b/docs/rfc/INDEX.md @@ -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 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 e58fcab37a..2c3126793f 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 @@ -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, type-equivalence blocks, and scoped-event resolver map are generated or freshness-gated from source. The scoped-event generator derives each resolver from `this: Scoped<…>` signatures and real `scopeTarget` key types, compiles it against merged `Events`, and uses `@dshScopeScan unsupported` only when an external key permits presence checks alone. +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. diff --git a/docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.i18n.yaml b/docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.i18n.yaml new file mode 100644 index 0000000000..eb411f1158 --- /dev/null +++ b/docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.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-14-typescript-program-backed-semantic-gates.md: 3e7a76e86d83080ae1a4f91ca97cc9749c90ef29 +2026-07-14-typescript-program-backed-semantic-gates.zh.md: 0a13452012e7f6cbd3ad7994845ba1985355c089 diff --git a/docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md b/docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md new file mode 100644 index 0000000000..3e7a76e86d --- /dev/null +++ b/docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md @@ -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` 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`, 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. diff --git a/docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.zh.md b/docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.zh.md new file mode 100644 index 0000000000..0a13452012 --- /dev/null +++ b/docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.zh.md @@ -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` 的 Cordis `Events` 成员,并在每个事件参数及其一层公开属性中搜索类型;移除 `null` 和 `undefined` 后,候选类型必须与路由键类型完全相同。 + +恰好一个匹配项会生成解析函数。存在多个匹配项时,含义不明确,生成器会失败。没有匹配项时,事件必须标记 `@dshScopeScan unsupported`;该标记只用于路由键有意留在事件参数之外的情况,例如按所属 agent(智能体)路由的会话事件和按父 agent 路由的 subagent 生命周期事件。此标记只表示扫描不受支持,不编码事件名、参数下标、属性路径或替代类型。 + +仓库提交的 [`scoped-events.generated.ts`](../../../../packages/support/invariants/src/scoped-events.generated.ts) 会导入每个带作用域的事件声明方,使它们从类型侧合并进 `Events`。每个生成函数都接收 `Parameters`,完整对象则满足基于 `ScopedEventName` 联合类型派生出的 `Record`。因此,常规 TypeScript 编译会检查事件是否存在、参数位置、属性访问和带作用域的事件集合完整性。唯一的类型断言只负责将 Cordis 运行时的 `unknown[]` dispatch 边界适配到已经通过类型检查的解析函数。 + +不变式插件消费这份生成的运行时表,不再维护自己的事件表。新增的事件声明方包只作为 `dsh-invariants` 的开发依赖和项目引用存在,不进入对等依赖,因此编译期聚合不会扩大插件的运行时依赖闭包。 + +### 语义缺口必须显式失败 + +遇到声明缺失、配置诊断、事件名被拓宽或保持泛型、路由键类型不一致、事件参数匹配不唯一、不必要的 unsupported 标记,或生成产物陈旧时,生成器都会拒绝继续。通过本地辅助函数调用点恢复信息的能力被刻意限制在窄范围内:如果数据流经过导出或无法解析的边界,应新增通用语义规则,而不是添加特定包的覆盖项。 + +## 验证 + +`verify-doc-graphs` 对语义生产方/监听方扫描执行新鲜度检查,`verify-scoped-events` 对生成的解析函数表执行新鲜度检查。根 TypeScript 构建会将解析函数与合并后的 `Events` 一起编译;workspace 约束和运行时依赖闭包检查则确保仅参与类型聚合的依赖不会变成部署依赖。 + +## 考虑过的替代方案 + +- **保留语法扫描、接收者白名单和手写覆盖项。** 每个例外都容易单独处理,但重命名和新增辅助函数形态时还必须更新第二份表示。完整性检查能够发现生产方缺失,却无法证明覆盖项仍与源码一致。 + +## 后果 + +- 事件关系生成依据语义接收者身份和封闭事件值,不再依赖局部命名约定; +- 带作用域的事件成员关系、主体提取和运行时不变式覆盖来自事件声明与真实 dispatch 契约,不再来自手写表; +- 修改事件名、参数位置、主体属性或路由键类型时,会在其所属契约处触发生成或编译失败; +- 构建扁平化 Program 比解析孤立文件消耗更多启动时间和内存,语义门禁也依赖有效的根项目图; +- 生成的 TypeScript 仍属于提交到仓库的源码:事件声明方或 dispatch 形态发生变化后,必须重新生成该文件和受影响的文档。