@ttsc/evidence 0.25.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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +279 -0
  3. package/lib/index.d.ts +65 -0
  4. package/lib/index.js +87 -0
  5. package/lib/internal/loadPrismaModels.d.ts +1 -0
  6. package/lib/internal/loadPrismaModels.js +197 -0
  7. package/lib/internal/loadSwaggerOperations.d.ts +1 -0
  8. package/lib/internal/loadSwaggerOperations.js +156 -0
  9. package/lib/internal/normalizeSwaggerDocument.d.ts +1 -0
  10. package/lib/internal/normalizeSwaggerDocument.js +7 -0
  11. package/lib/structures/ITtscEvidenceDocumentedConfig.d.ts +32 -0
  12. package/lib/structures/ITtscEvidenceDocumentedConfig.js +2 -0
  13. package/lib/structures/ITtscEvidenceGraphClaim.d.ts +14 -0
  14. package/lib/structures/ITtscEvidenceGraphClaim.js +2 -0
  15. package/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +102 -0
  16. package/lib/structures/ITtscEvidenceGraphClaimBase.js +2 -0
  17. package/lib/structures/ITtscEvidenceGraphConfig.d.ts +19 -0
  18. package/lib/structures/ITtscEvidenceGraphConfig.js +2 -0
  19. package/lib/structures/ITtscEvidenceGraphMarkdownClaim.d.ts +40 -0
  20. package/lib/structures/ITtscEvidenceGraphMarkdownClaim.js +2 -0
  21. package/lib/structures/ITtscEvidenceGraphMarkdownReference.d.ts +76 -0
  22. package/lib/structures/ITtscEvidenceGraphMarkdownReference.js +2 -0
  23. package/lib/structures/ITtscEvidenceGraphPrismaClaim.d.ts +65 -0
  24. package/lib/structures/ITtscEvidenceGraphPrismaClaim.js +2 -0
  25. package/lib/structures/ITtscEvidenceGraphPrismaReference.d.ts +75 -0
  26. package/lib/structures/ITtscEvidenceGraphPrismaReference.js +2 -0
  27. package/lib/structures/ITtscEvidenceGraphReference.d.ts +14 -0
  28. package/lib/structures/ITtscEvidenceGraphReference.js +2 -0
  29. package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +73 -0
  30. package/lib/structures/ITtscEvidenceGraphReferenceBase.js +2 -0
  31. package/lib/structures/ITtscEvidenceGraphSwaggerReference.d.ts +39 -0
  32. package/lib/structures/ITtscEvidenceGraphSwaggerReference.js +2 -0
  33. package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.d.ts +54 -0
  34. package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.js +2 -0
  35. package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +83 -0
  36. package/lib/structures/ITtscEvidenceGraphTypeScriptReference.js +2 -0
  37. package/lib/structures/index.d.ts +13 -0
  38. package/lib/structures/index.js +29 -0
  39. package/lib/typings/ITtscLintContributorRules.d.ts +34 -0
  40. package/lib/typings/ITtscLintContributorRules.js +2 -0
  41. package/lib/typings/ITtscLintRuleOptionsMap.d.ts +20 -0
  42. package/lib/typings/ITtscLintRuleOptionsMap.js +2 -0
  43. package/lib/typings/TtscEvidenceGraphMarkdownSymbol.d.ts +32 -0
  44. package/lib/typings/TtscEvidenceGraphMarkdownSymbol.js +2 -0
  45. package/lib/typings/TtscEvidenceGraphPrismaSymbol.d.ts +38 -0
  46. package/lib/typings/TtscEvidenceGraphPrismaSymbol.js +2 -0
  47. package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.d.ts +64 -0
  48. package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.js +2 -0
  49. package/lib/typings/index.d.ts +5 -0
  50. package/lib/typings/index.js +21 -0
  51. package/native/base.go +298 -0
  52. package/native/config.go +798 -0
  53. package/native/declaration.go +240 -0
  54. package/native/documented.go +353 -0
  55. package/native/exports.go +484 -0
  56. package/native/glob.go +207 -0
  57. package/native/graph.go +1409 -0
  58. package/native/health.go +45 -0
  59. package/native/hints.go +261 -0
  60. package/native/imports.go +373 -0
  61. package/native/inputs.go +125 -0
  62. package/native/loader.go +424 -0
  63. package/native/markdown.go +423 -0
  64. package/native/model.go +296 -0
  65. package/native/prisma.go +719 -0
  66. package/native/prisma_cache.go +213 -0
  67. package/native/prisma_locator.go +417 -0
  68. package/native/singular.go +369 -0
  69. package/native/swagger.go +331 -0
  70. package/native/swagger_cache.go +274 -0
  71. package/native/todo.go +138 -0
  72. package/native/typescript.go +1199 -0
  73. package/native/typescript_cache.go +96 -0
  74. package/package.json +64 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jeongho Nam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,279 @@
