docspack 0.3.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 +75 -9
- 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 +90 -3
- 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/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 +17 -3
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +116 -21
- package/dist/search.js.map +1 -1
- package/dist/spec.d.ts +6 -0
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js +12 -3
- package/dist/spec.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 +55 -27
- 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 +103 -10
- 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 +100 -4
- package/src/document.ts +13 -4
- 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 +158 -24
- package/src/spec.ts +21 -3
- package/src/surface.ts +265 -0
- package/src/sync.ts +66 -2
- package/src/verify.ts +57 -27
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";
|
|
3
|
-
import { formatDocumentedLibrary, isCommunityPackage } from "./spec.js";
|
|
1
|
+
import type { PackageKind, SearchHit, Store } from "./db.js";
|
|
2
|
+
import { discoverLibraries, discoverPackages } from "./discovery.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;
|
|
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,16 +28,24 @@ 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
|
-
* Libraries
|
|
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
|
|
31
|
-
* answer states it rather than leaving the reader to infer it from the package name.
|
|
47
|
+
* answer states it rather than leaving the reader to infer it from the package name. A chunk
|
|
48
|
+
* that names its own libraries answers for those; otherwise the package's list stands.
|
|
32
49
|
*/
|
|
33
50
|
readonly documents?: readonly string[];
|
|
34
51
|
}
|
|
@@ -44,6 +61,12 @@ export interface QueryResult {
|
|
|
44
61
|
* covers this question".
|
|
45
62
|
*/
|
|
46
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[];
|
|
47
70
|
}
|
|
48
71
|
|
|
49
72
|
export async function queryDocs(options: QueryOptions): Promise<QueryResult> {
|
|
@@ -56,40 +79,137 @@ export async function queryDocs(options: QueryOptions): Promise<QueryResult> {
|
|
|
56
79
|
|
|
57
80
|
// Read from the installed manifests rather than the index: the store is a cache of chunk text,
|
|
58
81
|
// and adding a column to it would make every existing store need a rebuild to answer this.
|
|
82
|
+
// Keyed by package id and by chunk id in the same map, because a chunk id already carries its
|
|
83
|
+
// package: `@acme/docspack@1.4.0/api-auth` cannot collide with `@acme/docspack@1.4.0`.
|
|
59
84
|
const documented = new Map<string, readonly string[]>();
|
|
60
85
|
for (const pkg of installed ?? []) {
|
|
61
86
|
const documents = pkg.manifest.documents;
|
|
62
87
|
if (documents !== undefined && documents.length > 0) {
|
|
63
88
|
documented.set(pkg.id, documents.map(formatDocumentedLibrary));
|
|
64
89
|
}
|
|
90
|
+
for (const chunk of pkg.manifest.chunks) {
|
|
91
|
+
if (chunk.documents === undefined || chunk.documents.length === 0) continue;
|
|
92
|
+
documented.set(chunkId(pkg.id, chunk.id), chunk.documents.map(formatDocumentedLibrary));
|
|
93
|
+
}
|
|
65
94
|
}
|
|
66
95
|
|
|
67
|
-
const
|
|
96
|
+
const maxTokens = options.maxTokens ?? DEFAULT_MAX_TOKENS;
|
|
97
|
+
const prose = options.store
|
|
68
98
|
.search(options.query, {
|
|
69
99
|
...(packageIds === undefined ? {} : { packageIds }),
|
|
70
100
|
...(options.packageFilter === undefined
|
|
71
101
|
? {}
|
|
72
102
|
: { packageFilter: `%${options.packageFilter}%` }),
|
|
73
103
|
limit: options.limit ?? DEFAULT_LIMIT,
|
|
74
|
-
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"],
|
|
75
109
|
})
|
|
76
|
-
.map((hit)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
});
|
|
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];
|
|
87
120
|
|
|
88
121
|
return {
|
|
89
122
|
hits,
|
|
90
123
|
tokens: hits.reduce((total, hit) => total + hit.tokens, 0),
|
|
91
124
|
untrusted: hits.some((hit) => !hit.trusted),
|
|
92
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 }),
|
|
93
213
|
};
|
|
94
214
|
}
|
|
95
215
|
|
|
@@ -120,18 +240,32 @@ export function renderAnswer(result: QueryResult, query: string): string {
|
|
|
120
240
|
hit.documents === undefined || hit.documents.length === 0
|
|
121
241
|
? ""
|
|
122
242
|
: ` · documents ${hit.documents.join(", ")}`;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
].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");
|
|
129
248
|
});
|
|
130
249
|
|
|
250
|
+
if (result.undocumented.length > 0) sections.push(undocumentedNotice(result));
|
|
131
251
|
if (result.untrusted) sections.push(UNTRUSTED_NOTICE);
|
|
132
252
|
return sections.join("\n\n---\n\n");
|
|
133
253
|
}
|
|
134
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
|
+
|
|
135
269
|
/** Splits `@stripe/docspack@2025.4.1` into its name and version. */
|
|
136
270
|
export function splitPackageId(id: string): { name: string; version: string } {
|
|
137
271
|
const at = id.lastIndexOf("@");
|
package/src/spec.ts
CHANGED
|
@@ -18,6 +18,12 @@ export interface ChunkSpec {
|
|
|
18
18
|
readonly tokens?: number;
|
|
19
19
|
readonly tags: readonly string[];
|
|
20
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[];
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
/** A library release the package documents, e.g. `acme` at `1.4.0`. */
|
|
@@ -116,7 +122,7 @@ export function parseManifest(raw: unknown, where: string): PackageManifest {
|
|
|
116
122
|
if (typeof version !== "string" || version.length === 0) fail('missing string field "version"');
|
|
117
123
|
if (!Array.isArray(root.chunks)) fail('missing "chunks" array');
|
|
118
124
|
|
|
119
|
-
const
|
|
125
|
+
const packageDocuments = parseDocuments(root.documents, fail);
|
|
120
126
|
const seen = new Set<string>();
|
|
121
127
|
const chunks = (root.chunks as unknown[]).map((entry, index): ChunkSpec => {
|
|
122
128
|
if (typeof entry !== "object" || entry === null)
|
|
@@ -142,16 +148,24 @@ export function parseManifest(raw: unknown, where: string): PackageManifest {
|
|
|
142
148
|
return fail(`chunk "${id}" has an invalid "tokens" value; omit it to have it estimated`);
|
|
143
149
|
}
|
|
144
150
|
|
|
151
|
+
const documents = parseDocuments(chunk.documents, fail);
|
|
152
|
+
|
|
145
153
|
return {
|
|
146
154
|
id,
|
|
147
155
|
file,
|
|
148
156
|
...(typeof tokens === "number" ? { tokens } : {}),
|
|
149
157
|
tags: stringArray(chunk.tags, `chunk "${id}" field "tags"`, fail),
|
|
150
158
|
entities: stringArray(chunk.entities, `chunk "${id}" field "entities"`, fail),
|
|
159
|
+
...(documents === undefined ? {} : { documents }),
|
|
151
160
|
};
|
|
152
161
|
});
|
|
153
162
|
|
|
154
|
-
return {
|
|
163
|
+
return {
|
|
164
|
+
name,
|
|
165
|
+
version,
|
|
166
|
+
...(packageDocuments === undefined ? {} : { documents: packageDocuments }),
|
|
167
|
+
chunks,
|
|
168
|
+
};
|
|
155
169
|
}
|
|
156
170
|
|
|
157
171
|
/**
|
|
@@ -192,7 +206,11 @@ export function serializeManifest(manifest: PackageManifest): string {
|
|
|
192
206
|
name: manifest.name,
|
|
193
207
|
version: manifest.version,
|
|
194
208
|
...(documents.length === 0 ? {} : { documents: documents.map(formatDocumentedLibrary) }),
|
|
195
|
-
chunks: manifest.chunks
|
|
209
|
+
chunks: manifest.chunks.map((chunk) =>
|
|
210
|
+
chunk.documents === undefined
|
|
211
|
+
? chunk
|
|
212
|
+
: { ...chunk, documents: chunk.documents.map(formatDocumentedLibrary) },
|
|
213
|
+
),
|
|
196
214
|
},
|
|
197
215
|
null,
|
|
198
216
|
2,
|