Evidence Tags
Two tags satisfy every obligation the graph declares, and two more record that one of them was verified.
@evidence <target> <reason>
@evidenceExclude <target> <reason>
@evidenceReview <target> <description>
@evidenceReview <target> #<fingerprint> <description>
@evidenceExcludeReview <target> <description>
@evidenceExcludeReview <target> #<fingerprint> <description>These are not tags you maintain by hand. An agent writes each one as it implements, and your job is to review the stated reasons. In an agent workflow they cost nothing extra: the citation is written at the moment the author knows why it is true.
Grammar
The target is one whitespace-delimited token, except that a target opening with {@link, {@linkcode, or {@linkplain runs to its closing brace. Everything after the target is prose. A declaration may carry any number of tags, and each is validated on its own.
The reason is required. It sits beside the exact unit it claims to honor, so a misreading surfaces where a reader can see both. The compiler never judges whether a reason is true; it forces a concrete claim, and a claim is reviewable in a way that silence is not.
A reason may run past one line. Continuation lines join it until the next tag of this grammar, and inside a JSDoc or Prisma comment any other line-opening @tag ends it too. That boundary is what keeps a neighboring @param or another tool’s @namespace out of the reason above it. An HTML comment has no field syntax, so there @architecture approved this stays prose and only this grammar’s own tags close a reason.
Target forms
| Target | Cites |
|---|---|
docs/sales.md | A Markdown document and every selected heading below it |
docs/sales.md#sale-price | A heading section and its selected descendants |
prisma:Sale | A Prisma model and its selected columns and relations |
prisma:Sale.price | One column or relation |
POST:/members | One Swagger or OpenAPI operation |
{@link sales.IShoppingSale} | An exported type, function, or namespace, with its selected descendants |
{@link sales.IShoppingSale.price} | One property of an exported type |
A path-addressed target is one whitespace-delimited token, which is why a Swagger operation is written POST:/members rather than POST /members. The second form still parses, as target POST and reason /members ..., and that grammar is preserved deliberately because POST may be the name of a TypeScript symbol.
Identity is exact. Paths are case-sensitive on every host, Swagger methods canonicalize to uppercase while Swagger paths do not normalize at all, and a Prisma target always carries its prisma: prefix so a model named Sale cannot compete with a TypeScript type of the same name.
Markdown anchors
A heading’s anchor is the explicit {#anchor} suffix when it has one, and a slug generated from the heading text otherwise. The slug lowercases the title, keeps letters, digits, and underscores, and turns spaces and hyphens into single - separators.
## Coupon Stacking {#coupon-stacking}Prefer the explicit form on anything a citation points at. A generated anchor changes when someone rewords the heading, and every citation of it breaks at once. An explicit anchor survives the rewording, which is the point of naming it.
Where a tag lives
TypeScript, in JSDoc
/**
* @evidence docs/sales.md#sale-price This DTO exposes the documented price.
*/
export interface IShoppingSale {
price: number;
}A JSDoc block is the only place a TypeScript citation is read from, which is why evidence/documented exists: an export with no block cannot cite anything, and the obligation silently shifts onto whichever sibling does have one.
One declaration stacks as many tags as the rules it honors:
/**
* @evidence docs/sales.md#sale-price Renders the price exactly as the pricing rule defines it.
* @evidence docs/discount.md#discount-display Shows the discounted price next to the original.
*/
export function SalePrice({ sale }: { sale: IShoppingSale }) {
return <strong>{formatPrice(sale.price)}</strong>;
}Markdown, in an HTML comment
# Pricing Guide
<!-- @evidence docs/requirements/pricing.md#sale-price Uses the approved sale-price definition. -->Rendered prose stays clean, so the document a reader sees is unchanged. A file citation sits before the document’s first heading; a heading citation sits right below its heading.
A heading citation belongs to the nearest preceding heading, and that heading’s exact level has to be selected by the claim. Under symbol: ["h2"], a tag written below an H3 is an H3 declaration rather than a contribution to the H2 above it, so it hosts nothing and is reported.
Prisma, in a documentation comment
/// @evidence docs/requirements/pricing.md#discount-policy Discount columns exist for this policy.
model Sale {
/// @evidence docs/requirements/pricing.md#coupon-stacking The stacking limit is stored here.
coupon_limit Int
}A /* */ block comment hosts one too. Both forms reach the generated client types, which is Prisma’s own rule rather than a choice made here.
A // line comment is discarded by Prisma itself and cannot host a citation, so a tag written in one is reported rather than ignored. So is one buried behind an extra slash: //// @evidence arrives as content beginning with a slash and opens no tag.
A comment documents whatever declaration immediately follows it. A blank line before a top-level block detaches it, a blank line inside a field’s run does not, an intervening // line does not break a run, and a comment above a block attribute or a closing brace documents nothing. Each of those placements is reported with the move that fixes it.
Citing a TypeScript symbol
A TypeScript target is written as an inline link and resolved through the citing module’s own imports:
import type * as sales from "./contracts/IShoppingSale.js";
/**
* @evidence {@link sales.IShoppingSale} Renders the price exactly as the contract declares it.
*/
export function SalePrice(): null {
return null;
}The braces are load-bearing. TypeScript resolves a name inside an inline link and counts it as a use, so an import that exists only to carry a citation survives noUnusedLocals. It does not resolve names inside an unknown tag, so a bare @evidence sales.IShoppingSale would leave that import unreferenced and raise TS6133.
Use import type. It is erased at emit, so a citation creates no runtime dependency and no import cycle. A project also running @typescript-eslint/no-unused-vars still sees a false positive there, because that rule does not count JSDoc usage.
$ npx ttsc check
error TS16411: [evidence/graph] Unimported evidence target '{@link contracts.ISale}' at src/ui/SalePrice.ts:2
for Claim 1 across reference 1 (typescript, symbols: type): 'contracts' is not imported by this module,
so the citation names a symbol this file does not reference. Import it; 'import type' is enough and is
erased at emit.Resolving through the module’s own imports also removes an ambiguity that has no fix. A generated SDK puts the same leaf name in many modules, so get alone names several symbols. Resolved from one file’s bindings, {@link api.functional.questions.get} names exactly one.
Only a TypeScript claim may cite TypeScript evidence. Any other artifact would have to match a bare name against every exported symbol in the repository, so two modules exporting IPage would make the citation impossible and the only repair would be renaming your code. Configure the obligation the other way round and let the code cite the document, the schema, or the operation.
Stacking and overlap
- The same unit may be cited by several declarations. One requirement can need several implementations.
- Parent and child evidence scopes may overlap. A narrow target documents a narrow implementation; a parent target deliberately accepts responsibility for the whole selected subtree.
- Repeating the same resolved scope on one declaration is rejected. Keep the truthful reason, or fold the useful detail into it.
- A citation may sit on any declaration of a merged identity. Placement changes neither resolution nor coverage.
uniqueEvidence and singleEvidencePerSymbol on a reference tighten the first two rules where an obligation needs an owner. Reference policies covers both.
Exclusions
@evidenceExclude <target> <reason> records that a claim intentionally does not use the target scope.
## Editorial Terminology
<!-- @evidenceExclude docs/requirements/coupons.md#coupon-stacking This section defines wording and intentionally does not implement coupon behavior. -->It follows the same hierarchy as @evidence, so excluding an H2 also excludes its selected H3 and H4 descendants, and excluding a type or namespace excludes its selected children.
Three properties decide whether an exclusion is worth anything.
- The reason is mandatory. “Not applicable”, “internal only”, and “later” are conclusions, not reasons. Name the actual owner or the observable alternative, and name the condition that would make this exclusion wrong.
- It belongs to one claim. Another claim referencing the same source still owes its own acknowledgement, and a reference declaring
noEvidenceExcluderefuses it outright. - It must not overlap. One claim-reference obligation may exclude a selected scope only once, even across different carriers, because the reason needs one reviewable owner. An evidence scope and an exclusion scope covering the same unit are rejected as contradictory intent.
Where an exclusion may sit
Carrier eligibility is wider than ownership evidence and no wider than the claim’s file population.
- A TypeScript exclusion may sit on any supported public export in a claim file, even when that export’s symbol kind is not selected by the claim. Unexported declarations, unsupported locations, and files outside the population do not qualify.
- A Prisma exclusion may sit on a selected model or field, or in an unattached top-level
///run in a matching claim file. That permits a lint-only.schemaledger outside the Prisma generation glob, which adds no model to the schema. The same unattached position never accepts@evidence, because ownership evidence belongs directly above the declaration that carries it. - A Markdown exclusion keeps the selected-symbol behavior of ownership evidence: it sits on a host kind the claim selects. Swagger hosts neither tag, because it never makes a claim in the first place.
evidenceExcludeCarriers on the claim narrows this further to named ledger files. The benchmark template uses one per claim, so a reviewer opens CONTROLLER_EVIDENCE_EXCLUDE.ts instead of reading every controller:
/**
* Central exclusions for API-operation evidence claims.
*
* Keep real ownership evidence on the controller method that implements it.
* Add only reviewed non-applicability decisions here.
*
* @evidenceExclude docs/analysis/example.md#section Frontend owns this presentation-only requirement; reject this exclusion if the API gains a related response or refusal.
*/
export const CONTROLLER_EVIDENCE_EXCLUDE = true;Never auto-exclude, auto-retarget, or delete an artifact to make a graph green. Every diagnostic names the repair, and the repair belongs to the author.
Reviews
A review answers a different question from the reason beside it. The reason says why this declaration answers for a target; the review says what was checked. Only the first is written unless something asks for the second, which is what evidence/review does.
There are two review tags because there are two acknowledgements, and neither answers the other. @evidenceReview verifies a citation: the code does what the cited unit describes. @evidenceExcludeReview verifies an exclusion: the unit genuinely does not apply here, which no reading of the code establishes. One tag for both would let the easier check discharge the harder one.
/**
* @evidence docs/discount.md#coupon-stacking <reason>
* @evidenceReview docs/discount.md#coupon-stacking <what you checked>
* @evidenceExclude docs/discount.md#tax <reason>
* @evidenceExcludeReview docs/discount.md#tax <what you checked>
*/The target is spelled exactly as the acknowledgement spells it, including the inline-link form, and it is compared as a token rather than resolved a second time. The pairing key is the tag together with the target, so one host may both cite and exclude one target and then owes two reviews. A review is not an acknowledgement: it discharges no coverage and changes no evidence/graph diagnostic.
The #-prefixed token is a fingerprint of the cited content, optional in the grammar and required by a reference declaring requireReview. It is what makes a review expire: when the cited section, operation, model, or symbol changes, the fingerprint stops matching and the build fails again with the new value stated in the diagnostic. Without it a review is written once and stays green forever, which is the same as never having asked.
The # is required rather than inferred, the way a braced code target’s braces are. A bare fixed-width hex token would have to be guessed at, and ordinary prose supplies the counter-examples.
Editor completions
While evidence/graph reports nothing, it publishes its configured targets as completion items in ttscserver, so typing @evidence docs/spec.md# offers the anchors that actually exist. The host publishes a rule’s corpus only on the cycles where that rule passes, so a broken graph never suggests targets from a stale index.
At the @evidenceExclude trigger, a target selected only by references that refuse exclusions is omitted. A target any enabled reference still allows stays on offer, because the completion API has no cursor-specific claim context to narrow it further.
What a violation looks like
## Coupon Stacking {#coupon-stacking}
At most one seller coupon and one platform coupon may combine on a single order.$ npx ttsc check
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).
Use @evidence on a selected typescript host or @evidenceExclude on an eligible carrier.
Found 1 error.The section exists in the spec and no component cites it, so the build fails. The diagnostic names the exact section, the obligation that owes it, and both repairs.
Match on [evidence/graph] rather than on TS16411. A contributor rule’s numeric code is assigned deterministically from the complete set of loaded contributors, so changing that set can renumber it.
Next
→ Rules for the five rules that read these tags, or Full-Stack Wiring for a monorepo graph built from them.