关于 candor
candor-ts 分析 TypeScript 函数可传递触及的副作用,并通过确定性策略门检查规则,面向需要审查代码影响范围和边界的开发者。其 MCP 服务读取预先生成的报告,供编码 Agent 查询影响范围、调用路径和策略结果。
主要功能
- 01追踪跨文件调用链中的副作用
- 02标记无法解析的 Unknown
- 03按策略检查并阻止违规
- 04比较基线中的副作用变化
- 05查询影响范围、调用者与路径
适用场景
- 01评估修改函数的运行时影响范围
- 02确认代码是否会触及网络或数据库
- 03在 CI 中阻止新增副作用
- 04让编码 Agent 查询预生成分析报告
MCP 相关
candor-ts-mcp 将只读查询作为 MCP 工具提供,包括影响范围、可达效果、调用路径、策略门和基线差异。它只读取预先生成的报告,不执行扫描。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
16收录的工具列表 · 更新于 3 天前
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| fn | string | 是 | 要評価的函数/単位 |
| report | string | 否 | 报告前缀(可选;默认为 $CANDOR_REPORT) |
详细介绍
candor-ts
适用于 TypeScript 的 candor:以确定性策略门控,传递分析每个函数的副作用。 candor-ts 通过 TypeScript 编译器 API 解析每次调用,并报告项目中每个函数可能触及的效果——Net、Fs、Db、Exec、Env、Clock、……——包括通过跨文件的任意调用链继承的效果;在解析失败的任何位置(例如回调值、any 类型的被调用方)都会明确标记为 Unknown,绝不会静默视为纯函数。它是 candor-spec 的一种实现,与 Rust 和 JVM 引擎并列。
网站: candor.poly.io — 五分钟了解实测案例。
npm install # typescript + @types/node
node scan.mjs <project-dir> # tsconfig.json honored; tests excluded; writes
# <dir>/.candor/report.json + .callgraph.json
node scan.mjs . --policy .candor/policy # the §6.2 gate: exit 1 on violation, 2 if unreadable
node scan.mjs . --gate-json gate.json # + the structured verdict {spec, ok, violations} (§3.3)
CANDOR_BASELINE=saved.json node scan.mjs . # AS-EFF-005 guard: exit 1 if an existing fn GAINED an
# effect vs the saved report; 2 if it can't evaluate
node scan.mjs --version # installed build + spec contract (offline), + upgrade line
node query.mjs show .candor/report db.save 1 # a function's effects (match ladder)
node query.mjs where .candor/report Net 1 # direct sources vs inheritors
node query.mjs callers .candor/report db.save 1 # the blast radius (transitive callers)
node query.mjs map .candor/report 1 # module → effects overview
node query.mjs containment .candor/report # §6.1 boundary-effect dispersion (+ baseline = ratchet)
node query.mjs blindspots .candor/report # the Unknown SOURCES, ranked by blast radius
node query.mjs whatif .candor/report db.save Net policy # pre-edit gate verdict (exit 1)
node query.mjs diff .candor/report baseline 1 # per-function effect delta (exit 1 on a gain;
# a baseline from a DIFFERENT build ⇒ disclosed ⚠ + exit 0)
node query.mjs gate --report dep/.candor/report.json --policy arch.policy --gate-json -
# ⟨0.24⟩ apply a policy to an EXISTING report, NO scan
# (exit 0/1/2) — the supply-chain verb: gate a
# DEPENDENCY's published report without its source.
# Reads that report and nothing else; a rule the wire
# cannot answer (`forbid`, `allow`, a class-scoped
# `deny` with the scoping field absent) is REFUSED
# with exit 2, never evaluated on partial evidence.
已提交到仓库的 .candor/config(规范 §3.4)可替代环境变量配置——每行写入 policy arch.policy / baseline <report.json> / deps <report paths>,从扫描目标开始向上查找;相对路径相对于配置所在的仓库解析,因此 CI 只需“指向仓库”。已配置但不可用的配置文件、策略或基线会明确报错(退出码 2),绝不会静默绕过门控。
扫描时的基线防护(AS-EFF-005,规范 §7)可阻止效果回归通过发布:将 CANDOR_BASELINE(或配置中的 baseline 键)指向已保存的报告,任何新增效果的函数都会导致扫描失败——退出码 1,相关记录会并入 --gate-json 判定,每条记录都包含 origin(existing / new / unknown)。⟨0.40⟩ 基线中不存在的函数不会进行比较,因此新增的有效果函数也会导致失败;新增的纯函数会通过,而仅有 Unknown 的新函数会在说明中列出。Module.fn 键意味着重命名文件会被视为不存在:重新记录基线前,请先用 candor diff <this run's report> <baseline> 检查。与策略门控一样,此防护采用失败关闭策略:若基线文件存在但无法解析,或由不同的引擎构建版本生成(§2.1——升级引擎会使基线失效),则退出码为 2,不进行评估;只有文件确实不存在时,才会输出一行说明(防护未启用)。请区分这两种方式:query diff 是只读比较——它会披露生成版本不匹配的情况(⚠,退出码 0)并提供参考;扫描时的防护则是门控级的失败关闭机制,CI 应依赖这一机制。语义与参考引擎(candor-java)完全一致。
**保持最新:**检查已安装的版本并升级——参阅 candor/AGENTS.md §2a。npx -y candor-ts --version 会显示构建版本、规范版本和升级命令(离线运行;candor 绝不会主动联系服务器)。
函数名称采用以 . 分隔的模块限定形式(src.db.save),因此策略作用域读起来很自然。在 TS namespace 中声明的函数会在 fn 和调用图键中包含命名空间各段(src.util.Ns.helper),因此针对命名空间的分层策略会生效;而 §2 的 hash 联结键仍使用不带限定的本地名称。0.8.7 之前的构建版本不会包含这些段,因此跨越该版本时已保存的基线会失效(需重新生成)。pure <scope> 规则禁止所有效果,但不禁止 Unknown——§4 中的信任标记表示不确定性,而非效果(与参考引擎 candor-java 一致);对于还必须排除不可验证情况的边界,可使用显式规则 deny Unknown <scope>。
# .candor/policy
deny Net domain # the domain layer reaches no network, even through helpers
pure parse # parsing is effect-free
allow Db in db orders audit_log # the db layer touches ONLY these tables
allow Net in billing api.stripe.com # billing talks ONLY to Stripe
forbid domain -> infra # the domain layer must not depend on infra
该报告会在声明可判定的四种字面作用面上携带信息 ——
Net 调用中的 hosts、Db 调用中的 tables(SQL 表位置,与 Rust/JVM
提取器完全一致,另加通过接收者的 Repository<T> 类型参数读取的 TypeORM @Entity("user") 声明)、Exec 中的 cmds、Fs 中形如路径的 paths ——
绝不取自运行时计算的值,也不会传递传播,并由上述 allow 规则强制执行。在真实的 Nest 应用中,这使表级策略得以生效:allow Db in article.service article comments 会标记出访问 user 和 follows 的服务。
分类器经过精心整理(与其他引擎一样,宁可少报也会明确说明):Node 内置模块(fs、net/http/tls、dns、child_process、worker_threads、node:sqlite、node:vm、process.env、时钟)、Web 网络全局对象(fetch、XMLHttpRequest、navigator.sendBeacon,以及 WebSocket/EventSource ——
构造、send 和 close 的处理方式完全相同,无论它们是通过 lib.dom 还是通过 @types/node 重新导出的 undici-types 解析)、HTTP/队列/邮件层(axios/got/node-fetch/undici/ws/
socket.io/nodemailer、gaxios + googleapis-common + google-auth-library、stripe、@sentry/*、
posthog-node、bull/bullmq)、数据库驱动(pg/mysql2/mongodb/redis/ioredis/sqlite3/
better-sqlite3/knex)以及 ORM 层(TypeORM — 包含 @Entity("…") 表提取 —
Prisma、Mongoose、Sequelize、drizzle-orm),还有 execa/cross-spawn/shelljs/open、fs-extra/
graceful-fs/rimraf/glob/chokidar、dotenv、winston/pino/bunyan。未列出的 npm 包不会贡献任何内容 ——
坦诚绝不猜测作用;但扫描会列出它:回执的覆盖清单行(标记:classifier doesn't cover)会列出代码中有明确调用、但分类器既未分类也未审查为纯函数的每个包,而每个函数都会附带它(经传递)访问的 invisible 列表。
⟨0.32⟩ Node 核心是分类器中唯一采用拒绝名单而非精选名单的部分。 内置模块集合有限,因此每个模块的作用面都经过审查;核心成员若既未分类,也未审查为无副作用,就会读取为 Unknown[native:<module>.<member>] ——
绝不会被视为纯函数。这就是为什么 v8.writeHeapSnapshot() 是 Fs、inspector.open() 是 Net,而 repl.start()、
process.dlopen() 和 new worker_threads.Worker(file) 都是可见的漏洞,而非默默遗漏。
MCP 服务 — 将坦诚作为 Agent 的真实依据
candor-ts-mcp 将只读查询作为 MCP 服务公开,因此
编码 Agent 可以询问**“如果我更改此处,运行时影响范围有多大?”或“哪些内容会访问
网络?”**,并从预先计算的报告中获得确定的真实依据——无需手动追踪调用图、消耗大量 Token
(影响范围问题的实测 Token 节省约为 700–2000 倍)。
// in an MCP client config — point it at a report you've already scanned
{ "command": "npx", "args": ["-y", "candor-ts-mcp"],
"env": { "CANDOR_REPORT": ".candor/report.myPkg.scan" } }
工具:candor_impact(反向影响范围)、candor_reachable(运行时会执行什么)、
candor_where(效应范围)、candor_path(如何触发某个效应)、candor_callers、
candor_show、candor_map、candor_containment、candor_blindspots、
candor_whatif(编辑前门禁检查——给定但不可读的策略会报出醒目错误,绝不会给出无异常结论)、
candor_gate(已签入的 .candor/config 策略判定)、candor_diff/candor_gains(基线差异)。
每个工具都可选择指定 report 前缀(否则使用 $CANDOR_REPORT);--root <dir> 会将服务
锁定到一个工作区。此服务仅供查询——它绝不会扫描(分析器自身的边界,规范
§7.12:由 Agent 或钩子生成报告;服务只读取报告,仅使用 Fs)。查询逻辑来自共享的
query-core.mjs,与 CLI 给出的结果一致。
candor-lsp 会在代码所在位置呈现同一份报告,适用于任何原生支持 LSP 的编辑器(helix、
neovim;JetBrains 插件内置此功能):为每个有副作用的函数显示一个 CodeLens(⚡ Db, Net · blast radius 12),悬停时显示溯源信息(从继承效应到执行该效应位置的调用链),并将仓库的策略
判定作为诊断显示。与 MCP 服务一样,它只是纯粹的报告使用者——可读取任何引擎生成的报告——
绝不会扫描。(两个基于报告计算的门禁都只是建议;引擎自身的 --gate-json 运行还会将被
屏蔽或不完整的字面量范围判为失败,是 CI 的权威判定方式。)
它还会就地回答编辑前的问题:在函数内部,为该函数尚未执行的每种边界效应提供一个代码操作
——candor: what if handler performed Net?——运行与 candor-ts-query whatif/
candor_whatif 相同的 whatif(影响范围 + 将会触发的策略规则),并以消息和函数处的临时
诊断显示判定结果(下次打开或保存该文件时清除;如果未发现策略,会明确说明并仅报告影响
范围)。它使用纯粹的 textDocument/codeAction + workspace/executeCommand(candor.whatif)
——在 helix、neovim、VS Code 和 JetBrains via LSP4IJ 中均可直接使用,无需修改。
实时循环——candor-ts-watch 会在 Agent 编辑时保持报告最新,因此回答反映的是当前
代码,而不是过时的快照:
candor-ts-watch ./src --out .candor/report # re-scans only when a tracked source actually changes
它通过内容哈希追踪项目源文件,并在发生实际更改时重新扫描(无操作保存或无关写入不会产生
任何影响),并写入 MCP 服务读取的相同前缀。因此:Agent 编辑 → 监视器刷新报告 → Agent
询问 candor_impact 并获得编辑后的答案。它还会报告编辑差异——不仅说明报告已更新,
还指出这次编辑对效应范围造成了什么影响(re-scanned (1 changed: app.ts) — Δ f +Net),
让 Agent 了解自身更改带来的后果。v1 会针对每次更改执行完整(可靠)的扫描;更深入的性能
优化——只重新分析已更改文件的子图,而非整个项目——是下一阶段的工作(内容哈希门控是第一步)。
信任约定(spec §4)
凡是 candor-ts 无法解析的内容,都会被设计为读取为 Unknown,绝不会默认为纯净——已知的缺口列在 candor-spec 的 SOUNDNESS.md 中(例如 R780、R803、R804):调用函数类型的参数或字段、被调用者类型为 any,以及解析结果落在类型而非函数体上。
未审查的依赖项可以通过在其 package.json 中声明其副作用来选择不再被视为 Unknown/默认为纯净——"candorEffects": ["Net"](spec §5.1,副作用清单)。candor-ts 会将其视为“已声明、未经验证”层级:该包的调用会归类到声明的集合中,不再是覆盖率账本中的盲点。§1 词汇表之外的名称会让声明明确失效(拼写错误绝不会悄悄缩小作用范围)。而 candor-ts-query gains <cur> <base> 会标记供应链差异——一个作用范围在两份报告之间新增的副作用。
在 rimraf 上测得的实际结果(50 个文件,分析了 55 个函数):由于其采用 DI 风格的 fs 注入,许多函数会读取为 Unknown,并明确披露——这说明约定正常生效,而非噪声。报告说的是“可能触及”,而不是“确实触及”;缺少字面量绝不代表确认其不存在。
框架依赖注入(spec §2 的有界 CHA 条款)。 rimraf 的注入是函数类型的参数,会读取为 Unknown。DI 容器单独测量,使用 NestJS 10 和已执行的 fixture:
-
如果某个 provider 条目
{ provide: Store, useClass | useValue | useFactory | useExisting: … }的 token 是项目中的 CLASS,则会被读取为转换:替代项会加入Store的候选项,因此this.store.save()会计入Store.save和替代项的save的并集(SOUNDNESS R1061——此前会静默处理,现在三种形式都已执行); -
类型为 INTERFACE 的参数(
@Inject("SINK") sink: Sink)会在项目中可见的Sink实现者上进行分派,因此实现了Sink的useClass会被计入;什么都没实现的useValue对象字面量会读取为Unknown(dispatch:)。 -
TOKEN 会通过检查器的类型读取,因此
const TOK = Store、导入的 token、TOKENS.store以及对[Store]的.map都会识别为Store;helper 的token/impl参数会通过其可见的调用点读取;类型为Function/Type<any>的 token 会将其替代项加入其可赋值给的每个项目类(R1061); -
如果扫描无法识别替代项(
useClass: impls[i]的类型为Type<Store>,或useValue的类型为any),则对该 token 类的分派会读取为Unknown(dispatch:)。
**明确未覆盖的情况:**扫描完全看不到的 provider 列表(来自未扫描的包),以及依赖项为其拥有的 token 提供的替代项。对 DI 密集型服务使用 deny <E> Unknown 和 deny <E> 进行门禁检查。
引擎间一致性——机器检查
candor-ts 是四个代码引擎之一(还有参考引擎 candor-java、Rust 引擎和 candor-swift),它们通过 spec 的跨引擎一致性测试套件保持一致:共享的副作用集合判定器、§6.2 策略语法测试组(包括 allow Db)、§3.1 查询形态与匹配阶梯检查、门禁退出码约定,以及一直到纯净与 Unknown 判定的新部分(PART 16)——每次向 spec 推送时,各引擎都必须给出完全一致的答案。
分析核心实现了什么(规范在哪些地方说明了实现方式)
| 部分 | 规范来源 |
|---|---|
使用编译器 API(getResolvedSignature)解析每次调用,绝不依赖语法 | CLASSIFIER §1 |
分类器映射已解析目标的模块(node:fs→Fs、node:net→Net,……) | CLASSIFIER §2、TS notes |
读取 process.env 属性 → Env;Date.now → Clock | SPEC §1 |
| 本地边(跨文件)+ 最小不动点传播 | SEMANTICS §5a |
| 闭包主体归属最近的外层函数 | SEMANTICS §2 |
调用解析到类型(函数类型的字段/参数)→ Unknown,绝不静默视为纯函数 | SPEC §4 |
| 未匹配的外部调用不产生任何贡献(精选分类器的注意事项) | SEMANTICS §8 C1 |
字面量表面:hosts/cmds/paths/tables,仅读取字面量 | SPEC §2 |
{ candor: { version, toolchain, spec: "0.40" }, functions } 封装;省略纯函数 | SPEC §2/§2.1 |
| 调用图 sidecar,每个已分析函数都作为键 | SPEC §2.2 |
| 门控:AS-EFF-006 / 008 / 009,策略不可读时明确报错 | SPEC §6.2 |
起源:可推导性证明
这个引擎最初是一个刻意保持精简的单文件实现切片,仅依据规范文档编写
(SPEC.md、SEMANTICS.md、CLASSIFIER.md),未查阅 Rust 或 JVM 源码,旨在通过实际运行回答:规范是否足以推导出一种新语言的实现? 是 — 共享 oracle 上达到 20/20。 这一纯净室声明冻结于提交 a29b152;此后的所有内容
(多文件项目、查询表面、门控、字面量表面)虽依据规范实现,但均为事后添加,其保证是上述符合性差异测试,而非纯净室溯源。原始推导所需的唯一引擎修复(调用落在函数类型声明上时,在应用 §4 前被视为纯函数)仍是证明要点:修复的依据是“照 §4 所述执行”,而不是“去阅读 Rust 源码”。
状态
遵循 candor-spec 0.40(包版本见 package.json):分析核心、门控(--policy / --gate-json /
.candor/config)、完整的 §3.1 查询表面(包括 containment、blindspots、
--include-unknown 调度边界,以及 ⟨0.24⟩ gate --report — 将门控应用于一个现有报告,其判定与 scan --policy 完全逐字节等效)、MCP 服务、LSP 服务和监视循环均已实现,并通过行为测试(npm test — 覆盖六种测试框架的行为测试套件),已通过有实证成效的健全性模糊测试(node fuzz.mjs — 规范 §7.13:生成经过每种已编码调用形式的效果链,任何静默视为纯函数的情况都会判红),且与 Rust/JVM/Swift 引擎保持符合性。npm 分类器层经过有意筛选,并逐个案例持续扩充。入口点(Nest/Next 的函数集合)、unknownWhy 来源、reachable、跨包继承(CANDOR_DEPS + 规范 §2 的 hash,依 §2.1 信任版本)和 --allow-js 均已纳入。
在 npm 上使用:npx -y candor-ts <dir>。各版本详情(⚠ 表示会影响报告/判定的变更):
CHANGELOG.md。
开发
无需构建步骤 — 引擎直接在 Node 上运行。
npm install
npm test # the full CI gate: lint + unit (node:test) + behavioural + MCP + watch + the
# fabrication probe + the §7.13 soundness fuzzer
npm run test:unit # just the native unit tests — the query algebra + policy DSL + the scan-core
# classifier/literal leaves (query-core / policy / scan-core)
npm run lint # eslint (the recommended ruleset; the CI lint gate)
node scan.mjs <dir | file.ts | tsconfig.json> --out .candor/report # scan a project
纯核心已拆分为可导入模块 — query-core.mjs(§3.1 查询)、
policy.mjs(§6.2 DSL + 字面量匹配器)和 scan-core.mjs(分类器 + SQL/
命令/主机提取器)— 因此可以直接对它们进行单元测试;由 TS 编译器驱动的遍历仍保留在 scan.mjs 中。
Agent:将 candor 安装为 MCP 服务
candor-ts 将 candor 的只读查询接口作为 MCP 服务提供,这样 Agent 可以询问 “更改这个的影响范围有多大?” 或 “哪些内容会访问网络?”,并从预先生成的报告中获得确定的答案,而不是通过 grep 查找。
npx -y candor-ts --mcp # the server, over stdio
candor mcp install # or let the umbrella write/merge .mcp.json for you
注册说明来自你已安装的包——candor mcp --help 会打印 .mcp.json 片段和 claude mcp add 命令,candor mcp install 则会写入这些内容。无需获取、篡改或自动执行远程文件,这里的任何内容都不会指示 Agent 运行它此前未主动选择安装的东西。
发现过程是被动的:清单是 server.json,并已发布到官方 MCP Registry,以便客户端和目录能够找到 candor,而无需收到脚本。
Registry 会根据 package.json 中的 mcpName 字段验证命名空间所有权——这是 npm 机制。(mcp-name: README 标记是 crates.io 约定,在这里不起作用;最初版本使用了它,结果 Registry 拒绝发布,并提示 missing required 'mcpName' field。保留下面这一行,仅因为人们可以用它来 grep 查找命名空间。)
mcp-name: io.github.tombaldwin/candor
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。