Configuration
Every obligation the compiler counts is declared here.
This page is the value you pass to evidence/graph in lint.config.ts. Its type is ITtscEvidenceGraphConfig, and its only property is claims, which must not be empty.
import { evidence, type ITtscEvidenceGraphConfig } from "@ttsc/evidence";
import type { ITtscLintConfig } from "@ttsc/lint";
const graph: ITtscEvidenceGraphConfig = {
claims: [
{
name: "components implement the requirements",
type: "typescript",
files: ["src/components/**/*.tsx"],
symbol: "function",
reference: {
type: "markdown",
files: ["docs/requirements/**/*.md"],
symbol: ["h2", "h3"],
},
},
],
};
export default {
plugins: { evidence },
rules: { "evidence/graph": ["error", graph] },
} satisfies ITtscLintConfig;The two sides
A claim owns the outgoing side of every edge it declares: its files host the citations. A reference owns the incoming side: its units are what must be cited, and they are the denominator of the coverage number.
Four words carry the rest of this page.
- A population is the set of files a
filesglob selects. - A unit is one addressable thing inside it, such as a heading or an exported symbol.
- A host is a declaration that may carry a tag.
- A carrier is a file where an exclusion is allowed to live.
Separate claims stay separate obligations. Two teams each covering half of one document are never reported as one complete use of it, and that is why the count is never pooled.
The reference property takes one reference or an array. An array is not a union, it is a list of independently complete obligations, so a host citing one element never contributes to another.
{
type: "typescript",
files: ["src/controllers/**/*.ts"],
symbol: "function",
reference: [
{ type: "markdown", files: ["docs/requirements/**/*.md"], symbol: ["h2", "h3"] },
{ type: "prisma", files: ["prisma/schema/**/*.prisma"], symbol: ["model"] },
],
}That controller population owes complete coverage of the requirements and complete coverage of the schema. Answering one of them fully leaves the other exactly as red as it was.
What each artifact kind can do
| Artifact | Hosts tags | Materializes units |
|---|---|---|
| Markdown | yes | yes |
| Prisma | yes | yes |
| TypeScript | yes | yes |
| Swagger | no | yes |
Swagger is reference-only because an API operation has no authored comment to host a tag. It grounds a claim; it never makes one. How each kind’s targets are spelled belongs to Evidence Tags.
A document answering to the document above it is the same shape with both sides in Markdown.
{
type: "markdown",
files: ["docs/requirements/**/*.md"],
symbol: ["h2", "h3"],
reference: {
type: "markdown",
files: ["docs/ideas/**/*.md"],
symbol: "h2",
},
}Its citations live in HTML comments, so every idea note owes an answer while the page a stakeholder reads stays clean.
TypeScript evidence carries one more restriction: only a TypeScript claim may cite it, because a citation of a symbol resolves through the citing module’s own imports and no other artifact kind has any. The pairing is refused when the configuration is decoded rather than failing later as a mystery.
Claim properties
| Property | Required | Meaning |
|---|---|---|
type | yes | "markdown", "prisma", or "typescript". |
files | yes | Globs for the artifacts that must cite. At least one positive pattern. |
reference | yes | One reference, or an array of independently complete references. |
name | no | Label carried by this claim’s diagnostics, and nothing else. |
severity | no | Inherits the rule level when omitted or undefined; "off" disables the claim. |
symbol | no | Which declaration kinds may host a tag. Defaults differ per artifact kind. |
root | no | Directory the files patterns resolve against. Defaults to the project root. |
disabled | no | Validates the shape and contributes nothing else. |
evidenceExcludeCarriers | no | Globs for the only files that may host this claim’s exclusions. |
A named claim reads as Claim 1 ('dto-types') wherever it is reported, which is worth the line as soon as a config holds more than two claims.
Severity
Per-claim and per-reference severity requires an @ttsc/lint release newer than 0.28.6. Update @ttsc/evidence and @ttsc/lint together.
Both claims and references accept optional severity. Reference diagnostics use reference.severity, then claim.severity, then the outer evidence/graph rule level. Omitting a property or setting it to undefined inherits the enclosing level. The values are the same as @ttsc/lint: "off", "warning" (also "warn"), and "error", or 0, 1, and 2.
Keep the graph at "error" and lower one obligation to a warning:
{
type: "typescript",
files: ["src/controllers/**/*.ts"],
symbol: "function",
severity: undefined, // Inherit the outer rule's "error".
reference: [
{ type: "markdown", files: ["docs/requirements/**/*.md"], symbol: "h2" },
{ type: "markdown", files: ["docs/drafts/**/*.md"], symbol: "h2", severity: "warning" },
{ type: "prisma", files: ["prisma/**/*.prisma"], severity: "off" },
],
}Set severity on the claim to change its own diagnostics and the default for all its references. A claim’s "off" disables the whole claim, including references with an explicit level. A reference’s "off" disables only that reference. Disabled entries are still validated but contribute no populations, obligations, completion hints, or watched inputs. A claim whose references are all off contributes nothing. The outer rule’s "off" disables the entire graph, and disabled: true on a claim continues to disable it regardless of severity.
Coverage and reference-policy findings use the reference level. Malformed or unresolved claim tags use the claim level because no reference owns a resolved obligation yet. A shared source or tag finding keeps the strongest level among its owners. Configuration errors and an invalid project root use the outer rule level. Warnings are printed without failing the command; they still represent an incomplete graph, so completion hints remain withheld until the graph has no findings.
Globs, not regular expressions
* matches inside one path segment, ** crosses any number of them, and ? matches one character. Both / and \ are accepted as separators, while path identity stays case-sensitive on every operating system.
Patterns are read left to right. A ! prefix removes matches and a later positive pattern can bring them back, so the array must contain at least one positive pattern to select anything.
A bare directory such as src or src/ does not include its children, so write src/** whenever the subtree is what you meant.
Roots
root names one directory, never a glob. It may sit inside the project, above it as ../../docs, or on an absolute path, and a symbolic link or Windows junction to a directory is read through. A drive-relative Windows path such as C:docs is refused, because it resolves against wherever that drive currently sits instead of against a stable base.
For a Markdown reference the root does more than resolve patterns: it is the base its targets are addressed from. With root: "../../docs" and files: ["requirements/**"], a section is cited as requirements/pricing.md#discounts, so two packages sharing one requirements set write the identical citation instead of spelling out each package’s distance from the documents.
A TypeScript claim’s root is different. It never scans a directory and never adds files to the Program, so only sources the owning tsconfig.json already includes can match.
Symbol selectors
On a claim, symbol decides where a tag may sit. On a reference, it decides which units are obligations. The two defaults differ for that reason: a claim’s default is the widest set of hosts, while a reference’s default promises a denominator.
| Kind | Claim default | Reference default | Values |
|---|---|---|---|
| Markdown | ["file", "h1", "h2", "h3", "h4"] | ["file", "h1", "h2", "h3", "h4"] | file, h1, h2, h3, h4 |
| Prisma | ["model", "column", "relation"] | ["model"] | model, column, relation |
| TypeScript | ["type", "function", "property"] | "type" | type, function, property |
| Swagger | not applicable | every operation under paths | none |
Selecting a narrower set does not make the wider one unaddressable. Ancestors of a selected unit stay resolvable as aggregate scopes, so a reference selecting only h3 can still be acknowledged by a citation of the H2 above it, and the H2 is not itself an obligation. Evidence Tags has the containment rules behind that.
What counts as a TypeScript kind
The three TypeScript kinds are semantic rather than a list of AST node names, so a few ordinary declarations land where a first guess would not put them.
type is the exported interfaces, type aliases, classes, and namespaces. An enum is not a type unit.
function is the callable forms: function declarations, a const initialized with an arrow or function expression, public instance and static methods, method signatures, and the same forms exported from a namespace. An overload run is one unit, and constructors and accessors are not selected at all.
property is everything else a container declares, plus exported const, let, and var at module or namespace scope, and every exported leaf of a binding pattern.
| Declaration | Kind | Why |
|---|---|---|
export const charge = () => {} | function | A const with a callable initializer. |
export let charge = () => {} | property | Only const keeps the callable classification. |
charge: () => void on an interface | function | The function type is written out. |
charge: Handler on an interface | property | An alias of the same type reads as data. |
get total() on a class | neither | Accessors are not selected in either kind. |
private rate: number | neither | private and protected members are withheld. |
constructor(override rate: number) | property | A parameter property declares a public field. |
The member test is syntactic because the rule reads no type checker, which is exactly why charge: () => void and charge: Handler part ways. A citation for a parameter property belongs on the parameter, since a constructor’s own block could not say which of two fields it meant.
Exclusion carriers
@evidenceExclude is the one acknowledgement that discharges an obligation without anything being built, so every exclusion a claim owns has to be read by a person. Scattered through the population, reading them means reading the population.
evidenceExcludeCarriers gathers them. Declare the file or files an exclusion may live in, and an exclusion written anywhere else is reported where it sits and discharges nothing, leaving its target still owing positive evidence.
{
type: "typescript",
files: ["src/structures/**/*.ts"],
evidenceExcludeCarriers: ["src/structures/DTO_EVIDENCE_EXCLUDE.ts"],
symbol: "type",
reference: { type: "prisma", files: ["prisma/schema/**/*.prisma"], symbol: ["model"] },
}The patterns narrow and never widen. A carrier must already be selected by files, and this is independent of noEvidenceExclude, which decides whether one reference accepts an exclusion at all. A claim whose every reference refuses exclusions gains nothing from declaring carriers.
A checklist reference refuses these globs, because a checklist makes every acknowledgement one host’s own answer and confining exclusions to another file would leave every other host unable to record that an item does not apply. A checklist that also declares noEvidenceExclude has no exclusion to gather, so the pairing is allowed.
Disabling a claim
disabled: true validates the claim’s shape and contributes nothing else: no populations, no references, no coverage, no completions, no watched inputs. This is the switch Adoption turns claims on with, one at a time, on a codebase that would otherwise report its entire backlog at once.
Reference properties
Every reference declares its type and may declare severity and any of the policies below. The rest is per kind.
Markdown takes files, an optional root, an optional symbol, and checklist. Every matching regular file is parsed as Markdown regardless of extension, so keep images and other assets out of the patterns.
Prisma takes files, an optional root, and an optional symbol. Every matching file joins one schema, which lets a lint-only .schema ledger sit beside the schema Prisma itself generates from. A model name is unique across the whole folder, so a target never names the file a model lives in and moving one between files cannot break a citation. A view is a model unit, because Prisma’s own parser returns it among the models; enums, composite types, indexes, and datasource settings are outside the contract entirely.
TypeScript takes files, which only a package reference may omit, plus an optional symbol. A local population is read from the ttsc Program, so a file outside the Program is not a match however well it matches the glob.
Swagger takes exactly one file, which is a path or an http:/https: URL rather than a glob. The document is normalized from Swagger 2.0 or OpenAPI 3.0, 3.1, and 3.2, in JSON or YAML, before its operations are indexed. A URL is fetched while the rule runs, so an unavailable document fails the build instead of quietly emptying the population. Owe coverage to two documents by writing two references.
TypeScript populations
A TypeScript files glob selects modules, and the population is what those modules publish. A barrel therefore carries in the surface it forwards even when the declaring file sits outside the globs, which is the usual reason a population turns out wider than the directory it names.
A unit is addressed the way a consumer reaches it rather than the way its file spells it. export * as functional nests a path segment, export * from flattens one, and export { A as B } is cited as B, so a citation written against the local binding matches nothing.
Identity still belongs to the declaring file. A declaration reached through two barrels answers to both addresses and stays one coverage unit rather than two obligations, and containment follows the declaration hierarchy rather than the address text.
Referencing a published package
A TypeScript reference with package reads declarations from disk instead of from the Program, and that 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.
{
type: "typescript",
files: ["tests/**/*.ts"],
symbol: "function",
reference: {
type: "typescript",
package: "@samchon/shopping-api",
files: ["src/functional/**/*.ts"],
symbol: ["function"],
noEvidenceExclude: true,
singleEvidencePerSymbol: true,
},
}Every published operation now owes exactly one test that answers for it. Omitting files is also legal there: the package’s declaration entry becomes the population, and each symbol is addressed by its accessor path from that entry, which is what makes api.functional.questions.get nameable.
The obligation set of a package belongs to whoever publishes it, so a minor release that adds exports adds obligations to your build. Pin the version or narrow the selection when the population is not yours.
Swagger documents
A local document is re-normalized only when its bytes change. A remote one is fetched once per process and answered from memory afterwards, so a served document that changes mid-session is not seen until the session restarts, while a one-shot ttsc run always fetches. A refused URL is never remembered, so a transient outage recovers on the next cycle.
A remote source also makes your build depend on that host being up and reachable, which a sandboxed CI runner often is not. Vendor the document into the repository when that dependency is not one you want, and let the pull request that updates it be where the new operations arrive.
A fetch failure, a non-2xx response, an invalid document, a 30-second timeout, and a document over 16 MiB all fail the build rather than quietly producing an empty population. Only operations under paths become units; webhooks and component schemas are outside this reference kind.
Prisma schemas
The model, column, and relation split follows Prisma’s own resolution rather than the schema text, which is what classifies a relation back-reference such as Seller.sales correctly even though it carries no @relation attribute.
Selecting "column" puts every id, created_at, and back-reference into the denominator. Turn it on where a claim really does owe an answer per member, and expect to reach for @evidenceExclude on the ones it deliberately does not use.
The schema is parsed by Prisma itself, resolved from your project when it can resolve one and from this package’s pinned schema engine otherwise. A schema Prisma rejects fails the build with Prisma’s own message and location, rather than becoming an empty population whose obligations are all vacuously satisfied.
Policies
Ordinary coverage is permissive on purpose. Either tag answers a unit, one host may cite any number of units, and one acknowledgement per unit is enough. That is right for a documentary obligation and too weak for a proof obligation, where one exclusion or one thorough host can discharge a whole population without proving anything.
Each policy tightens one reference and never the graph, so a strict operation obligation and an ordinary requirement obligation sit inside the same claim without either inheriting the other’s intent. Every one of them is opt-in, and its default is the behavior that existed before it did.
Four of the five tighten a count inside the obligation. checklist is the exception and belongs to Markdown alone, because it does not tighten a count at all: it gives the obligation a second dimension, the hosts.
| Policy | What it demands | Reach for it when |
|---|---|---|
noEvidenceExclude | The population refuses @evidenceExclude as an answer. | ”Not applicable” is the sentence that would hide missing work. |
uniqueEvidence | At most one host may cite each unit. | The evidence is meant to have one owner who is answerable for it. |
singleEvidencePerSymbol | Each selected host must cite exactly one unit. | One host answers for one thing, and eight names in one block prove nothing. |
requireReview | Every acknowledgement carries an unexpired review of its own kind. | The citation’s truth is what you need recorded, not just its presence. |
checklist | Every host answers every item. Markdown references only. | The document is read down a column: principles, review rules, a gate. |
uniqueEvidence counts distinct semantic hosts rather than declarations or tags, so an overload run and a merged declaration stay one host, repeated tags count once, and an exclusion contributes no host at all. A unit no host cites is reported as missing coverage instead.
singleEvidencePerSymbol counts the claim’s complete selected host population as its denominator, so a host carrying no tag fails exactly as a host citing two units does. An aggregate target contributes every selected descendant in its scope, which means citing a parent of two selected units counts as two.
requireReview is what makes a verdict expire. The review carries a #-prefixed fingerprint of the cited scope’s content, and when that content changes the fingerprint stops matching and the build fails again with the new value in the diagnostic. Expect a TypeScript citation to expire on any change inside the cited declaration, since a declaration’s digest is its own text and a nested member sits inside it; cite the narrowest symbol that actually answers when that breadth is unwelcome.
Checklists
Ordinary coverage asks its question once for the whole claim: has some declaration, anywhere, acknowledged this unit. One host citing a principles document therefore discharges it for every other host, which is the wrong question for a document meant to be read down a column.
checklist: true gives the obligation a host dimension. The denominator becomes every selected host times every selected item, so a host carrying no tag owes every item rather than being absent from the count.
{
name: "every function answers every engineering principle",
type: "typescript",
files: ["src/**/*.ts"],
symbol: "function",
reference: {
type: "markdown",
files: [".agents/skills/principles/SKILL.md"],
symbol: "h2",
checklist: true,
requireReview: true,
},
}A positive citation answers the item it names and nothing beneath it, and a target naming no item at all is refused as an aggregate; otherwise one citation of the whole document would tick every box, which is the state this option exists to end. An exclusion keeps the cascade, because “none of this applies here” is one reviewed decision however many items it covers.
Duplicate and conflict detection moves to the host as well. Two hosts excluding one item, and one host citing an item another host excludes, are the expected state of a checklist rather than a contradiction.
uniqueEvidence and singleEvidencePerSymbol are refused beside checklist, at configuration time rather than as coverage failures. They are not redundant here, they are contradictory: a checklist requires every host to cite every unit, which the first forbids as soon as a claim has two hosts and the second forbids as soon as the population has two units.
Pairing checklist with requireReview is what most checklists are actually for. Each host’s answer then carries the fingerprint of that item alone, so editing one principle expires every host’s answer to that principle and touches nothing else.
Rules
@ttsc/evidence contributes five rules. One is the graph, and the other four answer questions the graph does not ask.
// lint.config.ts
import { evidence, type ITtscEvidenceGraphConfig } from "@ttsc/evidence";
import type { ITtscLintConfig } from "@ttsc/lint";
const graph: ITtscEvidenceGraphConfig = {
/* claims */
};
export default {
plugins: { evidence },
rules: {
"evidence/graph": ["error", graph],
"evidence/documented": "error",
"evidence/singular": "error",
"evidence/review": "error",
"evidence/todo": "error",
},
} satisfies ITtscLintConfig;That block registers everything the plugin has, which is not where anyone should start: Choosing a set at the end of this page is the order to switch them on in.
Each rule is enabled on its own and carries the severities @ttsc/lint accepts, so "warning" and "off" work everywhere "error" does. Two of the five take options and the other three take a bare severity, because they have nothing to configure.
evidence/graph
The configured graph itself. Every target a declaration writes must resolve, and every selected evidence unit must be acknowledged, under the claims you declare in ITtscEvidenceGraphConfig.
"evidence/graph": ["error", graph]An artifact that cites nothing has no proof it was needed, and an artifact citing a target no configured population declares has proof of nothing. Both states are compile errors here, which is the whole product in one sentence.
The rule also feeds the editor: its configured targets become completions while it is green, which Evidence Tags covers with the rest of what an author sees.
Watched inputs
The rule declares its Markdown globs, Prisma globs, and local Swagger paths to the ttsc host, so editing a specification section or regenerating an OpenAPI document starts the next --watch cycle by itself, with no TypeScript file touched.
A path stays declared while it is missing, which is what lets a document that has not been generated yet be picked up the moment it appears, and a population above the project is declared on the same terms. An http: or https: Swagger source is the one exception, since a URL has no filesystem event to observe.
evidence/documented
A JSDoc block on every selected export.
"evidence/documented": ["error", { symbol: ["type", "function"] }]A JSDoc block is the only place a TypeScript declaration’s @evidence tag is read from, so an export without one cannot cite anything. Worse than the missing tag is what happens next: the obligation shifts onto whichever sibling does have a block, and the graph reports success while the wrong declaration answers for the work.
// reported: Missing JSDoc on exported function 'createOrder'
export function createOrder(props: IProps): IOrder;
// accepted
/** Creates one order from a validated cart. */
export function createOrder(props: IProps): IOrder;An empty block is reported separately from a missing one, since the two need different repairs: one needs a sentence, the other needs a block.
symbol takes one kind or an array of "type", "function", and "property", and defaults to all three. That default is deliberate, since the population which must be able to hold a tag is exactly the population a claim can select as a host.
Presence is the entire check. The rule never judges what the prose says, how long it is, or whether it is sincere, because a rule that tried would only teach authors to write filler that satisfies it.
evidence/singular
One public identity per TypeScript file, named after the file.
"evidence/singular": "error"TypeScript targets carry no file path, so two selected files exposing the same qualified name make every citation of that name ambiguous. This rule removes the class of problem instead of reporting each instance: when a file holds one public identity and that identity is the file’s name, the file path and the target agree by construction.
// src/createOrder.ts, reported: a file declares exactly one public identity
export function createOrder(props: IProps): IOrder;
export interface IOrder {}
// src/createOrder.ts, accepted
export function createOrder(props: IProps): IOrder;It changes how a codebase is laid out, which is why it ships as its own rule rather than as part of the graph. Adopt it where citations are dense enough that ambiguity is a recurring cost.
evidence/review
An @evidenceReview beside every @evidence, and an @evidenceExcludeReview beside every @evidenceExclude, naming the same target.
"evidence/review": "error"The citation’s reason says why this declaration answers for a target. Nothing in it says what was actually verified, and an unverified citation is byte-identical to a verified one. This rule asks the second question.
Where it is enabled matters more than whether it is. A package that only writes citations ships it "off" and the review pass that owns those claims turns it on, so every acknowledgement reports itself as unreviewed until a reviewer has reached it.
Expiry is a separate switch. This rule demands that a review exists; requireReview on a reference demands that it carries an unexpired fingerprint of the content it was written against.
evidence/todo
No remaining JSDoc @todo tag anywhere in a checked file, exported or not.
"evidence/todo": "error"The tag is reported wherever it sits, with the text it carries.
/**
* Settles one order against its coupons.
*
* @todo apply the per-issuer stacking limit
*/
export function settleOrder(props: IProps): ISettlement;A @todo is an unrealized contract someone wrote down. Each one is reported with its own text, so the diagnostic list reads as the ledger of what is left rather than as a count.
This is what keeps a scaffold honest during delegated work. An agent that stubs a declaration and records the gap cannot then report the work finished, because its own note is a build error until the gap closes.
Choosing a set
Start with evidence/graph alone. It is the rule that carries the product, and the first red build teaches the tag grammar faster than any page here can.
Add evidence/documented as soon as citations are being written, since it is the cheapest way to stop an obligation from silently moving to the wrong declaration. Add evidence/review when the question turns from whether the work was covered to whether the coverage is true, and evidence/todo when an agent is authoring against a scaffold you did not write.
Leave evidence/singular for last, and only where you want the file layout it implies.
Where to go next
Evidence Tags is the other half of this page: the configuration decides what is owed, and the tags are how it gets answered. Spec-Driven Development shows these properties in real configurations.