@astryxdesign/cli 0.6.3-canary.7ada42e → 0.6.3-canary.7ec7be0

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 (221) hide show
  1. package/README.md +21 -19
  2. package/api/build/_adapter.d.mts +50 -0
  3. package/api/build/_adapter.mjs +60 -0
  4. package/api/build/build.doc.mjs +13 -7
  5. package/api/build/build.test.mjs +184 -6
  6. package/api/build/build.type.d.mts +57 -2
  7. package/api/build/build.type.mjs +30 -7
  8. package/api/build/help/help.d.mts +4 -0
  9. package/api/build/help/help.mjs +24 -11
  10. package/api/build/kit/kit.d.mts +4 -1
  11. package/api/build/kit/kit.mjs +165 -49
  12. package/api/build/kit/rank.d.mts +44 -0
  13. package/api/build/kit/rank.mjs +432 -0
  14. package/api/build/kit/rank.test.mjs +196 -0
  15. package/api/component/_adapter.d.mts +6 -12
  16. package/api/component/_adapter.mjs +20 -10
  17. package/api/component/component.mjs +60 -12
  18. package/api/component/list/list.mjs +3 -2
  19. package/api/docs/_adapter.d.mts +251 -28
  20. package/api/docs/_adapter.mjs +918 -35
  21. package/api/docs/detail/detail.mjs +11 -3
  22. package/api/docs/detail/section/section.mjs +24 -13
  23. package/api/docs/detail/section/section.test.mjs +15 -6
  24. package/api/docs/docs.d.mts +5 -3
  25. package/api/docs/docs.doc.mjs +38 -17
  26. package/api/docs/docs.mjs +44 -8
  27. package/api/docs/docs.test.mjs +158 -4
  28. package/api/docs/docs.type.d.mts +181 -2
  29. package/api/docs/docs.type.mjs +117 -3
  30. package/api/docs/index/index.mjs +11 -3
  31. package/api/docs/index/index.test.mjs +1 -1
  32. package/api/docs/integration-tree.test.mjs +555 -0
  33. package/api/docs/integrationDocs.test.mjs +14 -14
  34. package/api/docs/list/list.mjs +25 -5
  35. package/api/docs/node/node.d.mts +43 -0
  36. package/api/docs/node/node.mjs +192 -0
  37. package/api/docs/reference-blocks.test.mjs +406 -0
  38. package/api/doctor/doctor.d.mts +30 -2
  39. package/api/doctor/doctor.mjs +151 -8
  40. package/api/doctor/doctor.test.mjs +95 -7
  41. package/api/index.d.mts +1 -1
  42. package/api/index.mjs +1 -0
  43. package/api/init/init.doc.mjs +1 -1
  44. package/api/integration/add-contribution.d.mts +2 -1
  45. package/api/integration/add-contribution.mjs +107 -5
  46. package/api/integration/add-contribution.test.mjs +137 -0
  47. package/api/integration/authoring-checks.mjs +138 -28
  48. package/api/integration/authoring-checks.test.mjs +171 -3
  49. package/api/integration/authoring-checks.type.mjs +6 -1
  50. package/api/integration/integration-authoring.type.d.mts +2 -0
  51. package/api/integration/integration-authoring.type.mjs +2 -0
  52. package/api/integration/integrationAdd.doc.mjs +6 -0
  53. package/api/integration/integrationAddDoc.doc.mjs +7 -1
  54. package/api/integration/integrationDocConflicts.doc.mjs +1 -1
  55. package/api/integration/integrationPackCheck.doc.mjs +1 -1
  56. package/api/integration/integrationTemplateConflicts.doc.d.mts +3 -0
  57. package/api/integration/integrationTemplateConflicts.doc.mjs +3 -0
  58. package/api/integration/pack-check.mjs +34 -0
  59. package/api/integration/pack-check.test.mjs +121 -0
  60. package/api/integration/pack-check.type.d.mts +26 -2
  61. package/api/integration/pack-check.type.mjs +14 -1
  62. package/api/integration/template-conflict-compatibility.test.mjs +73 -0
  63. package/api/integration/validate-integration.test.mjs +3 -3
  64. package/api/integration/validateIntegration.doc.mjs +1 -1
  65. package/api/json/index.ts +2 -0
  66. package/api/layout/_adapter.mjs +20 -5
  67. package/api/search/search.d.mts +59 -1
  68. package/api/search/search.doc.mjs +4 -3
  69. package/api/search/search.mjs +442 -71
  70. package/api/search/search.test.mjs +91 -2
  71. package/api/search/search.type.d.mts +13 -1
  72. package/api/search/search.type.mjs +4 -1
  73. package/api/template/list/list.mjs +1 -0
  74. package/api/template/table-floating-bulk-actions.test.mjs +66 -0
  75. package/api/template/template-integration.test.mjs +1072 -3
  76. package/api/template/template.doc.mjs +27 -7
  77. package/api/template/template.mjs +45 -8
  78. package/api/template/template.type.d.mts +6 -8
  79. package/api/template/template.type.mjs +3 -2
  80. package/api/theme/template/template.test.mjs +5 -0
  81. package/api/theme/themeTemplate.doc.mjs +1 -1
  82. package/assets/docs/getting-started.doc.mjs +2 -2
  83. package/assets/docs/layout.doc.dense.mjs +2 -2
  84. package/assets/docs/layout.doc.mjs +1 -1
  85. package/assets/docs/principles.doc.mjs +6 -6
  86. package/assets/docs/styling-libraries.doc.mjs +3 -3
  87. package/assets/docs/styling.doc.mjs +4 -4
  88. package/assets/docs/theme.doc.mjs +4 -4
  89. package/assets/docs/tokens.doc.mjs +1 -1
  90. package/assets/docs/tree/api.doc.mjs +30 -0
  91. package/assets/docs/tree/cli.doc.mjs +23 -0
  92. package/assets/docs/tree/commands.doc.mjs +25 -0
  93. package/assets/docs/{cli-integrations.doc.mjs → tree/integrations.doc.mjs} +37 -9
  94. package/assets/docs/tree/integrations.test.mjs +62 -0
  95. package/assets/docs/tree/writing-docs.doc.mjs +286 -0
  96. package/assets/docs/working-with-ai.doc.mjs +3 -3
  97. package/assets/templates/blocks/components/CheckboxList/CheckboxListSelectAllPattern.doc.mjs +1 -1
  98. package/assets/templates/blocks/components/CheckboxList/CheckboxListSelectAllPattern.tsx +1 -3
  99. package/assets/templates/blocks/components/Table/TableBulkActionsTable.doc.mjs +14 -0
  100. package/assets/templates/blocks/components/Table/TableBulkActionsTable.tsx +71 -0
  101. package/assets/templates/blocks/components/Table/TableFloatingBulkActionsTable.doc.mjs +19 -0
  102. package/assets/templates/blocks/components/Table/TableFloatingBulkActionsTable.tsx +139 -0
  103. package/authoring/debug/parse.d.mts +3 -3
  104. package/authoring/doctypes/_schema.d.mts +119 -117
  105. package/authoring/doctypes/_schema.mjs +54 -3
  106. package/authoring/doctypes/base/graph-fields.doc.mjs +7 -5
  107. package/authoring/doctypes/base/type.ts +6 -5
  108. package/authoring/doctypes/command/command.doc.mjs +1 -1
  109. package/authoring/doctypes/command/type.ts +2 -2
  110. package/authoring/doctypes/enum/enum.doc.mjs +1 -1
  111. package/authoring/doctypes/enum/type.ts +1 -1
  112. package/authoring/doctypes/function/function.doc.mjs +3 -2
  113. package/authoring/doctypes/function/type.ts +3 -2
  114. package/authoring/doctypes/load-contract.test.mjs +26 -1
  115. package/authoring/doctypes/namespace/namespace.doc.mjs +6 -10
  116. package/authoring/doctypes/namespace/parse.test.mjs +23 -25
  117. package/authoring/doctypes/namespace/type.ts +5 -2
  118. package/authoring/doctypes/parse.mjs +3 -2
  119. package/authoring/doctypes/reference/reference.doc.mjs +34 -5
  120. package/authoring/doctypes/reference/type.ts +28 -11
  121. package/authoring/doctypes/schema/schema.doc.mjs +1 -1
  122. package/authoring/doctypes/schema/type.ts +1 -1
  123. package/authoring/doctypes/template/parse.d.mts +2 -0
  124. package/authoring/doctypes/template/parse.mjs +4 -0
  125. package/authoring/doctypes/template/parse.test.mjs +18 -0
  126. package/authoring/doctypes/template/template.doc.mjs +6 -0
  127. package/authoring/doctypes/template/type.ts +8 -0
  128. package/authoring/identity/identity.doc.mjs +2 -2
  129. package/authoring/index.d.ts +1 -0
  130. package/authoring/integration/type.ts +1 -1
  131. package/clients/cli/commands/build-theme.adaptations.test.mjs +100 -1
  132. package/clients/cli/commands/build-theme.mjs +1 -1
  133. package/clients/cli/commands/build.doc.mjs +11 -6
  134. package/clients/cli/commands/build.mjs +105 -67
  135. package/clients/cli/commands/build.text-fields.test.mjs +41 -0
  136. package/clients/cli/commands/component/index.mjs +1 -1
  137. package/clients/cli/commands/component-ownership.test.mjs +3 -3
  138. package/clients/cli/commands/discover.broken-integration.test.mjs +7 -8
  139. package/clients/cli/commands/discover.mjs +1 -1
  140. package/clients/cli/commands/docs.doc.mjs +20 -8
  141. package/clients/cli/commands/docs.mjs +183 -69
  142. package/clients/cli/commands/docs.test.mjs +120 -16
  143. package/clients/cli/commands/doctor-integration-docs.doc.mjs +2 -2
  144. package/clients/cli/commands/doctor-integration-templates.doc.mjs +14 -7
  145. package/clients/cli/commands/doctor-integration.test.mjs +75 -1
  146. package/clients/cli/commands/doctor.mjs +53 -16
  147. package/clients/cli/commands/integration-add.doc.mjs +10 -0
  148. package/clients/cli/commands/integration.mjs +1 -0
  149. package/clients/cli/commands/search.mjs +11 -2
  150. package/clients/cli/commands/setup-nudge.test.mjs +6 -0
  151. package/clients/cli/commands/template.doc.mjs +23 -6
  152. package/clients/cli/commands/template.mjs +4 -91
  153. package/clients/cli/index.mjs +8 -0
  154. package/clients/cli/lib/manifest.mjs +9 -2
  155. package/foundation/agent-docs/agent-docs.d.mts +3 -2
  156. package/foundation/agent-docs/agent-docs.mjs +13 -9
  157. package/foundation/agent-docs/agent-docs.test.mjs +19 -1
  158. package/foundation/config/project.d.mts +12 -11
  159. package/foundation/config/project.mjs +73 -59
  160. package/foundation/config/project.test.mjs +141 -5
  161. package/foundation/discovery/authoring-self-docs.d.mts +12 -0
  162. package/foundation/discovery/authoring-self-docs.mjs +20 -8
  163. package/foundation/discovery/authoring-self-docs.test.mjs +25 -9
  164. package/foundation/discovery/cli-self-docs.d.mts +40 -34
  165. package/foundation/discovery/cli-self-docs.mjs +91 -115
  166. package/foundation/discovery/cli-self-docs.test.mjs +104 -166
  167. package/foundation/discovery/component-discovery.d.mts +38 -0
  168. package/foundation/discovery/component-discovery.mjs +48 -0
  169. package/foundation/discovery/docs-discovery.d.mts +109 -9
  170. package/foundation/discovery/docs-discovery.mjs +223 -24
  171. package/foundation/discovery/docs-discovery.test.mjs +103 -27
  172. package/foundation/discovery/docs-output-budget.d.mts +2 -2
  173. package/foundation/discovery/docs-output-budget.mjs +1 -1
  174. package/foundation/discovery/docs-section-key.d.mts +28 -10
  175. package/foundation/discovery/docs-section-key.mjs +134 -33
  176. package/foundation/discovery/docs-section-key.test.mjs +41 -19
  177. package/foundation/discovery/template-adapter.d.mts +92 -6
  178. package/foundation/discovery/template-adapter.mjs +453 -17
  179. package/foundation/discovery/template-adapter.test.mjs +42 -0
  180. package/foundation/discovery/template-conflict-release.d.mts +13 -0
  181. package/foundation/discovery/template-conflict-release.mjs +40 -0
  182. package/foundation/discovery/template-conflict-release.test.mjs +40 -0
  183. package/foundation/doc-compiler/bundle.mjs +61 -1
  184. package/foundation/doc-compiler/bundle.test.mjs +21 -10
  185. package/foundation/doc-compiler/compile.d.mts +113 -132
  186. package/foundation/doc-compiler/compile.mjs +57 -10
  187. package/foundation/doc-compiler/diagnostics.mjs +28 -0
  188. package/foundation/doc-compiler/doc-loads.test.mjs +2 -2
  189. package/foundation/doc-compiler/inputs.d.mts +2 -2
  190. package/foundation/doc-compiler/inputs.mjs +6 -1
  191. package/foundation/doc-compiler/ir.mjs +7 -0
  192. package/foundation/doc-compiler/lenses.d.mts +8 -5
  193. package/foundation/doc-compiler/lenses.mjs +50 -4
  194. package/foundation/doc-compiler/links.d.mts +162 -0
  195. package/foundation/doc-compiler/links.mjs +294 -0
  196. package/foundation/doc-compiler/links.test.mjs +192 -0
  197. package/foundation/doc-compiler/lower-doc.test.mjs +5 -2
  198. package/foundation/doc-compiler/read.d.mts +3 -2
  199. package/foundation/doc-compiler/read.mjs +6 -1
  200. package/foundation/doc-compiler/tree.d.mts +288 -0
  201. package/foundation/doc-compiler/tree.mjs +876 -0
  202. package/foundation/doc-compiler/tree.test.mjs +598 -0
  203. package/foundation/integrations/autolink.test.mjs +1 -1
  204. package/foundation/integrations/cli-requirement.d.mts +45 -0
  205. package/foundation/integrations/cli-requirement.mjs +154 -0
  206. package/foundation/integrations/cli-requirement.test.mjs +84 -0
  207. package/foundation/integrations/contribution-inventory.d.mts +2 -1
  208. package/foundation/integrations/contribution-inventory.mjs +10 -4
  209. package/foundation/integrations/contribution-inventory.test.mjs +19 -0
  210. package/foundation/integrations/integration-warnings.d.mts +9 -2
  211. package/foundation/integrations/integration-warnings.mjs +51 -26
  212. package/foundation/integrations/integration-warnings.test.mjs +74 -1
  213. package/foundation/integrations/validate-contributions.mjs +25 -27
  214. package/foundation/response/response-types.doc.d.mts +5 -1
  215. package/foundation/response/response-types.doc.mjs +19 -10
  216. package/foundation/text/string-utils.d.mts +8 -0
  217. package/foundation/text/string-utils.mjs +18 -0
  218. package/foundation/xle/expand.mjs +1 -1
  219. package/foundation/xle/xle.test.mjs +13 -0
  220. package/package.json +9 -9
  221. package/assets/docs/cli.doc.mjs +0 -15
