@astryxdesign/cli 0.6.3-canary.d774400 → 0.6.3-canary.d7eed85

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 (33) hide show
  1. package/api/docs/_adapter.d.mts +49 -7
  2. package/api/docs/_adapter.mjs +284 -33
  3. package/api/docs/docs.doc.mjs +4 -3
  4. package/api/docs/docs.type.d.mts +21 -5
  5. package/api/docs/docs.type.mjs +21 -5
  6. package/api/docs/integration-tree.test.mjs +9 -1
  7. package/api/docs/reference-blocks.test.mjs +406 -0
  8. package/api/doctor/doctor.mjs +5 -1
  9. package/api/integration/authoring-checks.mjs +14 -1
  10. package/assets/docs/styling-libraries.doc.mjs +1 -1
  11. package/assets/docs/theme.doc.mjs +1 -1
  12. package/assets/templates/blocks/components/CheckboxList/CheckboxListSelectAllPattern.doc.mjs +1 -1
  13. package/assets/templates/blocks/components/CheckboxList/CheckboxListSelectAllPattern.tsx +1 -3
  14. package/authoring/doctypes/_schema.d.mts +72 -0
  15. package/authoring/doctypes/_schema.mjs +25 -1
  16. package/authoring/doctypes/load-contract.test.mjs +25 -0
  17. package/authoring/doctypes/reference/reference.doc.mjs +31 -4
  18. package/authoring/doctypes/reference/type.ts +20 -5
  19. package/clients/cli/commands/docs.mjs +2 -2
  20. package/clients/cli/commands/doctor-integration-docs.doc.mjs +1 -1
  21. package/foundation/discovery/authoring-self-docs.d.mts +12 -0
  22. package/foundation/discovery/authoring-self-docs.mjs +20 -8
  23. package/foundation/discovery/authoring-self-docs.test.mjs +7 -2
  24. package/foundation/discovery/docs-discovery.d.mts +4 -1
  25. package/foundation/discovery/docs-discovery.mjs +40 -6
  26. package/foundation/discovery/docs-discovery.test.mjs +40 -0
  27. package/foundation/doc-compiler/lenses.d.mts +5 -3
  28. package/foundation/doc-compiler/lenses.mjs +48 -3
  29. package/foundation/doc-compiler/links.d.mts +30 -3
  30. package/foundation/doc-compiler/links.mjs +46 -5
  31. package/foundation/doc-compiler/links.test.mjs +53 -0
  32. package/foundation/integrations/cli-requirement.test.mjs +17 -42
  33. package/package.json +9 -9
@@ -23,8 +23,9 @@ export function builtinCatalog(): DocsCatalog;
23
23
  /**
24
24
  * One topic, lowered for `lang` with every link between docs resolved
25
25
  * (spec:AST-047 FR9): an inline `{@link <target>}` reads as the command that
26
- * opens its doc, and a `reference` or `workflow` block carries the doc it
27
- * names. Memoized per catalog and frozen, like the lowered node.
26
+ * opens its doc, and a `reference` block carries the doc it names and the
27
+ * `content` it includes of it. Memoized per catalog and frozen, like the
28
+ * lowered node.
28
29
  * @param {DocsCatalog} catalog
29
30
  * @param {DocsTopicEntry} entry
30
31
  * @param {string | null} [lang]
@@ -51,9 +52,8 @@ export function projectTree(catalog: DocsCatalog, { fresh }?: {
51
52
  fresh?: boolean;
52
53
  }): Promise<DocsTree>;
53
54
  /**
54
- * How a doc's links find their targets (spec:AST-047 FR9): a doc in the
55
- * project's docs tree by its identity, or a flat topic by its provider and
56
- * name. A target that matches neither is a problem, never a guess.
55
+ * How a doc's links find their targets (spec:AST-047 FR9), as
56
+ * {@link docFinder} finds them: each resolves to the link a read shows.
57
57
  * @param {DocsCatalog} catalog
58
58
  * @param {string} fromProvider the provider id of the doc the links sit in
59
59
  * @returns {Promise<LinkResolver>}
@@ -64,11 +64,15 @@ export function linkResolver(catalog: DocsCatalog, fromProvider: string): Promis
64
64
  * guide the tree places, and each typed doc.
65
65
  * @param {DocsCatalog} catalog
66
66
  * @param {DocsTree} tree
67
- * @param {{owner?: string}} [options] `owner`: only the docs this package owns
67
+ * @param {{owner?: string, references?: boolean}} [options] `owner`: only the
68
+ * docs this package owns. `references`: instead of the links, each reference
69
+ * block that cannot include what it names; a reader loses that content,
70
+ * where a link that names no doc still prints as written
68
71
  * @returns {Promise<string[]>}
69
72
  */
