docspack 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/dist/build.d.ts +21 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +194 -31
- package/dist/build.js.map +1 -1
- package/dist/cli.js +74 -131
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +7 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +20 -1
- package/dist/config.js.map +1 -1
- package/dist/db.d.ts +8 -4
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +13 -7
- package/dist/db.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +47 -4
- package/dist/doctor.js.map +1 -1
- package/dist/document.d.ts +2 -0
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +7 -3
- package/dist/document.js.map +1 -1
- package/dist/eval.d.ts +52 -0
- package/dist/eval.d.ts.map +1 -0
- package/dist/eval.js +101 -0
- package/dist/eval.js.map +1 -0
- package/dist/help.d.ts +31 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/help.js +342 -0
- package/dist/help.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +4 -2
- package/dist/preview.js.map +1 -1
- package/dist/search.d.ts +7 -0
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +33 -3
- package/dist/search.js.map +1 -1
- package/dist/spec.d.ts +27 -1
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js +53 -5
- package/dist/spec.js.map +1 -1
- package/dist/stopwords.d.ts +14 -0
- package/dist/stopwords.d.ts.map +1 -0
- package/dist/stopwords.js +121 -0
- package/dist/stopwords.js.map +1 -0
- package/dist/sync.js +1 -1
- package/dist/sync.js.map +1 -1
- package/dist/verify.d.ts +8 -2
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +84 -23
- package/dist/verify.js.map +1 -1
- package/package.json +1 -1
- package/src/build.ts +261 -33
- package/src/cli.ts +91 -138
- package/src/config.ts +35 -5
- package/src/db.ts +13 -7
- package/src/doctor.ts +49 -5
- package/src/document.ts +13 -4
- package/src/eval.ts +149 -0
- package/src/help.ts +382 -0
- package/src/index.ts +20 -0
- package/src/preview.ts +4 -1
- package/src/search.ts +41 -3
- package/src/spec.ts +84 -6
- package/src/stopwords.ts +120 -0
- package/src/sync.ts +1 -1
- package/src/verify.ts +108 -28
package/src/search.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SearchHit, Store } from "./db.js";
|
|
2
2
|
import { discoverPackages } from "./discovery.js";
|
|
3
|
-
import { isCommunityPackage } from "./spec.js";
|
|
3
|
+
import { chunkId, formatDocumentedLibrary, isCommunityPackage } from "./spec.js";
|
|
4
4
|
|
|
5
5
|
/** Ceiling on how much context one query may return, per the blueprint's context-exhaustion rule. */
|
|
6
6
|
export const DEFAULT_MAX_TOKENS = 3000;
|
|
@@ -25,6 +25,13 @@ export interface QueryHit extends SearchHit {
|
|
|
25
25
|
readonly name: string;
|
|
26
26
|
readonly version: string;
|
|
27
27
|
readonly trusted: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Libraries this chunk documents, from its manifest. A docs package version cannot always
|
|
30
|
+
* imply this — a monorepo documents many libraries at many versions from one surface — so the
|
|
31
|
+
* answer states it rather than leaving the reader to infer it from the package name. A chunk
|
|
32
|
+
* that names its own libraries answers for those; otherwise the package's list stands.
|
|
33
|
+
*/
|
|
34
|
+
readonly documents?: readonly string[];
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
export interface QueryResult {
|
|
@@ -48,6 +55,22 @@ export async function queryDocs(options: QueryOptions): Promise<QueryResult> {
|
|
|
48
55
|
.filter((pkg) => !options.store.hasPackage(pkg.id))
|
|
49
56
|
.map((pkg) => pkg.id);
|
|
50
57
|
|
|
58
|
+
// Read from the installed manifests rather than the index: the store is a cache of chunk text,
|
|
59
|
+
// and adding a column to it would make every existing store need a rebuild to answer this.
|
|
60
|
+
// Keyed by package id and by chunk id in the same map, because a chunk id already carries its
|
|
61
|
+
// package: `@acme/docspack@1.4.0/api-auth` cannot collide with `@acme/docspack@1.4.0`.
|
|
62
|
+
const documented = new Map<string, readonly string[]>();
|
|
63
|
+
for (const pkg of installed ?? []) {
|
|
64
|
+
const documents = pkg.manifest.documents;
|
|
65
|
+
if (documents !== undefined && documents.length > 0) {
|
|
66
|
+
documented.set(pkg.id, documents.map(formatDocumentedLibrary));
|
|
67
|
+
}
|
|
68
|
+
for (const chunk of pkg.manifest.chunks) {
|
|
69
|
+
if (chunk.documents === undefined || chunk.documents.length === 0) continue;
|
|
70
|
+
documented.set(chunkId(pkg.id, chunk.id), chunk.documents.map(formatDocumentedLibrary));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
51
74
|
const hits = options.store
|
|
52
75
|
.search(options.query, {
|
|
53
76
|
...(packageIds === undefined ? {} : { packageIds }),
|
|
@@ -59,7 +82,15 @@ export async function queryDocs(options: QueryOptions): Promise<QueryResult> {
|
|
|
59
82
|
})
|
|
60
83
|
.map((hit): QueryHit => {
|
|
61
84
|
const { name, version } = splitPackageId(hit.packageId);
|
|
62
|
-
|
|
85
|
+
// The chunk's own libraries win: they are the narrower, and therefore the truer, claim.
|
|
86
|
+
const documents = documented.get(hit.chunkId) ?? documented.get(hit.packageId);
|
|
87
|
+
return {
|
|
88
|
+
...hit,
|
|
89
|
+
name,
|
|
90
|
+
version,
|
|
91
|
+
trusted: !isCommunityPackage(name),
|
|
92
|
+
...(documents === undefined ? {} : { documents }),
|
|
93
|
+
};
|
|
63
94
|
});
|
|
64
95
|
|
|
65
96
|
return {
|
|
@@ -90,9 +121,16 @@ export function renderAnswer(result: QueryResult, query: string): string {
|
|
|
90
121
|
|
|
91
122
|
const sections = result.hits.map((hit) => {
|
|
92
123
|
const trust = hit.trusted ? "" : " (community)";
|
|
124
|
+
// Which release the chunk describes, on the line that already carries provenance. An agent
|
|
125
|
+
// otherwise has to assume the docs package version is the library version, which a
|
|
126
|
+
// repository publishing eighteen packages from one docs surface cannot make true.
|
|
127
|
+
const describes =
|
|
128
|
+
hit.documents === undefined || hit.documents.length === 0
|
|
129
|
+
? ""
|
|
130
|
+
: ` · documents ${hit.documents.join(", ")}`;
|
|
93
131
|
return [
|
|
94
132
|
`## ${hit.chunkId}${trust}`,
|
|
95
|
-
`Source: ${hit.name}@${hit.version} — ${hit.filePath}`,
|
|
133
|
+
`Source: ${hit.name}@${hit.version} — ${hit.filePath}${describes}`,
|
|
96
134
|
"",
|
|
97
135
|
hit.content,
|
|
98
136
|
].join("\n");
|
package/src/spec.ts
CHANGED
|
@@ -14,14 +14,33 @@ export interface ChunkSpec {
|
|
|
14
14
|
readonly id: string;
|
|
15
15
|
/** Path relative to the package's `.llms/` directory. */
|
|
16
16
|
readonly file: string;
|
|
17
|
-
|
|
17
|
+
/** Absent means "estimate it". Never 0: an empty chunk is a different problem. */
|
|
18
|
+
readonly tokens?: number;
|
|
18
19
|
readonly tags: readonly string[];
|
|
19
20
|
readonly entities: readonly string[];
|
|
21
|
+
/**
|
|
22
|
+
* What this chunk describes, when that is narrower than what the package describes. A
|
|
23
|
+
* repository publishing eighteen libraries at four versions from one documentation surface
|
|
24
|
+
* has a package-level answer that is true of the pack and useless about any one chunk.
|
|
25
|
+
*/
|
|
26
|
+
readonly documents?: readonly DocumentedLibrary[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A library release the package documents, e.g. `acme` at `1.4.0`. */
|
|
30
|
+
export interface DocumentedLibrary {
|
|
31
|
+
readonly name: string;
|
|
32
|
+
/** A version or a range, as the author wrote it. Absent when the docs are not release-scoped. */
|
|
33
|
+
readonly version?: string;
|
|
20
34
|
}
|
|
21
35
|
|
|
22
36
|
export interface PackageManifest {
|
|
23
37
|
readonly name: string;
|
|
24
38
|
readonly version: string;
|
|
39
|
+
/**
|
|
40
|
+
* What the package documents, which the package's own version cannot always say: a monorepo
|
|
41
|
+
* publishes many libraries at many versions from one documentation surface.
|
|
42
|
+
*/
|
|
43
|
+
readonly documents?: readonly DocumentedLibrary[];
|
|
25
44
|
readonly chunks: readonly ChunkSpec[];
|
|
26
45
|
}
|
|
27
46
|
|
|
@@ -50,6 +69,21 @@ export function chunkId(pkgId: string, chunk: string): string {
|
|
|
50
69
|
return `${pkgId}/${chunk}`;
|
|
51
70
|
}
|
|
52
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Reads `acme`, `acme@1.4.0` or `@acme/sdk@^1.4.0` as a library and an optional version. The
|
|
74
|
+
* separator is the last `@`, since a scope puts one at the start of the name as well.
|
|
75
|
+
*/
|
|
76
|
+
export function parseDocumentedLibrary(spec: string): DocumentedLibrary {
|
|
77
|
+
const at = spec.lastIndexOf("@");
|
|
78
|
+
if (at <= 0) return { name: spec };
|
|
79
|
+
return { name: spec.slice(0, at), version: spec.slice(at + 1) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** `acme@1.4.0`, or just the name when no version is scoped. The form an answer is labelled with. */
|
|
83
|
+
export function formatDocumentedLibrary(library: DocumentedLibrary): string {
|
|
84
|
+
return library.version === undefined ? library.name : `${library.name}@${library.version}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
53
87
|
/**
|
|
54
88
|
* Resolves a manifest `file` entry inside the package's `.llms/` directory, refusing anything
|
|
55
89
|
* that escapes it. Manifests are third-party input, so this is a security boundary, not a
|
|
@@ -88,6 +122,7 @@ export function parseManifest(raw: unknown, where: string): PackageManifest {
|
|
|
88
122
|
if (typeof version !== "string" || version.length === 0) fail('missing string field "version"');
|
|
89
123
|
if (!Array.isArray(root.chunks)) fail('missing "chunks" array');
|
|
90
124
|
|
|
125
|
+
const packageDocuments = parseDocuments(root.documents, fail);
|
|
91
126
|
const seen = new Set<string>();
|
|
92
127
|
const chunks = (root.chunks as unknown[]).map((entry, index): ChunkSpec => {
|
|
93
128
|
if (typeof entry !== "object" || entry === null)
|
|
@@ -106,21 +141,48 @@ export function parseManifest(raw: unknown, where: string): PackageManifest {
|
|
|
106
141
|
return fail(`chunk "${id}" is missing its "file"`);
|
|
107
142
|
}
|
|
108
143
|
|
|
144
|
+
// 0 is refused rather than read as "estimate it": the schema accepts it as a deliberate
|
|
145
|
+
// count, the reader would treat it as absent, and no chunk is genuinely 0 tokens.
|
|
109
146
|
const tokens = chunk.tokens;
|
|
110
|
-
if (tokens !== undefined && (!Number.isInteger(tokens) || (tokens as number) <
|
|
111
|
-
return fail(`chunk "${id}" has an invalid "tokens" value`);
|
|
147
|
+
if (tokens !== undefined && (!Number.isInteger(tokens) || (tokens as number) < 1)) {
|
|
148
|
+
return fail(`chunk "${id}" has an invalid "tokens" value; omit it to have it estimated`);
|
|
112
149
|
}
|
|
113
150
|
|
|
151
|
+
const documents = parseDocuments(chunk.documents, fail);
|
|
152
|
+
|
|
114
153
|
return {
|
|
115
154
|
id,
|
|
116
155
|
file,
|
|
117
|
-
|
|
156
|
+
...(typeof tokens === "number" ? { tokens } : {}),
|
|
118
157
|
tags: stringArray(chunk.tags, `chunk "${id}" field "tags"`, fail),
|
|
119
158
|
entities: stringArray(chunk.entities, `chunk "${id}" field "entities"`, fail),
|
|
159
|
+
...(documents === undefined ? {} : { documents }),
|
|
120
160
|
};
|
|
121
161
|
});
|
|
122
162
|
|
|
123
|
-
return {
|
|
163
|
+
return {
|
|
164
|
+
name,
|
|
165
|
+
version,
|
|
166
|
+
...(packageDocuments === undefined ? {} : { documents: packageDocuments }),
|
|
167
|
+
chunks,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* `["acme@1.4.0", "@acme/sdk@^2"]`. An array even for one library, because that is what the
|
|
173
|
+
* schema declares — a reader that accepts a shape the validator rejects is the same defect as a
|
|
174
|
+
* validator that accepts a value the reader reinterprets.
|
|
175
|
+
*/
|
|
176
|
+
function parseDocuments(
|
|
177
|
+
value: unknown,
|
|
178
|
+
fail: (message: string) => never,
|
|
179
|
+
): readonly DocumentedLibrary[] | undefined {
|
|
180
|
+
if (value === undefined) return undefined;
|
|
181
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry === "")) {
|
|
182
|
+
fail('"documents" must be an array of package names, optionally with a version');
|
|
183
|
+
}
|
|
184
|
+
const specs = value as string[];
|
|
185
|
+
return specs.length === 0 ? undefined : specs.map(parseDocumentedLibrary);
|
|
124
186
|
}
|
|
125
187
|
|
|
126
188
|
function stringArray(
|
|
@@ -135,6 +197,22 @@ function stringArray(
|
|
|
135
197
|
return value as string[];
|
|
136
198
|
}
|
|
137
199
|
|
|
200
|
+
/** Writes the manifest, with `documents` back in the string form the schema declares. */
|
|
138
201
|
export function serializeManifest(manifest: PackageManifest): string {
|
|
139
|
-
|
|
202
|
+
const documents = manifest.documents ?? [];
|
|
203
|
+
return `${JSON.stringify(
|
|
204
|
+
{
|
|
205
|
+
$schema: SCHEMA_URL,
|
|
206
|
+
name: manifest.name,
|
|
207
|
+
version: manifest.version,
|
|
208
|
+
...(documents.length === 0 ? {} : { documents: documents.map(formatDocumentedLibrary) }),
|
|
209
|
+
chunks: manifest.chunks.map((chunk) =>
|
|
210
|
+
chunk.documents === undefined
|
|
211
|
+
? chunk
|
|
212
|
+
: { ...chunk, documents: chunk.documents.map(formatDocumentedLibrary) },
|
|
213
|
+
),
|
|
214
|
+
},
|
|
215
|
+
null,
|
|
216
|
+
2,
|
|
217
|
+
)}\n`;
|
|
140
218
|
}
|
package/src/stopwords.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Words that carry no discrimination in a documentation corpus.
|
|
3
|
+
*
|
|
4
|
+
* Heading words become tags, and tags are weighted 3× against prose in the ranking. A section
|
|
5
|
+
* titled "How to use it" therefore carried `how`, `to` and `it` as high-weight terms, and every
|
|
6
|
+
* question beginning "how do I…" matched it — ahead of the chunk that answers the question.
|
|
7
|
+
*
|
|
8
|
+
* The list is closed-class only: articles, pronouns, prepositions, conjunctions, auxiliaries,
|
|
9
|
+
* modals, demonstratives and question words. No content word is in it, however generic it
|
|
10
|
+
* looks. `use`, `add`, `get` and `set` all name real APIs, and a list that grows past the
|
|
11
|
+
* function words starts deciding which of an author's own words count.
|
|
12
|
+
*/
|
|
13
|
+
export const STOPWORDS: ReadonlySet<string> = new Set([
|
|
14
|
+
"about",
|
|
15
|
+
"after",
|
|
16
|
+
"all",
|
|
17
|
+
"also",
|
|
18
|
+
"an",
|
|
19
|
+
"and",
|
|
20
|
+
"any",
|
|
21
|
+
"are",
|
|
22
|
+
"as",
|
|
23
|
+
"at",
|
|
24
|
+
"be",
|
|
25
|
+
"been",
|
|
26
|
+
"before",
|
|
27
|
+
"being",
|
|
28
|
+
"between",
|
|
29
|
+
"both",
|
|
30
|
+
"but",
|
|
31
|
+
"by",
|
|
32
|
+
"can",
|
|
33
|
+
"could",
|
|
34
|
+
"did",
|
|
35
|
+
"do",
|
|
36
|
+
"does",
|
|
37
|
+
"doing",
|
|
38
|
+
"each",
|
|
39
|
+
"for",
|
|
40
|
+
"from",
|
|
41
|
+
"had",
|
|
42
|
+
"has",
|
|
43
|
+
"have",
|
|
44
|
+
"he",
|
|
45
|
+
"her",
|
|
46
|
+
"here",
|
|
47
|
+
"hers",
|
|
48
|
+
"him",
|
|
49
|
+
"his",
|
|
50
|
+
"how",
|
|
51
|
+
"if",
|
|
52
|
+
"in",
|
|
53
|
+
"into",
|
|
54
|
+
"is",
|
|
55
|
+
"it",
|
|
56
|
+
"its",
|
|
57
|
+
"may",
|
|
58
|
+
"me",
|
|
59
|
+
"might",
|
|
60
|
+
"must",
|
|
61
|
+
"my",
|
|
62
|
+
"no",
|
|
63
|
+
"nor",
|
|
64
|
+
"not",
|
|
65
|
+
"of",
|
|
66
|
+
"off",
|
|
67
|
+
"on",
|
|
68
|
+
"once",
|
|
69
|
+
"only",
|
|
70
|
+
"or",
|
|
71
|
+
"other",
|
|
72
|
+
"our",
|
|
73
|
+
"ours",
|
|
74
|
+
"out",
|
|
75
|
+
"over",
|
|
76
|
+
"own",
|
|
77
|
+
"same",
|
|
78
|
+
"shall",
|
|
79
|
+
"she",
|
|
80
|
+
"should",
|
|
81
|
+
"so",
|
|
82
|
+
"some",
|
|
83
|
+
"such",
|
|
84
|
+
"than",
|
|
85
|
+
"that",
|
|
86
|
+
"the",
|
|
87
|
+
"their",
|
|
88
|
+
"theirs",
|
|
89
|
+
"them",
|
|
90
|
+
"then",
|
|
91
|
+
"there",
|
|
92
|
+
"these",
|
|
93
|
+
"they",
|
|
94
|
+
"this",
|
|
95
|
+
"those",
|
|
96
|
+
"through",
|
|
97
|
+
"to",
|
|
98
|
+
"too",
|
|
99
|
+
"under",
|
|
100
|
+
"until",
|
|
101
|
+
"up",
|
|
102
|
+
"very",
|
|
103
|
+
"was",
|
|
104
|
+
"we",
|
|
105
|
+
"were",
|
|
106
|
+
"what",
|
|
107
|
+
"when",
|
|
108
|
+
"where",
|
|
109
|
+
"which",
|
|
110
|
+
"while",
|
|
111
|
+
"who",
|
|
112
|
+
"whom",
|
|
113
|
+
"why",
|
|
114
|
+
"will",
|
|
115
|
+
"with",
|
|
116
|
+
"would",
|
|
117
|
+
"you",
|
|
118
|
+
"your",
|
|
119
|
+
"yours",
|
|
120
|
+
]);
|
package/src/sync.ts
CHANGED
|
@@ -102,7 +102,7 @@ async function readChunks(
|
|
|
102
102
|
chunks.push({
|
|
103
103
|
chunkId: chunkId(pkg.id, chunk.id),
|
|
104
104
|
filePath: chunk.file,
|
|
105
|
-
tokens: chunk.tokens
|
|
105
|
+
tokens: chunk.tokens ?? estimateTokens(text),
|
|
106
106
|
content: text,
|
|
107
107
|
tags: [...chunk.tags, ...chunk.entities],
|
|
108
108
|
});
|
package/src/verify.ts
CHANGED
|
@@ -3,8 +3,17 @@ import { join } from "node:path";
|
|
|
3
3
|
import { readBuildConfig } from "./config.js";
|
|
4
4
|
import { discoverPackages, resolvePackageDir } from "./discovery.js";
|
|
5
5
|
import { DocspackError } from "./errors.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { readExportSurface } from "./exports.js";
|
|
7
|
+
import {
|
|
8
|
+
chunkId,
|
|
9
|
+
type DocumentedLibrary,
|
|
10
|
+
formatDocumentedLibrary,
|
|
11
|
+
LLMS_DIR,
|
|
12
|
+
MANIFEST_FILE,
|
|
13
|
+
type PackageManifest,
|
|
14
|
+
parseDocumentedLibrary,
|
|
15
|
+
parseManifest,
|
|
16
|
+
} from "./spec.js";
|
|
8
17
|
|
|
9
18
|
export interface DriftFinding {
|
|
10
19
|
readonly chunkId: string;
|
|
@@ -22,8 +31,14 @@ export type VerifyStatus = "verified" | "skipped";
|
|
|
22
31
|
export interface VerifiedPackage {
|
|
23
32
|
readonly id: string;
|
|
24
33
|
readonly status: VerifyStatus;
|
|
25
|
-
/**
|
|
26
|
-
readonly documents?: string;
|
|
34
|
+
/** Libraries the package declares it documents, as written. */
|
|
35
|
+
readonly documents?: readonly string[];
|
|
36
|
+
/**
|
|
37
|
+
* Documented libraries whose declarations could not be read, when others could. The check ran
|
|
38
|
+
* against a smaller surface than the package claims, which is worth saying: an entity those
|
|
39
|
+
* libraries export is one this run could have accused.
|
|
40
|
+
*/
|
|
41
|
+
readonly unchecked?: readonly string[];
|
|
27
42
|
/** Why nothing was checked, when status is `skipped`. */
|
|
28
43
|
readonly reason?: string;
|
|
29
44
|
readonly checked: number;
|
|
@@ -181,38 +196,65 @@ async function verifyPackage(
|
|
|
181
196
|
): Promise<VerifiedPackage> {
|
|
182
197
|
const empty = { id, checked: 0, matched: 0, unrecognised: 0, findings: [] };
|
|
183
198
|
|
|
184
|
-
const
|
|
185
|
-
|
|
199
|
+
const packageLibraries = await documentedLibraries(packageDir, manifest);
|
|
200
|
+
// A chunk may name its own libraries, which is how a monorepo says that one chunk describes
|
|
201
|
+
// `@acme/api@1.2.0` and its neighbour describes `@acme/cli@3.0.0`. Those are what that chunk
|
|
202
|
+
// is checked against; the package's list covers every chunk that names none.
|
|
203
|
+
const scopes = manifest.chunks.map((chunk) => chunk.documents ?? packageLibraries);
|
|
204
|
+
if (packageLibraries.length === 0 && scopes.every((scope) => scope.length === 0)) {
|
|
186
205
|
return {
|
|
187
206
|
...empty,
|
|
188
207
|
status: "skipped",
|
|
189
|
-
reason: `declares no "
|
|
208
|
+
reason: `declares no "documents", so there is nothing to check against`,
|
|
190
209
|
};
|
|
191
210
|
}
|
|
192
211
|
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
212
|
+
const documents = [
|
|
213
|
+
...new Set([...packageLibraries, ...scopes.flat()].map(formatDocumentedLibrary)),
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
// One resolution per library, however many chunks name it: reading a type surface is the
|
|
217
|
+
// expensive part, and a monorepo pack names the same eighteen libraries throughout.
|
|
218
|
+
const surfaces = new Map<string, ReadonlySet<string>>();
|
|
219
|
+
const resolve = async (library: DocumentedLibrary): Promise<ReadonlySet<string>> => {
|
|
220
|
+
const cached = surfaces.get(library.name);
|
|
221
|
+
if (cached !== undefined) return cached;
|
|
222
|
+
|
|
223
|
+
const libraryDir =
|
|
224
|
+
(await resolvePackageDir(library.name, packageDir)) ??
|
|
225
|
+
(await resolvePackageDir(library.name, cwd));
|
|
226
|
+
const surface = libraryDir === undefined ? undefined : await readExportSurface(libraryDir);
|
|
227
|
+
const names = surface?.names ?? new Set<string>();
|
|
228
|
+
surfaces.set(library.name, names);
|
|
229
|
+
return names;
|
|
230
|
+
};
|
|
208
231
|
|
|
209
|
-
const declared = new Set([...surface.names].map(normalize));
|
|
210
232
|
const findings: DriftFinding[] = [];
|
|
233
|
+
const missing = new Set<string>();
|
|
211
234
|
let checked = 0;
|
|
212
235
|
let matched = 0;
|
|
213
236
|
let unrecognised = 0;
|
|
237
|
+
let readAny = false;
|
|
238
|
+
|
|
239
|
+
for (const [index, chunk] of manifest.chunks.entries()) {
|
|
240
|
+
const scope = scopes[index] ?? packageLibraries;
|
|
241
|
+
if (scope.length === 0) continue;
|
|
242
|
+
|
|
243
|
+
// An entity is fine if any library in scope declares it: a monorepo documents eighteen
|
|
244
|
+
// packages from one surface, and a name belongs to whichever of them exports it.
|
|
245
|
+
const names = new Set<string>();
|
|
246
|
+
for (const library of scope) {
|
|
247
|
+
const surface = await resolve(library);
|
|
248
|
+
if (surface.size === 0) {
|
|
249
|
+
missing.add(library.name);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
for (const name of surface) names.add(name);
|
|
253
|
+
}
|
|
254
|
+
if (names.size === 0) continue;
|
|
255
|
+
readAny = true;
|
|
214
256
|
|
|
215
|
-
|
|
257
|
+
const declared = new Set([...names].map(normalize));
|
|
216
258
|
for (const entity of chunk.entities) {
|
|
217
259
|
const symbol = checkableSymbol(entity);
|
|
218
260
|
if (symbol === undefined) continue;
|
|
@@ -225,7 +267,7 @@ async function verifyPackage(
|
|
|
225
267
|
continue;
|
|
226
268
|
}
|
|
227
269
|
|
|
228
|
-
const suggestion = nearestName(symbol,
|
|
270
|
+
const suggestion = nearestName(symbol, names);
|
|
229
271
|
if (suggestion === undefined) {
|
|
230
272
|
unrecognised += 1;
|
|
231
273
|
continue;
|
|
@@ -241,7 +283,45 @@ async function verifyPackage(
|
|
|
241
283
|
}
|
|
242
284
|
}
|
|
243
285
|
|
|
244
|
-
|
|
286
|
+
if (!readAny) {
|
|
287
|
+
const unread = [...missing];
|
|
288
|
+
return {
|
|
289
|
+
...empty,
|
|
290
|
+
status: "skipped",
|
|
291
|
+
documents,
|
|
292
|
+
reason:
|
|
293
|
+
unread.length === 0
|
|
294
|
+
? "has no chunk naming a library to check against"
|
|
295
|
+
: `${unread.join(", ")} ${unread.length === 1 ? "is" : "are"} not installed, or ship no type declarations`,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
id,
|
|
301
|
+
status: "verified",
|
|
302
|
+
documents,
|
|
303
|
+
...(missing.size === 0 ? {} : { unchecked: [...missing] }),
|
|
304
|
+
checked,
|
|
305
|
+
matched,
|
|
306
|
+
unrecognised,
|
|
307
|
+
findings,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* What a package says it documents.
|
|
313
|
+
*
|
|
314
|
+
* The manifest is the answer when it carries one, because that is the only copy a consumer of an
|
|
315
|
+
* installed package can see. The `docspack` key is read as a fallback: it is build configuration
|
|
316
|
+
* rather than payload, and a package built before `documents` reached the manifest has only that.
|
|
317
|
+
*/
|
|
318
|
+
async function documentedLibraries(
|
|
319
|
+
packageDir: string,
|
|
320
|
+
manifest: PackageManifest,
|
|
321
|
+
): Promise<readonly DocumentedLibrary[]> {
|
|
322
|
+
if (manifest.documents !== undefined && manifest.documents.length > 0) return manifest.documents;
|
|
323
|
+
const { documents } = await readBuildConfig(packageDir);
|
|
324
|
+
return (documents ?? []).map(parseDocumentedLibrary);
|
|
245
325
|
}
|
|
246
326
|
|
|
247
327
|
/**
|
|
@@ -275,13 +355,13 @@ function normalize(name: string): string {
|
|
|
275
355
|
* is left alone. In particular a differing first word is never a match: `includeLanguages` and
|
|
276
356
|
* `excludeLanguages` are two edits apart and mean opposite things.
|
|
277
357
|
*/
|
|
278
|
-
function nearestName(symbol: string,
|
|
358
|
+
function nearestName(symbol: string, names: ReadonlySet<string>): string | undefined {
|
|
279
359
|
const lower = normalize(symbol);
|
|
280
360
|
const words = camelWords(symbol);
|
|
281
361
|
let best: string | undefined;
|
|
282
362
|
let bestDistance = Number.POSITIVE_INFINITY;
|
|
283
363
|
|
|
284
|
-
for (const candidate of
|
|
364
|
+
for (const candidate of names) {
|
|
285
365
|
if (candidate.length < MIN_SYMBOL) continue;
|
|
286
366
|
const candidateWords = camelWords(candidate);
|
|
287
367
|
if (isRequalified(words, candidateWords)) return candidate;
|