@ttsc/evidence 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,19 +27,31 @@ error TS16411: [evidence/graph] Missing acknowledgement for 'docs/discount.md#co
27
27
  (Markdown H2 'Coupon Stacking' at docs/discount.md:3)
28
28
  in Claim 1 reference 1 (markdown, symbols: h2, h3).
29
29
 
30
- Use @evidence on a selected typescript host or @evidenceExclude on an eligible carrier.
30
+ Cite the artifact that answers for this unit with @evidence on a selected
31
+ typescript host, building that artifact first when none does, or write
32
+ @evidenceExclude on an eligible carrier when nothing here owes it. Never
33
+ leave an untrue tag standing just to pass this check; it removes the error,
34
+ not the problem.
31
35
 
32
36
  error TS16411: [evidence/graph] Missing acknowledgement for 'POST:/orders/{orderId}/coupons'
33
37
  (Swagger operation 'POST /orders/{orderId}/coupons' at api/openapi.json)
34
38
  in Claim 1 reference 2 (swagger operations).
35
39
 
36
- Use @evidence on a selected typescript host or @evidenceExclude on an eligible carrier.
40
+ Cite the artifact that answers for this unit with @evidence on a selected
41
+ typescript host, building that artifact first when none does, or write
42
+ @evidenceExclude on an eligible carrier when nothing here owes it. Never
43
+ leave an untrue tag standing just to pass this check; it removes the error,
44
+ not the problem.
37
45
 
38
46
  error TS16411: [evidence/graph] Missing acknowledgement for 'useCouponStacking'
39
47
  (TypeScript function 'useCouponStacking' at src/lib/coupons/hooks.ts:12)
40
48
  in Claim 1 reference 3 (typescript, symbols: function).
41
49
 
42
- Use @evidence on a selected typescript host or @evidenceExclude on an eligible carrier.
50
+ Cite the artifact that answers for this unit with @evidence on a selected
51
+ typescript host, building that artifact first when none does, or write
52
+ @evidenceExclude on an eligible carrier when nothing here owes it. Never
53
+ leave an untrue tag standing just to pass this check; it removes the error,
54
+ not the problem.
43
55
 
44
56
  Found 3 errors.
45
57
  ```
@@ -96,9 +108,10 @@ One sentence: the components under `src` implement the docs, so every H2 and H3
96
108
  | Rule | Takes | What it does |
97
109
  | --- | --- | --- |
98
110
  | `evidence/graph` | [`ITtscEvidenceGraphConfig`](https://github.com/samchon/ttsc/blob/master/packages/evidence/src/structures/ITtscEvidenceGraphConfig.ts) | The graph itself. Project-scoped, so its entry declares no `files`. |
99
- | `evidence/documented` | nothing | Requires a JSDoc block on every selected export, since a block is the only place a citation can live. |
111
+ | `evidence/documented` | [`ITtscEvidenceDocumentedConfig`](https://github.com/samchon/ttsc/blob/master/packages/evidence/src/structures/ITtscEvidenceDocumentedConfig.ts) | Requires a JSDoc block on every selected export, since a block is the only place a citation can live. Members count, so a class field, a method, a parameter property, and an interface member each need their own. `symbol` narrows the kinds; it defaults to all three. |
100
112
  | `evidence/singular` | nothing | Keeps one public identity per file, named after the file. |
101
113
  | `evidence/todo` | nothing | Fails on every remaining JSDoc `@todo`, with its own text. |
114
+ | `evidence/review` | nothing | Requires an `@evidenceReview` beside every `@evidence` and an `@evidenceExcludeReview` beside every `@evidenceExclude`, naming the same target and stating what was checked. |
102
115
 
103
116
  Each takes `"error"`, `"warning"`, or `"off"`.
104
117
 
@@ -214,7 +227,11 @@ A unit is addressed the way a consumer reaches it, so `export * as functional` n
214
227
  | TypeScript | `type`, `function`, `property` | all three | `type` |
215
228
  | Swagger | none, every operation is selected | not applicable | every operation |
216
229
 
217
- Units keep their hierarchy, so a target acknowledges itself and every selected descendant: citing a heading covers its subsections, an interface covers its properties, and `prisma:Sale` covers the columns beneath it. An ancestor stays addressable even when its own kind is not selected.
230
+ Units keep their hierarchy, so a target acknowledges itself and every selected descendant: citing a heading covers its subsections, an interface or a class covers the members it declares, and `prisma:Sale` covers the columns beneath it. An ancestor stays addressable even when its own kind is not selected.
231
+
232
+ A class is a `type`, its methods are `function`, and its fields are `property`. A member written as a callable joins the methods instead: `handler = () => {}` and `charge: () => void` are both `function`. An interface and an object-shaped type alias are classified by the same rule, so `run(): void` on either is a `function` just as a class method is, and an overload run is one unit. The test is on how the member is written, not on what its type resolves to, because this rule reads no type checker, so `charge: Handler` stays a `property` even where `Handler` is an alias of `() => void`.
233
+
234
+ A class member that is `private` or `protected` materializes no unit, whichever syntax declared it. A constructor parameter carrying any property modifier declares a field, and it classifies exactly as the same field written in the class body would, so a `private` or `protected` one materializes nothing just as the body form does not. The property modifiers are TypeScript's own five: `public`, `protected`, `private`, `readonly`, and `override`. The last is the one to know about, because its meaning is about the base class rather than about the field, so it does not read as a field declaration. On a class extending one that declares `rate`, `constructor(override rate: number)` is a public instance field and a selected `property` unit.
218
235
 
219
236
  A declaration whose documentation carries `@internal`, `@hidden`, or `@ignore` leaves the population entirely. It owes nothing and can carry nothing, and citing one is reported rather than silently ignored.
220
237
 
@@ -243,7 +260,7 @@ It is the only acknowledgement that settles an obligation without anything being
243
260
 
244
261
  ### Strict references
245
262
 
246
- Ordinary coverage is permissive, which is right for a document several modules honor and too weak for a proof obligation, where one exclusion or one host citing everything discharges the whole population. Three properties tighten a single reference, and they never pool across references.
263
+ Ordinary coverage is permissive, which is right for a document several modules honor and too weak for a proof obligation, where one exclusion or one host citing everything discharges the whole population. Four properties tighten a single reference, and they never pool across references.
247
264
 
248
265
  ```ts
