@optique/core 1.4.0-dev.2899 → 1.4.0-dev.2935

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.
@@ -0,0 +1,98 @@
1
+ import { HiddenVisibility } from "./usage.js";
2
+ import { DocPage } from "./doc.js";
3
+ import { Mode, Parser } from "./internal/parser.js";
4
+
5
+ //#region src/doc-pages.d.ts
6
+
7
+ /** A command page with an explicit canonical path. @since 1.4.0 */
8
+ interface CommandDocPage {
9
+ /** Canonical tokens, excluding program name and positional values. */
10
+ readonly path: readonly string[];
11
+ /** Constructed documentation, with command tokens already in usage. */
12
+ readonly page: DocPage;
13
+ /** Visible aliases of the final command token. */
14
+ readonly aliases?: readonly string[];
15
+ /** Accepted alternative names reserved for validation, never display. */
16
+ readonly hiddenAliases?: readonly string[];
17
+ /** Documentation visibility; hidden ancestors suppress descendants. */
18
+ readonly hidden?: HiddenVisibility;
19
+ /** Default alias display for this command. */
20
+ readonly showAliases?: boolean;
21
+ }
22
+ /** An author-supplied replacement or additional page. @since 1.4.0 */
23
+ interface SuppliedCommandDocPage extends CommandDocPage {
24
+ /**
25
+ * A page assertion covers this page only. A subtree assertion declares that
26
+ * the author supplied every visible descendant too; opaque descendants cannot
27
+ * be verified independently. Defaults to page.
28
+ */
29
+ readonly coverage?: "page" | "subtree";
30
+ }
31
+ /** A collected page and the origin of its documentation. @since 1.4.0 */
32
+ interface CollectedCommandDocPage extends CommandDocPage {
33
+ /** Whether metadata or the author provided this page. */
34
+ readonly source: "automatic" | "supplied";
35
+ /** Coverage asserted by the author, for supplied pages only. */
36
+ readonly coverage?: "page" | "subtree";
37
+ }
38
+ /** A visible scope whose documentation could not be collected. @since 1.4.0 */
39
+ interface DocPageCollectionGap {
40
+ /** Affected page or visible enclosing opaque scope; never positional values. */
41
+ readonly path: readonly string[];
42
+ /** Whether missing information affects this page or an opaque subtree. */
43
+ readonly scope: "page" | "subtree";
44
+ /** A stable diagnostic for the unsupported operation. */
45
+ readonly reason: "unregistered-parser" | "value-metadata-unavailable" | "static-default-format-unavailable" | "functional-usage" | "ambiguous-command-path" | "missing-command-page";
46
+ }
47
+ /** Result data for the supplied parser, without runner augmentation. @since 1.4.0 */
48
+ interface DocPageCollectionResult {
49
+ /** Root and supported commands in definition order, followed by additions. */
50
+ readonly entries: readonly CollectedCommandDocPage[];
51
+ /** Uncovered visible scopes; an empty list does not inspect author imports. */
52
+ readonly unresolved: readonly DocPageCollectionGap[];
53
+ /** Runner-added help/version/completion are outside this parser scope. */
54
+ readonly scope: "parser";
55
+ }
56
+ /** Options for metadata-only collection. @since 1.4.0 */
57
+ interface DocPageCollectionOptions {
58
+ /** Throw for uncovered scopes. Defaults to true. */
59
+ readonly strict?: boolean;
60
+ /** Replacements win at their canonical paths; duplicate supplied paths fail. */
61
+ readonly entries?: readonly SuppliedCommandDocPage[];
62
+ }
63
+ /** Strict collection failed with explicit partial result data. @since 1.4.0 */
64
+ declare class DocPageCollectionError extends TypeError {
65
+ /** Collected pages and every unresolved visible scope. */
66
+ readonly result: DocPageCollectionResult;
67
+ /**
68
+ * Constructs an error from a partial collection.
69
+ * @param result Collected pages and gaps.
70
+ */
71
+ constructor(result: DocPageCollectionResult);
72
+ }
73
+ /**
74
+ * Collects documentation through core-owned constructor metadata, without
75
+ * parsing, defaults, normalization, validators, sources, suggestions, handlers,
76
+ * or custom parser/value-parser documentation/formatting methods.
77
+ *
78
+ * Supported constructors are command/or/object/seq, option/flag/argument,
79
+ * optional/multiple/withDefault; safe value metadata comes from string/integer/
80
+ * choice. Unregistered wrappers are opaque. Custom value parsers retain only
81
+ * enclosing primitive metadata captured at construction. Function defaults are
82
+ * omitted unless withDefault supplies a documentation message. Unsupported
83
+ * static default formatting is reported instead of evaluated.
84
+ *
85
+ * Imports and definition/page construction remain application-owned and are
86
+ * not sandboxed. Supply inert definitions separately from executable startup.
87
+ *
88
+ * @param parser The parser definition to document.
89
+ * @param options Strictness and author-supplied page/subtree replacements.
90
+ * @returns Canonical pages and explicit unresolved scopes for the user parser.
91
+ * @throws {DocPageCollectionError} If strict mode finds unresolved scopes.
92
+ * @throws {TypeError} If supplied paths/aliases are invalid or ambiguous,
93
+ * or supplied canonical paths are duplicated.
94
+ * @since 1.4.0
95
+ */
96
+ declare function collectDocPages<M extends Mode, T, S>(parser: Parser<M, T, S>, options?: DocPageCollectionOptions): DocPageCollectionResult;
97
+ //#endregion
98
+ export { CollectedCommandDocPage, CommandDocPage, DocPageCollectionError, DocPageCollectionGap, DocPageCollectionOptions, DocPageCollectionResult, SuppliedCommandDocPage, collectDocPages };
@@ -0,0 +1,479 @@
1
+ import { validateCommandNames } from "./validate.js";
2
+ import { cloneUsageTerm, isDocHidden, mergeHidden } from "./usage.js";
3
+ import { cloneMessage } from "./message.js";
4
+ import { cloneDocEntry, deduplicateDocFragments } from "./doc.js";
5
+ import { getDocNode, getDocValue } from "./internal/doc-metadata.js";
6
+
7
+ //#region src/doc-pages.ts
8
+ /** Strict collection failed with explicit partial result data. @since 1.4.0 */
9
+ var DocPageCollectionError = class extends TypeError {
10
+ /** Collected pages and every unresolved visible scope. */
11
+ result;
12
+ /**
13
+ * Constructs an error from a partial collection.
14
+ * @param result Collected pages and gaps.
15
+ */
16
+ constructor(result) {
17
+ super("Documentation collection has unresolved scopes.");
18
+ this.name = "DocPageCollectionError";
19
+ this.result = result;
20
+ }
21
+ };
22
+ const emptyProjection = {
23
+ usage: [],
24
+ fragments: []
25
+ };
26
+ function key(path) {
27
+ if (path.length > 0) validateCommandNames(path, "Documentation path");
28
+ if (path.some((token) => !token.isWellFormed())) throw new TypeError("Documentation paths must contain well-formed Unicode tokens.");
29
+ return JSON.stringify(path);
30
+ }
31
+ function prefix(parent, child) {
32
+ return parent.length <= child.length && parent.every((token, index) => token === child[index]);
33
+ }
34
+ function hasPath(parser, path) {
35
+ const node = getDocNode(parser);
36
+ if (node == null || path.length === 0 || isDocHidden(node.hidden) || ambiguousSequence(node)) return false;
37
+ if (node.kind === "command") return !isDocHidden(node.hidden) && node.name === path[0] && (path.length === 1 || hasPath(node.children[0], path.slice(1)));
38
+ return node.children?.some((child) => hasPath(child, path)) ?? false;
39
+ }
40
+ function hasCommands(parser) {
41
+ const node = getDocNode(parser);
42
+ if (node == null || isDocHidden(node.hidden)) return false;
43
+ return node.kind === "command" || node.children?.some(hasCommands) === true;
44
+ }
45
+ function ambiguousSequence(node) {
46
+ return node.kind === "seq" && (node.children?.filter(hasCommands).length ?? 0) > 1;
47
+ }
48
+ function hideUsage(usage, pathTerms) {
49
+ return usage.map((term) => {
50
+ if (pathTerms.includes(term)) return term;
51
+ if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") return {
52
+ ...term,
53
+ terms: hideUsage(term.terms, pathTerms)
54
+ };
55
+ if (term.type === "exclusive") return {
56
+ ...term,
57
+ terms: term.terms.map((terms) => hideUsage(terms, pathTerms))
58
+ };
59
+ if (term.type === "option" || term.type === "argument" || term.type === "command" || term.type === "passthrough") return {
60
+ ...term,
61
+ hidden: mergeHidden(term.hidden, "usage")
62
+ };
63
+ return term;
64
+ });
65
+ }
66
+ function leafTerm(usage) {
67
+ for (const term of usage) if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") {
68
+ const found = leafTerm(term.terms);
69
+ if (found != null) return found;
70
+ } else return term;
71
+ return void 0;
72
+ }
73
+ function staticDefault(parser, value) {
74
+ const node = getDocNode(parser);
75
+ if (node == null) return void 0;
76
+ if (node.kind !== "leaf") {
77
+ const preservesValue = node.kind === "command" || node.kind === "or" || node.kind === "optional" || node.kind === "default";
78
+ return preservesValue && node.children?.length === 1 ? staticDefault(node.children[0], value) : void 0;
79
+ }
80
+ if (node.boolean && typeof value === "boolean") return [{
81
+ type: "text",
82
+ text: String(value)
83
+ }];
84
+ const metadata = node.valueParser == null ? void 0 : getDocValue(node.valueParser);
85
+ if (metadata == null || metadata.canFormatDefault === false || typeof value !== metadata.kind) return void 0;
86
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "bigint") return void 0;
87
+ return [{
88
+ type: "text",
89
+ text: metadata.preserveNegativeZero && Object.is(value, -0) ? "-0" : String(value)
90
+ }];
91
+ }
92
+ function fragmentsEntries(fragments) {
93
+ return fragments.flatMap((fragment) => fragment.type === "entry" ? [fragment] : fragment.entries);
94
+ }
95
+ function defaultFragments(fragments, value) {
96
+ return fragments.map((fragment) => fragment.type === "entry" ? {
97
+ ...fragment,
98
+ default: value
99
+ } : {
100
+ ...fragment,
101
+ entries: fragment.entries.map((entry) => ({
102
+ ...entry,
103
+ default: value
104
+ }))
105
+ });
106
+ }
107
+ function sections(fragments) {
108
+ const result = [];
109
+ const named = /* @__PURE__ */ new Map();
110
+ for (const fragment of deduplicateDocFragments(fragments)) {
111
+ const title = fragment.type === "entry" ? void 0 : fragment.title;
112
+ let entries = named.get(title);
113
+ if (entries == null) {
114
+ entries = [];
115
+ named.set(title, entries);
116
+ result.push({
117
+ title,
118
+ entries
119
+ });
120
+ }
121
+ entries.push(...(fragment.type === "entry" ? [fragment] : fragment.entries).map(cloneDocEntry));
122
+ }
123
+ return result;
124
+ }
125
+ function publicUsage(usage) {
126
+ return usage.map((term) => {
127
+ if (term.type === "command") return {
128
+ type: "command",
129
+ name: term.name,
130
+ ...term.hidden != null && { hidden: term.hidden }
131
+ };
132
+ if (term.type === "exclusive") return {
133
+ ...term,
134
+ terms: term.terms.map(publicUsage)
135
+ };
136
+ if (term.type === "optional" || term.type === "multiple" || term.type === "sequence") return {
137
+ ...term,
138
+ terms: publicUsage(term.terms)
139
+ };
140
+ return cloneUsageTerm(term);
141
+ });
142
+ }
143
+ function docHidden(entry, entries) {
144
+ for (let length = 1; length <= entry.path.length; length++) if (isDocHidden(entries.get(key(entry.path.slice(0, length)))?.hidden)) return true;
145
+ return false;
146
+ }
147
+ /**
148
+ * Collects documentation through core-owned constructor metadata, without
149
+ * parsing, defaults, normalization, validators, sources, suggestions, handlers,
150
+ * or custom parser/value-parser documentation/formatting methods.
151
+ *
152
+ * Supported constructors are command/or/object/seq, option/flag/argument,
153
+ * optional/multiple/withDefault; safe value metadata comes from string/integer/
154
+ * choice. Unregistered wrappers are opaque. Custom value parsers retain only
155
+ * enclosing primitive metadata captured at construction. Function defaults are
156
+ * omitted unless withDefault supplies a documentation message. Unsupported
157
+ * static default formatting is reported instead of evaluated.
158
+ *
159
+ * Imports and definition/page construction remain application-owned and are
160
+ * not sandboxed. Supply inert definitions separately from executable startup.
161
+ *
162
+ * @param parser The parser definition to document.
163
+ * @param options Strictness and author-supplied page/subtree replacements.
164
+ * @returns Canonical pages and explicit unresolved scopes for the user parser.
165
+ * @throws {DocPageCollectionError} If strict mode finds unresolved scopes.
166
+ * @throws {TypeError} If supplied paths/aliases are invalid or ambiguous,
167
+ * or supplied canonical paths are duplicated.
168
+ * @since 1.4.0
169
+ */
170
+ function collectDocPages(parser, options = {}) {
171
+ const discovered = new Map([["[]", { path: [] }]]);
172
+ const reports = /* @__PURE__ */ new Map();
173
+ const report = (path, scope, reason) => {
174
+ const gap = {
175
+ path: [...path],
176
+ scope,
177
+ reason
178
+ };
179
+ reports.set(JSON.stringify(gap), gap);
180
+ };
181
+ function enumerate(input, path) {
182
+ const node = getDocNode(input);
183
+ if (node == null) {
184
+ report(path, "subtree", "unregistered-parser");
185
+ return;
186
+ }
187
+ if (isDocHidden(node.hidden)) return;
188
+ if (ambiguousSequence(node)) {
189
+ report(path, "subtree", "ambiguous-command-path");
190
+ return;
191
+ }
192
+ if (node.kind === "command") {
193
+ if (isDocHidden(node.hidden)) return;
194
+ const childPath = [...path, node.name];
195
+ const pathKey = key(childPath);
196
+ if (discovered.has(pathKey)) report(childPath, "subtree", "ambiguous-command-path");
197
+ else discovered.set(pathKey, {
198
+ path: childPath,
199
+ node
200
+ });
201
+ enumerate(node.children[0], childPath);
202
+ } else for (const child of node.children ?? []) enumerate(child, path);
203
+ }
204
+ enumerate(parser, []);
205
+ function project(input, target, path, pagePath) {
206
+ const node = getDocNode(input);
207
+ if (node == null) {
208
+ report(path, "subtree", "unregistered-parser");
209
+ if (key(path) !== key(pagePath)) report(pagePath, "page", "unregistered-parser");
210
+ return {
211
+ ...emptyProjection,
212
+ unresolvedUsage: true
213
+ };
214
+ }
215
+ if (isDocHidden(node.hidden)) return {
216
+ ...emptyProjection,
217
+ usage: node.usage
218
+ };
219
+ if (ambiguousSequence(node)) {
220
+ report(path, "subtree", "ambiguous-command-path");
221
+ if (key(path) !== key(pagePath)) report(pagePath, "page", "ambiguous-command-path");
222
+ return {
223
+ ...emptyProjection,
224
+ unresolvedUsage: true
225
+ };
226
+ }
227
+ if (node.kind === "leaf") {
228
+ const term = leafTerm(node.usage);
229
+ if (term == null || "hidden" in term && isDocHidden(term.hidden)) return {
230
+ ...emptyProjection,
231
+ usage: node.usage
232
+ };
233
+ const metadata = node.valueParser == null ? void 0 : getDocValue(node.valueParser);
234
+ if (node.valueParser != null && metadata == null) report(pagePath, "page", "value-metadata-unavailable");
235
+ const choices = metadata?.choices == null ? void 0 : [{
236
+ type: "text",
237
+ text: metadata.choices.map((value) => metadata.preserveNegativeZero && Object.is(value, -0) ? "-0" : String(value)).join(", ")
238
+ }];
239
+ return {
240
+ usage: node.usage,
241
+ fragments: [{
242
+ type: "entry",
243
+ term,
244
+ description: node.description,
245
+ choices
246
+ }]
247
+ };
248
+ }
249
+ if (node.kind === "command") {
250
+ if (isDocHidden(node.hidden)) return {
251
+ ...emptyProjection,
252
+ usage: node.usage
253
+ };
254
+ if (target.length === 0) return {
255
+ usage: node.usage,
256
+ fragments: [{
257
+ type: "entry",
258
+ term: {
259
+ type: "command",
260
+ name: node.name,
261
+ aliases: node.aliases,
262
+ hidden: node.hidden
263
+ },
264
+ description: node.brief ?? node.description,
265
+ showAliases: node.showAliases
266
+ }]
267
+ };
268
+ if (target[0] !== node.name) return emptyProjection;
269
+ const selected = project(node.children[0], target.slice(1), [...path, node.name], pagePath);
270
+ let usage$1 = selected.usage;
271
+ let unresolvedUsage = selected.unresolvedUsage;
272
+ if (target.length === 1 && node.usageLine != null) if (typeof node.usageLine === "function") {
273
+ report([...path, node.name], "page", "functional-usage");
274
+ unresolvedUsage = true;
275
+ } else {
276
+ usage$1 = node.usageLine;
277
+ unresolvedUsage = false;
278
+ }
279
+ const pathTerm = {
280
+ type: "command",
281
+ name: node.name
282
+ };
283
+ return {
284
+ ...selected,
285
+ usage: [pathTerm, ...usage$1],
286
+ pathTerms: [pathTerm, ...selected.pathTerms ?? []],
287
+ unresolvedUsage,
288
+ brief: selected.brief ?? node.brief,
289
+ description: selected.description ?? node.description,
290
+ footer: selected.footer ?? node.footer,
291
+ showUsage: selected.showUsage ?? node.showUsage
292
+ };
293
+ }
294
+ if (node.kind === "optional" || node.kind === "multiple" || node.kind === "default") {
295
+ const inner = project(node.children[0], target, path, pagePath);
296
+ if (node.kind === "default") {
297
+ const usage$1 = [{
298
+ type: "optional",
299
+ terms: inner.usage
300
+ }];
301
+ if (node.defaultMessage != null) return {
302
+ ...inner,
303
+ usage: usage$1,
304
+ fragments: defaultFragments(inner.fragments, node.defaultMessage)
305
+ };
306
+ if (typeof node.defaultValue !== "function" && fragmentsEntries(inner.fragments).length > 0) {
307
+ const formatted = staticDefault(node.children[0], node.defaultValue);
308
+ if (formatted != null) return {
309
+ ...inner,
310
+ usage: usage$1,
311
+ fragments: defaultFragments(inner.fragments, formatted)
312
+ };
313
+ report(pagePath, "page", "static-default-format-unavailable");
314
+ }
315
+ return {
316
+ ...inner,
317
+ usage: usage$1
318
+ };
319
+ }
320
+ return {
321
+ ...inner,
322
+ usage: [node.kind === "optional" ? {
323
+ type: "optional",
324
+ terms: inner.usage
325
+ } : {
326
+ ...node.usage.find((term) => term.type === "multiple"),
327
+ type: "multiple",
328
+ terms: inner.usage,
329
+ min: node.min ?? 0
330
+ }]
331
+ };
332
+ }
333
+ const children = node.children ?? [];
334
+ if (node.kind === "or") {
335
+ if (target.length > 0) {
336
+ const selected = children.filter((child) => hasPath(child, target));
337
+ if (selected.length !== 1) {
338
+ report(path, "subtree", "ambiguous-command-path");
339
+ if (key(path) !== key(pagePath)) report(pagePath, "page", "ambiguous-command-path");
340
+ return {
341
+ ...emptyProjection,
342
+ unresolvedUsage: true
343
+ };
344
+ }
345
+ return project(selected[0], target, path, pagePath);
346
+ }
347
+ const projected$1 = children.map((child) => project(child, [], path, pagePath));
348
+ return {
349
+ usage: node.usage,
350
+ fragments: projected$1.flatMap((child) => child.fragments),
351
+ unresolvedUsage: projected$1.some((child) => child.unresolvedUsage)
352
+ };
353
+ }
354
+ const projected = children.map((child) => project(child, hasPath(child, target) ? target : [], path, pagePath));
355
+ const fragments = projected.flatMap((child) => child.fragments);
356
+ const pathTerms = projected.flatMap((child) => child.pathTerms ?? []);
357
+ const usage = node.kind === "seq" ? [{
358
+ type: "sequence",
359
+ terms: projected.flatMap((child) => child.usage)
360
+ }] : projected.flatMap((child) => child.usage);
361
+ return {
362
+ usage: node.hidden === "usage" ? hideUsage(usage, pathTerms) : usage,
363
+ pathTerms,
364
+ fragments: node.label == null ? fragments : [...fragments.filter((fragment) => fragment.type === "section" && fragment.title != null), {
365
+ type: "section",
366
+ title: node.label,
367
+ entries: fragmentsEntries(fragments.filter((fragment) => fragment.type === "entry" || fragment.title == null))
368
+ }],
369
+ unresolvedUsage: projected.some((child) => child.unresolvedUsage),
370
+ brief: projected.find((child) => child.brief != null)?.brief,
371
+ description: projected.find((child) => child.description != null)?.description,
372
+ footer: projected.find((child) => child.footer != null)?.footer,
373
+ showUsage: projected.find((child) => child.showUsage != null)?.showUsage
374
+ };
375
+ }
376
+ const entries = /* @__PURE__ */ new Map();
377
+ for (const { path, node } of discovered.values()) {
378
+ const page = project(parser, path, [], path);
379
+ entries.set(key(path), {
380
+ path,
381
+ aliases: node?.aliases?.filter((alias) => !node.hiddenAliases?.includes(alias)),
382
+ hiddenAliases: node?.hiddenAliases?.slice(),
383
+ hidden: node?.hidden,
384
+ showAliases: node?.showAliases,
385
+ source: "automatic",
386
+ page: {
387
+ usage: page.unresolvedUsage ? void 0 : publicUsage(page.usage),
388
+ sections: sections(page.fragments),
389
+ brief: page.brief == null ? void 0 : cloneMessage(page.brief),
390
+ description: page.description == null ? void 0 : cloneMessage(page.description),
391
+ footer: page.footer == null ? void 0 : cloneMessage(page.footer),
392
+ showUsage: page.showUsage
393
+ }
394
+ });
395
+ }
396
+ const supplied = /* @__PURE__ */ new Map();
397
+ for (const entry of options.entries ?? []) {
398
+ const entryKey = key(entry.path);
399
+ if (supplied.has(entryKey)) throw new TypeError(`Duplicate supplied command path: ${entryKey}`);
400
+ if (entry.aliases?.length || entry.hiddenAliases?.length) validateCommandNames([...entry.aliases ?? [], ...entry.hiddenAliases ?? []], "Documentation alias");
401
+ if (entry.path.length === 0 && (entry.aliases?.length || entry.hiddenAliases?.length || entry.hidden)) throw new TypeError("The root page cannot have aliases or hidden visibility.");
402
+ if (entry.coverage != null && entry.coverage !== "page" && entry.coverage !== "subtree") throw new TypeError("Supplied coverage must be page or subtree.");
403
+ supplied.set(entryKey, entry);
404
+ entries.set(entryKey, {
405
+ ...entries.get(entryKey),
406
+ ...entry,
407
+ path: [...entry.path],
408
+ source: "supplied",
409
+ coverage: entry.coverage ?? "page"
410
+ });
411
+ }
412
+ const visible = [...entries.values()].filter((entry) => !docHidden(entry, entries));
413
+ const unresolved = [...reports.values()].filter((gap) => {
414
+ const enclosing = entries.get(key(gap.path));
415
+ if (enclosing != null && docHidden(enclosing, entries)) return false;
416
+ if (gap.reason === "functional-usage") return supplied.get(key(gap.path))?.page.usage == null;
417
+ if (!supplied.has(key(gap.path))) return true;
418
+ if (gap.scope === "page") return false;
419
+ return ![...supplied.values()].some((entry) => entry.coverage === "subtree" && prefix(entry.path, gap.path));
420
+ });
421
+ const missing = /* @__PURE__ */ new Map();
422
+ const spellings = /* @__PURE__ */ new Map();
423
+ for (const entry of entries.values()) {
424
+ if (entry.path.length === 0) continue;
425
+ for (const spelling of [
426
+ entry.path.at(-1),
427
+ ...entry.aliases ?? [],
428
+ ...entry.hiddenAliases ?? []
429
+ ]) {
430
+ const spellingKey = key([...entry.path.slice(0, -1), spelling]);
431
+ const previous = spellings.get(spellingKey);
432
+ if (previous != null && previous !== key(entry.path)) throw new TypeError(`Ambiguous command spelling: ${spellingKey}`);
433
+ spellings.set(spellingKey, key(entry.path));
434
+ }
435
+ }
436
+ for (const entry of visible) {
437
+ for (let length = 0; length < entry.path.length; length++) {
438
+ const parent = entry.path.slice(0, length);
439
+ if (!entries.has(key(parent))) missing.set(key(parent), parent);
440
+ }
441
+ for (const item of entry.page.sections.flatMap((section) => section.entries)) {
442
+ if (item.term.type !== "command" || isDocHidden(item.term.hidden)) continue;
443
+ const child = [...entry.path, item.term.name];
444
+ if (!entries.has(key(child))) missing.set(key(child), child);
445
+ }
446
+ }
447
+ for (const path of missing.values()) unresolved.push({
448
+ path,
449
+ scope: "page",
450
+ reason: "missing-command-page"
451
+ });
452
+ const filtered = visible.map((entry) => {
453
+ const pageSections = entry.page.sections.map((section) => ({
454
+ ...section,
455
+ entries: section.entries.filter((item) => {
456
+ if (item.term.type !== "command") return true;
457
+ const target = entries.get(key([...entry.path, item.term.name]));
458
+ return target == null || !docHidden(target, entries);
459
+ })
460
+ }));
461
+ return pageSections.some((section, index) => section.entries.length !== entry.page.sections[index].entries.length) ? {
462
+ ...entry,
463
+ page: {
464
+ ...entry.page,
465
+ sections: pageSections
466
+ }
467
+ } : entry;
468
+ });
469
+ const result = {
470
+ entries: filtered,
471
+ unresolved,
472
+ scope: "parser"
473
+ };
474
+ if ((options.strict ?? true) && unresolved.length > 0) throw new DocPageCollectionError(result);
475
+ return result;
476
+ }
477
+
478
+ //#endregion
479
+ export { DocPageCollectionError, collectDocPages };
package/dist/facade.cjs CHANGED
@@ -818,7 +818,7 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
818
818
  options = optionsParam ?? {};