1
+ # `@ttsc/evidence`
2
+
3
+ ![banner of @ttsc/evidence](https://ttsc.dev/og-evidence.png)
4
+
5
+ [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/ttsc/blob/master/LICENSE) [![NPM Version](https://img.shields.io/npm/v/@ttsc/evidence.svg)](https://www.npmjs.com/package/@ttsc/evidence) [![NPM Downloads](https://img.shields.io/npm/dm/@ttsc/evidence.svg)](https://www.npmjs.com/package/@ttsc/evidence) [![Build Status](https://github.com/samchon/ttsc/actions/workflows/build.yml/badge.svg)](https://github.com/samchon/ttsc/actions/workflows/build.yml) [![Guide Documents](https://img.shields.io/badge/Guide-Documents-forestgreen)](https://ttsc.dev/docs/evidence) [![Discord Badge](https://img.shields.io/badge/discord-samchon-d91965?style=flat&labelColor=5866f2&logo=discord&logoColor=white&link=https://discord.gg/E94XhzrUCZ)](https://discord.gg/E94XhzrUCZ)
6
+
7
+ Evidence Graph, your spec as a compile error no coding agent can skip.
8
+
9
+ ```tsx
10
+ /**
11
+ * @evidence docs/discount.md#coupon-stacking
12
+ * States the per-issuer stacking limit
13
+ * this section defines, in the buyer's words.
14
+ * @evidence POST:/orders/{orderId}/coupons
15
+ * Explains the rejection this endpoint returns
16
+ * for an over-stacked coupon set.
17
+ * @evidence {@link hooks.useCouponStacking} Renders the limit this hook resolves.
18
+ */
19
+ export function CouponStackingNotice(props: IProps): JSX.Element;
20
+ ```
21
+
22
+ `@evidence <target> <reason>` names one unit of the spec and why this declaration answers for it. A target is a document section, an API operation, a database schema model, or a TypeScript symbol as an inline link.
23
+
24
+ ```bash
25
+ $ npx ttsc
26
+ error TS16411: [evidence/graph] Missing acknowledgement for 'docs/discount.md#coupon-stacking'
27
+ (Markdown H2 'Coupon Stacking' at docs/discount.md:3)
28
+ in Claim 1 reference 1 (markdown, symbols: h2, h3).
29
+
30
+ Use @evidence on a selected typescript host or @evidenceExclude on an eligible carrier.
31
+
32
+ error TS16411: [evidence/graph] Missing acknowledgement for 'POST:/orders/{orderId}/coupons'
33
+ (Swagger operation 'POST /orders/{orderId}/coupons' at api/openapi.json)
34
+ in Claim 1 reference 2 (swagger operations).
35
+
36
+ Use @evidence on a selected typescript host or @evidenceExclude on an eligible carrier.
37
+
38
+ error TS16411: [evidence/graph] Missing acknowledgement for 'useCouponStacking'
39
+ (TypeScript function 'useCouponStacking' at src/lib/coupons/hooks.ts:12)
40
+ in Claim 1 reference 3 (typescript, symbols: function).
41
+
42
+ Use @evidence on a selected typescript host or @evidenceExclude on an eligible carrier.
43
+
44
+ Found 3 errors.
45
+ ```
46
+
47
+ Without those tags, the build fails once per obligation, because one reference never covers another.
48
+
49
+ An AI coding agent has to clear them to finish, and clearing them means citing each target and writing down why its code answers for it. Coverage reaches 100% on its own, as the residue of the errors it closed.
50
+
51
+ ![Coverage and token spend across all four subjects](https://raw.githubusercontent.com/samchon/ttsc/gh-pages/benchmark/png/evidence-summary.png)
52
+
53
+ ## Setup
54
+
55
+ ### Install
56
+
57
+ ```bash
58
+ npm install -D typescript ttsc @ttsc/lint
59
+ npm install -D @ttsc/evidence
60
+ ```
61
+
62
+ This is a rule contributor to [`@ttsc/lint`](https://github.com/samchon/ttsc/tree/master/packages/lint) 0.22 or newer, so it runs on [`ttsc`](https://github.com/samchon/ttsc) rather than on stock `tsc` with ESLint.
63
+
64
+ ### Configure
65
+
66
+ ```ts
67
+ // lint.config.ts
68
+ import { evidence, type ITtscEvidenceGraphConfig } from "@ttsc/evidence";
69
+ import type { ITtscLintConfig } from "@ttsc/lint";
70
+
71
+ const graph: ITtscEvidenceGraphConfig = {
72
+ claims: [
73
+ {
74
+ type: "typescript",
75
+ files: ["src/components/**/*.tsx"],
76
+ symbol: "function",
77
+ reference: {
78
+ type: "markdown",
79
+ files: ["docs/**/*.md"],
80
+ symbol: ["h2", "h3"],
81
+ },
82
+ },
83
+ ],
84
+ };
85
+
86
+ export default {
87
+ plugins: { evidence },
88
+ rules: { "evidence/graph": ["error", graph] },
89
+ } satisfies ITtscLintConfig;
90
+ ```
91
+
92
+ One sentence: the components under `src` implement the docs, so every H2 and H3 under `docs` must be cited by a component. Violations arrive in the same stream as type errors, so there is no CI job to add.
93
+
94
+ ### Rules
95
+
96
+ | Rule | Takes | What it does |
97
+ | --- | --- | --- |
98
+ | `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. |
100
+ | `evidence/singular` | nothing | Keeps one public identity per file, named after the file. |
101
+ | `evidence/todo` | nothing | Fails on every remaining JSDoc `@todo`, with its own text. |
102
+
103
+ Each takes `"error"`, `"warning"`, or `"off"`.
104
+
105
+ ## Claims and references
106
+
107
+ A claim is the population that owes a citation, a reference is what it owes, and every claim and reference pair is its own 100% obligation. Listing two references means both must be satisfied, and a citation toward one never counts toward the other. They all join the same `claims` array.
108
+
109
+ | Kind | Units | Claim | Reference | Cites in |
110
+ | --- | --- | --- | --- | --- |
111
+ | Markdown | file, H1 to H4 sections | yes | yes | an HTML comment |
112
+ | Prisma | model, column, relation | yes | yes | a `///` comment |
113
+ | TypeScript | types, functions, properties | yes | yes | JSDoc |
114
+ | Swagger / OpenAPI | each operation under `paths` | no | yes | nothing, it cannot host a tag |
115
+
116
+ Every population takes glob patterns in `files`, resolved against the `ttsc` project root. Add `root` to resolve against another directory instead, inside the project, above it, or absolute, which is how several packages in a monorepo share one requirements set and write the same citation.
117
+
118
+ ### Documents
119
+
120
+ ```ts
121
+ {
122
+ type: "markdown",
123
+ files: ["docs/requirements/**/*.md"],
124
+ reference: {
125
+ type: "markdown",
126
+ files: ["docs/meetings/**/*.md"],
127
+ symbol: ["h2", "h3"],
128
+ },
129
+ }
130
+ ```
131
+
132
+ Markdown grounds Markdown, so a decision taken in a meeting and never written into the requirements fails the build. Deleting a requirement breaks every document that leaned on it, which is what stops a ghost spec from outliving its own source.
133
+
134
+ ```md
135
+ ## Coupon Stacking {#coupon-stacking}
136
+
137
+ <!-- @evidence docs/meetings/2026-01-12.md#discount-policy Carries the limit agreed in that meeting. -->
138
+ ```
139
+
140
+ A citation sits in an HTML comment, so rendered prose stays clean, and a heading declares its own anchor with the `{#id}` suffix. A section citation sits under its heading; a whole-file citation sits at the top.
141
+
142
+ ### Database schema
143
+
144
+ ```ts
145
+ {
146
+ type: "prisma",
147
+ files: ["prisma/schema/**/*.prisma"],
148
+ symbol: "model",
149
+ reference: {
150
+ type: "markdown",
151
+ files: ["docs/requirements/**/*.md"],
152
+ symbol: "h2",
153
+ },
154
+ }
155
+ ```
156
+
157
+ Every model justifies itself against a requirement, so a table nobody asked for has nothing to cite. Reverse the pair to make a provider owe the model it reads.
158
+
159
+ ```prisma
160
+ /// @evidence docs/requirements/pricing.md#discount-policy Discount columns exist for this policy.
161
+ model Sale {
162
+ /// @evidence docs/requirements/pricing.md#coupon-stacking The stacking limit is stored here.
163
+ coupon_limit Int
164
+ }
165
+ ```
166
+
167
+ A model is addressed as `prisma:Sale` and a member as `prisma:Sale.price`, never through the file it sits in, so moving a model between files cannot break a citation. Every matched file is parsed as one schema.
168
+
169
+ ### API operations
170
+
171
+ ```ts
172
+ {
173
+ type: "typescript",
174
+ files: ["src/controllers/**/*.ts"],
175
+ reference: {
176
+ type: "swagger",
177
+ file: "https://raw.githubusercontent.com/samchon/shopping/refs/heads/master/packages/api/swagger.json",
178
+ },
179
+ }
180
+ ```
181
+
182
+ Each operation under `paths` is one obligation, so an operation the spec adds and nobody implemented is a compile error on the next build. The singular `file` names one local path or one `http:` URL, never a glob; use a `reference` array for several documents.
183
+
184
+ An operation is cited as `POST:/orders`, one whitespace-free token, so the method and the path stay one target. Swagger grounds a claim and never makes one, because an operation has nowhere to host a tag.
185
+
186
+ ### Symbols
187
+
188
+ ```ts
189
+ {
190
+ type: "typescript",
191
+ files: ["src/lib/*/hooks.ts"],
192
+ symbol: "function",
193
+ reference: {
194
+ type: "typescript",
195
+ package: "@ORGANIZATION/PROJECT-api",
196
+ files: ["src/functional/**/*.ts"],
197
+ symbol: ["function"],
198
+ },
199
+ }
200
+ ```
201
+
202
+ `files` selects modules and the population is what those modules publish, so a barrel carries in the surface it forwards. A `package` population is read from disk rather than from the program, because a symbol nothing imports is absent from the program and is exactly the one an obligation needs to name.
203
+
204
+ A unit is addressed the way a consumer reaches it, so `export * as functional` nests a segment and `export { A as B }` answers to `B`. Only TypeScript may cite TypeScript: `{@link}` resolves through the citing module's own imports, and no document has an import scope to resolve in.
205
+
206
+ ### Symbol selectors
207
+
208
+ `symbol` picks which units a reference materializes, and on a claim it restricts which declarations may host a tag.
209
+
210
+ | Kind | Values | Default on a claim | Default on a reference |
211
+ | --- | --- | --- | --- |
212
+ | Markdown | `file`, `h1`, `h2`, `h3`, `h4` | all five | all five |
213
+ | Prisma | `model`, `column`, `relation` | all three | `model` |
214
+ | TypeScript | `type`, `function`, `property` | all three | `type` |
215
+ | Swagger | none, every operation is selected | not applicable | every operation |
216
+
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.
218
+
219
+ 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
+
221
+ ### Exclusions
222
+
223
+ ```md
224
+ <!-- @evidenceExclude docs/requirements/pricing.md#coupon-stacking
225
+ This release ships a single coupon. Stacking waits for the settlement policy. -->
226
+ ```
227
+
228
+ `@evidenceExclude <target> <reason>` records that a claim intentionally does not use a scope. It follows the same hierarchy as `@evidence`, affects only the claim it is written in, and one obligation may exclude a scope only once.
229
+
230
+ It is the only acknowledgement that settles an obligation without anything being built, so it exists to be vetoed. "Not applicable" is a conclusion rather than a reason.
231
+
232
+ ```ts
233
+ {
234
+ type: "typescript",
235
+ files: ["src/components/**/*.tsx", "src/components/EXCLUSIONS.ts"],
236
+ evidenceExcludeCarriers: ["src/components/EXCLUSIONS.ts"],
237
+ symbol: "function",
238
+ reference: { type: "markdown", files: ["docs/**/*.md"], symbol: "h2" },
239
+ }
240
+ ```
241
+
242
+ `evidenceExcludeCarriers` confines them to a named ledger. Scattered through the population, reading every exclusion means reading the population; gathered in one file it means opening one file. An exclusion written anywhere else is reported where it sits and discharges nothing.
243
+
244
+ ### Strict references
245
+
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.
247
+
248
+ ```ts
249
+ {
250
+ type: "typescript",
251
+ package: "@ORGANIZATION/PROJECT-api",
252
+ files: ["src/functional/**/*.ts"],
253
+ symbol: ["function"],
254
+ noEvidenceExclude: true,
255
+ singleEvidencePerSymbol: true,
256
+ }
257
+ ```
258
+
259
+ - `noEvidenceExclude` refuses exclusions, so the target still owes positive evidence. A published accessor no hook consumes is an omission rather than a decision.
260
+ - `uniqueEvidence` allows at most one host per unit, so one host is answerable for it rather than several.
261
+ - `singleEvidencePerSymbol` requires exactly one unit from every selected host, so a host citing nothing and a host citing everything both fail.
262
+
263
+ 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
+
265
+ ## Sponsors
266
+
267
+ [![Sponsors](https://raw.githubusercontent.com/samchon/sponsor-images/refs/heads/master/public/circle.svg)](https://github.com/sponsors/samchon)
268
+
269
+ Thanks for your support.
270
+
271
+ Your [donation](https://github.com/sponsors/samchon) encourages `@ttsc/evidence` development.
272
+
273
+ ## References
274
+
275
+ - [`ttsc`](https://github.com/samchon/ttsc): the TypeScript-Go toolchain this plugin runs on.
276
+ - [`@ttsc/lint`](https://github.com/samchon/ttsc/tree/master/packages/lint): the lint engine that links this rule into the compiler.
277
+ - [Guide Documents](https://ttsc.dev/docs/evidence)
278
+ - [Benchmark Diagram](https://ttsc.dev/docs/benchmark/evidence)
279
+ - [`samchon/evidence-benchmark-results`](https://github.com/samchon/evidence-benchmark-results)
package/lib/index.d.ts ADDED
@@ -0,0 +1,65 @@
1
+ export * from "./structures/index";
2
+ export * from "./typings/index";
3
+ /**
4
+ * The `@ttsc/lint` contributor that checks a project's evidence graph.
5
+ *
6
+ * Import this value into `lint.config.ts` and register it under the
7
+ * `"evidence"` plugin name. You can then enable `"evidence/graph"` and pass an
8
+ * {@link ITtscEvidenceGraphConfig} that describes which documents and TypeScript
9
+ * symbols must remain connected.
10
+ *
11
+ * The plugin contributes four rules, enabled independently.
12
+ *
13
+ * - `"evidence/graph"` — the configured evidence graph. Every declaration target
14
+ * must resolve, and every selected evidence unit must be acknowledged. Takes
15
+ * an {@link ITtscEvidenceGraphConfig}. It also offers the configured targets
16
+ * as editor completions, on the cycles where it passes: the host publishes a
17
+ * rule's completions only while that rule reports nothing.
18
+ * - `"evidence/singular"` — one public identity per TypeScript file, named after
19
+ * the file. Takes no options, so it carries a bare severity.
20
+ * - `"evidence/documented"` — a JSDoc block on every selected export, which is
21
+ * the only place an `@evidence` tag is ever read from. Takes an
22
+ * {@link ITtscEvidenceDocumentedConfig}.
23
+ * - `"evidence/todo"` — no remaining JSDoc `@todo` tag anywhere in a checked
24
+ * file, exported or not. Each tag is an unrealized contract reported with its
25
+ * own text, so the diagnostics read as the ledger of what remains to realize.
26
+ * Takes no options, so it carries a bare severity.
27
+ *
28
+ * @example <caption>Configure the plugin in `lint.config.ts`</caption>
29
+ * import { type ITtscEvidenceGraphConfig, evidence } from "@ttsc/evidence";
30
+ * import type { ITtscLintConfig } from "@ttsc/lint";
31
+ *
32
+ * const graph: ITtscEvidenceGraphConfig = {
33
+ * claims: [
34
+ * {
35
+ * type: "typescript",
36
+ * files: ["src/**"],
37
+ * reference: {
38
+ * type: "markdown",
39
+ * files: ["docs/*.md"],
40
+ * },
41
+ * },
42
+ * ],
43
+ * };
44
+ *
45
+ * export default {
46
+ * plugins: {
47
+ * evidence: evidence,
48
+ * },
49
+ * files: ["src/**"],
50
+ * rules: {
51
+ * "evidence/graph": ["error", graph],
52
+ * "evidence/singular": "error",
53
+ * },
54
+ * } satisfies ITtscLintConfig;
55
+ */
56
+ export declare const evidence: {
57
+ meta: {
58
+ readonly name: "@ttsc/evidence";
59
+ readonly namespace: "evidence";
60
+ readonly version: string;
61
+ };
62
+ rules: readonly ["graph", "singular", "documented", "todo"];
63
+ source: string;
64
+ };
65
+ export default evidence;
package/lib/index.js ADDED
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ var __importDefault = (this && this.__importDefault) || function (mod) {
17
+ return (mod && mod.__esModule) ? mod : { "default": mod };
18
+ };
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.evidence = void 0;
21
+ const node_path_1 = __importDefault(require("node:path"));
22
+ const package_json_1 = require("../package.json");
23
+ __exportStar(require("./structures/index"), exports);
24
+ __exportStar(require("./typings/index"), exports);
25
+ /**
26
+ * The `@ttsc/lint` contributor that checks a project's evidence graph.
27
+ *
28
+ * Import this value into `lint.config.ts` and register it under the
29
+ * `"evidence"` plugin name. You can then enable `"evidence/graph"` and pass an
30
+ * {@link ITtscEvidenceGraphConfig} that describes which documents and TypeScript
31
+ * symbols must remain connected.
32
+ *
33
+ * The plugin contributes four rules, enabled independently.
34
+ *
35
+ * - `"evidence/graph"` — the configured evidence graph. Every declaration target
36
+ * must resolve, and every selected evidence unit must be acknowledged. Takes
37
+ * an {@link ITtscEvidenceGraphConfig}. It also offers the configured targets
38
+ * as editor completions, on the cycles where it passes: the host publishes a
39
+ * rule's completions only while that rule reports nothing.
40
+ * - `"evidence/singular"` — one public identity per TypeScript file, named after
41
+ * the file. Takes no options, so it carries a bare severity.
42
+ * - `"evidence/documented"` — a JSDoc block on every selected export, which is
43
+ * the only place an `@evidence` tag is ever read from. Takes an
44
+ * {@link ITtscEvidenceDocumentedConfig}.
45
+ * - `"evidence/todo"` — no remaining JSDoc `@todo` tag anywhere in a checked
46
+ * file, exported or not. Each tag is an unrealized contract reported with its
47
+ * own text, so the diagnostics read as the ledger of what remains to realize.
48
+ * Takes no options, so it carries a bare severity.
49
+ *
50
+ * @example <caption>Configure the plugin in `lint.config.ts`</caption>
51
+ * import { type ITtscEvidenceGraphConfig, evidence } from "@ttsc/evidence";
52
+ * import type { ITtscLintConfig } from "@ttsc/lint";
53
+ *
54
+ * const graph: ITtscEvidenceGraphConfig = {
55
+ * claims: [
56
+ * {
57
+ * type: "typescript",
58
+ * files: ["src/**"],
59
+ * reference: {
60
+ * type: "markdown",
61
+ * files: ["docs/*.md"],
62
+ * },
63
+ * },
64
+ * ],
65
+ * };
66
+ *
67
+ * export default {
68
+ * plugins: {
69
+ * evidence: evidence,
70
+ * },
71
+ * files: ["src/**"],
72
+ * rules: {
73
+ * "evidence/graph": ["error", graph],
74
+ * "evidence/singular": "error",
75
+ * },
76
+ * } satisfies ITtscLintConfig;
77
+ */
78
+ exports.evidence = {
79
+ meta: {
80
+ name: "@ttsc/evidence",
81
+ namespace: "evidence",
82
+ version: package_json_1.version,
83
+ },
84
+ rules: ["graph", "singular", "documented", "todo"],
85
+ source: node_path_1.default.resolve(__dirname, "..", "native"),
86
+ };
87
+ exports.default = exports.evidence;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,197 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loadPrismaModels = void 0;
7
+ const node_crypto_1 = require("node:crypto");
8
+ const promises_1 = __importDefault(require("node:fs/promises"));
9
+ const node_module_1 = require("node:module");
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const MAX_SCHEMA_BYTES = 16 * 1024 * 1024;
12
+ const ANSI_PATTERN = /\x1b\[[0-9;]*m/gu;
13
+ /**
14
+ * The separator between a path and its hash inside the composite digest.
15
+ *
16
+ * NUL, because a path may contain a space while a hex digest contains nothing
17
+ * outside `[0-9a-f]` — an ambiguous separator would let two different sets
18
+ * compose one key, which is the single thing a cache key may not do. The native
19
+ * side writes the same byte.
20
+ *
21
+ * Both sides spell it as an escape rather than as a literal on purpose. This
22
+ * was a literal control character once, and the two implementations then
23
+ * disagreed on a byte neither source showed, in the one place where a
24
+ * disagreement produces no error and no wrong answer — only a cache that never
25
+ * hits again.
26
+ */
27
+ const SEPARATOR = "\u0000";
28
+ /**
29
+ * Loads and classifies every configured Prisma schema set for the native rule.
30
+ *
31
+ * The native contributor is Go and Prisma's parser is a WebAssembly module with
32
+ * 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.
35
+ *
36
+ * One **set** is one request. A Prisma schema folder is several files that form
37
+ * a single namespace — a model in one file may point at a model in another — so
38
+ * a file cannot be parsed alone and the reuse key belongs to the ordered set.
39
+ *
40
+ * @internal
41
+ */
42
+ const loadPrismaModels = async (request) => {
43
+ const parser = resolveParser(request.root);
44
+ const loaded = await Promise.all(request.sets.map(async (set) => {
45
+ let digest = "";
46
+ try {
47
+ const read = await readSet(request.root, set.files);
48
+ digest = read.digest;
49
+ return {
50
+ id: set.id,
51
+ models: modelsOf(parser, read.files),
52
+ digest,
53
+ };
54
+ }
55
+ catch (error) {
56
+ return {
57
+ id: set.id,
58
+ message: errorMessage(error),
59
+ digest,
60
+ };
61
+ }
62
+ }));
63
+ return {
64
+ documents: loaded.filter(isInventory),
65
+ problems: loaded.filter(isProblem),
66
+ };
67
+ };
68
+ exports.loadPrismaModels = loadPrismaModels;
69
+ /**
70
+ * Resolves the schema parser, preferring the one the consumer's project can
71
+ * see.
72
+ *
73
+ * The preference is not politeness. A parser build validates the schema it
74
+ * parses, and Prisma's own rules move between major versions — a Prisma 7 build
75
+ * rejects a `datasource` block containing `url`, which every Prisma 6 schema
76
+ * has. Parsing a consumer's schema with a parser they did not choose therefore
77
+ * turns a valid schema into a build failure, so their copy wins whenever one
78
+ * exists and the pinned dependency is only the fallback.
79
+ *
80
+ * Which one answered is carried on the parser, because a rejection produced by
81
+ * the fallback needs to say so; a reader whose schema is fine has no other way
82
+ * to learn that the verdict came from a version they never installed.
83
+ */
84
+ const resolveParser = (root) => {
85
+ const failures = [];
86
+ for (const origin of ["project", "plugin"]) {
87
+ try {
88
+ const resolver = origin === "project"
89
+ ? (0, node_module_1.createRequire)(node_path_1.default.join(root, "package.json"))
90
+ : (0, node_module_1.createRequire)(__filename);
91
+ const manifest = resolver("@prisma/prisma-schema-wasm/package.json");
92
+ const wasm = resolver("@prisma/prisma-schema-wasm");
93
+ if (typeof wasm.get_datamodel !== "function")
94
+ throw new Error("the module exposes no get_datamodel export");
95
+ return {
96
+ get_datamodel: wasm.get_datamodel,
97
+ version: manifest.version ?? "unknown",
98
+ origin,
99
+ };
100
+ }
101
+ catch (error) {
102
+ failures.push(`${origin}: ${errorMessage(error)}`);
103
+ }
104
+ }
105
+ throw new Error(`no Prisma schema parser could be resolved (${failures.join("; ")})`);
106
+ };
107
+ const readSet = async (root, sources) => {
108
+ const files = [];
109
+ // Hashed as it goes, over the bytes as they were read and in the order the
110
+ // caller sent. The native side composes the same digest from the same
111
+ // per-file hashes, so the two agree by construction; hashing the decoded text
112
+ // instead would agree only where the round trip happens to be exact.
113
+ const composite = (0, node_crypto_1.createHash)("sha256");
114
+ for (const source of sources) {
115
+ // A schema file may sit above the project or on an absolute path, because a
116
+ // population resolves against the root its configuration declares rather
117
+ // than against the project. The caller sends the paths its own walk
118
+ // produced, so this side only resolves them the same way.
119
+ const location = node_path_1.default.resolve(root, source);
120
+ const stat = await promises_1.default.stat(location);
121
+ if (!stat.isFile())
122
+ throw new Error(`the Prisma schema source '${source}' is not a file`);
123
+ if (stat.size > MAX_SCHEMA_BYTES)
124
+ throw new Error(`the Prisma schema '${source}' exceeds the ${MAX_SCHEMA_BYTES} byte limit`);
125
+ const content = await promises_1.default.readFile(location);
126
+ files.push([source, decodeUtf8(content)]);
127
+ composite.update(source, "utf8");
128
+ composite.update(SEPARATOR);
129
+ composite.update((0, node_crypto_1.createHash)("sha256").update(content).digest("hex"));
130
+ composite.update("\n");
131
+ }
132
+ return { files, digest: composite.digest("hex") };
133
+ };
134
+ const decodeUtf8 = (content) => new TextDecoder("utf-8", { fatal: true }).decode(content);
135
+ /**
136
+ * Runs the parser over one whole set and classifies what it declares.
137
+ *
138
+ * `get_datamodel` is used rather than the full DMMF deliberately. Measured on a
139
+ * 200-model schema, the datamodel answers in 11 ms and 369 KB while the full
140
+ * DMMF takes 262 ms and 16 MB — and that payload has to cross a process
141
+ * boundary as JSON. Nothing in the extra 16 MB is a unit this graph can cite.
142
+ */
143
+ const modelsOf = (parser, files) => {
144
+ let payload;
145
+ try {
146
+ payload = parser.get_datamodel(JSON.stringify({ prismaSchema: files }));
147
+ }
148
+ catch (error) {
149
+ throw new Error(`${parserFailure(error)} (parsed by @prisma/prisma-schema-wasm@${parser.version} resolved from the ${parser.origin})`);
150
+ }
151
+ const datamodel = JSON.parse(payload);
152
+ const models = [];
153
+ for (const model of datamodel.models ?? []) {
154
+ if (typeof model.name !== "string" || model.name.length === 0)
155
+ continue;
156
+ models.push({
157
+ name: model.name,
158
+ documentation: typeof model.documentation === "string" ? model.documentation : "",
159
+ fields: (model.fields ?? [])
160
+ .filter((field) => typeof field.name === "string" && field.name.length !== 0)
161
+ .map((field) => ({
162
+ name: field.name,
163
+ // Prisma classifies a relation field as `object`; everything a table
164
+ // actually stores is `scalar`. An `enum`-typed field stores a value
165
+ // and is therefore a column, which is why the test is for `object`
166
+ // rather than for `scalar`.
167
+ symbol: field.kind === "object" ? "relation" : "column",
168
+ documentation: typeof field.documentation === "string" ? field.documentation : "",
169
+ })),
170
+ });
171
+ }
172
+ return models;
173
+ };
174
+ /**
175
+ * Unwraps the parser's own error into the sentence its author has to act on.
176
+ *
177
+ * A rejection arrives as an `Error` whose message is a JSON envelope carrying
178
+ * an error code and an ANSI-coloured report — and that report is the one place
179
+ * a location survives at all, because a successful parse carries no positions.
180
+ * Both halves are preserved: the colour codes are stripped because a diagnostic
181
+ * stream is not a terminal, and the text is not, because it names the line.
182
+ */
183
+ const parserFailure = (error) => {
184
+ const raw = errorMessage(error);
185
+ try {
186
+ const envelope = JSON.parse(raw);
187
+ if (typeof envelope.message === "string")
188
+ return envelope.message.replace(ANSI_PATTERN, "").trim();
189
+ }
190
+ catch {
191
+ // Not an envelope; the raw message is the whole of what is known.
192
+ }
193
+ return raw.replace(ANSI_PATTERN, "").trim();
194
+ };
195
+ const isInventory = (value) => "models" in value;
196
+ const isProblem = (value) => "message" in value;
197
+ const errorMessage = (error) => error instanceof Error ? error.message : String(error);
@@ -0,0 +1 @@
1
+ export {};