@@ -74,16 +74,16 @@ describe('discoverBuiltinTopics', () => {
74
74
 
75
75
  describe('discoverIntegrationDocs', () => {
76
76
  it('contributes nothing when no docs root is declared or the root is gone', async () => {
77
- expect(await discoverIntegrationDocs({name: '@acme/widgets'})).toEqual({
78
- records: [],
79
- errors: [],
80
- });
77
+ const nothing = {records: [], errors: [], namespaces: [], guides: []};
78
+ expect(await discoverIntegrationDocs({name: '@acme/widgets'})).toEqual(
79
+ nothing,
80
+ );
81
81
  expect(
82
82
  await discoverIntegrationDocs({
83
83
  name: '@acme/widgets',
84
84
  docs: path.join(tmpDir, 'nope'),
85
85
  }),
86
- ).toEqual({records: [], errors: []});
86
+ ).toEqual(nothing);
87
87
  });
88
88
 
89
89
  it('reads every topic under the root, with what it declares', async () => {
@@ -169,10 +169,7 @@ describe('discoverIntegrationDocs', () => {
169
169
  id: 'steps',
170
170
  title: 'Steps',
171
171
  content: [
172
- {
173
- type: 'workflow',
174
- steps: [{title: 'Install', description: 'Run install.'}],
175
- },
172
+ {type: 'collection', source: {slot: 'guides'}},
176
173
  ],
177
174
  },
178
175
  ],
@@ -180,12 +177,12 @@ describe('discoverIntegrationDocs', () => {
180
177
  }),
181
178
  );
182
179
  expect(records).toEqual([]);
183
- expect(errors[0].message).toContain('requires the compiled graph renderer');
180
+ expect(errors[0].message).toContain('Invalid discriminator value');
184
181
  });
185
182
 
186
- it('names a namespace doc instead of listing topic fields it lacks', async () => {
187
- const {records, errors} = await discoverIntegrationDocs(
188
- integration('@acme/namespace', {
183
+ it('hands namespace docs and placed guides to the docs tree, by provider id', async () => {
184
+ const found = await discoverIntegrationDocs({
185
+ ...integration('@acme/namespace', {
189
186
  'guides.doc.mjs': {
190
187
  type: 'namespace',
191
188
  name: 'guides',
@@ -193,14 +190,56 @@ describe('discoverIntegrationDocs', () => {
193
190
  summary: 'Every guide.',
194
191
  slots: {guides: {title: 'Guides', accepts: {kinds: ['generic']}}},
195
192
  },
193
+ 'setup.doc.mjs': topic({
194
+ name: 'setup',
195
+ title: 'Set up',
196
+ placement: {parent: 'namespace:guides', slot: 'guides', order: 1},
197
+ }),
198
+ 'deploying.doc.mjs': topic(),
199
+ }),
200
+ providerId: '@acme/tree-provider',
201
+ });
202
+ expect(found.errors).toEqual([]);
203
+ expect(found.records.map(record => record.name)).toEqual(['deploying']);
204
+ expect(found.records[0].providerId).toBe('@acme/tree-provider');
205
+ expect(found.namespaces).toMatchObject([
206
+ {
207
+ provider: '@acme/namespace',
208
+ providerId: '@acme/tree-provider',
209
+ source: '@acme/namespace/guides.doc.mjs',
210
+ doc: {name: 'guides'},
211
+ },
212
+ ]);
213
+ expect(found.guides).toMatchObject([
214
+ {
215
+ provider: '@acme/namespace',
216
+ providerId: '@acme/tree-provider',
217
+ kind: 'generic',
218
+ name: 'setup',
219
+ placement: {parent: 'namespace:guides', slot: 'guides', order: 1},
220
+ },
221
+ ]);
222
+ });
223
+
224
+ it('names what a namespace doc lacks, and refuses a placed guide that replaces a topic', async () => {
225
+ const {errors, namespaces, guides} = await discoverIntegrationDocs(
226
+ integration('@acme/namespace', {
227
+ 'guides.doc.mjs': {type: 'namespace', name: 'guides', title: 'Guides'},
228
+ 'setup.doc.mjs': topic({
229
+ name: 'setup',
230
+ replaces: 'getting-started',
231
+ placement: {parent: 'namespace:guides', slot: 'guides'},
232
+ }),
196
233
  }),
197
234
  );
198
- expect(records).toEqual([]);
199
- expect(errors).toHaveLength(1);
200
- expect(errors[0].message).toContain(
201
- '"guides" is a namespace doc. Only the docs graph reads namespace docs',
235
+ expect(namespaces).toEqual([]);
236
+ expect(guides).toEqual([]);
237
+ expect(errors.map(error => error.message).join('\n')).toMatch(
238
+ /guides\.doc\.mjs.*slots/,
239
+ );
240
+ expect(errors.map(error => error.message).join('\n')).toContain(
241
+ 'is placed in the docs tree and also declares `replaces`',
202
242
  );
203
- expect(errors[0].message).not.toContain('sections:');
204
243
  });
205
244
  });
206
245
 
@@ -271,6 +310,46 @@ describe('problemsInTopic', () => {
271
310
  }
272
311
  });
273
312
 
313
+ it('takes a reference block, and checks its projection and presentation', () => {
314
+ /** @param {any[]} content */
315
+ const withBlocks = content =>
316
+ problemsInTopic(topic({sections: [{title: 'Overview', content}]}));
317
+ expect(
318
+ withBlocks([
319
+ {type: 'reference', target: 'schema:integration'},
320
+ {
321
+ type: 'reference',
322
+ target: '@astryxdesign/cli:schema:integration',
323
+ projection: {fields: ['docs']},
324
+ presentation: 'compact',
325
+ },
326
+ ]),
327
+ ).toEqual([]);
328
+ expect(
329
+ withBlocks([
330
+ {type: 'reference', target: ''},
331
+ {type: 'reference', target: 'schema:x', presentation: 'card'},
332
+ {type: 'reference', target: 'schema:x', projection: {fields: []}},
333
+ {type: 'reference', target: 'schema:x', projection: {rows: ['a']}},
334
+ {type: 'reference', target: 'schema:x', projection: ['docs']},
335
+ {type: 'reference', target: 'schema:x', link: {}},
336
+ ]),
337
+ ).toEqual([
338
+ 'sections[0].content[0].target: expected a non-empty string',
339
+ 'sections[0].content[1].presentation: "card" is not one of summary, compact, full',
340
+ 'sections[0].content[2].projection.fields: expected a non-empty array of names',
341
+ 'sections[0].content[3].projection.rows: not a field of a projection',
342
+ 'sections[0].content[4].projection: expected {fields?, sections?}, naming the parts of the doc to include',
343
+ 'sections[0].content[5].link: not a field of a reference block',
344
+ ]);
345
+ // Workflow and collection blocks stay in a namespace doc's `blocks`.
346
+ for (const type of ['workflow', 'collection']) {
347
+ expect(withBlocks([{type}]).join('\n')).toContain(
348
+ 'requires the compiled graph renderer',
349
+ );
350
+ }
351
+ });
352
+
274
353
  it('rejects a name that is not URL-safe', () => {
275
354
  expect(problemsInTopic(topic({name: 'not a topic'})).join('\n')).toContain(
276
355
  'URL-safe',
@@ -529,12 +608,10 @@ describe('problemsInTopic section keys', () => {
529
608
  })),
530
609
  });
531
610
 
532
- it('rejects two sections that derive the same key', () => {
611
+ it('keeps legacy title-only key conflicts readable', () => {
533
612
  expect(
534
613
  problemsInTopic(doc([{title: 'Quick Start'}, {title: 'Quick-start'}])),
535
- ).toEqual([
536
- expect.stringContaining('"quick-start" is already used by sections[0]'),
537
- ]);
614
+ ).toEqual([]);
538
615
  });
539
616
 
540
617
  it('rejects an unsafe id', () => {
@@ -543,10 +620,8 @@ describe('problemsInTopic section keys', () => {
543
620
  ).toEqual([expect.stringContaining('is not a stable key')]);
544
621
  });
545
622
 
546
- it('rejects a title no key derives from, unless it has an id', () => {
547
- expect(problemsInTopic(doc([{title: '亮/暗模式'}]))).toEqual([
548
- expect.stringContaining('Give the section an id'),
549
- ]);
623
+ it('keeps a legacy title that has no derived key readable', () => {
624
+ expect(problemsInTopic(doc([{title: '亮/暗模式'}]))).toEqual([]);
550
625
  expect(
551
626
  problemsInTopic(doc([{id: 'light-dark', title: '亮/暗模式'}])),
552
627
  ).toEqual([]);
@@ -580,7 +655,7 @@ describe('mergeTopic by section key', () => {
580
655
  ]);
581
656
  });
582
657
 
583
- it('replaces the section a title variant derives the same key as', () => {
658
+ it('keeps a legacy title variant as a separate section', () => {
584
659
  const merged = mergeTopic(base, {
585
660
  sections: [
586
661
  {title: 'Light-Dark Mode', content: [{type: 'prose', text: 'acme'}]},
@@ -588,6 +663,7 @@ describe('mergeTopic by section key', () => {
588
663
  });
589
664
  expect(titles(merged)).toEqual([
590
665
  [null, 'Quick Start', 'base'],
666
+ [null, 'Light/Dark Mode', 'base'],
591
667
  [null, 'Light-Dark Mode', 'acme'],
592
668
  ]);
593
669
  });
@@ -9,10 +9,10 @@
9
9
  */
10
10
  export function docPayloadBytes(payload: unknown): number;
11
11
  /**
12
- * @param {import('../../api/docs/docs.type.mjs').DocsIndex} index
12
+ * @param {Omit<import('../../api/docs/docs.type.mjs').DocsIndex, 'links'> | import('../../api/docs/docs.type.mjs').DocsIndex} index
13
13
  * @returns {number}
14
14
  */
15
- export function docsIndexBytes(index: import("../../api/docs/docs.type.mjs").DocsIndex): number;
15
+ export function docsIndexBytes(index: Omit<import("../../api/docs/docs.type.mjs").DocsIndex, "links"> | import("../../api/docs/docs.type.mjs").DocsIndex): number;
16
16
  /**
17
17
  * The sections a single read would return more than `budget` bytes for.
18
18
  * @param {any[]} sections
@@ -26,7 +26,7 @@ export function docPayloadBytes(payload) {
26
26
  }
27
27
 
28
28
  /**
29
- * @param {import('../../api/docs/docs.type.mjs').DocsIndex} index
29
+ * @param {Omit<import('../../api/docs/docs.type.mjs').DocsIndex, 'links'> | import('../../api/docs/docs.type.mjs').DocsIndex} index
30
30
  * @returns {number}
31
31
  */
32
32
  export function docsIndexBytes(index) {
@@ -31,9 +31,24 @@ export function sectionTitleKey(title: unknown): string;
31
31
  */
32
32
  export function sectionKey(section: any): string;
33
33
  /**
34
- * Problems with the keys of a topic's sections: an authored id that is not a
35
- * stable key, a title no key derives from, and two sections sharing a key.
36
- * Keys are never suffixed to make them unique, because readers link to them.
34
+ * A name as a docs-tree route segment: lowercase words joined by hyphens, so
35
+ * `integrationPackCheck` and `integration pack` both read naturally.
36
+ * @param {string} name
37
+ * @returns {string}
38
+ */
39
+ export function routeSegment(name: string): string;
40
+ /**
41
+ * Fatal key problems: only an explicitly authored `id` can make a previously
42
+ * readable topic invalid. Derived-key collisions and titles with no Latin
43
+ * letters or digits are handled by {@link withSectionKeys} for 0.6.x
44
+ * compatibility.
45
+ * @param {any[]} sections
46
+ * @returns {string[]}
47
+ */
48
+ export function sectionKeyErrors(sections: any[]): string[];
49
+ /**
50
+ * Compatibility notices for section keys. These inputs loaded in 0.6.x, so
51
+ * readers keep accepting them and assign deterministic fallback keys.
37
52
  * @param {any[]} sections
38
53
  * @returns {string[]}
39
54
  */
@@ -49,13 +64,16 @@ export function withSectionKeys<T extends {
49
64
  sections: any[];
50
65
  }>(doc: T): T;
51
66
  /**
52
- * Find the section a reader asked for: by key, then by exact title (or the
53
- * key the query derives), then by a title that contains the query. More than
54
- * one match is refused rather than guessed.
67
+ * Find the section a reader asked for. The 0.6.x first-match title-substring
68
+ * lookup remains first in the authored-title view. In a localized view, an
69
+ * exact stable key resolves before translated title substrings because those
70
+ * translations cannot participate in language-stable key allocation. Generated
71
+ * keys avoid authored-title conflicts, and authored IDs that would shadow an
72
+ * authored legacy query are rejected.
55
73
  * @param {any[]} sections
56
74
  * @param {string} query
57
- * @returns {{section: any | null, candidates: any[]}} `candidates` lists the
58
- * matches when the query is ambiguous, and is empty when nothing matches
75
+ * @returns {{section: any | null, candidates: any[]}} `candidates` lists every
76
+ * legacy title match when the query is ambiguous
59
77
  */
60
78
  export function findDocSection(sections: any[], query: string): {
61
79
  section: any | null;
@@ -73,14 +91,14 @@ export function sectionSummary(section: any, max?: number): string;
73
91
  * The index a topic-only read returns: what the topic is, and one entry per
74
92
  * section with the key to read it by.
75
93
  * @param {{name: string, title: string, description: string, sections: any[]}} doc
76
- * @returns {import('../../api/docs/docs.type.mjs').DocsIndex}
94
+ * @returns {Omit<import('../../api/docs/docs.type.mjs').DocsIndex, 'links'>}
77
95
  */
78
96
  export function buildDocsIndexData(doc: {
79
97
  name: string;
80
98
  title: string;
81
99
  description: string;
82
100
  sections: any[];
83
- }): import("../../api/docs/docs.type.mjs").DocsIndex;
101
+ }): Omit<import("../../api/docs/docs.type.mjs").DocsIndex, "links">;
84
102
  /**
85
103
  * @file Stable section keys, section lookup, and the topic index.
86
104
  *
@@ -34,7 +34,10 @@ const SOURCE_TITLE = Symbol('astryx.docs.sourceTitle');
34
34
  * @returns {T}
35
35
  */
36
36
  export function withSourceTitle(section, title) {
37
- Object.defineProperty(section, SOURCE_TITLE, {value: title, configurable: true});
37
+ Object.defineProperty(section, SOURCE_TITLE, {
38
+ value: title,
39
+ configurable: true,
40
+ });
38
41
  return section;
39
42
  }
40
43
 
@@ -77,9 +80,77 @@ export function sectionKey(section) {
77
80
  }
78
81
 
79
82
  /**
80
- * Problems with the keys of a topic's sections: an authored id that is not a
81
- * stable key, a title no key derives from, and two sections sharing a key.
82
- * Keys are never suffixed to make them unique, because readers link to them.
83
+ * The first section the 0.6.x title-substring lookup would return for a query.
84
+ * @param {any[]} sections
85
+ * @param {string} query
86
+ * @returns {number}
87
+ */
88
+ function legacyTitleMatchIndex(sections, query) {
89
+ const lower = query.toLowerCase();
90
+ return sections.findIndex(section => {
91
+ const title = sourceTitle(section);
92
+ return typeof title === 'string' && title.toLowerCase().includes(lower);
93
+ });
94
+ }
95
+
96
+ /**
97
+ * A name as a docs-tree route segment: lowercase words joined by hyphens, so
98
+ * `integrationPackCheck` and `integration pack` both read naturally.
99
+ * @param {string} name
100
+ * @returns {string}
101
+ */
102
+ export function routeSegment(name) {
103
+ return String(name)
104
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
105
+ .toLowerCase()
106
+ .replace(/[^a-z0-9]+/g, '-')
107
+ .replace(/^-+|-+$/g, '');
108
+ }
109
+
110
+ /**
111
+ * Fatal key problems: only an explicitly authored `id` can make a previously
112
+ * readable topic invalid. Derived-key collisions and titles with no Latin
113
+ * letters or digits are handled by {@link withSectionKeys} for 0.6.x
114
+ * compatibility.
115
+ * @param {any[]} sections
116
+ * @returns {string[]}
117
+ */
118
+ export function sectionKeyErrors(sections) {
119
+ /** @type {string[]} */
120
+ const problems = [];
121
+ /** @type {Map<string, number>} */
122
+ const seen = new Map();
123
+ sections.forEach((section, s) => {
124
+ if (section?.id == null) return;
125
+ const at = `sections[${s}]`;
126
+ if (typeof section.id !== 'string' || !SECTION_KEY_RE.test(section.id)) {
127
+ problems.push(
128
+ `${at}.id: ${JSON.stringify(section.id)} is not a stable key. Use lowercase letters and digits joined by single hyphens.`,
129
+ );
130
+ return;
131
+ }
132
+ const first = seen.get(section.id);
133
+ if (first != null) {
134
+ problems.push(
135
+ `${at}: the key "${section.id}" is already used by sections[${first}]. Give one of them a distinct id.`,
136
+ );
137
+ return;
138
+ }
139
+ const legacyOwner = legacyTitleMatchIndex(sections, section.id);
140
+ if (legacyOwner !== -1 && legacyOwner !== s) {
141
+ problems.push(
142
+ `${at}.id: the key "${section.id}" is already a legacy title query for sections[${legacyOwner}]. Choose an id that resolves to this section.`,
143
+ );
144
+ return;
145
+ }
146
+ seen.set(section.id, s);
147
+ });
148
+ return problems;
149
+ }
150
+
151
+ /**
152
+ * Compatibility notices for section keys. These inputs loaded in 0.6.x, so
153
+ * readers keep accepting them and assign deterministic fallback keys.
83
154
  * @param {any[]} sections
84
155
  * @returns {string[]}
85
156
  */
@@ -90,7 +161,6 @@ export function sectionKeyProblems(sections) {
90
161
  const seen = new Map();
91
162
  sections.forEach((section, s) => {
92
163
  const at = `sections[${s}]`;
93
- // A missing title is reported where titles are checked; no key to judge.
94
164
  if (
95
165
  section?.id == null &&
96
166
  (typeof section?.title !== 'string' || section.title === '')
@@ -109,14 +179,14 @@ export function sectionKeyProblems(sections) {
109
179
  const key = sectionKey(section);
110
180
  if (key === '') {
111
181
  problems.push(
112
- `${at}: no key derives from the title ${JSON.stringify(section?.title)}. Give the section an id.`,
182
+ `${at}: no key derives from the title ${JSON.stringify(section?.title)}. A deterministic section-N compatibility key is used; add an explicit id.`,
113
183
  );
114
184
  return;
115
185
  }
116
186
  const first = seen.get(key);
117
187
  if (first != null) {
118
188
  problems.push(
119
- `${at}: the key "${key}" is already used by sections[${first}]. Give one of them a distinct id or title.`,
189
+ `${at}: the key "${key}" is already used by sections[${first}]. A suffixed compatibility key is used for title-derived collisions; give one of them a distinct id or title.`,
120
190
  );
121
191
  return;
122
192
  }
@@ -133,48 +203,79 @@ export function sectionKeyProblems(sections) {
133
203
  * @returns {T}
134
204
  */
135
205
  export function withSectionKeys(doc) {
206
+ const reserved = new Set(
207
+ doc.sections
208
+ .map(section => section?.id)
209
+ .filter(id => typeof id === 'string' && SECTION_KEY_RE.test(id)),
210
+ );
211
+ const used = new Set();
136
212
  return {
137
213
  ...doc,
138
- sections: doc.sections.map(section =>
139
- section.id != null
140
- ? section
141
- : withSourceTitle(
142
- {...section, id: sectionKey(section)},
143
- sourceTitle(section),
144
- ),
145
- ),
214
+ sections: doc.sections.map((section, index) => {
215
+ if (section.id != null) {
216
+ used.add(section.id);
217
+ return section;
218
+ }
219
+ const base =
220
+ sectionTitleKey(sourceTitle(section)) || `section-${index + 1}`;
221
+ let key = base;
222
+ let suffix = 2;
223
+ while (
224
+ reserved.has(key) ||
225
+ used.has(key) ||
226
+ (legacyTitleMatchIndex(doc.sections, key) !== -1 &&
227
+ legacyTitleMatchIndex(doc.sections, key) !== index)
228
+ ) {
229
+ key = `${base}-${suffix}`;
230
+ suffix += 1;
231
+ }
232
+ used.add(key);
233
+ return withSourceTitle({...section, id: key}, sourceTitle(section));
234
+ }),
146
235
  };
147
236
  }
148
237
 
149
238
  /**
150
- * Find the section a reader asked for: by key, then by exact title (or the
151
- * key the query derives), then by a title that contains the query. More than
152
- * one match is refused rather than guessed.
239
+ * Find the section a reader asked for. The 0.6.x first-match title-substring
240
+ * lookup remains first in the authored-title view. In a localized view, an
241
+ * exact stable key resolves before translated title substrings because those
242
+ * translations cannot participate in language-stable key allocation. Generated
243
+ * keys avoid authored-title conflicts, and authored IDs that would shadow an
244
+ * authored legacy query are rejected.
153
245
  * @param {any[]} sections
154
246
  * @param {string} query
155
- * @returns {{section: any | null, candidates: any[]}} `candidates` lists the
156
- * matches when the query is ambiguous, and is empty when nothing matches
247
+ * @returns {{section: any | null, candidates: any[]}} `candidates` lists every
248
+ * legacy title match when the query is ambiguous
157
249
  */
158
250
  export function findDocSection(sections, query) {
159
251
  const wanted = query.trim();
160
252
  const byKey = sections.find(section => sectionKey(section) === wanted);
161
- if (byKey) return {section: byKey, candidates: []};
253
+ const hasLocalizedTitles = sections.some(
254
+ section => sourceTitle(section) !== section.title,
255
+ );
256
+ if (byKey && hasLocalizedTitles) {
257
+ return {section: byKey, candidates: []};
258
+ }
162
259
 
163
260
  const lower = wanted.toLowerCase();
164
- const derived = sectionTitleKey(wanted);
165
- const exact = sections.filter(
166
- section =>
167
- section.title.toLowerCase() === lower ||
168
- (derived !== '' && sectionTitleKey(sourceTitle(section)) === derived),
261
+ const legacy = sections.filter(section =>
262
+ section.title.toLowerCase().includes(lower),
169
263
  );
170
- if (exact.length === 1) return {section: exact[0], candidates: []};
171
- if (exact.length > 1) return {section: null, candidates: exact};
264
+ if (legacy.length > 0) {
265
+ return {section: legacy[0], candidates: legacy.length > 1 ? legacy : []};
266
+ }
172
267
 
173
- const partial = sections.filter(section =>
174
- section.title.toLowerCase().includes(lower),
268
+ const byFallbackKey = sections.find(
269
+ section => sectionKey(section) === wanted,
270
+ );
271
+ if (byFallbackKey) return {section: byFallbackKey, candidates: []};
272
+
273
+ const derived = sectionTitleKey(wanted);
274
+ const byDerivedKey = sections.find(
275
+ section =>
276
+ derived !== '' && sectionTitleKey(sourceTitle(section)) === derived,
175
277
  );
176
- if (partial.length === 1) return {section: partial[0], candidates: []};
177
- return {section: null, candidates: partial};
278
+ return {section: byDerivedKey ?? null, candidates: []};
178
279
  }
179
280
 
180
281
  /**
@@ -205,7 +306,7 @@ export function sectionSummary(section, max = SECTION_SUMMARY_MAX) {
205
306
  * The index a topic-only read returns: what the topic is, and one entry per
206
307
  * section with the key to read it by.
207
308
  * @param {{name: string, title: string, description: string, sections: any[]}} doc
208
- * @returns {import('../../api/docs/docs.type.mjs').DocsIndex}
309
+ * @returns {Omit<import('../../api/docs/docs.type.mjs').DocsIndex, 'links'>}
209
310
  */
210
311
  export function buildDocsIndexData(doc) {
211
312
  return {
@@ -7,6 +7,7 @@ import {
7
7
  buildDocsIndexData,
8
8
  findDocSection,
9
9
  sectionKey,
10
+ sectionKeyErrors,
10
11
  sectionKeyProblems,
11
12
  sectionSummary,
12
13
  sectionTitleKey,
@@ -68,12 +69,17 @@ describe('sectionKeyProblems', () => {
68
69
  section('Quick-start'),
69
70
  ]);
70
71
  expect(problems).toHaveLength(1);
71
- expect(problems[0]).toContain('"quick-start" is already used by sections[0]');
72
+ expect(problems[0]).toContain(
73
+ '"quick-start" is already used by sections[0]',
74
+ );
72
75
  });
73
76
 
74
77
  it('rejects an authored id that collides with a derived key', () => {
75
78
  expect(
76
- sectionKeyProblems([section('Install'), section('Setup', {id: 'install'})]),
79
+ sectionKeyProblems([
80
+ section('Install'),
81
+ section('Setup', {id: 'install'}),
82
+ ]),
77
83
  ).toHaveLength(1);
78
84
  });
79
85
 
@@ -85,13 +91,13 @@ describe('sectionKeyProblems', () => {
85
91
  }
86
92
  });
87
93
 
88
- it('rejects a title no key derives from', () => {
94
+ it('reports a title no key derives from', () => {
89
95
  expect(sectionKeyProblems([section('亮/暗模式')])[0]).toContain(
90
- 'Give the section an id',
91
- );
92
- expect(sectionKeyProblems([section('亮/暗模式', {id: 'light-dark'})])).toEqual(
93
- [],
96
+ 'compatibility key is used',
94
97
  );
98
+ expect(
99
+ sectionKeyProblems([section('亮/暗模式', {id: 'light-dark'})]),
100
+ ).toEqual([]);
95
101
  });
96
102
 
97
103
  it('leaves a missing title to the title check', () => {
@@ -107,12 +113,21 @@ describe('withSectionKeys', () => {
107
113
  expect(doc.sections.map(s => s.id)).toEqual(['quick-start', 'use']);
108
114
  });
109
115
 
110
- it('keeps the authored title of a translated section', () => {
116
+ it('keeps authored titles and keys stable for translated sections', () => {
111
117
  const doc = withSectionKeys({
112
- sections: [withSourceTitle(section('快速开始'), 'Quick Start')],
118
+ sections: [
119
+ withSourceTitle(section('高级安装'), 'Install advanced'),
120
+ withSourceTitle(section('安装'), 'Install'),
121
+ ],
113
122
  });
114
- expect(doc.sections[0].id).toBe('quick-start');
115
- expect(sourceTitle(doc.sections[0])).toBe('Quick Start');
123
+ expect(doc.sections.map(s => s.id)).toEqual([
124
+ 'install-advanced',
125
+ 'install-2',
126
+ ]);
127
+ for (const target of doc.sections) {
128
+ expect(findDocSection(doc.sections, target.id).section).toBe(target);
129
+ }
130
+ expect(sourceTitle(doc.sections[0])).toBe('Install advanced');
116
131
  });
117
132
  });
118
133
 
@@ -127,7 +142,14 @@ describe('findDocSection', () => {
127
142
  ],
128
143
  }).sections;
129
144
 
130
- it('finds a section by key first', () => {
145
+ it('finds every emitted key without changing a legacy query', () => {
146
+ const colliding = withSectionKeys({
147
+ sections: [section('Install advanced'), section('Install')],
148
+ }).sections;
149
+ expect(findDocSection(colliding, 'install').section).toBe(colliding[0]);
150
+ for (const target of colliding) {
151
+ expect(findDocSection(colliding, target.id).section).toBe(target);
152
+ }
131
153
  expect(findDocSection(sections, 'tokens').section.title).toBe(
132
154
  'Theme Tokens',
133
155
  );
@@ -149,15 +171,15 @@ describe('findDocSection', () => {
149
171
  expect(findDocSection(sections, 'props').section.id).toBe('theme-props');
150
172
  });
151
173
 
152
- it('refuses an ambiguous exact title and lists the candidates', () => {
174
+ it('keeps the first ambiguous exact-title match and lists the candidates', () => {
153
175
  const {section: match, candidates} = findDocSection(sections, 'Overview');
154
- expect(match).toBeNull();
176
+ expect(match?.id).toBe('overview-a');
155
177
  expect(candidates.map(s => s.id)).toEqual(['overview-a', 'overview-b']);
156
178
  });
157
179
 
158
- it('refuses an ambiguous partial title', () => {
180
+ it('keeps the first ambiguous partial-title match', () => {
159
181
  const {section: match, candidates} = findDocSection(sections, 'theme');
160
- expect(match).toBeNull();
182
+ expect(match?.id).toBe('theme-props');
161
183
  expect(candidates.map(s => s.id)).toEqual(['theme-props', 'tokens']);
162
184
  });
163
185
 
@@ -185,9 +207,9 @@ describe('sectionSummary', () => {
185
207
  });
186
208
 
187
209
  it('is empty when the section has no text', () => {
188
- expect(sectionSummary({content: [{type: 'table', headers: [], rows: []}]})).toBe(
189
- '',
190
- );
210
+ expect(
211
+ sectionSummary({content: [{type: 'table', headers: [], rows: []}]}),
212
+ ).toBe('');
191
213
  });
192
214
 
193
215
  it('cuts a long summary at a word boundary', () => {