news 2026/9/20 16:00:54

DeepSeek Harness 语义门禁:基于 TypeScript Program 与 TypeChecker 的强类型仓库检查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DeepSeek Harness 语义门禁:基于 TypeScript Program 与 TypeChecker 的强类型仓库检查
  • 人工智能
  • AI Agent
  • Agent 框架
  • DeepSeek

【免费下载链接】deepseek-harness

DeepSeek Harness: Everything is a Plugin.

项目地址:https://gitcode.com/gh_mirrors/de/deepseek-harness
点击查看免费下载

导读

DeepSeek Harness 是一个「万物皆插件」的多包仓库(monorepo),其代码门禁经常需要判断 TypeScript 语法本身并不携带的事实——某个调用接收者是不是 CordisContext、哪些具体事件名会流入转发辅助函数、声明合并是否改变了事件签名。本篇文章基于仓库中已落地(Status: implemented)的实现方案,讲解 DeepSeek Harness 如何用ts.Program汇集项目级类型信息、借助TypeChecker提取强类型事实,替换掉命名约定、手写表格与 JSDoc 元数据,为事件关系图(docs/event-producer-consumer.md)与 scoped 事件路由解析表(packages/core/scope/src/scoped-events.generated.ts)两套门禁提供单一语义真源。读完你既能复现这两个生成器的运行方式,也能掌握在大型 TS monorepo 中做「跨文件语义检查」的通用方法。

背景:语法解析门禁的先天局限

仓库门禁在演进中遇到了三类语法层面不可见的事实:

  • 接收者身份:某次调用到底是不是在向 CordisContextAgentEventDispatchEventsService发事件;
  • 事件名集合:哪些具体事件名会经由转发辅助函数进入EventsService.dispatch()
  • 声明合并declare module '@deepseek-ai/cordis'中的合并是否会改变事件签名。

此前的门禁基于 TypeScript 单文件语法解析,用命名约定、手写的表格、JSDoc 注解来维护这些信息。问题在于:这类「第二份表示」与源码极易失同步——重命名事件、新增辅助函数形态时,手写表格必须同步更新,而完备性检查只能发现「生产方缺失」,无法证明某个覆盖项仍然与源码一致。

因此仓库需要一个语义真源,同时满足三条硬约束:

  1. 不引入运行时包之间的循环依赖;
  2. 不做宽泛的兜底启发式逻辑;
  3. 不复述 TypeScript 本已掌握的信息(即不做机器可读的元数据标注)。

核心决策:用 ts.Program + TypeChecker 提取强类型事实

仓库的决策是:门禁通过ts.Program汇集项目级类型信息,再用TypeChecker提取强类型事实,从而把对命名约定、手写表格和 JSDoc 的依赖降到最低。这一模型被应用到两个门禁上:

  • 门禁 Agen-doc-graphs生成事件生产者/消费者关系矩阵(docs/event-producer-consumer.md);
  • 门禁 Bgen-scoped-events生成 scoped 事件的路由解析函数表(packages/core/scope/src/scoped-events.generated.ts)。

一个项目模型展开根项目配置:TypeScriptProject

两个生成器共享同一个封装:scripts/ts-project.ts中的TypeScriptProject类。它做了三件事:

  1. 解析根 tsconfig 并递归展开项目引用loadProjectGraph()从根目录读取tsconfig.host.jsonCompilerFace = 'host' | 'client'),对每个项目引用调用ts.resolveProjectReferencePath递归收集fileNames,把所有引用项目的源码根合并为一个不输出文件的语义 Program;
  2. 清除仅发射选项semanticCompilerOptions()noEmit设为true,同时关闭compositedeclarationdeclarationMapsourceMapincremental,得到一个纯语义视图;
  3. 统一暴露配置诊断、语义编译选项、仓库相对路径、源码查找和共享 checkersourceFiles()relativePath()sourceFile()三个方法与共享的program/checker属性,让各门禁不再自行按文件通配模式扫描包源码,也不再各自构建不完整的 Program。
// scripts/ts-project.ts(节选) const graph = loadProjectGraph(projectRoot, face) this.program = ts.createProgram(graph.rootNames, semanticCompilerOptions(graph.options)) this.checker = this.program.getTypeChecker()

