docsgov 0.1.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 (159) hide show
  1. package/README.md +242 -0
  2. package/dist/apispec/apispec.js +401 -0
  3. package/dist/apispec/apispec.test.js +444 -0
  4. package/dist/apispec/errors.js +17 -0
  5. package/dist/apispec/index.js +2 -0
  6. package/dist/check/doclinks.js +167 -0
  7. package/dist/check/index.js +8 -0
  8. package/dist/check/run.js +391 -0
  9. package/dist/check/run.test.js +513 -0
  10. package/dist/check/suggest.js +134 -0
  11. package/dist/check/suggest.test.js +92 -0
  12. package/dist/check/tokens.js +125 -0
  13. package/dist/cmd/main.js +330 -0
  14. package/dist/cmd/main.test.js +422 -0
  15. package/dist/codeq/cache.js +71 -0
  16. package/dist/codeq/cache.test.js +67 -0
  17. package/dist/codeq/errors.js +52 -0
  18. package/dist/codeq/grammars/tree-sitter-go.wasm +0 -0
  19. package/dist/codeq/grammars/tree-sitter-java.wasm +0 -0
  20. package/dist/codeq/grammars/tree-sitter-javascript.wasm +0 -0
  21. package/dist/codeq/grammars/tree-sitter-tsx.wasm +0 -0
  22. package/dist/codeq/grammars/tree-sitter-typescript.wasm +0 -0
  23. package/dist/codeq/index.js +11 -0
  24. package/dist/codeq/resolve.test.js +109 -0
  25. package/dist/codeq/resolver.js +128 -0
  26. package/dist/codeq/resolver.test.js +124 -0
  27. package/dist/codeq/resolvers/go.js +242 -0
  28. package/dist/codeq/resolvers/go.test.js +143 -0
  29. package/dist/codeq/resolvers/java.js +349 -0
  30. package/dist/codeq/resolvers/java.test.js +138 -0
  31. package/dist/codeq/resolvers/java_queries.js +63 -0
  32. package/dist/codeq/resolvers/javascript.js +412 -0
  33. package/dist/codeq/resolvers/javascript.test.js +125 -0
  34. package/dist/codeq/resolvers/javascript_queries.js +46 -0
  35. package/dist/codeq/resolvers/typescript.js +366 -0
  36. package/dist/codeq/resolvers/typescript.test.js +180 -0
  37. package/dist/codeq/resolvers/typescript_queries.js +78 -0
  38. package/dist/codeq/signature.js +50 -0
  39. package/dist/codeq/signature.test.js +50 -0
  40. package/dist/codeq/suggest.js +96 -0
  41. package/dist/codeq/treesitter.js +122 -0
  42. package/dist/codeq/treesitter.test.js +118 -0
  43. package/dist/config/config.js +74 -0
  44. package/dist/config/config.test.js +98 -0
  45. package/dist/config/fs.js +116 -0
  46. package/dist/config/glob.js +82 -0
  47. package/dist/config/glob.test.js +61 -0
  48. package/dist/config/index.js +4 -0
  49. package/dist/dedup/analyzer/analyzer.js +533 -0
  50. package/dist/dedup/analyzer/analyzer.test.js +530 -0
  51. package/dist/dedup/analyzer/canonical.js +74 -0
  52. package/dist/dedup/analyzer/canonical.test.js +70 -0
  53. package/dist/dedup/analyzer/cosine_clusters.js +169 -0
  54. package/dist/dedup/analyzer/cosine_clusters.test.js +131 -0
  55. package/dist/dedup/analyzer/distinctive.js +85 -0
  56. package/dist/dedup/analyzer/distinctive.test.js +49 -0
  57. package/dist/dedup/analyzer/exact_clusters.js +63 -0
  58. package/dist/dedup/analyzer/exact_clusters.test.js +81 -0
  59. package/dist/dedup/analyzer/index.js +14 -0
  60. package/dist/dedup/analyzer/multiplicity.js +110 -0
  61. package/dist/dedup/analyzer/multiplicity.test.js +123 -0
  62. package/dist/dedup/analyzer/order.js +22 -0
  63. package/dist/dedup/analyzer/partial_overlaps.js +65 -0
  64. package/dist/dedup/analyzer/partial_overlaps.test.js +161 -0
  65. package/dist/dedup/analyzer/preview.js +84 -0
  66. package/dist/dedup/analyzer/preview.test.js +46 -0
  67. package/dist/dedup/analyzer/safety.js +27 -0
  68. package/dist/dedup/analyzer/safety.test.js +39 -0
  69. package/dist/dedup/config.js +18 -0
  70. package/dist/dedup/configload.js +299 -0
  71. package/dist/dedup/configload.test.js +410 -0
  72. package/dist/dedup/dedup.index.test.js +203 -0
  73. package/dist/dedup/dedup.js +143 -0
  74. package/dist/dedup/dedup.test.js +212 -0
  75. package/dist/dedup/dedupcfg/config.js +112 -0
  76. package/dist/dedup/dedupcfg/config.test.js +70 -0
  77. package/dist/dedup/dedupcfg/index.js +1 -0
  78. package/dist/dedup/deduptypes/index.js +1 -0
  79. package/dist/dedup/deduptypes/types.js +9 -0
  80. package/dist/dedup/deduptypes/types.test.js +34 -0
  81. package/dist/dedup/embedder/cache.js +23 -0
  82. package/dist/dedup/embedder/cache.test.js +50 -0
  83. package/dist/dedup/embedder/constants.js +10 -0
  84. package/dist/dedup/embedder/embedder.js +76 -0
  85. package/dist/dedup/embedder/embedder.mock.test.js +128 -0
  86. package/dist/dedup/embedder/embedder.test.js +96 -0
  87. package/dist/dedup/embedder/errors.js +20 -0
  88. package/dist/dedup/embedder/errors.test.js +35 -0
  89. package/dist/dedup/embedder/index.js +4 -0
  90. package/dist/dedup/embedder/session.js +78 -0
  91. package/dist/dedup/embedder/session.test.js +172 -0
  92. package/dist/dedup/gitignore.js +97 -0
  93. package/dist/dedup/gitignore.test.js +98 -0
  94. package/dist/dedup/index.js +11 -0
  95. package/dist/dedup/indexdb/errors.js +48 -0
  96. package/dist/dedup/indexdb/index.js +6 -0
  97. package/dist/dedup/indexdb/indexdb.js +302 -0
  98. package/dist/dedup/indexdb/indexdb.test.js +739 -0
  99. package/dist/dedup/indexdb/load.js +110 -0
  100. package/dist/dedup/indexdb/migrations.js +58 -0
  101. package/dist/dedup/indexdb/schema.js +83 -0
  102. package/dist/dedup/indexer/index.js +9 -0
  103. package/dist/dedup/indexer/indexer.js +501 -0
  104. package/dist/dedup/indexer/indexer.test.js +510 -0
  105. package/dist/dedup/indexer/links.js +89 -0
  106. package/dist/dedup/mdsection/anchor.js +60 -0
  107. package/dist/dedup/mdsection/anchor.test.js +39 -0
  108. package/dist/dedup/mdsection/blocks.js +409 -0
  109. package/dist/dedup/mdsection/blocks.test.js +359 -0
  110. package/dist/dedup/mdsection/index.js +4 -0
  111. package/dist/dedup/mdsection/parse.js +21 -0
  112. package/dist/dedup/mdsection/section.js +234 -0
  113. package/dist/dedup/mdsection/section.test.js +221 -0
  114. package/dist/dedup/report/floatfmt.js +71 -0
  115. package/dist/dedup/report/floatfmt.test.js +42 -0
  116. package/dist/dedup/report/index.js +8 -0
  117. package/dist/dedup/report/quote.js +77 -0
  118. package/dist/dedup/report/quote.test.js +67 -0
  119. package/dist/dedup/report/text.js +251 -0
  120. package/dist/dedup/report/text.test.js +420 -0
  121. package/dist/dedup/report_types.js +8 -0
  122. package/dist/dedup/sectionid/index.js +1 -0
  123. package/dist/dedup/sectionid/sectionid.js +16 -0
  124. package/dist/dedup/sectionid/sectionid.test.js +49 -0
  125. package/dist/guard/api/errors.js +12 -0
  126. package/dist/guard/api/index.js +2 -0
  127. package/dist/guard/api/parser.js +81 -0
  128. package/dist/guard/api/parser.test.js +58 -0
  129. package/dist/guard/api/types.js +1 -0
  130. package/dist/guard/code/errors.js +16 -0
  131. package/dist/guard/code/index.js +2 -0
  132. package/dist/guard/code/parser.js +54 -0
  133. package/dist/guard/code/parser.test.js +111 -0
  134. package/dist/guard/code/types.js +6 -0
  135. package/dist/index.js +1 -0
  136. package/dist/index.test.js +5 -0
  137. package/dist/repo/boundary.js +92 -0
  138. package/dist/repo/boundary.test.js +65 -0
  139. package/dist/repo/errors.js +56 -0
  140. package/dist/repo/errors.test.js +85 -0
  141. package/dist/repo/exists.test.js +72 -0
  142. package/dist/repo/filename.js +46 -0
  143. package/dist/repo/filename.test.js +39 -0
  144. package/dist/repo/fs.js +53 -0
  145. package/dist/repo/index.js +7 -0
  146. package/dist/repo/overlay.js +36 -0
  147. package/dist/repo/overlay.test.js +80 -0
  148. package/dist/repo/repo.js +353 -0
  149. package/dist/repo/repo.test.js +255 -0
  150. package/dist/repo/testutil.js +27 -0
  151. package/dist/repo/write.test.js +125 -0
  152. package/dist/report/color.js +73 -0
  153. package/dist/report/index.js +1 -0
  154. package/dist/report/report.js +112 -0
  155. package/dist/report/report.test.js +368 -0
  156. package/dist/violation/index.js +1 -0
  157. package/dist/violation/types.js +22 -0
  158. package/dist/violation/types.test.js +70 -0
  159. package/package.json +48 -0