70
- export function docsLinkProblems(catalog: DocsCatalog, tree: DocsTree, { owner }?: {
73
+ export function docsLinkProblems(catalog: DocsCatalog, tree: DocsTree, { owner, references }?: {
71
74
  owner?: string;
75
+ references?: boolean;
72
76
  }): Promise<string[]>;
73
77
  /**
74
78
  * What \`astryx doctor integration docs\` checks in one integration's docs: the
@@ -93,6 +97,22 @@ export function packageDocsProblems(integration: {
93
97
  * @returns {(topic: string) => Promise<import('../../foundation/doc-compiler/compile.mjs').CompiledReferenceNode | null>}
94
98
  */
95
99
  export function referenceTargets(catalog: DocsCatalog, lang: string | null): (topic: string) => Promise<import("../../foundation/doc-compiler/compile.mjs").CompiledReferenceNode | null>;
100
+ /**
101
+ * Each reference block in one integration's docs that cannot include what it
102
+ * names (spec:AST-047 FR9): a target that names no doc, a field its schema
103
+ * does not have, or a projection its doc cannot take. A reader would lose
104
+ * that content, so `astryx doctor integration docs` fails on each.
105
+ * @param {{name: string}} integration
106
+ * @param {{records: import('../../foundation/discovery/docs-discovery.mjs').DocsTopicRecord[], namespaces: import('../../foundation/doc-compiler/tree.mjs').TreeNamespaceInput[], guides: import('../../foundation/doc-compiler/tree.mjs').TreeDocInput[]}} discovered
107
+ * @returns {Promise<string[]>}
108
+ */
109
+ export function packageReferenceProblems(integration: {
110
+ name: string;
111
+ }, discovered: {
112
+ records: import("../../foundation/discovery/docs-discovery.mjs").DocsTopicRecord[];
113
+ namespaces: import("../../foundation/doc-compiler/tree.mjs").TreeNamespaceInput[];
114
+ guides: import("../../foundation/doc-compiler/tree.mjs").TreeDocInput[];
115
+ }): Promise<string[]>;
96
116
  /**
97
117
  * One topic, compiled for `lang`: lowered, then every token reference linked.
98
118
  * @param {DocsCatalog} catalog
@@ -245,10 +265,32 @@ export function resolveTopicDocs(topic: unknown, options?: {
245
265
  lang: string | null;
246
266
  entry: import("../../foundation/discovery/docs-discovery.mjs").DocsTopicEntry;
247
267
  }>;
268
+ /**
269
+ * A doc a link found: what a read shows of the link, and the typed doc behind
270
+ * it when a reference block can include it.
271
+ */
272
+ export type FoundDoc = {
273
+ link: import("../../foundation/doc-compiler/links.mjs").DocLink;
274
+ /**
275
+ * the doc's kind
276
+ */
277
+ kind: string;
278
+ /**
279
+ * a
280
+ * schema, command, function, or enum doc: a leaf of the docs tree (`tree`),
281
+ * or a section of `astryx docs authoring`; null for any other kind
282
+ */
283
+ typed: {
284
+ doc: any;
285
+ providerId: string;
286
+ tree: boolean;
287
+ } | null;
288
+ };
248
289
  export type DocsTree = import("../../foundation/doc-compiler/tree.mjs").DocsTree;
249
290
  export type TreeNode = import("../../foundation/doc-compiler/tree.mjs").TreeNode;
250
291
  export type LinkProblem = import("../../foundation/doc-compiler/links.mjs").LinkProblem;
251
292
  export type LinkResolver = import("../../foundation/doc-compiler/links.mjs").LinkResolver;
293
+ export type DocIncluder = import("../../foundation/doc-compiler/links.mjs").DocIncluder;
252
294
  export type DocsTopicEntry = import("../../foundation/discovery/docs-discovery.mjs").DocsTopicEntry;
253
295
  import { DocsCatalog } from '../../foundation/discovery/docs-discovery.mjs';
254
296
  import { AstryxError } from '../error.mjs';
@@ -47,6 +47,12 @@ import {
47
47
  cliDocIndex,
48
48
  cliDocSection,
49
49
  } from '../../foundation/discovery/cli-self-docs.mjs';
50
+ import {
51
+ loadAuthoringSelfDocs,
52
+ schemaFieldTable,
53
+ selfDocSection,
54
+ } from '../../foundation/discovery/authoring-self-docs.mjs';
55
+ import {CLI_PROVIDER_ID} from '../../foundation/identity/providers.mjs';
50
56
  import {AstryxError} from '../error.mjs';
51
57
  import {ERROR_CODES} from '../../foundation/response/error-codes.mjs';
52
58
 
@@ -119,6 +125,7 @@ function lowerRawTopic(catalog, entry, lang = null) {
119
125
  * @typedef {import('../../foundation/doc-compiler/tree.mjs').TreeNode} TreeNode
120
126
  * @typedef {import('../../foundation/doc-compiler/links.mjs').LinkProblem} LinkProblem
121
127
  * @typedef {import('../../foundation/doc-compiler/links.mjs').LinkResolver} LinkResolver
128
+ * @typedef {import('../../foundation/doc-compiler/links.mjs').DocIncluder} DocIncluder
122
129
  * @typedef {import('../../foundation/discovery/docs-discovery.mjs').DocsTopicEntry} DocsTopicEntry
123
130
  */
124
131
 
@@ -146,8 +153,9 @@ function providerOf(entry) {
146
153
  /**
147
154
  * One topic, lowered for `lang` with every link between docs resolved
148
155
  * (spec:AST-047 FR9): an inline `{@link <target>}` reads as the command that
149
- * opens its doc, and a `reference` or `workflow` block carries the doc it
150
- * names. Memoized per catalog and frozen, like the lowered node.
156
+ * opens its doc, and a `reference` block carries the doc it names and the
157
+ * `content` it includes of it. Memoized per catalog and frozen, like the
158
+ * lowered node.
151
159
  * @param {DocsCatalog} catalog
152
160
  * @param {DocsTopicEntry} entry
153
161
  * @param {string | null} [lang]
@@ -183,16 +191,19 @@ function linkTopic(catalog, entry, lang) {
183
191
  if (!linked) {
184
192
  linked = (async () => {
185
193
  const raw = await lowerRawTopic(catalog, entry, lang);
186
- /** @type {Map<string, LinkResolver>} */
187
- const resolvers = new Map();
194
+ /** @type {Map<string, {resolve: LinkResolver, include: DocIncluder}>} */
195
+ const linkers = new Map();
188
196
  /** @param {string} provider */
189
- const resolverFor = async provider => {
190
- let resolve = resolvers.get(provider);
191
- if (!resolve) {
192
- resolve = await linkResolver(catalog, provider);
193
- resolvers.set(provider, resolve);
197
+ const linkerFor = async provider => {
198
+ let linker = linkers.get(provider);
199
+ if (!linker) {
200
+ linker = {
201
+ resolve: await linkResolver(catalog, provider),
202
+ include: await docIncluder(catalog, provider),
203
+ };
204
+ linkers.set(provider, linker);
194
205
  }
195
- return resolve;
206
+ return linker;
196
207
  };
197
208
  /** @type {LinkProblem[]} */
198
209
  const problems = [];
@@ -202,12 +213,14 @@ function linkTopic(catalog, entry, lang) {
202
213
  // base topic's.
203
214
  for (const section of raw.doc.sections) {
204
215
  const provider = raw.sectionProviders?.[section.id];
216
+ const {resolve, include} = await linkerFor(
217
+ provider == null ? providerOf(entry) : normalizeProviderId(provider),
218
+ );
205
219
  const linked = await linkBlocks(
206
220
  section.content,
207
- await resolverFor(
208
- provider == null ? providerOf(entry) : normalizeProviderId(provider),
209
- ),
221
+ resolve,
210
222
  {section: section.id ?? section.title},
223
+ include,
211
224
  );
212
225
  problems.push(...linked.problems);
213
226
  sections.push({...section, content: linked.content});
@@ -332,15 +345,52 @@ function identitiesOf(tree) {
332
345
  return index;
333
346
  }
334
347
 
348
+ /**
349
+ * The CLI's authoring docs, by kind and name. `astryx docs authoring` reads
350
+ * each as one section keyed by its name, so a link to one opens that section.
351
+ * Loaded once per process, like the CLI's tree files.
352
+ * @type {Promise<Map<string, any>> | undefined}
353
+ */
354
+ let authoringDocs;
355
+
356
+ /**
357
+ * The CLI authoring doc a link names, while `astryx docs authoring` is the
358
+ * CLI's own topic.
359
+ * @param {DocsCatalog} catalog
360
+ * @param {string} kind
361
+ * @param {string} name
362
+ * @returns {Promise<any | null>}
363
+ */
364
+ async function authoringDoc(catalog, kind, name) {
365
+ if (catalog.resolve('authoring')?.package !== CLI_PROVIDER_ID) return null;
366
+ authoringDocs ??= loadAuthoringSelfDocs().then(
367
+ ({loaded}) =>
368
+ new Map(loaded.map(({doc}) => [`${doc.type}\u0000${doc.name}`, doc])),
369
+ );
370
+ return (await authoringDocs).get(`${kind}\u0000${name}`) ?? null;
371
+ }
372
+
373
+ /**
374
+ * A doc a link found: what a read shows of the link, and the typed doc behind
375
+ * it when a reference block can include it.
376
+ * @typedef {object} FoundDoc
377
+ * @property {import('../../foundation/doc-compiler/links.mjs').DocLink} link
378
+ * @property {string} kind the doc's kind
379
+ * @property {{doc: any, providerId: string, tree: boolean} | null} typed a
380
+ * schema, command, function, or enum doc: a leaf of the docs tree (`tree`),
381
+ * or a section of `astryx docs authoring`; null for any other kind
382
+ */
383
+
335
384
  /**
336
385
  * How a doc's links find their targets (spec:AST-047 FR9): a doc in the
337
- * project's docs tree by its identity, or a flat topic by its provider and
338
- * name. A target that matches neither is a problem, never a guess.
386
+ * project's docs tree by its identity, a CLI authoring doc (a section of
387
+ * `astryx docs authoring`), or a flat topic by its provider and name. A
388
+ * target that matches none is a problem, never a guess.
339
389
  * @param {DocsCatalog} catalog
340
390
  * @param {string} fromProvider the provider id of the doc the links sit in
341
- * @returns {Promise<LinkResolver>}
391
+ * @returns {Promise<(target: string) => Promise<FoundDoc | {problem: string}>>}
342
392
  */
343
- export async function linkResolver(catalog, fromProvider) {
393
+ async function docFinder(catalog, fromProvider) {
344
394
  const identities = identitiesOf(await projectTree(catalog));
345
395
  return async target => {
346
396
  const parsed = parseLinkTarget(target);
@@ -356,12 +406,36 @@ export async function linkResolver(catalog, fromProvider) {
356
406
  const node = identities.get(identityKey(provider, parsed.kind, parsed.name));
357
407
  if (node) {
358
408
  return {
359
- target,
360
- id: /** @type {string} */ (node.id),
361
- route: node.route,
362
- title: node.title,
363
- summary: node.summary,
364
- command: `astryx docs ${node.route}`,
409
+ link: {
410
+ target,
411
+ id: /** @type {string} */ (node.id),
412
+ route: node.route,
413
+ title: node.title,
414
+ summary: node.summary,
415
+ command: `astryx docs ${node.route}`,
416
+ },
417
+ kind: node.kind,
418
+ typed: node.ref?.selfDoc
419
+ ? {doc: node.ref.selfDoc, providerId: node.providerId, tree: true}
420
+ : null,
421
+ };
422
+ }
423
+ const authored =
424
+ provider === CLI_PROVIDER_ID
425
+ ? await authoringDoc(catalog, parsed.kind, parsed.name)
426
+ : null;
427
+ if (authored) {
428
+ return {
429
+ link: {
430
+ target,
431
+ id: createDocId(provider, authored.type, authored.name),
432
+ route: 'authoring',
433
+ title: authored.displayName ?? authored.name,
434
+ summary: authored.description ?? '',
435
+ command: `astryx docs authoring ${authored.name}`,
436
+ },
437
+ kind: parsed.kind,
438
+ typed: {doc: authored, providerId: CLI_PROVIDER_ID, tree: false},
365
439
  };
366
440
  }
367
441
  if (parsed.kind === 'generic') {
@@ -383,12 +457,16 @@ export async function linkResolver(catalog, fromProvider) {
383
457
  // link still opens it.
384
458
  }
385
459
  return {
386
- target,
387
- id: createDocId(provider, 'generic', parsed.name),
388
- route: entry.name,
389
- title,
390
- summary,
391
- command: `astryx docs ${entry.name}`,
460
+ link: {
461
+ target,
462
+ id: createDocId(provider, 'generic', parsed.name),
463
+ route: entry.name,
464
+ title,
465
+ summary,
466
+ command: `astryx docs ${entry.name}`,
467
+ },
468
+ kind: 'generic',
469
+ typed: null,
392
470
  };
393
471
  }
394
472
  }
@@ -398,20 +476,158 @@ export async function linkResolver(catalog, fromProvider) {
398
476
  };
399
477
  }
400
478
 
479
+ /**
480
+ * How a doc's links find their targets (spec:AST-047 FR9), as
481
+ * {@link docFinder} finds them: each resolves to the link a read shows.
482
+ * @param {DocsCatalog} catalog
483
+ * @param {string} fromProvider the provider id of the doc the links sit in
484
+ * @returns {Promise<LinkResolver>}
485
+ */
486
+ export async function linkResolver(catalog, fromProvider) {
487
+ const find = await docFinder(catalog, fromProvider);
488
+ return async target => {
489
+ const found = await find(target);
490
+ return 'problem' in found ? found : found.link;
491
+ };
492
+ }
493
+
494
+ /** How a problem names a doc kind that a reference block cannot include. */
495
+ const KIND_NAMES = /** @type {Record<string, string>} */ ({
496
+ generic: 'topic',
497
+ namespace: 'namespace',
498
+ });
499
+
500
+ /**
501
+ * How a topic's reference blocks include the docs they name (spec:AST-047
502
+ * FR9): a schema, command, function, or enum doc as `astryx docs` prints
503
+ * it, narrowed by the block's projection and presentation, with the included
504
+ * doc's own links resolved against its own provider. Any other doc shows its
505
+ * title and summary. Each part a block names that it cannot include is a
506
+ * problem, and a read marks where it is missing; a target that names no doc
507
+ * is the resolver's problem.
508
+ * @param {DocsCatalog} catalog
509
+ * @param {string} fromProvider the provider id of the doc the blocks sit in
510
+ * @returns {Promise<DocIncluder>}
511
+ */
512
+ async function docIncluder(catalog, fromProvider) {
513
+ const find = await docFinder(catalog, fromProvider);
514
+ const tree = await projectTree(catalog);
515
+ return async block => {
516
+ const found = await find(block.target);
517
+ if ('problem' in found) return {content: [], problems: []};
518
+ return includedContent(catalog, tree, block, found);
519
+ };
520
+ }
521
+
522
+ /**
523
+ * What one reference block includes of the doc it found.
524
+ * @param {DocsCatalog} catalog
525
+ * @param {DocsTree} tree
526
+ * @param {any} block
527
+ * @param {FoundDoc} found
528
+ * @returns {Promise<{content: any[], problems: string[]}>}
529
+ */
530
+ async function includedContent(catalog, tree, block, found) {
531
+ /** @type {string[]} */
532
+ const problems = [];
533
+ const {fields, sections} = block.projection ?? {};
534
+ const presentation = block.presentation ?? 'full';
535
+ if (sections != null) {
536
+ problems.push(
537
+ 'projection.sections: a reference block does not include topic sections; reference a schema, command, function, or enum doc, and name the fields of a schema with projection.fields',
538
+ );
539
+ }
540
+ const typed = found.typed;
541
+ if (typed == null) {
542
+ // A reference shows any other doc only by its title and summary.
543
+ if (fields != null || (block.presentation ?? 'summary') !== 'summary') {
544
+ problems.push(
545
+ `"${block.target}" is a ${KIND_NAMES[found.kind] ?? `${found.kind} doc`}, which a reference block shows only by its title and summary; remove the projection and the presentation, or reference a schema, command, function, or enum doc`,
546
+ );
547
+ }
548
+ return {content: [], problems};
549
+ }
550
+ if (presentation === 'summary') {
551
+ if (fields != null) {
552
+ problems.push(
553
+ "projection.fields: a summary includes no fields; remove projection.fields or presentation: 'summary'",
554
+ );
555
+ }
556
+ return {content: [], problems};
557
+ }
558
+ /** @type {any[]} */
559
+ let content;
560
+ if (fields != null && typed.doc.type === 'schema') {
561
+ /** @type {Map<string, any>} */
562
+ const byName = new Map(
563
+ (typed.doc.fields ?? []).map((/** @type {any} */ field) => [
564
+ field.name,
565
+ field,
566
+ ]),
567
+ );
568
+ const selected = [];
569
+ /** @type {any[]} */
570
+ const missing = [];
571
+ for (const name of fields) {
572
+ const field = byName.get(name);
573
+ if (field) {
574
+ selected.push(field);
575
+ continue;
576
+ }
577
+ problems.push(
578
+ `projection.fields: "${name}" is not a field of ${found.link.title} (${block.target}). Its fields: ${[...byName.keys()].join(', ')}`,
579
+ );
580
+ missing.push({
581
+ type: 'prose',
582
+ text: `[reference: field "${name}" not found in "${block.target}"]`,
583
+ });
584
+ }
585
+ const table = schemaFieldTable(selected);
586
+ content = [...(table ? [table] : []), ...missing];
587
+ } else {
588
+ if (fields != null) {
589
+ problems.push(
590
+ `projection.fields: names the fields of a schema doc, and "${block.target}" is a ${typed.doc.type} doc; remove it to include the whole doc`,
591
+ );
592
+ }
593
+ content = typed.tree
594
+ ? cliDocSection(typed.doc, typedDocIndex(tree)).content
595
+ : selfDocSection(typed.doc).content;
596
+ }
597
+ if (presentation === 'compact') {
598
+ content = content.filter(each => each?.type !== 'code');
599
+ }
600
+ // The included doc's own links resolve against its own provider. A link in
601
+ // it that names no doc is that doc's problem, reported where it is written.
602
+ const linked = await linkBlocks(
603
+ content,
604
+ await linkResolver(catalog, typed.providerId),
605
+ );
606
+ return {content: linked.content, problems};
607
+ }
608
+
401
609
  /**
402
610
  * Every link in the project's docs that names no doc: in each topic, each
403
611
  * guide the tree places, and each typed doc.
404
612
  * @param {DocsCatalog} catalog
405
613
  * @param {DocsTree} tree
406
- * @param {{owner?: string}} [options] `owner`: only the docs this package owns
614
+ * @param {{owner?: string, references?: boolean}} [options] `owner`: only the
615
+ * docs this package owns. `references`: instead of the links, each reference
616
+ * block that cannot include what it names; a reader loses that content,
617
+ * where a link that names no doc still prints as written
407
618
  * @returns {Promise<string[]>}
408
619
  */
409
- export async function docsLinkProblems(catalog, tree, {owner} = {}) {
620
+ export async function docsLinkProblems(
621
+ catalog,
622
+ tree,
623
+ {owner, references = false} = {},
624
+ ) {
410
625
  /** @type {string[]} */
411
626
  const problems = [];
412
627
  /** @param {string} where @param {LinkProblem[]} found */
413
628
  const note = (where, found) => {
414
629
  for (const problem of found) {
630
+ if ((problem.include === true) !== references) continue;
415
631
  problems.push(
416
632
  `${where}${problem.section ? ` \u00a7 ${problem.section}` : ''}: ${problem.message}`,
417
633
  );
@@ -487,6 +703,28 @@ export function referenceTargets(catalog, lang) {
487
703
  };
488
704
  }
489
705
 
706
+ /**
707
+ * Each reference block in one integration's docs that cannot include what it
708
+ * names (spec:AST-047 FR9): a target that names no doc, a field its schema
709
+ * does not have, or a projection its doc cannot take. A reader would lose
710
+ * that content, so `astryx doctor integration docs` fails on each.
711
+ * @param {{name: string}} integration
712
+ * @param {{records: import('../../foundation/discovery/docs-discovery.mjs').DocsTopicRecord[], namespaces: import('../../foundation/doc-compiler/tree.mjs').TreeNamespaceInput[], guides: import('../../foundation/doc-compiler/tree.mjs').TreeDocInput[]}} discovered
713
+ * @returns {Promise<string[]>}
714
+ */
715
+ export async function packageReferenceProblems(integration, discovered) {
716
+ const catalog = DocsCatalog.fromBuiltins();
717
+ for (const record of discovered.records) catalog.add(record);
718
+ catalog.addTreeInputs({
719
+ namespaces: discovered.namespaces.map(input => ({...input, rank: 1})),
720
+ guides: discovered.guides.map(input => ({...input, rank: 1})),
721
+ });
722
+ return docsLinkProblems(catalog, await projectTree(catalog), {
723
+ owner: integration.name,
724
+ references: true,
725
+ });
726
+ }
727
+
490
728
  /**
491
729
  * One topic, compiled for `lang`: lowered, then every token reference linked.
492
730
  * @param {DocsCatalog} catalog
@@ -535,6 +773,20 @@ const typedDocIndexes = new WeakMap();
535
773
  */
536
774
  export async function nodeContent(catalog, tree, node) {
537
775
  if (!node.ref?.selfDoc) return {content: [], problems: []};
776
+ const resolve = await linkResolver(catalog, node.providerId);
777
+ return linkBlocks(
778
+ cliDocSection(node.ref.selfDoc, typedDocIndex(tree)).content,
779
+ resolve,
780
+ );
781
+ }
782
+
783
+ /**
784
+ * The index a typed doc's content reads its cross-links from: every typed doc
785
+ * in the tree. Built once per tree.
786
+ * @param {DocsTree} tree
787
+ * @returns {ReturnType<typeof cliDocIndex>}
788
+ */
789
+ function typedDocIndex(tree) {
538
790
  let index = typedDocIndexes.get(tree);
539
791
  if (!index) {
540
792
  index = cliDocIndex(
@@ -544,8 +796,7 @@ export async function nodeContent(catalog, tree, node) {
544
796
  );
545
797
  typedDocIndexes.set(tree, index);
546
798
  }
547
- const resolve = await linkResolver(catalog, node.providerId);
548
- return linkBlocks(cliDocSection(node.ref.selfDoc, index).content, resolve);
799
+ return index;
549
800
  }
550
801
 
551
802
  /**
@@ -20,7 +20,8 @@ export const doc = {
20
20
  'ReferenceDoc, and `index: true` returns its section index (each ' +
21
21
  'section\'s key, title, and summary); a topic plus a section returns ' +
22
22
  'that one section. Token-ref blocks are ' +
23
- 'inlined in every read. The topic set is the CLI\'s own docs plus the ' +
23
+ 'inlined in every read, and so is a section\'s reference block: the doc it ' +
24
+ 'includes, then the command that opens that doc. The topic set is the CLI\'s own docs plus the ' +
24
25
  'ones the project\'s configured integrations contribute, including any ' +
25
26
  'topic an integration replaces or extends, so it depends on the cwd. ' +
26
27
  'A route opens a node of the docs tree instead: a namespace such as ' +
@@ -97,7 +98,7 @@ export const doc = {
97
98
  {
98
99
  type: 'docs.detail',
99
100
  description:
100
- "One topic's full ReferenceDoc, with token-ref blocks inlined, plus links.",
101
+ "One topic's full ReferenceDoc, with token-ref and reference blocks inlined, plus links.",
101
102
  },
102
103
  {
103
104
  type: 'docs.index',
@@ -107,7 +108,7 @@ export const doc = {
107
108
  {
108
109
  type: 'docs.detail.section',
109
110
  description:
110
- 'One ReferenceSection of the topic, found by key or title, with token-ref blocks inlined.',
111
+ 'One ReferenceSection of the topic, found by key or title, with token-ref and reference blocks inlined.',
111
112
  },
112
113
  {
113
114
  type: 'docs.node',
@@ -67,15 +67,31 @@ export type DocsDetailResponse = {
67
67
  /**
68
68
  * the whole doc, and the moves from it
69
69
  */
70
- data: import("@astryxdesign/cli/authoring").ReferenceDoc & {
70
+ data: DocsReadDoc & {
71
71
  links: DocsLinks;
72
72
  };
73
73
  };
74
+ /**
75
+ * A topic section as a read returns it. A read inlines each token reference
76
+ * and each `reference` block the author wrote, so its content holds only the
77
+ * stable ReferenceContentBlock kinds.
78
+ */
79
+ export type DocsReadSection = Omit<import("@astryxdesign/cli/authoring").ReferenceSection, "content"> & {
80
+ content: import("@astryxdesign/cli/authoring").ReferenceContentBlock[];
81
+ };
82
+ /**
83
+ * A topic as a read returns it: every section a {@link DocsReadSection}.
84
+ */
85
+ export type DocsReadDoc = Omit<import("@astryxdesign/cli/authoring").ReferenceDoc, "sections"> & {
86
+ sections: DocsReadSection[];
87
+ };
74
88
  /**
75
89
  * The doc a link opens (spec:AST-047 FR9). A docs read resolves every link:
76
- * an inline `{@link <target>}` reads as `link.command`, a `reference` block
77
- * carries `link` (null when the target names no doc), and each `workflow` step
78
- * carries `links`, one per reference.
90
+ * an inline `{@link <target>}` reads as `link.command`, a `reference` block in
91
+ * a namespace doc carries `link` (null when the target names no doc), and each
92
+ * `workflow` step carries `links`, one per reference. A topic read inlines a
93
+ * section's `reference` block as the doc it includes, then a line naming where
94
+ * that comes from and the command that opens it.
79
95
  */
80
96
  export type DocLink = import("../../foundation/doc-compiler/links.mjs").DocLink;
81
97
  /**
@@ -146,7 +162,7 @@ export type DocsDetailSectionResponse = {
146
162
  * the section, and the moves from it: up to its topic's index, and across to
147
163
  * the sections before and after it
148
164
  */
149
- data: import("@astryxdesign/cli/authoring").ReferenceSection & {
165
+ data: DocsReadSection & {
150
166
  links: DocsLinks;
151
167
  };
152
168
  };
@@ -56,15 +56,31 @@
56
56
  * astryx --json docs <topic>
57
57
  * @typedef {object} DocsDetailResponse
58
58
  * @property {'docs.detail'} type
59
- * @property {import('@astryxdesign/cli/authoring').ReferenceDoc & {links: DocsLinks}} data
59
+ * @property {DocsReadDoc & {links: DocsLinks}} data
60
60
  * the whole doc, and the moves from it
61
61
  */
62
62
 
63
+ /**
64
+ * A topic section as a read returns it. A read inlines each token reference
65
+ * and each `reference` block the author wrote, so its content holds only the
66
+ * stable ReferenceContentBlock kinds.
67
+ * @typedef {Omit<import('@astryxdesign/cli/authoring').ReferenceSection, 'content'>
68
+ * & {content: import('@astryxdesign/cli/authoring').ReferenceContentBlock[]}} DocsReadSection
69
+ */
70
+
71
+ /**
72
+ * A topic as a read returns it: every section a {@link DocsReadSection}.
73
+ * @typedef {Omit<import('@astryxdesign/cli/authoring').ReferenceDoc, 'sections'>
74
+ * & {sections: DocsReadSection[]}} DocsReadDoc
75
+ */
76
+
63
77
  /**
64
78
  * The doc a link opens (spec:AST-047 FR9). A docs read resolves every link:
65
- * an inline `{@link <target>}` reads as `link.command`, a `reference` block
66
- * carries `link` (null when the target names no doc), and each `workflow` step
67
- * carries `links`, one per reference.
79
+ * an inline `{@link <target>}` reads as `link.command`, a `reference` block in
80
+ * a namespace doc carries `link` (null when the target names no doc), and each
81
+ * `workflow` step carries `links`, one per reference. A topic read inlines a
82
+ * section's `reference` block as the doc it includes, then a line naming where
83
+ * that comes from and the command that opens it.
68
84
  * @typedef {import('../../foundation/doc-compiler/links.mjs').DocLink} DocLink
69
85
  */
70
86
 
@@ -110,7 +126,7 @@
110
126
  * astryx --json docs <topic> <section>
111
127
  * @typedef {object} DocsDetailSectionResponse
112
128
  * @property {'docs.detail.section'} type
113
- * @property {import('@astryxdesign/cli/authoring').ReferenceSection & {links: DocsLinks}} data
129
+ * @property {DocsReadSection & {links: DocsLinks}} data
114
130
  * the section, and the moves from it: up to its topic's index, and across to
115
131
  * the sections before and after it
116
132
  */
@@ -151,7 +151,15 @@ describe('integration docs in the docs tree', () => {
151
151
  title: 'Broken',
152
152
  description: 'A topic whose block no topic may hold.',
153
153
  sections: [
154
- {title: 'Only', content: [{type: 'reference', target: 'generic:setup'}]},
154
+ {
155
+ title: 'Only',
156
+ content: [
157
+ {
158
+ type: 'workflow',
159
+ steps: [{title: 'Set up', references: ['generic:setup']}],
160
+ },
161
+ ],
162
+ },
155
163
  ],
156
164
  },
157
165
  });