docspack 0.4.0 → 1.0.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/dist/agent.d.ts +48 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +243 -0
- package/dist/agent.js.map +1 -0
- package/dist/artifact.d.ts +32 -0
- package/dist/artifact.d.ts.map +1 -0
- package/dist/artifact.js +78 -0
- package/dist/artifact.js.map +1 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +17 -0
- package/dist/build.js.map +1 -1
- package/dist/changed.d.ts +31 -0
- package/dist/changed.d.ts.map +1 -0
- package/dist/changed.js +71 -0
- package/dist/changed.js.map +1 -0
- package/dist/cli.js +131 -10
- package/dist/cli.js.map +1 -1
- package/dist/coverage.d.ts +35 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/coverage.js +64 -0
- package/dist/coverage.js.map +1 -0
- package/dist/db.d.ts +38 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +109 -6
- package/dist/db.js.map +1 -1
- package/dist/discovery.d.ts +14 -0
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +31 -6
- package/dist/discovery.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +44 -0
- package/dist/doctor.js.map +1 -1
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +65 -4
- package/dist/help.js.map +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +3 -0
- package/dist/mcp.js.map +1 -1
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +1 -0
- package/dist/preview.js.map +1 -1
- package/dist/search.d.ts +14 -1
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +108 -21
- package/dist/search.js.map +1 -1
- package/dist/surface.d.ts +45 -0
- package/dist/surface.d.ts.map +1 -0
- package/dist/surface.js +208 -0
- package/dist/surface.js.map +1 -0
- package/dist/sync.d.ts +8 -1
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +50 -1
- package/dist/sync.js.map +1 -1
- package/dist/verify.d.ts +9 -0
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +1 -1
- package/dist/verify.js.map +1 -1
- package/package.json +1 -1
- package/src/agent.ts +308 -0
- package/src/artifact.ts +110 -0
- package/src/build.ts +19 -0
- package/src/changed.ts +99 -0
- package/src/cli.ts +167 -10
- package/src/coverage.ts +96 -0
- package/src/db.ts +168 -7
- package/src/discovery.ts +40 -5
- package/src/doctor.ts +52 -0
- package/src/help.ts +65 -4
- package/src/index.ts +30 -0
- package/src/mcp.ts +3 -0
- package/src/preview.ts +1 -0
- package/src/search.ts +148 -22
- package/src/surface.ts +265 -0
- package/src/sync.ts +66 -2
- package/src/verify.ts +1 -1
package/src/doctor.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Dirent } from "node:fs";
|
|
|
2
2
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { readBuildConfig } from "./config.js";
|
|
5
|
+
import { measureCoverage } from "./coverage.js";
|
|
5
6
|
import { PLACEHOLDER } from "./init/templates.js";
|
|
6
7
|
import {
|
|
7
8
|
estimateTokens,
|
|
@@ -48,6 +49,13 @@ export interface DoctorOptions {
|
|
|
48
49
|
readonly pedantic?: boolean;
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Below this share of a library's exported names, a package is documenting a fraction of what
|
|
54
|
+
* the library publishes. Reported, never gated on: a page listing every export and explaining
|
|
55
|
+
* none would score full marks, so this is a number for an author to read, not a wall.
|
|
56
|
+
*/
|
|
57
|
+
const THIN_COVERAGE = 0.6;
|
|
58
|
+
|
|
51
59
|
/** Over this, a chunk crowds out the response budget; under it, a chunk answers nothing. */
|
|
52
60
|
const MAX_CHUNK_TOKENS = 1500;
|
|
53
61
|
const MIN_CHUNK_TOKENS = 30;
|
|
@@ -199,6 +207,8 @@ export async function runDoctor(options: DoctorOptions): Promise<DoctorReport> {
|
|
|
199
207
|
});
|
|
200
208
|
}
|
|
201
209
|
|
|
210
|
+
findings.push(...(await coverageFindings(options.dir, llmsDir, manifest)));
|
|
211
|
+
|
|
202
212
|
if (manifest.chunks.every((chunk) => chunk.entities.length === 0)) {
|
|
203
213
|
findings.push({
|
|
204
214
|
check: "no-entities",
|
|
@@ -385,6 +395,48 @@ async function mtime(path: string): Promise<number> {
|
|
|
385
395
|
}
|
|
386
396
|
}
|
|
387
397
|
|
|
398
|
+
/**
|
|
399
|
+
* How much of the documented library's public surface the package mentions.
|
|
400
|
+
*
|
|
401
|
+
* A documentation site is written page by page around tasks and an API grows name by name, so
|
|
402
|
+
* they drift apart with nobody noticing. This is the check that notices — mechanical, from two
|
|
403
|
+
* files, with no model and no judgement in it.
|
|
404
|
+
*/
|
|
405
|
+
async function coverageFindings(
|
|
406
|
+
dir: string,
|
|
407
|
+
llmsDir: string,
|
|
408
|
+
manifest: PackageManifest,
|
|
409
|
+
): Promise<Finding[]> {
|
|
410
|
+
const findings: Finding[] = [];
|
|
411
|
+
let report: Awaited<ReturnType<typeof measureCoverage>>;
|
|
412
|
+
try {
|
|
413
|
+
report = await measureCoverage({ packageDir: dir, llmsDir, manifest, cwd: dir });
|
|
414
|
+
} catch {
|
|
415
|
+
// Coverage is a note about the documentation, never a reason a package fails to check.
|
|
416
|
+
return findings;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
for (const library of report.libraries) {
|
|
420
|
+
if (library.names === 0) continue;
|
|
421
|
+
const share = library.documented / library.names;
|
|
422
|
+
const percent = Math.round(100 * share);
|
|
423
|
+
findings.push({
|
|
424
|
+
check: "coverage",
|
|
425
|
+
severity: "info",
|
|
426
|
+
message:
|
|
427
|
+
`mentions ${library.documented} of ${library.names} names ${library.library} exports (${percent}%)` +
|
|
428
|
+
(library.uncovered.length === 0 ? "" : `; missing ${library.uncovered.join(", ")}`),
|
|
429
|
+
...(share < THIN_COVERAGE
|
|
430
|
+
? {
|
|
431
|
+
fix: "Document the exports nobody has written about, or narrow what the package claims to document.",
|
|
432
|
+
}
|
|
433
|
+
: {}),
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return findings;
|
|
438
|
+
}
|
|
439
|
+
|
|
388
440
|
async function readJson(path: string): Promise<Record<string, unknown> | undefined> {
|
|
389
441
|
try {
|
|
390
442
|
const parsed: unknown = JSON.parse(await readFile(path, "utf8"));
|
package/src/help.ts
CHANGED
|
@@ -37,12 +37,21 @@ export const COMMANDS: readonly CommandHelp[] = [
|
|
|
37
37
|
summary: "Index the docs packages this project depends on",
|
|
38
38
|
group: "core",
|
|
39
39
|
usage: "docspack sync [options]",
|
|
40
|
-
options: [
|
|
40
|
+
options: [
|
|
41
|
+
["--force", "re-index packages already in the store"],
|
|
42
|
+
["--no-artifacts", "skip the declarations derived from installed libraries"],
|
|
43
|
+
],
|
|
41
44
|
detail: [
|
|
42
45
|
"Reads node_modules and indexes every @vendor/docspack and @docspack-community/<name>",
|
|
43
46
|
"package this project declares. Makes no network requests.",
|
|
47
|
+
"",
|
|
48
|
+
"It also reads each installed library's own type declarations and indexes one entry per",
|
|
49
|
+
"exported name. Half of a well-documented library's exports are mentioned in no",
|
|
50
|
+
"documentation anyone published, and those declarations are the only local answer for",
|
|
51
|
+
"them. They are looked up by name, never ranked against prose, so they cannot crowd out",
|
|
52
|
+
"the documentation that does exist.",
|
|
44
53
|
],
|
|
45
|
-
examples: ["docspack sync", "docspack sync --force"],
|
|
54
|
+
examples: ["docspack sync", "docspack sync --force", "docspack sync --no-artifacts"],
|
|
46
55
|
},
|
|
47
56
|
{
|
|
48
57
|
name: "ask",
|
|
@@ -54,6 +63,10 @@ export const COMMANDS: readonly CommandHelp[] = [
|
|
|
54
63
|
"Answers from the installed versions, in the Markdown an agent should read. Exits 0 when",
|
|
55
64
|
"chunks were returned, 3 when a docs package is installed but not indexed, and 4 when",
|
|
56
65
|
"everything installed is indexed and nothing matched.",
|
|
66
|
+
"",
|
|
67
|
+
"When the question names something an installed library exports and no documentation",
|
|
68
|
+
"mentions, the answer leads with that name's declaration from the installed build and says",
|
|
69
|
+
"the documentation does not cover it. Ranking alone cannot tell that apart from a match.",
|
|
57
70
|
],
|
|
58
71
|
examples: [
|
|
59
72
|
'docspack ask "how do I verify a webhook signature"',
|
|
@@ -73,9 +86,57 @@ export const COMMANDS: readonly CommandHelp[] = [
|
|
|
73
86
|
name: "list",
|
|
74
87
|
summary: "Show this project's docs packages and their index state",
|
|
75
88
|
group: "core",
|
|
76
|
-
usage: "docspack list",
|
|
89
|
+
usage: "docspack list [options]",
|
|
90
|
+
options: [["--coverage", "how much of each documented library's exports the prose mentions"]],
|
|
91
|
+
detail: [
|
|
92
|
+
"Coverage is mechanical: the exported names a library declares, against the names its",
|
|
93
|
+
"documentation mentions anywhere. It is reported, never gated on — a page listing every",
|
|
94
|
+
"export and explaining none would score full marks.",
|
|
95
|
+
],
|
|
96
|
+
examples: ["docspack list", "docspack list --coverage", "docspack list --json"],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "agent",
|
|
100
|
+
summary: "Wire docspack into the agent tooling this project already uses",
|
|
101
|
+
group: "core",
|
|
102
|
+
usage: "docspack agent <install|check> [options]",
|
|
103
|
+
options: [
|
|
104
|
+
["--feedback", "also include recording documentation problems"],
|
|
105
|
+
["--hooks", "add a SessionStart hook that keeps the index in step"],
|
|
106
|
+
["--mcp", "add the MCP server to .mcp.json"],
|
|
107
|
+
["--dry-run", "print what would be written and write nothing"],
|
|
108
|
+
],
|
|
109
|
+
detail: [
|
|
110
|
+
"`install` writes a marked block into AGENTS.md or CLAUDE.md — whichever the project",
|
|
111
|
+
"already has — and a skill into .claude/skills/docspack/ when the project uses Claude",
|
|
112
|
+
"Code. Everything outside the markers is left alone, and re-running rewrites the block",
|
|
113
|
+
"rather than appending a second copy.",
|
|
114
|
+
"",
|
|
115
|
+
"`check` writes nothing and exits non-zero when the wiring is missing or out of date, so",
|
|
116
|
+
"CI notices a pasted instruction that has drifted from what the tool now does.",
|
|
117
|
+
],
|
|
118
|
+
examples: [
|
|
119
|
+
"docspack agent install",
|
|
120
|
+
"docspack agent install --feedback --hooks",
|
|
121
|
+
"docspack agent check",
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "changed",
|
|
126
|
+
summary: "What a library's exports gained and lost between two versions",
|
|
127
|
+
group: "core",
|
|
128
|
+
usage: "docspack changed <library>[@version]",
|
|
77
129
|
options: [],
|
|
78
|
-
|
|
130
|
+
detail: [
|
|
131
|
+
"Compares two versions already in the global store, which is shared by every project on",
|
|
132
|
+
"this machine, so nothing is fetched. Without a version it compares what is installed here",
|
|
133
|
+
"against the most recently indexed other version.",
|
|
134
|
+
"",
|
|
135
|
+
"Upgrades are overwhelmingly additive: the useful answer is what exists now that an older",
|
|
136
|
+
"release did not have, and which of those names no documentation here mentions — the ones",
|
|
137
|
+
"a model can know from neither its training data nor the vendor's pages.",
|
|
138
|
+
],
|
|
139
|
+
examples: ["docspack changed hono", "docspack changed hono@4.0.0"],
|
|
79
140
|
},
|
|
80
141
|
{
|
|
81
142
|
name: "verify",
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type AgentFile,
|
|
3
|
+
type AgentOptions,
|
|
4
|
+
type AgentPlan,
|
|
5
|
+
applyAgentSetup,
|
|
6
|
+
planAgentSetup,
|
|
7
|
+
type SurfaceKind,
|
|
8
|
+
withBlock,
|
|
9
|
+
} from "./agent.js";
|
|
10
|
+
export { type ArtifactPackage, readArtifact } from "./artifact.js";
|
|
1
11
|
export { type BuildOptions, type BuildResult, buildPackage } from "./build.js";
|
|
12
|
+
export { type ChangedOptions, changedSurface, type SurfaceChange } from "./changed.js";
|
|
2
13
|
export {
|
|
3
14
|
type BuildConfig,
|
|
4
15
|
CONFIG_KEY,
|
|
@@ -7,19 +18,30 @@ export {
|
|
|
7
18
|
parseFeedbackChannel,
|
|
8
19
|
readBuildConfig,
|
|
9
20
|
} from "./config.js";
|
|
21
|
+
export {
|
|
22
|
+
type CoverageOptions,
|
|
23
|
+
type CoverageReport,
|
|
24
|
+
identifiers,
|
|
25
|
+
type LibraryCoverage,
|
|
26
|
+
measureCoverage,
|
|
27
|
+
} from "./coverage.js";
|
|
10
28
|
export {
|
|
11
29
|
defaultStorePath,
|
|
12
30
|
type IndexedChunk,
|
|
13
31
|
type IndexedPackage,
|
|
32
|
+
type PackageKind,
|
|
14
33
|
type SearchHit,
|
|
15
34
|
type SearchOptions,
|
|
16
35
|
Store,
|
|
36
|
+
type SymbolHit,
|
|
17
37
|
silenceSqliteWarning,
|
|
18
38
|
toFtsQuery,
|
|
19
39
|
} from "./db.js";
|
|
20
40
|
export {
|
|
41
|
+
type DiscoveredLibrary,
|
|
21
42
|
type DiscoveredPackage,
|
|
22
43
|
type Discovery,
|
|
44
|
+
discoverLibraries,
|
|
23
45
|
discoverPackages,
|
|
24
46
|
projectPackageIds,
|
|
25
47
|
resolvePackageDir,
|
|
@@ -136,6 +158,13 @@ export {
|
|
|
136
158
|
type SubmitOptions,
|
|
137
159
|
type SubmitReport,
|
|
138
160
|
} from "./submit.js";
|
|
161
|
+
export {
|
|
162
|
+
declarationOf,
|
|
163
|
+
type ExportedSymbol,
|
|
164
|
+
hasJsdoc,
|
|
165
|
+
type PublicSurface,
|
|
166
|
+
readPublicSurface,
|
|
167
|
+
} from "./surface.js";
|
|
139
168
|
export {
|
|
140
169
|
type SyncedPackage,
|
|
141
170
|
type SyncOptions,
|
|
@@ -145,6 +174,7 @@ export {
|
|
|
145
174
|
} from "./sync.js";
|
|
146
175
|
export {
|
|
147
176
|
type DriftFinding,
|
|
177
|
+
documentedLibraries,
|
|
148
178
|
type VerifiedPackage,
|
|
149
179
|
type VerifyOptions,
|
|
150
180
|
type VerifyReport,
|
package/src/mcp.ts
CHANGED
|
@@ -22,6 +22,9 @@ const DESCRIPTION = [
|
|
|
22
22
|
"Returns Markdown excerpts from local, version-matched documentation packages.",
|
|
23
23
|
"Prefer this over recalling API details from memory: the local copy matches the",
|
|
24
24
|
"exact dependency versions in this project.",
|
|
25
|
+
"When the question names something an installed library exports that no",
|
|
26
|
+
"documentation mentions, the answer leads with that name's declaration from the",
|
|
27
|
+
"installed build and says the documentation does not cover it.",
|
|
25
28
|
].join(" ");
|
|
26
29
|
|
|
27
30
|
/**
|
package/src/preview.ts
CHANGED
|
@@ -85,6 +85,7 @@ export async function previewPackage(options: PreviewOptions): Promise<PreviewRe
|
|
|
85
85
|
...hit,
|
|
86
86
|
name: manifest.name,
|
|
87
87
|
version: manifest.version,
|
|
88
|
+
kind: "docs",
|
|
88
89
|
trusted: !isCommunityPackage(manifest.name),
|
|
89
90
|
...(documents.length === 0 ? {} : { documents }),
|
|
90
91
|
}),
|
package/src/search.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import type { SearchHit, Store } from "./db.js";
|
|
2
|
-
import { discoverPackages } from "./discovery.js";
|
|
1
|
+
import type { PackageKind, SearchHit, Store } from "./db.js";
|
|
2
|
+
import { discoverLibraries, discoverPackages } from "./discovery.js";
|
|
3
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;
|
|
7
7
|
export const DEFAULT_LIMIT = 3;
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* How many declarations one answer may carry. A question names one or two things; more than
|
|
11
|
+
* that and the names are incidental, and pinning them would spend the budget on noise.
|
|
12
|
+
*/
|
|
13
|
+
const MAX_DECLARATIONS = 2;
|
|
14
|
+
|
|
15
|
+
/** Below this a token is too short to be a name worth looking up: `c`, `id`. */
|
|
16
|
+
const MIN_SYMBOL = 3;
|
|
17
|
+
|
|
9
18
|
export interface QueryOptions {
|
|
10
19
|
readonly cwd: string;
|
|
11
20
|
readonly store: Store;
|
|
@@ -19,12 +28,19 @@ export interface QueryOptions {
|
|
|
19
28
|
* sees documentation for a version this project does not use.
|
|
20
29
|
*/
|
|
21
30
|
readonly scoped?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Answer a question that names an exported symbol from the installed library's own
|
|
33
|
+
* declarations when the documentation does not mention it. On by default.
|
|
34
|
+
*/
|
|
35
|
+
readonly artifacts?: boolean;
|
|
22
36
|
}
|
|
23
37
|
|
|
24
38
|
export interface QueryHit extends SearchHit {
|
|
25
39
|
readonly name: string;
|
|
26
40
|
readonly version: string;
|
|
27
41
|
readonly trusted: boolean;
|
|
42
|
+
/** Whether this text was published as documentation or derived from the installed build. */
|
|
43
|
+
readonly kind: PackageKind;
|
|
28
44
|
/**
|
|
29
45
|
* Libraries this chunk documents, from its manifest. A docs package version cannot always
|
|
30
46
|
* imply this — a monorepo documents many libraries at many versions from one surface — so the
|
|
@@ -45,6 +61,12 @@ export interface QueryResult {
|
|
|
45
61
|
* covers this question".
|
|
46
62
|
*/
|
|
47
63
|
readonly unindexed: readonly string[];
|
|
64
|
+
/**
|
|
65
|
+
* Names the question used that an installed library exports and no documentation mentions.
|
|
66
|
+
* This is the difference between "the documentation does not cover this" and "nothing
|
|
67
|
+
* matched", which a ranker alone cannot tell apart — it always returns its best three.
|
|
68
|
+
*/
|
|
69
|
+
readonly undocumented: readonly string[];
|
|
48
70
|
}
|
|
49
71
|
|
|
50
72
|
export async function queryDocs(options: QueryOptions): Promise<QueryResult> {
|
|
@@ -71,33 +93,123 @@ export async function queryDocs(options: QueryOptions): Promise<QueryResult> {
|
|
|
71
93
|
}
|
|
72
94
|
}
|
|
73
95
|
|
|
74
|
-
const
|
|
96
|
+
const maxTokens = options.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
97
|
+
const prose = options.store
|
|
75
98
|
.search(options.query, {
|
|
76
99
|
...(packageIds === undefined ? {} : { packageIds }),
|
|
77
100
|
...(options.packageFilter === undefined
|
|
78
101
|
? {}
|
|
79
102
|
: { packageFilter: `%${options.packageFilter}%` }),
|
|
80
103
|
limit: options.limit ?? DEFAULT_LIMIT,
|
|
81
|
-
maxTokens
|
|
104
|
+
maxTokens,
|
|
105
|
+
// Declarations are addressed by name, never ranked against prose: a library exports far
|
|
106
|
+
// more names than its documentation has pages, and ranking them together would answer
|
|
107
|
+
// every question with type machinery.
|
|
108
|
+
kinds: ["docs"],
|
|
82
109
|
})
|
|
83
|
-
.map((hit)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
};
|
|
94
|
-
});
|
|
110
|
+
.map((hit) => toQueryHit(hit, "docs", documented));
|
|
111
|
+
|
|
112
|
+
const { declarations, undocumented } =
|
|
113
|
+
options.artifacts === false
|
|
114
|
+
? { declarations: [] as QueryHit[], undocumented: [] as string[] }
|
|
115
|
+
: await findDeclarations(options, prose, maxTokens);
|
|
116
|
+
|
|
117
|
+
// Declarations first: for an agent about to write a call, the signature at the installed
|
|
118
|
+
// version is the load-bearing line, and the prose explains it afterwards.
|
|
119
|
+
const hits = [...declarations, ...prose];
|
|
95
120
|
|
|
96
121
|
return {
|
|
97
122
|
hits,
|
|
98
123
|
tokens: hits.reduce((total, hit) => total + hit.tokens, 0),
|
|
99
124
|
untrusted: hits.some((hit) => !hit.trusted),
|
|
100
125
|
unindexed,
|
|
126
|
+
undocumented,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Looks up the names a question used against the symbols the installed libraries export.
|
|
132
|
+
*
|
|
133
|
+
* Matching is case-sensitive on purpose. Someone asking about an API writes it as it is spelled,
|
|
134
|
+
* because they read it in a stack trace or an editor; matching loosely would let "how do I use
|
|
135
|
+
* data" pin hono's `Data` type ahead of the page that answers the question.
|
|
136
|
+
*/
|
|
137
|
+
async function findDeclarations(
|
|
138
|
+
options: QueryOptions,
|
|
139
|
+
prose: readonly QueryHit[],
|
|
140
|
+
maxTokens: number,
|
|
141
|
+
): Promise<{ declarations: QueryHit[]; undocumented: string[] }> {
|
|
142
|
+
const scope =
|
|
143
|
+
options.scoped === false
|
|
144
|
+
? undefined
|
|
145
|
+
: (await discoverLibraries(options.cwd)).map(
|
|
146
|
+
(library) => `${library.name}@${library.version}`,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const declarations: QueryHit[] = [];
|
|
150
|
+
const undocumented: string[] = [];
|
|
151
|
+
let spent = prose.reduce((total, hit) => total + hit.tokens, 0);
|
|
152
|
+
|
|
153
|
+
for (const term of symbolTerms(options.query)) {
|
|
154
|
+
if (declarations.length >= MAX_DECLARATIONS) break;
|
|
155
|
+
|
|
156
|
+
const found = options.store
|
|
157
|
+
.lookupSymbol(term, scope)
|
|
158
|
+
.filter(
|
|
159
|
+
(hit) =>
|
|
160
|
+
options.packageFilter === undefined || hit.packageId.includes(options.packageFilter),
|
|
161
|
+
);
|
|
162
|
+
if (found.length === 0) continue;
|
|
163
|
+
|
|
164
|
+
// The documentation covers it, so the prose that just came back is the better answer.
|
|
165
|
+
if (prose.some((hit) => mentions(hit.content, term))) continue;
|
|
166
|
+
undocumented.push(term);
|
|
167
|
+
|
|
168
|
+
// The name is exported and undocumented either way; whether a declaration can be shown for
|
|
169
|
+
// it is a separate question, and a package can export a name it declares nowhere readable.
|
|
170
|
+
const first = found[0];
|
|
171
|
+
if (first === undefined || first.chunkId.length === 0) continue;
|
|
172
|
+
const chunk = options.store.chunk(first.chunkId);
|
|
173
|
+
if (chunk === undefined) continue;
|
|
174
|
+
if (declarations.length > 0 && spent + chunk.tokens > maxTokens) break;
|
|
175
|
+
|
|
176
|
+
declarations.push(toQueryHit(chunk, "artifact", new Map()));
|
|
177
|
+
spent += chunk.tokens;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { declarations, undocumented };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Identifier-shaped words in a question, longest first so the specific name is tried first. */
|
|
184
|
+
function symbolTerms(query: string): string[] {
|
|
185
|
+
const terms = query.match(/[A-Za-z_$][\w$]*/g) ?? [];
|
|
186
|
+
const unique = [...new Set(terms.filter((term) => term.length >= MIN_SYMBOL))];
|
|
187
|
+
return unique.sort((a, b) => b.length - a.length);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function mentions(content: string, name: string): boolean {
|
|
191
|
+
return new RegExp(`(^|[^A-Za-z0-9_$])${escapeRegex(name)}([^A-Za-z0-9_$]|$)`).test(content);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function escapeRegex(value: string): string {
|
|
195
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function toQueryHit(
|
|
199
|
+
hit: SearchHit,
|
|
200
|
+
kind: PackageKind,
|
|
201
|
+
documented: ReadonlyMap<string, readonly string[]>,
|
|
202
|
+
): QueryHit {
|
|
203
|
+
const { name, version } = splitPackageId(hit.packageId);
|
|
204
|
+
// The chunk's own libraries win: they are the narrower, and therefore the truer, claim.
|
|
205
|
+
const documents = documented.get(hit.chunkId) ?? documented.get(hit.packageId);
|
|
206
|
+
return {
|
|
207
|
+
...hit,
|
|
208
|
+
name,
|
|
209
|
+
version,
|
|
210
|
+
kind,
|
|
211
|
+
trusted: !isCommunityPackage(name),
|
|
212
|
+
...(documents === undefined ? {} : { documents }),
|
|
101
213
|
};
|
|
102
214
|
}
|
|
103
215
|
|
|
@@ -128,18 +240,32 @@ export function renderAnswer(result: QueryResult, query: string): string {
|
|
|
128
240
|
hit.documents === undefined || hit.documents.length === 0
|
|
129
241
|
? ""
|
|
130
242
|
: ` · documents ${hit.documents.join(", ")}`;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
].join("\n");
|
|
243
|
+
const source =
|
|
244
|
+
hit.kind === "artifact"
|
|
245
|
+
? `Source: ${hit.name}@${hit.version} — declared in ${hit.filePath}, read from the installed package`
|
|
246
|
+
: `Source: ${hit.name}@${hit.version} — ${hit.filePath}${describes}`;
|
|
247
|
+
return [`## ${hit.chunkId}${trust}`, source, "", hit.content].join("\n");
|
|
137
248
|
});
|
|
138
249
|
|
|
250
|
+
if (result.undocumented.length > 0) sections.push(undocumentedNotice(result));
|
|
139
251
|
if (result.untrusted) sections.push(UNTRUSTED_NOTICE);
|
|
140
252
|
return sections.join("\n\n---\n\n");
|
|
141
253
|
}
|
|
142
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Saying what the documentation does *not* cover. A ranker always returns its best three matches
|
|
257
|
+
* and "best" is not "relevant", so without this an answer about a name nobody documented looks
|
|
258
|
+
* exactly like an answer about one they did.
|
|
259
|
+
*/
|
|
260
|
+
function undocumentedNotice(result: QueryResult): string {
|
|
261
|
+
const names = result.undocumented.map((name) => `\`${name}\``).join(", ");
|
|
262
|
+
const plural = result.undocumented.length === 1 ? "is" : "are";
|
|
263
|
+
const declared = result.hits.some((hit) => hit.kind === "artifact");
|
|
264
|
+
return declared
|
|
265
|
+
? `NOTE: ${names} ${plural} exported by the installed library but mentioned in no documentation package here. The declaration above is the installed build's own, not prose anyone wrote.`
|
|
266
|
+
: `NOTE: ${names} ${plural} exported by the installed library but mentioned in no documentation package here.`;
|
|
267
|
+
}
|
|
268
|
+
|
|
143
269
|
/** Splits `@stripe/docspack@2025.4.1` into its name and version. */
|
|
144
270
|
export function splitPackageId(id: string): { name: string; version: string } {
|
|
145
271
|
const at = id.lastIndexOf("@");
|