@@ -0,0 +1,391 @@
1
+ // Port of internal/check/run.go — the three-pass Run pipeline (Stage 5 of
2
+ // minimize-docgov) and the {{api:…}} qualifier dispatch from apitokens' caller.
3
+ //
4
+ // run() runs three independent passes (code, doc, api) over the scopes declared
5
+ // in cfg. Only present (non-undefined) sections are executed. Violations are
6
+ // collected from all passes and returned sorted by (file, line) for
7
+ // determinism. Only operational errors (FS walk failures, etc.) reject.
8
+ //
9
+ // FS RECONCILIATION: this is the integration point. config and repo now share
10
+ // repo's ASYNC FS (see config/fs.ts), so run() — and every pass — is async.
11
+ import { parse as parseSpec } from "../apispec/index.js";
12
+ import { inScope, walkMarkdown } from "../config/index.js";
13
+ import { parseApiRef } from "../guard/api/index.js";
14
+ import { parseCodeRef } from "../guard/code/index.js";
15
+ import { MalformedRefError as ApiMalformedRefError } from "../guard/api/errors.js";
16
+ import { MalformedRefError as CodeMalformedRefError } from "../guard/code/errors.js";
17
+ import { isNotExist } from "../repo/index.js";
18
+ import { Rules } from "../violation/index.js";
19
+ import { checkDocLinks } from "./doclinks.js";
20
+ import { suggestionSuffix } from "./suggest.js";
21
+ import { iterApiTokens, iterCodeTokens } from "./tokens.js";
22
+ /**
23
+ * run executes three independent guard passes (code, doc, api) as configured in
24
+ * cfg, collecting all violations. Passes for absent sections are skipped.
25
+ * Returns violations sorted by (file, line). Only operational errors (FS walk
26
+ * failures, etc.) reject.
27
+ */
28
+ export async function run(cfg, r, resolver) {
29
+ const fsys = r.fs();
30
+ let vs = [];
31
+ if (cfg.Code !== undefined) {
32
+ vs = vs.concat(await runCodePass(cfg.Code, fsys, resolver));
33
+ }
34
+ if (cfg.Doc !== undefined) {
35
+ vs = vs.concat(await runDocPass(cfg.Doc, fsys));
36
+ }
37
+ if (cfg.API !== undefined) {
38
+ vs = vs.concat(await runAPIPass(cfg.API, fsys));
39
+ }
40
+ vs.sort((a, b) => {
41
+ if (a.file !== b.file) {
42
+ return a.file < b.file ? -1 : 1;
43
+ }
44
+ return a.line - b.line;
45
+ });
46
+ return vs;
47
+ }
48
+ /** decodeBytes turns raw file bytes into a string (CRLF already normalised by repo). */
49
+ function decodeBytes(data) {
50
+ return new TextDecoder().decode(data);
51
+ }
52
+ /**
53
+ * runCodePass executes the code guard pass over all .md files in scope.boundary.
54
+ * For each {{code:…}} token: parse it, check the path is under scope.source, then
55
+ * resolve it. No status/sigil gating.
56
+ */
57
+ async function runCodePass(scope, fsys, resolver) {
58
+ const files = await walkMarkdown(fsys, scope.boundary);
59
+ const vs = [];
60
+ for (const file of files) {
61
+ let src;
62
+ try {
63
+ src = decodeBytes(await fsys.readFile(file));
64
+ }
65
+ catch (err) {
66
+ vs.push({
67
+ rule: Rules.guardCode,
68
+ file,
69
+ line: 0,
70
+ sectionID: "",
71
+ expected: "",
72
+ actual: "",
73
+ message: "cannot read file: " + errMsg(err),
74
+ });
75
+ continue;
76
+ }
77
+ for (const loc of iterCodeTokens(src)) {
78
+ const v = await checkCodeToken(file, loc, scope.source, fsys, resolver);
79
+ if (v !== null) {
80
+ vs.push(v);
81
+ }
82
+ }
83
+ }
84
+ return vs;
85
+ }
86
+ /**
87
+ * checkCodeToken evaluates one code token: parse, source-check, resolve. Returns
88
+ * null when the token passes all checks.
89
+ */
90
+ async function checkCodeToken(file, loc, source, fsys, resolver) {
91
+ let ref;
92
+ try {
93
+ ref = parseCodeRef(loc.token);
94
+ }
95
+ catch (err) {
96
+ if (err instanceof CodeMalformedRefError) {
97
+ return {
98
+ rule: Rules.guardCode,
99
+ file,
100
+ line: loc.line,
101
+ sectionID: "",
102
+ expected: "",
103
+ actual: loc.token,
104
+ message: "malformed code ref: " + err.message,
105
+ };
106
+ }
107
+ throw err;
108
+ }
109
+ if (!inScope(source, ref.Path)) {
110
+ return {
111
+ rule: Rules.guardCode,
112
+ file,
113
+ line: loc.line,
114
+ sectionID: "",
115
+ expected: "under code.source",
116
+ actual: loc.token,
117
+ message: "ref points outside source",
118
+ };
119
+ }
120
+ let found;
121
+ try {
122
+ found = await resolver.resolve(fsys, ref);
123
+ }
124
+ catch (err) {
125
+ return {
126
+ rule: Rules.guardCode,
127
+ file,
128
+ line: loc.line,
129
+ sectionID: "",
130
+ expected: "",
131
+ actual: loc.token,
132
+ message: "resolver error: " + errMsg(err),
133
+ };
134
+ }
135
+ if (!found) {
136
+ let message = "referenced symbol not found";
137
+ // Best-effort "did you mean": never let a suggestion fault drop the
138
+ // violation itself — the base message must always survive.
139
+ if (resolver.suggest !== undefined) {
140
+ try {
141
+ message += suggestionSuffix(ref, await resolver.suggest(fsys, ref));
142
+ }
143
+ catch {
144
+ // keep the base message
145
+ }
146
+ }
147
+ return {
148
+ rule: Rules.guardCode,
149
+ file,
150
+ line: loc.line,
151
+ sectionID: "",
152
+ expected: "symbol exists",
153
+ actual: loc.token,
154
+ message,
155
+ };
156
+ }
157
+ return null;
158
+ }
159
+ /**
160
+ * runDocPass executes the doc existence pass over all .md files in
161
+ * scope.boundary. It collects every link and image destination from the mdast
162
+ * tree, then applies the existence algorithm (URL/fragment skip, absolute
163
+ * reject, escape reject, existence check).
164
+ */
165
+ async function runDocPass(scope, fsys) {
166
+ const files = await walkMarkdown(fsys, scope.boundary);
167
+ const vs = [];
168
+ for (const file of files) {
169
+ let src;
170
+ try {
171
+ src = decodeBytes(await fsys.readFile(file));
172
+ }
173
+ catch (err) {
174
+ vs.push({
175
+ rule: Rules.guardDocs,
176
+ file,
177
+ line: 0,
178
+ sectionID: "",
179
+ expected: "",
180
+ actual: "",
181
+ message: "cannot read file: " + errMsg(err),
182
+ });
183
+ continue;
184
+ }
185
+ const fv = await checkDocLinks(file, src, fsys);
186
+ for (const v of fv) {
187
+ vs.push(v);
188
+ }
189
+ }
190
+ return vs;
191
+ }
192
+ /**
193
+ * runAPIPass executes the api guard pass:
194
+ * 1. Load all .json specs under scope.source.
195
+ * 2. For each .md in scope.boundary, scan {{api:…}} tokens and validate.
196
+ */
197
+ async function runAPIPass(scope, fsys) {
198
+ const { specs, specVs } = await loadAPISpecs(scope.source, fsys);
199
+ // If there were spec-load violations but some specs loaded, continue checking.
200
+ // If no specs loaded at all (and no load error), add a zero-spec violation.
201
+ const vs = [...specVs];
202
+ if (specs.length === 0 && specVs.length === 0) {
203
+ vs.push({
204
+ rule: Rules.guardAPI,
205
+ file: "",
206
+ line: 0,
207
+ sectionID: "",
208
+ expected: "",
209
+ actual: "",
210
+ message: "no OpenAPI spec found under api.source",
211
+ });
212
+ // Without specs we can still scan tokens for malformed-ref errors.
213
+ }
214
+ const files = await walkMarkdown(fsys, scope.boundary);
215
+ for (const file of files) {
216
+ let src;
217
+ try {
218
+ src = decodeBytes(await fsys.readFile(file));
219
+ }
220
+ catch (err) {
221
+ vs.push({
222
+ rule: Rules.guardAPI,
223
+ file,
224
+ line: 0,
225
+ sectionID: "",
226
+ expected: "",
227
+ actual: "",
228
+ message: "cannot read file: " + errMsg(err),
229
+ });
230
+ continue;
231
+ }
232
+ for (const loc of iterApiTokens(src)) {
233
+ for (const v of checkAPIToken(file, loc, specs)) {
234
+ vs.push(v);
235
+ }
236
+ }
237
+ }
238
+ return vs;
239
+ }
240
+ /**
241
+ * loadAPISpecs enumerates every .json file under source patterns, loads each,
242
+ * and returns the loaded specs plus any per-file load violations.
243
+ */
244
+ async function loadAPISpecs(source, fsys) {
245
+ const specs = [];
246
+ const specVs = [];
247
+ // Collect every .json path in the tree (slash paths), then filter by scope.
248
+ const jsonPaths = [];
249
+ async function descend(dirSlash) {
250
+ let entries;
251
+ try {
252
+ entries = await fsys.readDir(dirSlash === "" ? "." : dirSlash);
253
+ }
254
+ catch (err) {
255
+ if (isNotExist(err)) {
256
+ return;
257
+ }
258
+ throw err;
259
+ }
260
+ for (const entry of entries) {
261
+ const childSlash = dirSlash === "" ? entry.name() : `${dirSlash}/${entry.name()}`;
262
+ if (entry.isDir()) {
263
+ await descend(childSlash);
264
+ continue;
265
+ }
266
+ if (childSlash.endsWith(".json")) {
267
+ jsonPaths.push(childSlash);
268
+ }
269
+ }
270
+ }
271
+ await descend("");
272
+ for (const p of jsonPaths) {
273
+ if (!inScope(source, p)) {
274
+ continue;
275
+ }
276
+ let s;
277
+ try {
278
+ const data = decodeBytes(await fsys.readFile(p));
279
+ s = parseSpec(data, p);
280
+ }
281
+ catch (err) {
282
+ specVs.push({
283
+ rule: Rules.guardAPI,
284
+ file: p,
285
+ line: 0,
286
+ sectionID: "",
287
+ expected: "",
288
+ actual: "",
289
+ message: errMsg(err),
290
+ });
291
+ continue;
292
+ }
293
+ specs.push(s);
294
+ }
295
+ return { specs, specVs };
296
+ }
297
+ /**
298
+ * checkAPIToken validates one {{api:…}} token against the loaded specs. Returns
299
+ * one violation when: malformed, operation missing, or qualifier not satisfied.
300
+ */
301
+ function checkAPIToken(file, loc, specs) {
302
+ let ref;
303
+ try {
304
+ ref = parseApiRef(loc.token);
305
+ }
306
+ catch (err) {
307
+ if (err instanceof ApiMalformedRefError) {
308
+ return [
309
+ {
310
+ rule: Rules.guardAPI,
311
+ file,
312
+ line: loc.line,
313
+ sectionID: "",
314
+ expected: "",
315
+ actual: loc.token,
316
+ message: "malformed api ref: " + err.message,
317
+ },
318
+ ];
319
+ }
320
+ throw err;
321
+ }
322
+ // Check that at least one spec has the operation.
323
+ let opFound = false;
324
+ for (const s of specs) {
325
+ if (s.hasOperation(ref.Method, ref.Path)) {
326
+ opFound = true;
327
+ break;
328
+ }
329
+ }
330
+ if (!opFound) {
331
+ return [
332
+ {
333
+ rule: Rules.guardAPI,
334
+ file,
335
+ line: loc.line,
336
+ sectionID: "",
337
+ expected: "",
338
+ actual: loc.token,
339
+ message: `operation ${ref.Method} ${ref.Path} not found in spec`,
340
+ },
341
+ ];
342
+ }
343
+ // No qualifier — operation existence is sufficient.
344
+ if (ref.Qualifier === null) {
345
+ return [];
346
+ }
347
+ // Qualifier dispatch: pass if ANY spec satisfies it.
348
+ const name = ref.Qualifier.Path.join(".");
349
+ let satisfied = false;
350
+ for (const s of specs) {
351
+ if (qualifierSatisfied(s, ref, name)) {
352
+ satisfied = true;
353
+ break;
354
+ }
355
+ }
356
+ if (!satisfied) {
357
+ return [
358
+ {
359
+ rule: Rules.guardAPI,
360
+ file,
361
+ line: loc.line,
362
+ sectionID: "",
363
+ expected: "",
364
+ actual: loc.token,
365
+ message: `${ref.Qualifier.Kind} ${name} not found on ${ref.Method} ${ref.Path}`,
366
+ },
367
+ ];
368
+ }
369
+ return [];
370
+ }
371
+ /** qualifierSatisfied checks whether the given spec satisfies the qualifier on ref. */
372
+ function qualifierSatisfied(s, ref, name) {
373
+ const q = ref.Qualifier;
374
+ if (q === null) {
375
+ return false;
376
+ }
377
+ switch (q.Kind) {
378
+ case "param":
379
+ return s.hasParam(ref.Method, ref.Path, name);
380
+ case "header":
381
+ return s.hasHeader(ref.Method, ref.Path, name);
382
+ case "body":
383
+ return s.hasBodyField(ref.Method, ref.Path, q.Path);
384
+ case "response":
385
+ return s.hasResponseField(ref.Method, ref.Path, q.Path);
386
+ }
387
+ return false;
388
+ }
389
+ function errMsg(err) {
390
+ return err instanceof Error ? err.message : String(err);
391
+ }