两点设计细节值得注意:

  • 不直接以根 solution 配置建 Program:源码注释明确指出,「flattening host+client into one program collides the cordis Context merges」——直接把根配置转成普通 Program,TypeScript 可能把引用项目重定向到构建后的.d.ts声明文件,且 host/client 两面的Context合并会冲突。显式展开能保留各包src文件供 AST 遍历,并保持符号同一性;
  • 配置诊断即失败parseConfig()对每个 tsconfig 调用ts.getParsedCommandLineOfConfigFile,一旦出现任何配置诊断立刻抛错。语义门禁因此天然依赖一个有效的根项目图。

门禁 A:事件关系由接收者类型与值类型决定

gen-doc-graphs.ts的核心是EventRelationCollector类。它不读变量名,而是用isTypeAssignableTo接收者分类

// scripts/gen-doc-graphs.ts(节选) if (this.project.checker.isTypeAssignableTo(type, this.eventsServiceType)) return 'events-service' if (this.project.checker.isTypeAssignableTo(type, this.contextType)) return 'context' if (this.project.checker.isTypeAssignableTo(type, this.agentDispatchType)) return 'agent-dispatch'

三个锚点类型都从仓库真实声明解析而来(vendor/cordis/src/context.tsContextpackages/core/agent/src/dispatch.tsAgentEventDispatchvendor/cordis/src/events.tsEventsService),而不是手写的白名单。

有限事件名集合的恢复路径

  • Context 与 agent-dispatch 调用只贡献由字符串字面量构成的有限事件集合finiteStringValues()只接受字面量、闭合的字符串字面量联合类型,凡被拓宽成string或保持泛型的一律拒绝;AgentEventDispatch转发对象内的上下文参数通过isForwardedAgentEventParameter()被显式排除——泛型转发参数不算生产方,事件归属始终回到「传入封闭事件值」的调用点;
  • 直接EventsService.dispatch()调用eventNamesFromArgumentList()会沿数组字面量、const常量别名、条件分支逐级恢复事件槽位,并进一步通过未导出本地辅助函数的已解析调用点回溯参数;
  • 调用点预过滤EVENT_API_METHODS集合(ononceemitparallelserialwaterfalldispatch)先行过滤,只有方法名命中后才做接收者类型分类,避免对每个调用都求解签名。

需求式辅助函数索引:证明只影响开销,不影响结果

这是实现中最精巧的部分。callSitesFor()对每个辅助函数先尝试provenLocalCallee():如果一个函数未导出、位于真正的 ES 模块文件、且同文件所有引用都是直接调用位,那么按模块作用域规则,它的全部调用必然在本文件内——此时只索引这一个文件。任一前提无法证明(带导出修饰符、位于全局 script 文件、存在别名化引用如 re-export 或默认导出、存在无法归类的引用)就回退到原全部包源码索引

// scripts/gen-doc-graphs.ts(节选) if (!this.globalCallSites && !this.provenLocalCallee(owner)) { this.globalCallSites = this.buildCallSiteIndex(this.packageSourceFiles) } if (this.globalCallSites) return this.globalCallSites.get(owner) ?? [] // 仅索引 owner 所在文件

回退路径就是原来的语义本身,因此证明失败只会增加扫描成本,绝不改变结果。文档同时记录了被否决的方案:惰性单一全局索引被放弃,因为当前源码树确实会走到辅助函数参数路径,它仍要支付几乎全额的getResolvedSignature扫描成本。

完备性契约

renderEventRelations()生成的矩阵要求每个已声明的 harness 事件都存在扫描得到的生产方

  • 找不到生产方 → 直接抛错,视为「死词汇」或尚不支持的语义 dispatch 形态(docs/event-producer-consumer.md的生成失败信息会明确提示);
  • 没有监听方的扩展点仍然合法(listener-free extension points remain valid);
  • internal/dispatch插桩不会被当作它观察的每个事件的订阅,关系矩阵只记录直接的产品监听方;
  • 客户端声明的事件(packages/client/前缀)豁免生产方检查,因为关系扫描只以 host 聚合为种子(见源码 TODO 注释:host+client 不能共享一个 Program,Client 包仅在 host 文件 import 它时才进入)。

