mcpField
C
candor

candor

本地服务
candor.poly.io开发工具24 天前更新

关于 candor

candor-ts 分析 TypeScript 函数可传递触及的副作用,并通过确定性策略门检查规则,面向需要审查代码影响范围和边界的开发者。其 MCP 服务读取预先生成的报告,供编码 Agent 查询影响范围、调用路径和策略结果。

主要功能

  1. 01追踪跨文件调用链中的副作用
  2. 02标记无法解析的 Unknown
  3. 03按策略检查并阻止违规
  4. 04比较基线中的副作用变化
  5. 05查询影响范围、调用者与路径

适用场景

  1. 01评估修改函数的运行时影响范围
  2. 02确认代码是否会触及网络或数据库
  3. 03在 CI 中阻止新增副作用
  4. 04让编码 Agent 查询预生成分析报告

MCP 相关

candor-ts-mcp 将只读查询作为 MCP 工具提供,包括影响范围、可达效果、调用路径、策略门和基线差异。它只读取预先生成的报告,不执行扫描。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"tombaldwin-candor": {
"command": "npx",
"args": [
"-y",
"candor-ts"
],
"env": {
"CANDOR_ROOT": "<CANDOR_ROOT>"
}
}
}
}
启动命令npx -y candor-ts
需要配置的环境变量
CANDOR_ROOTLock the server to one workspace: every report prefix is resolved under this root. Unset, the server reads .candor/ relative to the client's working directory.

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
npmcandor-ts0.40.17

工具

16

收录的工具列表 · 更新于 3 天前

参数类型必填说明
fnstring是要評価的函数/単位
reportstring否报告前缀(可选;默认为 $CANDOR_REPORT)

详细介绍

candor-ts

Beaky,candor 金丝雀

适用于 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 → ClockSPEC §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

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。