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.
Files changed (89) hide show
  1. package/dist/agent.d.ts +48 -0
  2. package/dist/agent.d.ts.map +1 -0
  3. package/dist/agent.js +243 -0
  4. package/dist/agent.js.map +1 -0
  5. package/dist/artifact.d.ts +32 -0
  6. package/dist/artifact.d.ts.map +1 -0
  7. package/dist/artifact.js +78 -0
  8. package/dist/artifact.js.map +1 -0
  9. package/dist/build.d.ts.map +1 -1
  10. package/dist/build.js +75 -9
  11. package/dist/build.js.map +1 -1
  12. package/dist/changed.d.ts +31 -0
  13. package/dist/changed.d.ts.map +1 -0
  14. package/dist/changed.js +71 -0
  15. package/dist/changed.js.map +1 -0
  16. package/dist/cli.js +131 -10
  17. package/dist/cli.js.map +1 -1
  18. package/dist/coverage.d.ts +35 -0
  19. package/dist/coverage.d.ts.map +1 -0
  20. package/dist/coverage.js +64 -0
  21. package/dist/coverage.js.map +1 -0
  22. package/dist/db.d.ts +38 -2
  23. package/dist/db.d.ts.map +1 -1
  24. package/dist/db.js +109 -6
  25. package/dist/db.js.map +1 -1
  26. package/dist/discovery.d.ts +14 -0
  27. package/dist/discovery.d.ts.map +1 -1
  28. package/dist/discovery.js +31 -6
  29. package/dist/discovery.js.map +1 -1
  30. package/dist/doctor.d.ts.map +1 -1
  31. package/dist/doctor.js +90 -3
  32. package/dist/doctor.js.map +1 -1
  33. package/dist/document.d.ts +2 -0
  34. package/dist/document.d.ts.map +1 -1
  35. package/dist/document.js +7 -3
  36. package/dist/document.js.map +1 -1
  37. package/dist/help.d.ts.map +1 -1
  38. package/dist/help.js +65 -4
  39. package/dist/help.js.map +1 -1
  40. package/dist/index.d.ts +8 -3
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +7 -2
  43. package/dist/index.js.map +1 -1
  44. package/dist/mcp.d.ts.map +1 -1
  45. package/dist/mcp.js +3 -0
  46. package/dist/mcp.js.map +1 -1
  47. package/dist/preview.d.ts.map +1 -1
  48. package/dist/preview.js +1 -0
  49. package/dist/preview.js.map +1 -1
  50. package/dist/search.d.ts +17 -3
  51. package/dist/search.d.ts.map +1 -1
  52. package/dist/search.js +116 -21
  53. package/dist/search.js.map +1 -1
  54. package/dist/spec.d.ts +6 -0
  55. package/dist/spec.d.ts.map +1 -1
  56. package/dist/spec.js +12 -3
  57. package/dist/spec.js.map +1 -1
  58. package/dist/surface.d.ts +45 -0
  59. package/dist/surface.d.ts.map +1 -0
  60. package/dist/surface.js +208 -0
  61. package/dist/surface.js.map +1 -0
  62. package/dist/sync.d.ts +8 -1
  63. package/dist/sync.d.ts.map +1 -1
  64. package/dist/sync.js +50 -1
  65. package/dist/sync.js.map +1 -1
  66. package/dist/verify.d.ts +9 -0
  67. package/dist/verify.d.ts.map +1 -1
  68. package/dist/verify.js +55 -27
  69. package/dist/verify.js.map +1 -1
  70. package/package.json +1 -1
  71. package/src/agent.ts +308 -0
  72. package/src/artifact.ts +110 -0
  73. package/src/build.ts +103 -10
  74. package/src/changed.ts +99 -0
  75. package/src/cli.ts +167 -10
  76. package/src/coverage.ts +96 -0
  77. package/src/db.ts +168 -7
  78. package/src/discovery.ts +40 -5
  79. package/src/doctor.ts +100 -4
  80. package/src/document.ts +13 -4
  81. package/src/help.ts +65 -4
  82. package/src/index.ts +30 -0
  83. package/src/mcp.ts +3 -0
  84. package/src/preview.ts +1 -0
  85. package/src/search.ts +158 -24
  86. package/src/spec.ts +21 -3
  87. package/src/surface.ts +265 -0
  88. package/src/sync.ts +66 -2
  89. 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: [["--force", "re-index packages already in the store"]],
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
- examples: ["docspack list", "docspack list --json"],
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 the package documents, from its manifest. A docs package version cannot always
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 hits = options.store
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: options.maxTokens ?? DEFAULT_MAX_TOKENS,
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): QueryHit => {
77
- const { name, version } = splitPackageId(hit.packageId);
78
- const documents = documented.get(hit.packageId);
79
- return {
80
- ...hit,
81
- name,
82
- version,
83
- trusted: !isCommunityPackage(name),
84
- ...(documents === undefined ? {} : { documents }),
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
- return [
124
- `## ${hit.chunkId}${trust}`,
125
- `Source: ${hit.name}@${hit.version} — ${hit.filePath}${describes}`,
126
- "",
127
- hit.content,
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 documents = parseDocuments(root.documents, fail);
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 { name, version, ...(documents === undefined ? {} : { documents }), chunks };
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,