门禁 B:带作用域的事件路由生成强类型解析函数表

gen-scoped-events.ts负责dsh-scope的运行时不变式。它的输入契约是两段真实代码:

  1. 真实的scopeTarget(base, key)调用(定义在packages/core/scope/src/index.ts)——为每种 scoped 基础对象确立路由键类型;
  2. this: Scoped<Base>的 CordisEvents成员(通过isCordisModuleInterface()限定在declare module '@deepseek-ai/cordis'内部)。

生成器对每个事件成员执行三步:

  1. 解析路由键类型routingKeyType()收集所有 base 类型可赋值给该 scoped base 的scopeTarget调用,去重后若出现多个不同的键类型,报「inconsistent routing-key types」;
  2. 搜索 payload 候选subjectCandidates()枚举每个事件参数及其第一层公开属性(剔除__@内部符号与 private/protected 声明),用typesEquivalent()精确类型同一性比较(先getNonNullableType()移除null/undefined,拒绝any/unknown);
  3. 三分支裁决
    • 恰好一个匹配→ 生成解析函数,如'agent/created': args => (args[0] as Record<string, unknown>)['agent']
    • 多个匹配→ 含义不明确,收集 violation 并失败;
    • 零匹配→ 事件必须标记@dshScopeScan unsupported

@dshScopeScan unsupported只用于路由键有意留在事件参数之外的场景,例如按所属 agent 路由的会话事件(session/createdsession/event)和按父 agent 路由的 subagent 生命周期事件(subagent/startsubagent/end)。该标记只表达「扫描不受支持」,不编码事件名、参数下标、属性路径或替代类型——生成器对非unsupported形态的 tag、多余 tag、以及「有 tag 但并非 scoped 事件」等组合都会逐一报错。

生成的scoped-events.generated.ts纯运行时映射Object.freeze冻结的Record<string, ScopedSubjectResolver | null>加上一个scopedSubjectResolverFor(event)查询函数。它位于 scoped dispatch 所属的dsh-scope包内,不 import 任何事件声明方包——null解析器表示「payload 无法暴露外部路由键,不变式只检查载体存在性」,undefined表示「该事件不是 scope-filtered 事件」。

// packages/core/scope/src/scoped-events.generated.ts(节选) export function scopedSubjectResolverFor(event: string): ScopedSubjectResolver | null | undefined { return scopedSubjectResolvers[event] }

消费端packages/core/scope/src/invariant.ts直接 import 这份映射做主体提取,不再维护手写事件表。因为 Program 分析发生在仓库门禁内而非依赖生成的类型导入,dsh-scopedsh-invariants都无需依赖所有事件声明方——这正是文档强调的「没有循环依赖」约束的落地。

语义缺口必须显式失败

两个生成器对语义缺口一律失败快、失败明。触发拒绝的情况包括:

类别示例
声明缺失无法解析ContextEventsService等锚点类型
配置诊断任意 tsconfig 出现解析错误
事件名被拓宽或保持泛型finiteStringValues()返回undefined
路由键类型不一致同一 scoped base 出现多个键类型
事件参数匹配不唯一多个 payload 候选与键类型同一
不必要的 unsupported 标记payload 明明暴露了路由键却标注不支持
生成产物陈旧--check模式下与已提交文件不一致

设计上,通过本地辅助函数调用点恢复信息的能力被刻意限制在窄范围:如果数据流经过导出或无法解析的边界,正确的做法是新增一条通用语义规则,而不是添加特定包的覆盖项。

验证与门禁集成

两个生成器都提供「生成」与「校验」双模式,见根package.json的 scripts:

pnpm run gen-doc-graphs # 生成文档图(含事件关系矩阵) pnpm run verify-doc-graphs # 对语义生产方/监听方扫描做新鲜度检查 pnpm run gen-scoped-events # 生成 scoped 事件解析表 pnpm run verify-scoped-events # 重跑 Program 分析并校验生成映射新鲜度

在生成器内部,--check模式会把渲染结果与已提交文件逐字比对(gen-doc-graphs.tsmain()committed !== doc.content即判陈旧;gen-scoped-events.ts同理)。此外两套校验都被挂进统一门禁编排器scripts/run-gates.ts,与文档构建、类型等价、Cordis 目录、翻译配对等检查并行执行:

