candor
Per-function effect analysis for agents: blast radius, what reaches the network, gate verdicts.
التثبيت
أضف هذا إلى إعدادات MCP في عميلك (Claude Desktop: claude_desktop_config.json، وCursor: .cursor/mcp.json، ويستخدم VS Code وغيره الصيغة نفسها «mcpServers»).
candor-tsclaude mcp add candor -e 'CANDOR_ROOT=<CANDOR_ROOT>' -- npx -y candor-ts@0.40.17 --mcp
{
"mcpServers": {
"candor": {
"command": "npx",
"args": [
"-y",
"candor-ts@0.40.17",
"--mcp"
],
"env": {
"CANDOR_ROOT": "<CANDOR_ROOT>"
}
}
}
}
متغيرات البيئة
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.
التوثيق
مأخوذ من ملف README للمشروع على GitHub ويُحدَّث أسبوعيًا. تفتح الروابط على GitHub.
المحتويات
candor-ts
candor for TypeScript: per-function side effects, transitively, with a deterministic policy
gate. candor-ts resolves every call through the TypeScript compiler API and reports, for each
function in your project, which effects it can reach — Net, Fs, Db, Exec, Env, Clock,
… — including effects inherited through any chain of calls across files, with a disclosed
Unknown wherever resolution fails (a callback value, an any-typed callee — never silently
pure). A candor-spec implementation, sibling of the
Rust and
JVM engines.
Site: candor.poly.io — the measured case in five minutes.
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.
A checked-in .candor/config (spec §3.4) replaces the env wiring — policy arch.policy /
baseline <report.json> / deps <report paths> one per line, discovered by walking up from the
scan target; relative values resolve against the config's repo, so CI is "point at the repo". A
configured-but-unusable config/policy/baseline fails loud (exit 2), never silently gateless.
The scan-time baseline guard (AS-EFF-005, spec §7) makes effect regressions un-shippable:
point CANDOR_BASELINE (or the config's baseline key) at a saved report, and any
function that gained an effect fails the scan — exit 1, the records join the --gate-json
verdict, each carrying origin (existing / new / unknown). ⟨0.40⟩ A function ABSENT from the
baseline is compared against nothing, so a new effectful function fails too; a new pure one passes,
and a new Unknown-only one is named in a note. Module.fn keys mean a renamed file reads as absent:
review with candor diff <this run's report> <baseline> before re-recording. The guard is
fail-closed like the policy gate: a present-but-unparseable baseline, or one produced by a
different engine build (§2.1 — an engine upgrade is baseline-invalidating), exits 2 without
evaluating; only a genuinely absent file is a one-line note (guard not active). Keep the two
surfaces straight: query diff is the read-only comparison — it discloses a producing-build
mismatch (⚠, exit 0) and informs; the scan-time guard is the gate-grade fail-closed surface, the
one CI should hold. Semantics mirror the reference engine (candor-java) exactly.
Staying current: check your installed version and upgrade — candor/AGENTS.md §2a. npx -y candor-ts --version prints the build, the spec, and the upgrade one-liner (offline; candor never phones home).
Function names are module-qualified with . segments (src.db.save), so policy scopes read
naturally. A function declared inside a TS namespace carries the namespace segments in fn and
the callgraph keys (src.util.Ns.helper) — so layer policies on namespaces bite — while the §2
hash join key keeps the bare local name; builds before 0.8.7 omitted the segments, so crossing
that line invalidates saved baselines (regenerate them). A pure <scope> rule forbids every
effect but not Unknown — the §4 trust marker is uncertainty, not an effect (matching the
reference engine, candor-java); deny Unknown <scope> is the explicit knob for boundaries that
must also exclude the unverifiable case.
# .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
The report carries the four literal surfaces where a declaration makes them decidable —
hosts at Net calls, tables at Db calls (SQL table positions, mirroring the Rust/JVM
extractors exactly, plus TypeORM's @Entity("user") declarations read through the receiver's
Repository<T> type argument), cmds at Exec, path-shaped paths at Fs — never from a
runtime-computed value, propagated transitively, enforced by the allow rules above. On a real
Nest app this makes table-level policy live: allow Db in article.service article comments flags
the service reaching user and follows.
The classifier is curated (the same under-report-and-say-so posture as the other engines): the
Node builtins (fs, net/http/tls, dns, child_process, worker_threads, node:sqlite,
node:vm, process.env, the clock), the web network globals (fetch, XMLHttpRequest,
navigator.sendBeacon, and WebSocket/EventSource — construction, send and close, charged
identically whether they resolve through lib.dom or through @types/node's undici-types
re-export), the HTTP/queue/mail tier (axios/got/node-fetch/undici/ws/
socket.io/nodemailer, gaxios + googleapis-common + google-auth-library, stripe, @sentry/*,
posthog-node, bull/bullmq), the database drivers (pg/mysql2/mongodb/redis/ioredis/sqlite3/
better-sqlite3/knex) and the ORM tier (TypeORM — with @Entity("…") table extraction —
Prisma, Mongoose, Sequelize, drizzle-orm), plus execa/cross-spawn/shelljs/open, fs-extra/
graceful-fs/rimraf/glob/chokidar, dotenv, winston/pino/bunyan. An unlisted npm package contributes
nothing — candor never guesses an effect — but the scan names it: the receipt's coverage-ledger
line (marker: classifier doesn't cover) lists every package the code demonstrably calls that
candor's classifier neither classifies nor has reviewed-pure, and each function carries the
invisible list it (transitively) reaches.
⟨0.32⟩ Node core is the one part of the classifier that is a denylist, not a curated list. The
builtins are a finite set, so every module's surface is reviewed and a core member that is neither
classified nor reviewed effect-free reads Unknown[native:<module>.<member>] — never pure. That is
why v8.writeHeapSnapshot() is Fs, inspector.open() is Net, and repl.start(),
process.dlopen() and new worker_threads.Worker(file) are visible holes rather than silence.
MCP server — candor as agent ground truth
candor-ts-mcp exposes the read-only queries as an MCP server, so
a coding agent can ask "if I change this, what's the runtime blast radius?" or "what reaches the
network?" and get deterministic ground truth from a precomputed report — instead of burning tokens
tracing the call graph by hand (the measured ~700–2000× token win on blast-radius questions).
// 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" } }
Tools: candor_impact (backward blast radius), candor_reachable (what runs at runtime),
candor_where (effect surface), candor_path (how an effect is reached), candor_callers,
candor_show, candor_map, candor_containment, candor_blindspots, candor_whatif (pre-edit
gate check — a given-but-unreadable policy is a loud error, never a clean verdict), candor_gate
(the checked-in .candor/config policy verdict), candor_diff/candor_gains (baseline deltas).
Each takes an optional report prefix (else $CANDOR_REPORT); --root <dir> locks the server to
one workspace. The server is query-only — it never scans (the analyzer self-boundary, spec
§7.12: an agent or a hook produces the report; the server reads it, Fs only). The query logic is
the shared query-core.mjs, the same answers the CLI gives.
candor-lsp renders the same report where the code is, for any LSP-native editor (helix,
neovim; the JetBrains plugin bundles it): a CodeLens per effectful function (⚡ Db, Net · blast
radius 12), hover provenance (the hop chain to where an inherited effect is performed), and the
repo's policy verdict as diagnostics. Like the MCP server it is a pure report consumer — any
engine's report — and never scans. (Both report-computed gates are advisory: the engine's own
--gate-json run additionally fails masked/incomplete literal surfaces and is the authoritative
CI form.)
It also answers the pre-edit question in place: inside a function, a code action per boundary
effect the fn doesn't yet perform — candor: what if handler performed Net? — runs the same
whatif as candor-ts-query whatif/candor_whatif (blast radius + the policy rule that WOULD
fire) and shows the verdict as a message plus a transient diagnostic at the function (cleared on
the file's next open/save; with no policy discovered it says so and reports the radius alone).
Plain textDocument/codeAction + workspace/executeCommand (candor.whatif) — it works
unmodified in helix, neovim, VS Code, and JetBrains via LSP4IJ.
The live loop — candor-ts-watch keeps the report fresh as the agent edits, so the answers are
about the current code, not a stale snapshot:
candor-ts-watch ./src --out .candor/report # re-scans only when a tracked source actually changes
It tracks the project's sources by content hash and re-scans on a real change (a no-op save or an
unrelated write does nothing), writing the same prefix the MCP server reads. So: agent edits →
watcher refreshes the report → agent asks candor_impact and gets the post-edit answer. And it
reports the edit-delta — not just that the report is fresh but what the edit did to the effect
surface (re-scanned (1 changed: app.ts) — Δ f +Net), so the agent learns the consequence of its
own change. v1 runs a full (sound) scan per change; the deeper perf optimisation — re-analysing
only the changed file's subgraph instead of the whole project — is the staged next step (the
content-hash gate is its first increment).
Trust contract (spec §4)
Anything candor-ts can't resolve is designed to read Unknown, never silently pure — known open gaps
are listed in candor-spec's SOUNDNESS.md (e.g. R780, R803, R804): a function-valued parameter or
field being called, an any-typed callee, resolution landing on a type rather than a body.
An uncurated dependency can opt out of Unknown/silent-pure by declaring its effects in its
package.json — "candorEffects": ["Net"] (spec §5.1, the effect manifest). candor-ts reads it as
the declared-not-verified tier: the package's calls classify to the declared set, and it stops being
a coverage-ledger blind spot. A name outside the §1 vocabulary voids the declaration loudly (a typo must not
silently narrow a surface). And candor-ts-query gains <cur> <base> flags the supply-chain
delta — the effects a surface gained between two reports.
Real-world consequence, measured on rimraf (50 files, 55
functions analyzed): its DI-style fs injection means many functions read Unknown, disclosed —
that's the contract working, not noise. The report says "can reach", never "does"; an absent
literal is never a claim of absence.
Framework dependency injection (spec §2's bounded-CHA clause). rimraf's injection is a function-valued
parameter, which reads Unknown. A DI container is measured separately, on NestJS 10 with an executed fixture:
- a provider entry
{ provide: Store, useClass | useValue | useFactory | useExisting: … }whose token is a project CLASS is read as a conversion: the substitute joinsStore's candidates, sothis.store.save()is charged the union ofStore.saveand the substitute'ssave(SOUNDNESS R1061 — silent before, all three forms executed); - a parameter typed by an INTERFACE (
@Inject("SINK") sink: Sink) dispatches over the project's visible implementors ofSink, so auseClassthatimplements Sinkis charged; auseValueobject literal that implements nothing readsUnknown(dispatch:). - the TOKEN is read through the checker's type, so
const TOK = Store, an imported token,TOKENS.storeand a.mapover[Store]nameStore; a helper'stoken/implparameter is read through its visible call sites; a token typedFunction/Type<any>joins its substitute to every project class it is assignable to (R1061); - a substitute the scan cannot name (
useClass: impls[i]typedType<Store>, auseValuetypedany) makes a dispatch on the token class readUnknown(dispatch:).
Named miss: a provider list the scan cannot see at all (one imported from an unscanned package) and a substitute
supplied by a dependency for a token the dependency owns. Gate a DI-heavy service with deny <E> Unknown as well as
deny <E>.
Cross-engine consistency — machine-checked
candor-ts is one of the four code engines (with the reference engine candor-java, the Rust
engines, and candor-swift) held together by the spec's cross-engine conformance suite: the shared
effect-set oracle, the §6.2 policy-grammar battery (including allow Db), the §3.1 query-shape
and match-ladder checks, the gate exit-code contracts, and the newer parts up through the
pure-vs-Unknown ruling (PART 16) — the engines must answer identically, on every push to the spec.
What the analysis core implements (and where the spec told it how)
| Piece | Spec source |
|---|---|
Resolve every call via the compiler API (getResolvedSignature), never syntax | CLASSIFIER §1 |
The classifier maps the resolved target's module (node:fs→Fs, node:net→Net, …) | CLASSIFIER §2, TS notes |
process.env property read → Env; Date.now → Clock | SPEC §1 |
| Local edges (cross-file) + least-fixpoint propagation | SEMANTICS §5a |
| Closure bodies attribute to the nearest enclosing function | SEMANTICS §2 |
A call resolving to a type (function-typed field/param) → Unknown, never silent-pure | SPEC §4 |
| Unmatched external calls contribute nothing (curated-classifier caveat) | SEMANTICS §8 C1 |
The literal surfaces hosts/cmds/paths/tables, literal-read only | SPEC §2 |
{ candor: { version, toolchain, spec: "0.40" }, functions } envelope; pure fns omitted | SPEC §2/§2.1 |
| Call-graph sidecar with every analyzed function a key | SPEC §2.2 |
| The gate: AS-EFF-006 / 008 / 009, loud on an unreadable policy | SPEC §6.2 |
Origin: the derivability proof
This engine began as a deliberately minimal single-file slice written from the spec documents
alone (SPEC.md, SEMANTICS.md, CLASSIFIER.md) — without consulting the Rust or JVM sources — to
answer executably: is the spec enough to derive a new-language implementation? Yes — 20/20 on
the shared oracle. That clean-room claim is frozen at commit a29b152; everything since
(multi-file projects, the query surface, the gate, the literal surfaces) is spec-implemented but
post-hoc, and its guarantee is the conformance differential above, not clean-room provenance. The
one engine-fix the original derivation needed (a call landing on a function-type declaration read
as pure until §4 was applied to it) remains the proof point: the fix was "do what §4 says", not "go
read the Rust source".
Status
Speaking candor-spec 0.40 (the package version is in package.json): the analysis core, the gate (--policy / --gate-json /
.candor/config), the full §3.1 query surface (including containment, blindspots, the
--include-unknown dispatch frontier, and ⟨0.24⟩ gate --report — the gate applied to an EXISTING
report, byte-equivalent to scan --policy's verdict), the MCP server, the LSP server, and the watch loop are
real, behaviorally tested (npm test — the behavioral suite across six harnesses), soundness-fuzzed
with verified teeth (node fuzz.mjs — spec §7.13: generated effect chains through every encoded
call form, any silent-pure = red), and conformance-held against the Rust/JVM/Swift engines. The
npm classifier tier is deliberately curated and keeps growing case-by-case. Entry points
(Nest/Next populations), unknownWhy origins, reachable, cross-package inheritance
(CANDOR_DEPS + the spec §2 hash, version-trusted per §2.1), and --allow-js are all in.
On npm: npx -y candor-ts <dir>. Per-release detail (⚠ marks report/verdict-affecting changes):
CHANGELOG.md.
Development
No build step — the engine runs on Node directly.
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
The pure cores are factored into importable modules — query-core.mjs (the §3.1 queries),
policy.mjs (the §6.2 DSL + literal matchers), and scan-core.mjs (the classifier + the SQL/
command/host extractors) — so they're unit-tested directly; the TS-compiler-driven walk stays in scan.mjs.
Agents: install candor as an MCP server
candor-ts ships candor's read-only query surface as an MCP server, so an agent asks "what is the
blast radius of changing this?" or "what reaches the network?" and gets a deterministic answer from a
precomputed report instead of grepping for it.
npx -y candor-ts --mcp # the server, over stdio
candor mcp install # or let the umbrella write/merge .mcp.json for you
Registration instructions come from the package you already installed — candor mcp --help prints
the .mcp.json snippet and the claude mcp add line, and candor mcp install writes them. There is no
remote file to fetch, tamper with, or auto-execute, and nothing here tells an agent to run anything it
did not already choose to install.
Discovery is passive: the manifest is server.json, published to the official
MCP Registry so clients and directories can find candor
without being handed a script.
The registry verifies namespace ownership from the mcpName field in package.json — that is the
npm mechanism. (A mcp-name: README marker is the crates.io convention and does nothing here; the
first cut of this used it, and the registry refused the publish with
missing required 'mcpName' field. The line below is kept only because it is how a human greps for the
namespace.)
mcp-name: io.github.tombaldwin/candor
الأسئلة الشائعة
ما هو candor؟
Per-function effect analysis for agents: blast radius, what reaches the network, gate verdicts.
كيف أثبّت candor؟
يعمل على جهازك. في Claude Code: claude mcp add candor -e 'CANDOR_ROOT=<CANDOR_ROOT>' -- npx -y candor-ts@0.40.17 --mcp. وفي العملاء الآخرين، الصق إعداد JSON من هذه الصفحة في إعدادات MCP الخاصة بهم.
هل يحتاج candor إلى مفتاح API؟
لا يذكر إدراجه في السجل أي مفتاح أو سر إلزامي. قد تتطلب بعض الميزات حسابًا لدى الخدمة التي يتصل بها.
هل candor مفتوح المصدر؟
نعم. شيفرته متاحة على GitHub بترخيص Apache-2.0.
من يصون candor؟
ينشره tombaldwin. يدرجه AI.tn لكنه لا يصونه ولا يوصي به.
من سجل MCP الرسمي. الإدراج ليس توصية: راجع شيفرة الخادم وصلاحياته قبل منحه الوصول إلى بياناتك.