819
819
  }
820
820
  require_validate.validateProgramName(programName);
821
- const { colors, messageFormatter, theme: requestedTheme, maxWidth: requestedMaxWidth, termWidth, showDefault, showChoices, showAliases, showEnvironment, sectionOrder, helpSections, showUsage, usageLine, commandList = "recursive", aboveError = "usage", onError = () => {
821
+ const { colors, messageFormatter, theme: requestedTheme, maxWidth: requestedMaxWidth, termWidth, showDefault, showChoices, showAliases, showEnvironment, sectionOrder, helpSections, showUsage, usageLine, commandList = "recursive", aboveError = "usage", errorUsage = "root", onError = () => {
822
822
  throw new RunParserError("Failed to parse command line arguments.");
823
823
  }, stderr = console.error, stdout = console.log, brief, description, examples, author, bugs, footer } = options;
824
824
  let maxWidth = requestedMaxWidth;
@@ -1117,8 +1117,15 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
1117
1117
  })));
1118
1118
  }
1119
1119
  if (effectiveAboveError === "usage") {
1120
- resolveUsageWidth(augmentedParser.usage);
1121
- stderr(`${usageLabel()}${indentLines(require_usage.formatUsage(programName, augmentedParser.usage, {
1120
+ const rootUsage = augmentedParser.usage;
1121
+ const optionCount = Number(helpParsers.helpOption != null) + Number(versionParsers.versionOption != null);
1122
+ const alternativeCount = optionCount + Number(helpParsers.helpCommand != null) + Number(versionParsers.versionCommand != null) + Number(completionParsers.completionCommand != null);
1123
+ const errorUsageTerms = aboveError === "usage" && errorUsage === "command" ? require_usage_internals.projectCommandUsage(rootUsage, classified.commandPath, alternativeCount === 0 ? void 0 : {
1124
+ optionCount,
1125
+ alternativeCount
1126
+ }) ?? rootUsage : rootUsage;
1127
+ resolveUsageWidth(errorUsageTerms);
1128
+ stderr(`${usageLabel()}${indentLines(require_usage.formatUsage(programName, errorUsageTerms, {
1122
1129
  theme,
1123
1130
  colors,
1124
1131
  maxWidth: maxWidth == null ? void 0 : maxWidth - usageLabelWidth(),
package/dist/facade.d.cts CHANGED
@@ -326,6 +326,17 @@ interface RunOptions<THelp, TError> {
326
326
  * @default `"usage"`
327
327
  */
328
328
  readonly aboveError?: "usage" | "help" | "none";
329
+ /**
330
+ * Scope of the compact preamble when `aboveError` is `"usage"`.
331
+ * `"command"` uses the failed parse's canonical command path and usage
332
+ * metadata, retaining parent options. Hidden or unresolved paths and
333
+ * unsupported compositions fall back to root usage. Neither mode applies
334
+ * help-only `usageLine` overrides or evaluates the parser again.
335
+ *
336
+ * @default `"root"`
337
+ * @since 1.4.0
338
+ */
339
+ readonly errorUsage?: "root" | "command";
329
340
  /**
330
341
  * Callback invoked after error output for a parse failure, an invalid
331
342
  * command path before help, or a missing or unsupported completion shell.
package/dist/facade.d.ts CHANGED
@@ -326,6 +326,17 @@ interface RunOptions<THelp, TError> {
326
326
  * @default `"usage"`
327
327
  */
328
328
  readonly aboveError?: "usage" | "help" | "none";
329
+ /**
330
+ * Scope of the compact preamble when `aboveError` is `"usage"`.
331
+ * `"command"` uses the failed parse's canonical command path and usage
332
+ * metadata, retaining parent options. Hidden or unresolved paths and
333
+ * unsupported compositions fall back to root usage. Neither mode applies
334
+ * help-only `usageLine` overrides or evaluates the parser again.
335
+ *
336
+ * @default `"root"`
337
+ * @since 1.4.0
338
+ */
339
+ readonly errorUsage?: "root" | "command";
329
340
  /**
330
341
  * Callback invoked after error output for a parse failure, an invalid
331
342
  * command path before help, or a missing or unsupported completion shell.
package/dist/facade.js CHANGED
@@ -12,7 +12,7 @@ import { collectExplicitSourceValues, collectExplicitSourceValuesAsync, createDe
12
12
  import { createInputTrace } from "./input-trace.js";
13
13
  import { attemptParseAsync, attemptParseSync, createEffectfulCompletionSession, createParserContext, getDocPage } from "./internal/parser.js";
14
14
  import { bash, fish, nu, pwsh, zsh } from "./completion.js";
15
- import { collectActiveOptionNames, collectRootOptionNames } from "./usage-internals.js";
15
+ import { collectActiveOptionNames, collectRootOptionNames, projectCommandUsage } from "./usage-internals.js";
16
16
  import { allowDuplicateLeadingCommandNamesKey, hiddenCommandAliasesKey } from "./internal/command-alias.js";
17
17
  import { completeOrExtractPhase2Seed } from "./phase2-seed.js";
18
18
  import { multiple, optional, withDefault } from "./modifiers.js";
@@ -818,7 +818,7 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
818
818
  options = optionsParam ?? {};
819
819
  }
820
820
  validateProgramName(programName);
821
- const { colors, messageFormatter, theme: requestedTheme, maxWidth: requestedMaxWidth, termWidth, showDefault, showChoices, showAliases, showEnvironment, sectionOrder, helpSections, showUsage, usageLine, commandList = "recursive", aboveError = "usage", onError = () => {
821
+ const { colors, messageFormatter, theme: requestedTheme, maxWidth: requestedMaxWidth, termWidth, showDefault, showChoices, showAliases, showEnvironment, sectionOrder, helpSections, showUsage, usageLine, commandList = "recursive", aboveError = "usage", errorUsage = "root", onError = () => {
822
822
  throw new RunParserError("Failed to parse command line arguments.");
823
823
  }, stderr = console.error, stdout = console.log, brief, description, examples, author, bugs, footer } = options;
824
824
  let maxWidth = requestedMaxWidth;
@@ -1117,8 +1117,15 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
1117
1117
  })));
1118
1118
  }
1119
1119
  if (effectiveAboveError === "usage") {
1120
- resolveUsageWidth(augmentedParser.usage);
1121
- stderr(`${usageLabel()}${indentLines(formatUsage(programName, augmentedParser.usage, {
1120
+ const rootUsage = augmentedParser.usage;
1121
+ const optionCount = Number(helpParsers.helpOption != null) + Number(versionParsers.versionOption != null);
1122
+ const alternativeCount = optionCount + Number(helpParsers.helpCommand != null) + Number(versionParsers.versionCommand != null) + Number(completionParsers.completionCommand != null);
1123
+ const errorUsageTerms = aboveError === "usage" && errorUsage === "command" ? projectCommandUsage(rootUsage, classified.commandPath, alternativeCount === 0 ? void 0 : {
1124
+ optionCount,
1125
+ alternativeCount
1126
+ }) ?? rootUsage : rootUsage;
1127
+ resolveUsageWidth(errorUsageTerms);
1128
+ stderr(`${usageLabel()}${indentLines(formatUsage(programName, errorUsageTerms, {
1122
1129
  theme,
1123
1130
  colors,
1124
1131
  maxWidth: maxWidth == null ? void 0 : maxWidth - usageLabelWidth(),