pnpmScript('doc-graphs', 'verify-doc-graphs', { label: 'doc graphs' }), // ... pnpmScript('scoped-events', 'verify-scoped-events', { label: 'scoped events' }),

配套验证还包括:根 TypeScript 构建会编译运行时适配器(scoped-events.generated.tsdsh-scope包一起编译);workspace 约束与运行时依赖闭包检查确保事件声明方聚合不会进入部署依赖。

考虑过的替代方案

仓库明确评估并否决了「保留语法扫描 + 接收者白名单 + 手写覆盖项」的方案:

每个例外都容易单独处理,但重命名和新增辅助函数形态时还必须更新第二份表示。完整性检查能够发现生产方缺失,却无法证明覆盖项仍与源码一致。

这正是语义方案的核心优势:生成器自己就是完整性的证明——根 Program 枚举所有 scopedEvents声明与真实scopeTarget约定,用 checker 解析唯一 payload 路径,并在渲染unknown[]运行时边界之前拒绝缺失、陈旧或含义不明确的条目。

后果与权衡

  • 正向收益:事件关系生成依据语义接收者身份和封闭事件值,不再依赖局部命名约定;scoped 事件成员关系、主体提取和运行时不变式覆盖来自事件声明与真实 dispatch 约定,不再来自手写表;修改事件名、参数位置、主体属性或路由键类型时,会在其所属约定处触发生成失败——重构错误在提交前就被拦截;
  • 成本:构建扁平化 Program 比解析孤立文件消耗更多启动时间和内存;语义门禁依赖有效的根项目图,任何 tsconfig 破损都会让校验直接失败;
  • 提交约束:生成的 TypeScript 仍属于提交到仓库的源码。事件声明方或 dispatch 形态发生变化后,必须重新运行pnpm run gen-scoped-eventspnpm run gen-doc-graphs并提交受影响文件和文档——新鲜度检查(verify-*)正是为此设计。

这套模式给出了一条可迁移的路径:当语法无法表达的事实需要被门禁约束时,与其手写第二份表示,不如构建一次不输出的语义 Program,让 TypeChecker 替你回答。代价是更长的启动时间与对项目图完整性的依赖,换来的是重命名、签名变更、声明合并等场景下的即时、精确且可证明的失败。

  • 人工智能
  • AI Agent
  • Agent 框架
  • DeepSeek

【免费下载链接】deepseek-harness

DeepSeek Harness: Everything is a Plugin.

项目地址:https://gitcode.com/gh_mirrors/de/deepseek-harness
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 15:56:08

LLVM项目深度解析:从源码结构到编译优化实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 15:54:50

开源ASP.NET 8.0快速开发框架:MVC+SqlSugar+LayUI集成实战

简介&#xff1a;基于 ASP.NET 8.0 的开源后台管理框架&#xff0c;整合 MVC、API、SqlSugar 与 LayUI&#xff0c;面向需要快速交付企业级 Web 应用的 C#/.NET 开发团队&#xff0c;目标是减少权限、表单、数据隔离等基础功能的重复搭建。框架内置字段级数据权限、流程表单设计…

作者头像 李华
网站建设 2026/9/20 15:53:30

Bandizip:轻量高效的压缩工具全解析

## 1. 为什么选择Bandizip&#xff1f;轻量高效的压缩工具新选择第一次接触Bandizip是在帮同事解压一个损坏的RAR文件时。当时常见的压缩软件要么报错&#xff0c;要么需要付费修复&#xff0c;而Bandizip不仅成功解压&#xff0c;还保留了完整的文件目录结构。这款来自韩国的压…

作者头像 李华
网站建设 2026/9/20 15:44:38

Modbus协议取证实战:从流量抓包到事件溯源

1. 项目概述与整体思路拆解如果你负责工厂自动化系统的安全巡检&#xff0c;或者在做工控安全相关的应急响应&#xff0c;那么Modbus协议你一定绕不开。这套诞生于1979年的串行通信协议&#xff0c;到今天仍然是PLC、HMI、变频器、传感器之间最主流的通信方式之一。我经常跟团队…

作者头像 李华