@ttsc/evidence 0.25.0 → 0.26.1
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 +24 -6
- package/lib/index.d.ts +7 -2
- package/lib/index.js +7 -2
- package/lib/internal/canonicalDigest.d.ts +1 -0
- package/lib/internal/canonicalDigest.js +97 -0
- package/lib/internal/loadPrismaModels.js +9 -2
- package/lib/internal/loadSwaggerOperations.js +96 -5
- package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +48 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.d.ts +3 -3
- package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +4 -4
- package/lib/typings/ITtscLintContributorRules.d.ts +23 -0
- package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.d.ts +64 -29
- package/native/comments.go +162 -0
- package/native/config.go +2 -0
- package/native/declaration.go +213 -0
- package/native/documented.go +12 -12
- package/native/exports.go +64 -7
- package/native/fingerprint.go +351 -0
- package/native/graph.go +252 -16
- package/native/markdown.go +263 -0
- package/native/model.go +179 -1
- package/native/prisma.go +59 -3
- package/native/prisma_cache.go +7 -0
- package/native/review.go +355 -0
- package/native/swagger.go +8 -0
- package/native/typescript.go +697 -64
- package/native/unreadable.go +155 -0
- package/package.json +3 -3
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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` |
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
34
|
-
*
|
|
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
|
|
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.
|
|
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,
|
|
9
|
-
* public
|
|
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,
|
|
69
|
-
* namespaces. A single value selects one obligation kind; a non-empty
|
|
70
|
-
* selects the union. Selected units remain independent obligations
|
|
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
|
}
|