docspack 0.0.1 → 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 (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/bin/docspack.js +25 -0
  4. package/dist/build.d.ts +31 -0
  5. package/dist/build.d.ts.map +1 -0
  6. package/dist/build.js +435 -0
  7. package/dist/build.js.map +1 -0
  8. package/dist/cli.d.ts +3 -0
  9. package/dist/cli.d.ts.map +1 -0
  10. package/dist/cli.js +763 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/config.d.ts +41 -0
  13. package/dist/config.d.ts.map +1 -0
  14. package/dist/config.js +118 -0
  15. package/dist/config.js.map +1 -0
  16. package/dist/db.d.ts +60 -0
  17. package/dist/db.d.ts.map +1 -0
  18. package/dist/db.js +204 -0
  19. package/dist/db.js.map +1 -0
  20. package/dist/discovery.d.ts +31 -0
  21. package/dist/discovery.d.ts.map +1 -0
  22. package/dist/discovery.js +126 -0
  23. package/dist/discovery.js.map +1 -0
  24. package/dist/doctor.d.ts +25 -0
  25. package/dist/doctor.d.ts.map +1 -0
  26. package/dist/doctor.js +276 -0
  27. package/dist/doctor.js.map +1 -0
  28. package/dist/document.d.ts +13 -0
  29. package/dist/document.d.ts.map +1 -0
  30. package/dist/document.js +47 -0
  31. package/dist/document.js.map +1 -0
  32. package/dist/errors.d.ts +9 -0
  33. package/dist/errors.d.ts.map +1 -0
  34. package/dist/errors.js +10 -0
  35. package/dist/errors.js.map +1 -0
  36. package/dist/exports.d.ts +20 -0
  37. package/dist/exports.d.ts.map +1 -0
  38. package/dist/exports.js +100 -0
  39. package/dist/exports.js.map +1 -0
  40. package/dist/feedback.d.ts +68 -0
  41. package/dist/feedback.d.ts.map +1 -0
  42. package/dist/feedback.js +0 -0
  43. package/dist/feedback.js.map +1 -0
  44. package/dist/html.d.ts +4 -0
  45. package/dist/html.d.ts.map +1 -0
  46. package/dist/html.js +23 -0
  47. package/dist/html.js.map +1 -0
  48. package/dist/http.d.ts +30 -0
  49. package/dist/http.d.ts.map +1 -0
  50. package/dist/http.js +144 -0
  51. package/dist/http.js.map +1 -0
  52. package/dist/index.d.ts +25 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.js +25 -0
  55. package/dist/index.js.map +1 -0
  56. package/dist/init/detect.d.ts +16 -0
  57. package/dist/init/detect.d.ts.map +1 -0
  58. package/dist/init/detect.js +120 -0
  59. package/dist/init/detect.js.map +1 -0
  60. package/dist/init/plan.d.ts +43 -0
  61. package/dist/init/plan.d.ts.map +1 -0
  62. package/dist/init/plan.js +145 -0
  63. package/dist/init/plan.js.map +1 -0
  64. package/dist/init/run.d.ts +28 -0
  65. package/dist/init/run.d.ts.map +1 -0
  66. package/dist/init/run.js +96 -0
  67. package/dist/init/run.js.map +1 -0
  68. package/dist/init/templates.d.ts +24 -0
  69. package/dist/init/templates.d.ts.map +1 -0
  70. package/dist/init/templates.js +181 -0
  71. package/dist/init/templates.js.map +1 -0
  72. package/dist/init/write.d.ts +20 -0
  73. package/dist/init/write.d.ts.map +1 -0
  74. package/dist/init/write.js +56 -0
  75. package/dist/init/write.js.map +1 -0
  76. package/dist/kinds.d.ts +14 -0
  77. package/dist/kinds.d.ts.map +1 -0
  78. package/dist/kinds.js +15 -0
  79. package/dist/kinds.js.map +1 -0
  80. package/dist/llms-txt.d.ts +25 -0
  81. package/dist/llms-txt.d.ts.map +1 -0
  82. package/dist/llms-txt.js +94 -0
  83. package/dist/llms-txt.js.map +1 -0
  84. package/dist/mcp.d.ts +15 -0
  85. package/dist/mcp.d.ts.map +1 -0
  86. package/dist/mcp.js +158 -0
  87. package/dist/mcp.js.map +1 -0
  88. package/dist/preview.d.ts +18 -0
  89. package/dist/preview.d.ts.map +1 -0
  90. package/dist/preview.js +72 -0
  91. package/dist/preview.js.map +1 -0
  92. package/dist/prompt.d.ts +27 -0
  93. package/dist/prompt.d.ts.map +1 -0
  94. package/dist/prompt.js +79 -0
  95. package/dist/prompt.js.map +1 -0
  96. package/dist/search.d.ts +41 -0
  97. package/dist/search.d.ts.map +1 -0
  98. package/dist/search.js +60 -0
  99. package/dist/search.js.map +1 -0
  100. package/dist/snippet.d.ts +20 -0
  101. package/dist/snippet.d.ts.map +1 -0
  102. package/dist/snippet.js +29 -0
  103. package/dist/snippet.js.map +1 -0
  104. package/dist/spec.d.ts +38 -0
  105. package/dist/spec.d.ts.map +1 -0
  106. package/dist/spec.js +105 -0
  107. package/dist/spec.js.map +1 -0
  108. package/dist/style.d.ts +33 -0
  109. package/dist/style.d.ts.map +1 -0
  110. package/dist/style.js +94 -0
  111. package/dist/style.js.map +1 -0
  112. package/dist/submit.d.ts +61 -0
  113. package/dist/submit.d.ts.map +1 -0
  114. package/dist/submit.js +111 -0
  115. package/dist/submit.js.map +1 -0
  116. package/dist/sync.d.ts +29 -0
  117. package/dist/sync.d.ts.map +1 -0
  118. package/dist/sync.js +73 -0
  119. package/dist/sync.js.map +1 -0
  120. package/dist/verify.d.ts +44 -0
  121. package/dist/verify.d.ts.map +1 -0
  122. package/dist/verify.js +291 -0
  123. package/dist/verify.js.map +1 -0
  124. package/package.json +60 -5
  125. package/src/build.ts +572 -0
  126. package/src/cli.ts +883 -0
  127. package/src/config.ts +158 -0
  128. package/src/db.ts +261 -0
  129. package/src/discovery.ts +161 -0
  130. package/src/doctor.ts +344 -0
  131. package/src/document.ts +59 -0
  132. package/src/errors.ts +10 -0
  133. package/src/exports.ts +120 -0
  134. package/src/feedback.ts +0 -0
  135. package/src/html.ts +24 -0
  136. package/src/http.ts +190 -0
  137. package/src/index.ts +132 -0
  138. package/src/init/detect.ts +142 -0
  139. package/src/init/plan.ts +215 -0
  140. package/src/init/run.ts +142 -0
  141. package/src/init/templates.ts +200 -0
  142. package/src/init/write.ts +83 -0
  143. package/src/kinds.ts +17 -0
  144. package/src/llms-txt.ts +116 -0
  145. package/src/mcp.ts +196 -0
  146. package/src/preview.ts +98 -0
  147. package/src/prompt.ts +103 -0
  148. package/src/search.ts +96 -0
  149. package/src/snippet.ts +30 -0
  150. package/src/spec.ts +138 -0
  151. package/src/style.ts +111 -0
  152. package/src/submit.ts +189 -0
  153. package/src/sync.ts +112 -0
  154. package/src/verify.ts +355 -0
  155. package/bin/cli.js +0 -2
package/src/verify.ts ADDED
@@ -0,0 +1,355 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { readBuildConfig } from "./config.js";
4
+ import { discoverPackages, resolvePackageDir } from "./discovery.js";
5
+ import { DocspackError } from "./errors.js";
6
+ import { type ExportSurface, readExportSurface } from "./exports.js";
7
+ import { chunkId, LLMS_DIR, MANIFEST_FILE, type PackageManifest, parseManifest } from "./spec.js";
8
+
9
+ export interface DriftFinding {
10
+ readonly chunkId: string;
11
+ readonly file: string;
12
+ /** The entity as the documentation wrote it, e.g. `client.setKey`. */
13
+ readonly entity: string;
14
+ /** The part that was checked: the last segment. */
15
+ readonly symbol: string;
16
+ /** The declared name it most closely resembles. */
17
+ readonly suggestion: string;
18
+ }
19
+
20
+ export type VerifyStatus = "verified" | "skipped";
21
+
22
+ export interface VerifiedPackage {
23
+ readonly id: string;
24
+ readonly status: VerifyStatus;
25
+ /** Library the package declares it documents. */
26
+ readonly documents?: string;
27
+ /** Why nothing was checked, when status is `skipped`. */
28
+ readonly reason?: string;
29
+ readonly checked: number;
30
+ /** Entities that matched a declared name. */
31
+ readonly matched: number;
32
+ /** Entities absent from the type surface with nothing similar — ignored, not reported. */
33
+ readonly unrecognised: number;
34
+ readonly findings: readonly DriftFinding[];
35
+ }
36
+
37
+ export interface VerifyReport {
38
+ readonly packages: readonly VerifiedPackage[];
39
+ readonly ok: boolean;
40
+ }
41
+
42
+ export interface VerifyOptions {
43
+ readonly cwd: string;
44
+ /** Verify one package directory instead of the project's installed docs packages. */
45
+ readonly packageDir?: string;
46
+ }
47
+
48
+ /** Below this length a name is too generic to reason about. */
49
+ const MIN_SYMBOL = 4;
50
+
51
+ /**
52
+ * Names that appear in documentation everywhere and belong to the language or the platform
53
+ * rather than to any library.
54
+ */
55
+ const UNIVERSAL = new Set([
56
+ "console",
57
+ "log",
58
+ "error",
59
+ "warn",
60
+ "then",
61
+ "catch",
62
+ "finally",
63
+ "await",
64
+ "async",
65
+ "json",
66
+ "stringify",
67
+ "parse",
68
+ "length",
69
+ "process",
70
+ "env",
71
+ "module",
72
+ "exports",
73
+ "require",
74
+ "import",
75
+ "default",
76
+ "window",
77
+ "document",
78
+ "fetch",
79
+ "response",
80
+ "request",
81
+ ]);
82
+
83
+ /**
84
+ * Receivers that belong to the language or the runtime. `document.cookie` and `stream.Duplex`
85
+ * are documentation about the platform, and the library they appear in did not declare them.
86
+ */
87
+ const PLATFORM = new Set([
88
+ "array",
89
+ "buffer",
90
+ "console",
91
+ "crypto",
92
+ "document",
93
+ "fs",
94
+ "global",
95
+ "globalthis",
96
+ "intl",
97
+ "json",
98
+ "math",
99
+ "navigator",
100
+ "number",
101
+ "object",
102
+ "os",
103
+ "path",
104
+ "process",
105
+ "promise",
106
+ "reflect",
107
+ "regexp",
108
+ "stream",
109
+ "string",
110
+ "symbol",
111
+ "url",
112
+ "window",
113
+ ]);
114
+
115
+ /** `wrangler.jsonc` is a filename that survives the entity regex, not a member access. */
116
+ const EXTENSIONS = new Set([
117
+ "cjs",
118
+ "css",
119
+ "html",
120
+ "js",
121
+ "json",
122
+ "jsonc",
123
+ "lock",
124
+ "md",
125
+ "mdx",
126
+ "mjs",
127
+ "toml",
128
+ "ts",
129
+ "tsx",
130
+ "txt",
131
+ "yaml",
132
+ "yml",
133
+ ]);
134
+
135
+ /**
136
+ * Checks the identifiers documentation names against what the documented library declares.
137
+ *
138
+ * A finding is only produced when a name is absent *and* a very similar name exists — that is
139
+ * the signature of a rename. An absent name with nothing like it is far more likely to come
140
+ * from an example about some other library, so it is counted and ignored rather than reported.
141
+ * Precision matters more than recall here: a check that cries wolf is worse than no check.
142
+ */
143
+ export async function verifyProject(options: VerifyOptions): Promise<VerifyReport> {
144
+ const packages =
145
+ options.packageDir === undefined
146
+ ? await verifyInstalled(options.cwd)
147
+ : [await verifyOne(options.cwd, options.packageDir)];
148
+
149
+ return { packages, ok: packages.every((pkg) => pkg.findings.length === 0) };
150
+ }
151
+
152
+ async function verifyInstalled(cwd: string): Promise<VerifiedPackage[]> {
153
+ const { packages } = await discoverPackages(cwd);
154
+ const results: VerifiedPackage[] = [];
155
+
156
+ for (const pkg of packages) {
157
+ results.push(await verifyPackage(cwd, pkg.id, pkg.dir, pkg.manifest));
158
+ }
159
+ return results;
160
+ }
161
+
162
+ async function verifyOne(cwd: string, packageDir: string): Promise<VerifiedPackage> {
163
+ let manifest: PackageManifest;
164
+ try {
165
+ const raw = await readFile(join(packageDir, LLMS_DIR, MANIFEST_FILE), "utf8");
166
+ manifest = parseManifest(JSON.parse(raw), `${LLMS_DIR}/${MANIFEST_FILE}`);
167
+ } catch {
168
+ throw new DocspackError(`No ${LLMS_DIR}/${MANIFEST_FILE} in ${packageDir}`, {
169
+ hint: "Run `docspack build` first.",
170
+ });
171
+ }
172
+
173
+ return verifyPackage(cwd, `${manifest.name}@${manifest.version}`, packageDir, manifest);
174
+ }
175
+
176
+ async function verifyPackage(
177
+ cwd: string,
178
+ id: string,
179
+ packageDir: string,
180
+ manifest: PackageManifest,
181
+ ): Promise<VerifiedPackage> {
182
+ const empty = { id, checked: 0, matched: 0, unrecognised: 0, findings: [] };
183
+
184
+ const { documents } = await readBuildConfig(packageDir);
185
+ if (documents === undefined) {
186
+ return {
187
+ ...empty,
188
+ status: "skipped",
189
+ reason: `declares no "docspack.documents", so there is nothing to check against`,
190
+ };
191
+ }
192
+
193
+ const libraryDir =
194
+ (await resolvePackageDir(documents, packageDir)) ?? (await resolvePackageDir(documents, cwd));
195
+ if (libraryDir === undefined) {
196
+ return { ...empty, status: "skipped", documents, reason: `${documents} is not installed` };
197
+ }
198
+
199
+ const surface = await readExportSurface(libraryDir);
200
+ if (surface === undefined || surface.names.size === 0) {
201
+ return {
202
+ ...empty,
203
+ status: "skipped",
204
+ documents,
205
+ reason: `${documents} ships no type declarations to check against`,
206
+ };
207
+ }
208
+
209
+ const declared = new Set([...surface.names].map(normalize));
210
+ const findings: DriftFinding[] = [];
211
+ let checked = 0;
212
+ let matched = 0;
213
+ let unrecognised = 0;
214
+
215
+ for (const chunk of manifest.chunks) {
216
+ for (const entity of chunk.entities) {
217
+ const symbol = checkableSymbol(entity);
218
+ if (symbol === undefined) continue;
219
+
220
+ checked += 1;
221
+ // Case and leading underscores differ between documentation and declarations often
222
+ // enough that treating them as drift is wrong.
223
+ if (declared.has(normalize(symbol))) {
224
+ matched += 1;
225
+ continue;
226
+ }
227
+
228
+ const suggestion = nearestName(symbol, surface);
229
+ if (suggestion === undefined) {
230
+ unrecognised += 1;
231
+ continue;
232
+ }
233
+
234
+ findings.push({
235
+ chunkId: chunkId(id, chunk.id),
236
+ file: chunk.file,
237
+ entity,
238
+ symbol,
239
+ suggestion,
240
+ });
241
+ }
242
+ }
243
+
244
+ return { id, status: "verified", documents, checked, matched, unrecognised, findings };
245
+ }
246
+
247
+ /**
248
+ * The name worth checking in an entity like `client.setKey()`, or nothing when the entity is
249
+ * not about the library at all: a filename, a platform API, or a name too generic to place.
250
+ */
251
+ function checkableSymbol(entity: string): string | undefined {
252
+ const parts = entity.replace(/\(\)$/, "").split(".");
253
+ const symbol = parts[parts.length - 1] ?? entity;
254
+ const receiver = parts.length > 1 ? (parts[parts.length - 2] ?? "") : "";
255
+
256
+ if (symbol.length < MIN_SYMBOL) return undefined;
257
+ if (EXTENSIONS.has(symbol.toLowerCase())) return undefined;
258
+ if (UNIVERSAL.has(symbol.toLowerCase())) return undefined;
259
+ if (PLATFORM.has(receiver.toLowerCase())) return undefined;
260
+ return symbol;
261
+ }
262
+
263
+ /** Case and leading underscores are styling, not identity, when comparing two names. */
264
+ function normalize(name: string): string {
265
+ return name.replace(/^_+/, "").toLowerCase();
266
+ }
267
+
268
+ /**
269
+ * The declared name a symbol was most likely renamed from, or nothing if no declared name
270
+ * resembles it closely enough to say so.
271
+ *
272
+ * Two shapes count, and only two. A near-identical spelling of the same leading word, which
273
+ * catches typos and small edits; and a qualifier inserted between the same first and last word
274
+ * — `setKey` against `setApiKey` — which is what most real renames look like. Everything else
275
+ * is left alone. In particular a differing first word is never a match: `includeLanguages` and
276
+ * `excludeLanguages` are two edits apart and mean opposite things.
277
+ */
278
+ function nearestName(symbol: string, surface: ExportSurface): string | undefined {
279
+ const lower = normalize(symbol);
280
+ const words = camelWords(symbol);
281
+ let best: string | undefined;
282
+ let bestDistance = Number.POSITIVE_INFINITY;
283
+
284
+ for (const candidate of surface.names) {
285
+ if (candidate.length < MIN_SYMBOL) continue;
286
+ const candidateWords = camelWords(candidate);
287
+ if (isRequalified(words, candidateWords)) return candidate;
288
+
289
+ if (candidateWords[0] !== words[0]) continue;
290
+ const other = normalize(candidate);
291
+ if (Math.abs(other.length - lower.length) > 4) continue;
292
+
293
+ const distance = editDistance(lower, other, 3);
294
+ if (distance < bestDistance) {
295
+ bestDistance = distance;
296
+ best = candidate;
297
+ }
298
+ }
299
+
300
+ if (best === undefined || bestDistance > 2) return undefined;
301
+ // A two-character difference between two short names is coincidence more often than a rename.
302
+ if (bestDistance === 2 && symbol.length < 8) return undefined;
303
+ return best;
304
+ }
305
+
306
+ /** `setApiKey` becomes `["set", "api", "key"]`. */
307
+ function camelWords(name: string): string[] {
308
+ return name
309
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
310
+ .split(/[\s_$-]+/)
311
+ .filter((word) => word.length > 0)
312
+ .map((word) => word.toLowerCase());
313
+ }
314
+
315
+ /**
316
+ * True when the two names are the same multi-word name with a qualifier inserted, in either
317
+ * direction. Requiring the first and last word to match, and both names to have at least two
318
+ * words, is what keeps this from firing on every method that happens to start with `get`.
319
+ */
320
+ function isRequalified(a: readonly string[], b: readonly string[]): boolean {
321
+ const [shorter, longer] = a.length <= b.length ? [a, b] : [b, a];
322
+ if (shorter.length < 2 || longer.length <= shorter.length) return false;
323
+ if (shorter[0] !== longer[0]) return false;
324
+ if (shorter[shorter.length - 1] !== longer[longer.length - 1]) return false;
325
+
326
+ let index = 0;
327
+ for (const word of longer) {
328
+ if (word === shorter[index]) index += 1;
329
+ }
330
+ return index === shorter.length;
331
+ }
332
+
333
+ /** Levenshtein distance, abandoned once it passes `limit`. */
334
+ function editDistance(a: string, b: string, limit: number): number {
335
+ if (Math.abs(a.length - b.length) > limit) return limit + 1;
336
+
337
+ let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
338
+ for (let i = 1; i <= a.length; i += 1) {
339
+ const current = [i];
340
+ let rowBest = i;
341
+ for (let j = 1; j <= b.length; j += 1) {
342
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
343
+ const value = Math.min(
344
+ (current[j - 1] ?? 0) + 1,
345
+ (previous[j] ?? 0) + 1,
346
+ (previous[j - 1] ?? 0) + cost,
347
+ );
348
+ current.push(value);
349
+ rowBest = Math.min(rowBest, value);
350
+ }
351
+ if (rowBest > limit) return limit + 1;
352
+ previous = current;
353
+ }
354
+ return previous[b.length] ?? limit + 1;
355
+ }
package/bin/cli.js DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- console.log("docspack is currently in development.");