Show HN: Evidence Graph – type checking for the specs your AI agent implements
Evidence Graph is a tool that turns AI agent specifications into compile errors. It forces agents like Claude Code and Codex to explicitly acknowledge every requirement with a reason, preventing omission.
Notifications You must be signed in to change notification settings
Fork 1
Star 2
BranchesTags
Open more actions menu
Folders and files
NameName
Last commit message
Last commit date
Latest commit
History
149 Commits
149 Commits
.agents/skills
.agents/skills
.github
.github
.vscode
.vscode
config
config
packages/evidence
packages/evidence
tests/test-evidence
tests/test-evidence
.gitignore
.gitignore
AGENTS.md
AGENTS.md
CLAUDE.md
CLAUDE.md
LICENSE
LICENSE
README.md
README.md
og.jpg
og.jpg
package.json
package.json
pnpm-lock.yaml
pnpm-lock.yaml
pnpm-workspace.yaml
pnpm-workspace.yaml
prettier.config.js
prettier.config.js
Repository files navigation
The evidence graph for the AI coding era: the guardrail for goal mode.
Your spec is now a compile error.
When Claude Code or Codex works unattended, it can skip a requirement and still report "done." Evidence Graph makes every configured requirement demand an explicit acknowledgement from the code, test, or document that claims to satisfy it.
Every acknowledgement names the exact target and states why it applies. The compiler does not decide whether that reason is true—it forces the agent to commit to a concrete claim. A fabricated reason can no longer hide inside a plausible diff; it sits beside the declaration and evidence it contradicts.
An agent can still lie. It cannot lie by omission:
Complete: every configured obligation is accounted for, or the build fails.
Tested: every selected export is claimed by a test, by name.
Documented: decisions and code stay explicitly connected.
Honest: "done" comes with a target and a reason.
Integrity: no citation outlives its target.
/**
- @evidence docs/discount.md#coupon-stacking Renders the combination limit defined by this rule.
*/ export function CouponStackingNotice() { return
One seller coupon and one platform coupon may be combined.
; }
Without the @evidence citation, the next build stops:
$ npx ttsc error TS16411: [evidence/graph] Missing acknowledgement for 'docs/discount.md#coupon-stacking' (Markdown H2 'Coupon Stacking' at docs/discount.md:3) in Claim 1 reference 1 (markdown, symbols: h2, h3).
Add '@evidence docs/discount.md#coupon-stacking ' to a selected typescript host of this claim, or '@evidenceExclude docs/discount.md#coupon-stacking ' when this claim intentionally does not use it.
Found 1 error.
Setup
Install
npm install -D typescript ttsc @ttsc/lint npm install -D @samchon/lint-plugin-evidence
This is a lint plugin for @ttsc/lint. It runs on ttsc, not on stock tsc with ESLint. If your build does not run ttsc yet, adopt that toolchain first.
The first build can take several minutes; it links the rule into the lint binary once, and later builds reuse it.
Configure
// lint.config.ts import type { ITtscLintConfig } from "@ttsc/lint"; import { evidence, type IEvidenceGraphConfig } from "@samchon/lint-plugin-evidence";
const graph: IEvidenceGraphConfig = { claims: [ { type: "typescript", files: ["src/components//*.tsx"], symbol: "function", reference: { type: "markdown", files: ["docs//*.md"], symbol: ["h2", "h3"], }, }, ], };
export default { plugins: { "evidence": evidence, }, rules: { "evidence/graph": ["error", graph], "evidence/documented": "error", "evidence/singular": "error", }, } satisfies ITtscLintConfig;
Register the plugin in lint.config.ts and pass the graph declaration as the option of the evidence/graph rule. This graph reads as one sentence: the React components under src claim to implement the docs, so every H2 and H3 section under docs must be cited by a component.
evidence/graph is project-scoped, so its entry must have no files selector; the host rejects one that does. Scope a file rule in its own entry when you need to.
Violations surface in every ttsc build, every --noEmit check, and every ttsx run. They arrive in the same stream as type errors. No separate CI job.
Compose
const graph: IEvidenceGraphConfig = { claims: [ // 1. feature documents build on the requirements { type: "markdown", files: ["docs/features//*.md"], reference: { type: "markdown", files: ["docs/requirements//*.md"], symbol: ["h2", "h3"], }, }, // 2. components implement the feature rules { type: "typescript", files: ["src/components//*.tsx"], symbol: "function", reference: { type: "markdown", files: ["docs/features//*.md"], symbol: ["h2", "h3"], }, }, // 3. tests verify the feature rules and the components { type: "typescript", files: ["test/features//*.ts"], symbol: "function", reference: [ { type: "markdown", files: ["docs/features//*.md"], symbol: ["h2", "h3"], }, { type: "typescript", files: ["src/components/**/*.tsx"], symbol: "function", }, ], }, ], };
A graph is one claims array, and every claim-reference pair is an independent obligation:
Markdown can claim Markdown. The feature documents must acknowledge every requirement they build on.
Every feature rule must be cited by a React component; a rule no component mirrors is a compile error naming that rule.
A reference array is one obligation per element. The tests must verify every feature rule and claim every exported component, never one obligation borrowing the other's citation.
Symbols
Kind symbol values Default
"markdown" "file", "h1", "h2", "h3", "h4" ["file", "h1", "h2", "h3", "h4"]
"swagger" No symbol property; every operation under paths is selected every operation
"typescript" "type", "function", "property" all three for claims, "type" for references
For TypeScript, "type" selects exported interfaces, type aliases, and namespaces. "function" selects exported callables. "property" selects properties declared by exported type-level symbols and exported const, let, and var declarations at module or namespace scope; a const initialized with an arrow or function expression remains a function, while every other variable is a property. Qualified identities preserve their owner: Orders.Input.id is a property below Orders.Input, while Orders.state is namespace data.
Ambient namespace members follow TypeScript's implicit export semantics. Exported object and array binding patterns expose each local binding leaf as a property. A type-only namespace alias exposes its public type-space descendants and their properties without exposing namespace data or callables.
A reference's symbol selects the evidence units one obligation covers, and an array widens that unit set without creating a second obligation. The units retain their hierarchy: a Markdown file contains its heading outline, a TypeScript interface or object type contains its properties, and a namespace contains every nested public unit. A target acknowledges itself and every selected descendant. An ancestor remains addressable even when its own kind is omitted from the selector, so symbol: "property" can still be covered by one @evidence IShoppingSale ....
A claim's symbol uses the same selector for the opposite side: it restricts which symbol kinds may host an @evidence tag. Namespaces are type hosts, exported data variables are property hosts, and a mixed variable statement can host either of its resident kinds. Omit either selector to accept its documented default.
Swagger is reference-only. It cannot host declarations and has no symbol selector: each operation under the normalized document's paths object is one independent obligation.
File patterns
Every Markdown or TypeScript files property takes project-relative glob patterns, not regular expressions. * matches inside one path segment, crosses segments, and ? matches one character. A bare directory such as docs does not select its descendants; write docs/ for the subtree.
docs/**/*.md selects every document below docs.
backend/src/**/*.ts selects every backend source file.
frontend/src/components/**/*.tsx selects every React component.
test/features/**/*.ts selects every feature test function.
TypeScript populations
A TypeScript reference selects its population three ways, and the choice decides how its units are addressed.
// every exported type under src/contracts, addressed by its own name { type: "typescript", files: ["src/contracts/**"] }
// everything the entry exposes, addressed by its accessor path from that entry { type: "typescript", file: "src/sdk/index.ts" }
// the same, for a package a consumer installs { type: "typescript", package: "@ORGANIZATION/PROJECT-api" }
files and file are mutually exclusive, and a local reference must set one of them; there is no implicit project entry.
An entry-selected population is addressed the way a consumer reaches it, not the way the declaring file spells it: export * as functional nests a path segment, export * from flattens one, and export { A as B } addresses the symbol as B. That is what makes api.functional.questions.get nameable. Identity still belongs to the declaring file, so a symbol an entry exposes through two paths is one unit answering to two addresses — acknowledged once rather than owed twice.
A package population is read from disk rather than from the ttsc program, which is the point: a symbol nothing imports is absent from the program by definition, and it is exactly the symbol an obligation needs to name. Without file or files, the package's declaration entry is the population, resolved through the types condition of its exports map, then typesVersions, then types or typings — never main, which names the JavaScript a consumer runs rather than the declarations a citation can address. With files, the globs are package-relative.
The obligation set of a package reference belongs to whoever publishes it. A minor release that adds exports adds obligations, so pin the version or narrow the selection when the population is not yours.
Swagger API references
A Swagger reference owns exactly one document through its singular file property:
const graph: IEvidenceGraphConfig = { claims: [ { type: "typescript", files: ["src/controllers/**/*.ts"], reference: { type: "swagger", file: "api/openapi.yaml", }, }, ], };
file is either one exact project-relative path or one exact http:/https: URL; it is never a glob. Use a reference array when one claim owes separate coverage to several API documents.
Swagger 2.0 and OpenAPI 3.0, 3.1, and 3.2 JSON or YAML documents are normalized through @typia/utils to OpenApi.IDocument before indexing. A local document is read and a remote document is fetched on every evidence-graph project evaluation; failures, non-2xx responses, invalid documents, 30-second remote timeouts, and documents larger than 16 MiB fail the build.
Only operations under paths become evidence units. Webhooks and component schemas are outside this reference type. Standard and additional operation methods use the same target identity.
One-shot checks always evaluate the current Markdown, TypeScript, and Swagger sources. The current ttsc check --watch host does not start a new cycle for a standalone Markdown or local Swagger edit, and its LSP invalidates external changes without immediately republishing project diagnostics; the next TypeScript-triggered cycle is fresh. Upstream tracking lives in the external-input contract, CLI watch integration, and LSP diagnostic refresh.
Evidence Tags
The tags below are not yours to write. Your agent writes them as it implements, and your job is to review the stated reasons.
Cite
/**
- @evidence docs/sales.md#sale-price This DTO exposes the documented price.
*/ export interface IShoppingSale { price: number; }
A TypeScript declaration cites in its JSDoc. The tag is @evidence target reason: the target names one evidence unit as the root of an acknowledgement scope, and everything after it is the reason. The reason is required, because a citation that cannot say why it exists is filler.
The target takes these forms:
Target Cites
docs/sales.md A Markdown document and
[truncated for AI cost control]