249
266
  {
@@ -259,6 +276,7 @@ Ordinary coverage is permissive, which is right for a document several modules h
259
276
  - `noEvidenceExclude` refuses exclusions, so the target still owes positive evidence. A published accessor no hook consumes is an omission rather than a decision.
260
277
  - `uniqueEvidence` allows at most one host per unit, so one host is answerable for it rather than several.
261
278
  - `singleEvidencePerSymbol` requires exactly one unit from every selected host, so a host citing nothing and a host citing everything both fail.
279
+ - `requireReview` makes every acknowledgement owe a matching review carrying a fingerprint of the cited content, so the review fails again once that content changes. A citation is answered by `@evidenceReview` and an exclusion by `@evidenceExcludeReview`; neither answers the other. Every reference kind accepts it, because each bridge digests a unit's content where that content is understood.
262
280
 
263
281
  Counting is by identity rather than by text. Repeated tags for one unit count once, an overload set stays one host, and citing a parent of two selected units counts as two.
264
282
 
package/lib/index.d.ts CHANGED
@@ -8,7 +8,7 @@ export * from "./typings/index";
8
8
  * {@link ITtscEvidenceGraphConfig} that describes which documents and TypeScript
9
9
  * symbols must remain connected.
10
10
  *
11
- * The plugin contributes four rules, enabled independently.
11
+ * The plugin contributes five rules, enabled independently.
12
12
  *
13
13
  * - `"evidence/graph"` — the configured evidence graph. Every declaration target
14
14
  * must resolve, and every selected evidence unit must be acknowledged. Takes
@@ -24,6 +24,11 @@ export * from "./typings/index";
24
24
  * file, exported or not. Each tag is an unrealized contract reported with its
25
25
  * own text, so the diagnostics read as the ledger of what remains to realize.
26
26
  * Takes no options, so it carries a bare severity.
27
+ * - `"evidence/review"` — an `@evidenceReview` beside every `@evidence` and an
28
+ * `@evidenceExcludeReview` beside every `@evidenceExclude`, naming the same
29
+ * target. The citation states why this declaration answers for a target; the
30
+ * review states what was verified, which is a different question nothing else
31
+ * asks. Takes no options, so it carries a bare severity.
27
32
  *
28
33
  * @example <caption>Configure the plugin in `lint.config.ts`</caption>
29
34
  * import { type ITtscEvidenceGraphConfig, evidence } from "@ttsc/evidence";
@@ -59,7 +64,7 @@ export declare const evidence: {
59
64
  readonly namespace: "evidence";
60
65
  readonly version: string;
61
66
  };
62
- rules: readonly ["graph", "singular", "documented", "todo"];
67
+ rules: readonly ["graph", "singular", "documented", "todo", "review"];
63
68
  source: string;
64
69
  };
65
70
  export default evidence;
package/lib/index.js CHANGED
@@ -30,7 +30,7 @@ __exportStar(require("./typings/index"), exports);
30
30
  * {@link ITtscEvidenceGraphConfig} that describes which documents and TypeScript
31
31
  * symbols must remain connected.
32
32
  *
33
- * The plugin contributes four rules, enabled independently.
33
+ * The plugin contributes five rules, enabled independently.
34
34
  *
35
35
  * - `"evidence/graph"` — the configured evidence graph. Every declaration target
36
36
  * must resolve, and every selected evidence unit must be acknowledged. Takes
@@ -46,6 +46,11 @@ __exportStar(require("./typings/index"), exports);
46
46
  * file, exported or not. Each tag is an unrealized contract reported with its
47
47
  * own text, so the diagnostics read as the ledger of what remains to realize.
48
48
  * Takes no options, so it carries a bare severity.
49
+ * - `"evidence/review"` — an `@evidenceReview` beside every `@evidence` and an
50
+ * `@evidenceExcludeReview` beside every `@evidenceExclude`, naming the same
51
+ * target. The citation states why this declaration answers for a target; the
52
+ * review states what was verified, which is a different question nothing else
53
+ * asks. Takes no options, so it carries a bare severity.
49
54
  *
50
55
  * @example <caption>Configure the plugin in `lint.config.ts`</caption>
51
56
  * import { type ITtscEvidenceGraphConfig, evidence } from "@ttsc/evidence";
@@ -81,7 +86,7 @@ exports.evidence = {
81
86
  namespace: "evidence",
82
87
  version: package_json_1.version,
83
88
  },
84
- rules: ["graph", "singular", "documented", "todo"],
89
+ rules: ["graph", "singular", "documented", "todo", "review"],
85
90
  source: node_path_1.default.resolve(__dirname, "..", "native"),
86
91
  };
87
92
  exports.default = exports.evidence;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withoutKeys = exports.canonicalJson = exports.canonicalDigest = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ /**
6
+ * Digests one parsed declaration under a serialization that does not depend on
7
+ * how it was written.
8
+ *
9
+ * A review fingerprint answers whether the thing a reviewer read has changed,
10
+ * so two inputs that describe the same declaration have to produce one value.
11
+ * `JSON.stringify` emits keys in insertion order, and insertion order here is a
12
+ * function of the input dialect and of a converter's internals: a Swagger 2.0
13
+ * document and an OpenAPI 3.1 document that normalize to the same operation
14
+ * would otherwise hash differently, and a converter change would expire every
15
+ * review in a project.
16
+ *
17
+ * Object keys are therefore sorted and arrays are not, because an array's order
18
+ * is part of what it says: OpenAPI parameter order and `required` membership
19
+ * are both arrays, and reordering the second is a real edit a reviewer must
20
+ * see. Sorting them would hide it.
21
+ *
22
+ * `undefined` is dropped from objects and rendered as `null` inside arrays,
23
+ * which is what `JSON.stringify` does, so an absent property and a present one
24
+ * holding `undefined` cannot be told apart. That is the same thing to every
25
+ * consumer downstream of the parser.
26
+ *
27
+ * @internal
28
+ */
29
+ const canonicalDigest = (value) => (0, node_crypto_1.createHash)("sha256").update((0, exports.canonicalJson)(value)).digest("hex");
30
+ exports.canonicalDigest = canonicalDigest;
31
+ /**
32
+ * Renders a parsed value with object keys in sorted order and no incidental
33
+ * whitespace.
34
+ *
35
+ * @internal
36
+ */
37
+ const canonicalJson = (value) => render(value, new Set());
38
+ exports.canonicalJson = canonicalJson;
39
+ /**
40
+ * Renders one value, refusing to descend into a container already on the path
41
+ * above it.
42
+ *
43
+ * A source document can be cyclic: a YAML anchor referring to its own parent
44
+ * loads, upgrades, and would then recurse until the stack ends. The failure
45
+ * that produced was a diagnostic blaming an upgrade that had succeeded, so the
46
+ * cycle is answered here with a marker instead. Two documents that differ only
47
+ * in where a cycle closes still differ, because the marker is emitted at the
48
+ * position the cycle closes at.
49
+ */
50
+ const render = (value, seen) => {
51
+ if (value === null || typeof value !== "object")
52
+ return stringify(value);
53
+ if (seen.has(value))
54
+ return '"[circular]"';
55
+ seen.add(value);
56
+ try {
57
+ if (Array.isArray(value))
58
+ return `[${value.map((element) => render(element ?? null, seen)).join(",")}]`;
59
+ const entries = Object.entries(value).filter(([, element]) => element !== undefined);
60
+ entries.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
61
+ return `{${entries
62
+ .map(([key, element]) => `${JSON.stringify(key)}:${render(element, seen)}`)
63
+ .join(",")}}`;
64
+ }
65
+ finally {
66
+ seen.delete(value);
67
+ }
68
+ };
69
+ const stringify = (value) => {
70
+ const rendered = JSON.stringify(value);
71
+ return rendered ?? "null";
72
+ };
73
+ /**
74
+ * Drops named properties from a parsed declaration before it is digested.
75
+ *
76
+ * Documentation is the reason this exists, and a container of units is the
77
+ * other: a model's fields are units of their own, so hashing them into the
78
+ * model would make one field's edit expire a review of every sibling.
79
+ *
80
+ * A review of a declaration is written in that declaration's own comment, so a
81
+ * digest covering the comment moves the moment the review is written and the
82
+ * review is stale before the next build reads it. That is the non-terminating
83
+ * repair loop the whole feature exists to avoid, and it is why the one content
84
+ * field the Prisma payload carries in full is the one field that must not be
85
+ * hashed.
86
+ *
87
+ * @internal
88
+ */
89
+ const withoutKeys = (value, ...keys) => {
90
+ const copy = {
91
+ ...value,
92
+ };
93
+ for (const key of keys)
94
+ delete copy[key];
95
+ return copy;
96
+ };
97
+ exports.withoutKeys = withoutKeys;
@@ -8,6 +8,7 @@ const node_crypto_1 = require("node:crypto");
8
8
  const promises_1 = __importDefault(require("node:fs/promises"));
9
9
  const node_module_1 = require("node:module");
10
10
  const node_path_1 = __importDefault(require("node:path"));
11
+ const canonicalDigest_1 = require("./canonicalDigest");
11
12
  const MAX_SCHEMA_BYTES = 16 * 1024 * 1024;
12
13
  const ANSI_PATTERN = /\x1b\[[0-9;]*m/gu;
13
14
  /**
@@ -30,8 +31,9 @@ const SEPARATOR = "\u0000";
30
31
  *
31
32
  * The native contributor is Go and Prisma's parser is a WebAssembly module with
32
33
  * a JavaScript entry point. This function is the narrow process boundary
33
- * between them: it accepts only file locations and returns only model, column,
34
- * and relation identities with their doc comments.
34
+ * between them: it accepts only file locations and returns model, column, and
35
+ * relation identities with their doc comments and a digest of each
36
+ * declaration's content.
35
37
  *
36
38
  * One **set** is one request. A Prisma schema folder is several files that form
37
39
  * a single namespace — a model in one file may point at a model in another — so
@@ -156,6 +158,10 @@ const modelsOf = (parser, files) => {
156
158
  models.push({
157
159
  name: model.name,
158
160
  documentation: typeof model.documentation === "string" ? model.documentation : "",
161
+ // The model's own declaration is everything the parser reports about it
162
+ // except its fields, which are units in their own right, and except its
163
+ // documentation, which is where a review of it is written.
164
+ digest: (0, canonicalDigest_1.canonicalDigest)((0, canonicalDigest_1.withoutKeys)(model, "documentation", "fields")),
159
165
  fields: (model.fields ?? [])
160
166
  .filter((field) => typeof field.name === "string" && field.name.length !== 0)
161
167
  .map((field) => ({
@@ -166,6 +172,7 @@ const modelsOf = (parser, files) => {
166
172
  // rather than for `scalar`.
167
173
  symbol: field.kind === "object" ? "relation" : "column",
168
174
  documentation: typeof field.documentation === "string" ? field.documentation : "",
175
+ digest: (0, canonicalDigest_1.canonicalDigest)((0, canonicalDigest_1.withoutKeys)(field, "documentation")),
169
176
  })),
170
177
  });
171
178
  }
@@ -8,6 +8,7 @@ const node_crypto_1 = require("node:crypto");
8
8
  const promises_1 = __importDefault(require("node:fs/promises"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
10
  const yaml_1 = require("yaml");
11
+ const canonicalDigest_1 = require("./canonicalDigest");
11
12
  const normalizeSwaggerDocument_1 = require("./normalizeSwaggerDocument");
12
13
  const MAX_DOCUMENT_BYTES = 16 * 1024 * 1024;
13
14
  const REMOTE_TIMEOUT_MILLISECONDS = 30_000;
@@ -27,7 +28,9 @@ const METHODS = [
27
28
  *
28
29
  * The native contributor is Go, while the version converter is JavaScript. This
29
30
  * function is the narrow process boundary between them: it accepts only source
30
- * locations and returns only operation identities.
31
+ * locations and returns operation identities, each carrying a digest of the
32
+ * operation's content taken here because this is the only side that sees the
33
+ * document.
31
34
  *
32
35
  * @internal
33
36
  */
@@ -117,14 +120,16 @@ const readRemoteSource = async (source) => {
117
120
  const decodeUtf8 = (content) => new TextDecoder("utf-8", { fatal: true }).decode(content);
118
121
  const operationsOf = (document) => {
119
122
  const operations = [];
123
+ const components = (document.components ??
124
+ {});
120
125
  for (const [operationPath, item] of Object.entries(document.paths ?? {})) {
121
126
  for (const method of METHODS) {
122
127
  const operation = item[method];
123
128
  if (operation !== undefined)
124
- operations.push(operationOf(method, operationPath));
129
+ operations.push(operationOf(method, operationPath, operation, components));
125
130
  }
126
- for (const method of Object.keys(item.additionalOperations ?? {}))
127
- operations.push(operationOf(method, operationPath));
131
+ for (const [method, operation] of Object.entries(item.additionalOperations ?? {}))
132
+ operations.push(operationOf(method, operationPath, operation, components));
128
133
  }
129
134
  operations.sort((left, right) => {
130
135
  const leftTarget = `${left.method}:${left.path}`;
@@ -140,7 +145,7 @@ const operationsOf = (document) => {
140
145
  }
141
146
  return operations;
142
147
  };
143
- const operationOf = (method, operationPath) => {
148
+ const operationOf = (method, operationPath, operation, components) => {
144
149
  if (!operationPath.startsWith("/"))
145
150
  throw new Error(`OpenAPI path '${operationPath}' must start with '/' to form an operation target`);
146
151
  if (/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/u.test(method) === false ||
@@ -149,8 +154,94 @@ const operationOf = (method, operationPath) => {
149
154
  return {
150
155
  method: method.toUpperCase(),
151
156
  path: operationPath,
157
+ digest: (0, canonicalDigest_1.canonicalDigest)(withResolvedReferences(operation, components)),
152
158
  };
153
159
  };
160
+ /**
161
+ * Replaces every local `$ref` into `components` with what it names.
162
+ *
163
+ * The converter preserves references rather than inlining them, so an operation
164
+ * is often no more than `{"$ref": "#/components/schemas/IMember"}` where its
165
+ * request and response bodies should be. A digest over the operation as written
166
+ * therefore covers the name of a contract and not the contract, and changing
167
+ * every property of a DTO expires no review of the endpoint that carries it.
168
+ * That is the failure this feature exists to remove, on the artifact kind whose
169
+ * whole content lives behind a reference.
170
+ *
171
+ * Any pointer under `#/components/` is followed, not only one into `schemas`,
172
+ * because a request body, a response, a parameter, and a header are all
173
+ * declarable there and each is part of the operation a reviewer read. A pointer
174
+ * anywhere else is left as written: `#/paths/...` would fold one operation's
175
+ * content into another's and reintroduce the cross-expiry this replaces.
176
+ *
177
+ * Siblings of a `$ref` are kept and override what it resolves to, which is what
178
+ * OpenAPI 3.1 says they do. A reference already open on the path above is left
179
+ * as written, so a recursive schema terminates while two operations reaching
180
+ * one cycle by different routes still differ. An undeclared reference is left
181
+ * as written too: a broken document is not a digest question, and inventing an
182
+ * empty schema for it would make two different broken documents agree.
183
+ */
184
+ const withResolvedReferences = (value, components, open = new Set()) => {
185
+ if (value === null || typeof value !== "object")
186
+ return value;
187
+ if (Array.isArray(value))
188
+ return value.map((element) => withResolvedReferences(element, components, open));
189
+ const entries = Object.entries(value);
190
+ const reference = value["$ref"];
191
+ if (typeof reference !== "string" || open.has(reference))
192
+ return Object.fromEntries(entries.map(([key, element]) => [
193
+ key,
194
+ withResolvedReferences(element, components, open),
195
+ ]));
196
+ const target = componentAt(components, reference);
197
+ if (target === undefined)
198
+ return Object.fromEntries(entries.map(([key, element]) => [
199
+ key,
200
+ withResolvedReferences(element, components, open),
201
+ ]));
202
+ open.add(reference);
203
+ try {
204
+ const resolved = withResolvedReferences(target, components, open);
205
+ const siblings = entries
206
+ .filter(([key]) => key !== "$ref")
207
+ .map(([key, element]) => [
208
+ key,
209
+ withResolvedReferences(element, components, open),
210
+ ]);
211
+ if (siblings.length === 0)
212
+ return resolved;
213
+ if (resolved === null || typeof resolved !== "object")
214
+ return Object.fromEntries(siblings);
215
+ return {
216
+ ...resolved,
217
+ ...Object.fromEntries(siblings),
218
+ };
219
+ }
220
+ finally {
221
+ open.delete(reference);
222
+ }
223
+ };
224
+ const COMPONENT_REFERENCE_PREFIX = "#/components/";
225
+ /** Reads one `#/components/<section>/<name>` pointer, or nothing. */
226
+ const componentAt = (components, reference) => {
227
+ if (!reference.startsWith(COMPONENT_REFERENCE_PREFIX))
228
+ return undefined;
229
+ const segments = reference
230
+ .slice(COMPONENT_REFERENCE_PREFIX.length)
231
+ .split("/")
232
+ .map((segment) => decodeURIComponent(segment).replaceAll("~1", "/").replaceAll("~0", "~"));
233
+ let current = components;
234
+ for (const segment of segments) {
235
+ if (current === null ||
236
+ typeof current !== "object" ||
237
+ Array.isArray(current))
238
+ return undefined;
239
+ if (!(segment in current))
240
+ return undefined;
241
+ current = current[segment];
242
+ }
243
+ return current;
244
+ };
154
245
  const isInventory = (value) => "operations" in value;
155
246
  const isProblem = (value) => "message" in value;
156
247
  const errorMessage = (error) => error instanceof Error ? error.message : String(error);
@@ -70,4 +70,52 @@ export interface ITtscEvidenceGraphReferenceBase<Type extends string> {
70
70
  * @default false
71
71
  */
72
72
  singleEvidencePerSymbol?: boolean;
73
+ /**
74
+ * Whether every acknowledgement of this population must carry a reviewed,
75
+ * unexpired review of its own kind.
76
+ *
77
+ * The reason on a citation says why this declaration answers for a target.
78
+ * Nothing asks what was actually checked, so an unverified citation and a
79
+ * verified one are byte-identical in the source. This asks, and it makes the
80
+ * answer expire: the review carries a `#`-prefixed fingerprint of the cited
81
+ * scope's content, and when that content changes the fingerprint stops
82
+ * matching and the build fails again with the new value in the diagnostic.
83
+ *
84
+ * Expiry is the point. Without it a review is written once and stays green
85
+ * forever, and on a second pass over a large citation set there is no way to
86
+ * tell which reviews were written against content that has since moved.
87
+ *
88
+ * What this proves is narrow and worth stating plainly. It proves a
89
+ * separately addressed statement exists and was written against the cited
90
+ * content as it now stands. It does not prove anyone read that content,
91
+ * because the diagnostic states the expected fingerprint, and it does not
92
+ * judge whether the prose is sincere.
93
+ *
94
+ * The fingerprint covers the cited unit and its structural subtree, so it is
95
+ * a property of the address a citation names rather than of the reference
96
+ * that demanded it. One tag therefore carries one fingerprint no matter how
97
+ * many references it acknowledges.
98
+ *
99
+ * Expect a citation of a TypeScript symbol to expire on any change inside
100
+ * that declaration, including one behind a withdrawal tag. A declaration's
101
+ * digest is its own text and a nested member sits inside it, so the subtree
102
+ * cannot be carved out the way a Markdown section's subsections can. Cite the
103
+ * narrowest symbol that actually answers for the requirement if that breadth
104
+ * is unwelcome.
105
+ *
106
+ * The withdrawal tags are named in the tag guide rather than here, on
107
+ * purpose: `stripInternal` is on for this package, so writing one of them
108
+ * inside a published doc comment deletes the property it documents from the
109
+ * emitted declarations. That is how this very property went missing once.
110
+ *
111
+ * Every reference kind accepts this. A Swagger or Prisma reference used to be
112
+ * refused at decode, because those loaders reported unit identities and
113
+ * nothing else; each bridge now digests a unit's content on the side that
114
+ * understands it, which for an operation is its normalized definition and for
115
+ * a Prisma model or member is its parsed declaration without the
116
+ * documentation comment a review of it is written in.
117
+ *
118
+ * @default false
119
+ */
120
+ requireReview?: boolean;
73
121
  }
@@ -5,9 +5,9 @@ import type { ITtscEvidenceGraphClaimBase } from "./ITtscEvidenceGraphClaimBase"
5
5
  *
6
6
  * JSDoc puts an evidence edge on the public declaration making the claim,
7
7
  * rather than on the file around it. Supported hosts are exported interfaces,
8
- * type aliases, namespaces, functions, data variables, type properties, and the
9
- * public class callable forms documented by
10
- * {@link TtscEvidenceGraphTypeScriptSymbol}.
8
+ * type aliases, classes, namespaces, functions, data variables, and every
9
+ * public member an interface, an object-shaped type alias, or a class declares,
10
+ * in each of the forms documented by {@link TtscEvidenceGraphTypeScriptSymbol}.
11
11
  *
12
12
  * Both `@evidence <target> <reason>` and `@evidenceExclude <target> <reason>`
13
13
  * require a target and a non-empty explanation. Ownership evidence must sit on
@@ -65,10 +65,10 @@ export interface ITtscEvidenceGraphTypeScriptReference extends ITtscEvidenceGrap
65
65
  /**
66
66
  * Symbol kind or kinds eligible to become evidence units.
67
67
  *
68
- * Omit this property to select exported interfaces, type aliases, and
69
- * namespaces. A single value selects one obligation kind; a non-empty array
70
- * selects the union. Selected units remain independent obligations until an
71
- * ancestor type or namespace target acknowledges their shared scope.
68
+ * Omit this property to select exported interfaces, type aliases, classes,
69
+ * and namespaces. A single value selects one obligation kind; a non-empty
70
+ * array selects the union. Selected units remain independent obligations
71
+ * until an ancestor type or namespace target acknowledges their shared scope.
72
72
  * Ancestors of selected units are addressable even when their own kind is
73
73
  * omitted from this selector.
74
74
  *
@@ -30,5 +30,28 @@ declare module "@ttsc/lint" {
30
30
  * `files` setting of `lint.config.ts`.
31
31
  */
32
32
  "evidence/todo"?: TtscLintRuleSetting;
33
+ /**
34
+ * Requires a review beside every acknowledgement.
35
+ *
36
+ * Every `@evidence` and `@evidenceExclude` on a public identity must be
37
+ * answered by an `@evidenceReview` naming the same target. The citation
38
+ * states why this declaration answers for that target; the review states
39
+ * what was verified. Those are different questions, and only the first one
40
+ * is written unless something asks for the second.
41
+ *
42
+ * A review is an annotation of a citation, never an acknowledgement of a
43
+ * unit. It discharges no coverage, contributes no host to `uniqueEvidence`,
44
+ * and counts toward no `singleEvidencePerSymbol` total, so enabling this
45
+ * rule cannot change a single `evidence/graph` diagnostic.
46
+ *
47
+ * The optional `#`-prefixed fingerprint token is carried without being
48
+ * interpreted here. `evidence/graph` validates it against the cited content
49
+ * under a reference's `requireReview`, which is what makes a review expire
50
+ * when the thing it reviewed moves.
51
+ *
52
+ * The rule takes no options; per-directory scoping belongs in the outer
53
+ * `files` setting of `lint.config.ts`.
54
+ */
55
+ "evidence/review"?: TtscLintRuleSetting;
33
56
  }
34
57
  }