@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
@@ -0,0 +1,598 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Tests for the docs tree (spec:AST-046): one home per doc, decided by
5
+ * explicit placement, then one adoption rule; every way a placement can fail;
6
+ * generated kind levels; deterministic order; and the CLI's own tree.
7
+ *
8
+ * @input Plain fixture inputs for the pure builder, and this package's own
9
+ * tree files and typed docs for the loader.
10
+ * @output Assertions on routes, parents, slots, children order, and
11
+ * diagnostics.
12
+ * @position packages/cli/foundation/doc-compiler — tests for tree.mjs.
13
+ */
14
+
15
+ import {describe, expect, it} from 'vitest';
16
+ import {
17
+ buildDocsTree,
18
+ KIND_GROUPS,
19
+ loadDocsTree,
20
+ loadTreeInputs,
21
+ routeSegment,
22
+ } from './tree.mjs';
23
+ import {loadCliSelfDocs} from '../discovery/cli-self-docs.mjs';
24
+ import * as api from '../../api/index.mjs';
25
+ import {createDocId} from '../identity/provider-identity.mjs';
26
+ import {CLI_PROVIDER_ID} from '../identity/providers.mjs';
27
+
28
+ const SLOW = 60_000;
29
+ const P = '@acme/kit';
30
+
31
+ /**
32
+ * @param {string} name
33
+ * @param {object} [extra]
34
+ * @returns {import('./tree.mjs').TreeNamespaceInput}
35
+ */
36
+ const ns = (name, extra = {}) => ({
37
+ provider: P,
38
+ providerId: P,
39
+ source: `${P}/tree/${name}.doc.mjs`,
40
+ doc: /** @type {any} */ ({
41
+ type: 'namespace',
42
+ name,
43
+ title: name.toUpperCase(),
44
+ summary: `The ${name} level.`,
45
+ slots: {
46
+ items: {title: 'Items', accepts: {kinds: ['generic', 'namespace']}},
47
+ },
48
+ ...extra,
49
+ }),
50
+ });
51
+
52
+ /**
53
+ * @param {string} name
54
+ * @param {object} [extra]
55
+ * @returns {import('./tree.mjs').TreeDocInput}
56
+ */
57
+ const guide = (name, extra = {}) => ({
58
+ provider: P,
59
+ providerId: P,
60
+ source: `${P}/tree/${name}.doc.mjs`,
61
+ kind: 'generic',
62
+ name,
63
+ title: `Guide ${name}`,
64
+ summary: `About ${name}.`,
65
+ group: null,
66
+ placement: undefined,
67
+ ...extra,
68
+ });
69
+
70
+ /**
71
+ * @param {string} kind
72
+ * @param {string} name
73
+ * @param {string} group
74
+ * @returns {import('./tree.mjs').TreeDocInput}
75
+ */
76
+ const typed = (kind, name, group) => ({
77
+ provider: P,
78
+ providerId: P,
79
+ source: `${P}/api/${name}.doc.mjs`,
80
+ kind,
81
+ name,
82
+ title: name,
83
+ summary: `The ${name} ${kind}.`,
84
+ group,
85
+ placement: undefined,
86
+ });
87
+
88
+ /** @param {ReturnType<typeof buildDocsTree>} tree */
89
+ const routes = tree => [...tree.nodes.keys()];
90
+
91
+ /** @param {ReturnType<typeof buildDocsTree>} tree */
92
+ const problems = tree => tree.diagnostics.map(d => [d.code, d.message]);
93
+
94
+ describe('buildDocsTree', () => {
95
+
96
+ it('withdraws a namespace placed in a withdrawn namespace, and what is placed in it', () => {
97
+ const CLI = '@astryxdesign/cli';
98
+ const tree = buildDocsTree({
99
+ namespaces: [
100
+ ns('tokens'),
101
+ ns('alpha', {placement: {parent: 'namespace:tokens', slot: 'items'}}),
102
+ ],
103
+ docs: [guide('deep', {placement: {parent: 'namespace:alpha', slot: 'items'}})],
104
+ topics: [
105
+ {provider: CLI, providerId: CLI, name: 'tokens', title: 'Tokens', summary: 'Tokens.', source: `${CLI}/tokens.doc.mjs`},
106
+ ],
107
+ });
108
+ expect(tree.get('tokens')?.provider).toBe(CLI);
109
+ expect(routes(tree).filter(route => route.startsWith('tokens/'))).toEqual([]);
110
+ expect(problems(tree)).toEqual(
111
+ expect.arrayContaining([
112
+ ['duplicate_route', expect.stringContaining('takes the route "tokens"')],
113
+ ['invalid_placement', 'Namespace "alpha" has no route: the namespace it is placed in was withdrawn.'],
114
+ ['invalid_placement', expect.stringContaining('names a namespace that has no route')],
115
+ ]),
116
+ );
117
+ });
118
+
119
+ it("keeps a name a topic answers to through replaces as that topic's route", () => {
120
+ const tree = buildDocsTree({
121
+ namespaces: [ns('tokens')],
122
+ docs: [],
123
+ topics: [
124
+ {provider: '@acme/a', providerId: '@acme/a', name: 'acme-tokens', title: 'Acme tokens', summary: 'Acme tokens.', source: '@acme/a:acme-tokens', aliases: ['tokens', 'old-tokens']},
125
+ ],
126
+ });
127
+ expect(tree.get('tokens')).toBeUndefined();
128
+ expect(tree.get('acme-tokens')?.provider).toBe('@acme/a');
129
+ expect(problems(tree)).toEqual([
130
+ ['duplicate_route', expect.stringContaining('takes the route "tokens", which the topic "acme-tokens" also answers to')],
131
+ ]);
132
+ // Every name it answers to is reserved, not only the last one it replaced.
133
+ const older = buildDocsTree({
134
+ namespaces: [ns('old-tokens')],
135
+ docs: [],
136
+ topics: [
137
+ {provider: '@acme/a', providerId: '@acme/a', name: 'acme-tokens', title: 'Acme tokens', summary: 'Acme tokens.', source: '@acme/a:acme-tokens', aliases: ['tokens', 'old-tokens']},
138
+ ],
139
+ });
140
+ expect(older.get('old-tokens')).toBeUndefined();
141
+ });
142
+
143
+ it("keeps the CLI's own routes from an integration, compared without case", () => {
144
+ const CLI = '@astryxdesign/cli';
145
+ /** @param {string} name @param {string} [provider] */
146
+ const topic = (name, provider = P) => ({
147
+ provider,
148
+ providerId: provider,
149
+ name,
150
+ title: name,
151
+ summary: `The ${name} topic.`,
152
+ source: `${provider}/${name}.doc.mjs`,
153
+ });
154
+ const cliNs = {...ns('cli'), provider: CLI, providerId: CLI, source: `${CLI}/tree/cli.doc.mjs`};
155
+ // An integration's topics named `CLI` and `UNORGANIZED` claim the CLI's
156
+ // namespace and the generated level; both are withdrawn.
157
+ const tree = buildDocsTree({
158
+ namespaces: [cliNs],
159
+ docs: [],
160
+ topics: [topic('theme', CLI), topic('CLI'), topic('UNORGANIZED')],
161
+ });
162
+ expect(tree.get('cli')?.provider).toBe(CLI);
163
+ expect(tree.get('unorganized')?.provider).toBe(CLI);
164
+ expect(routes(tree)).not.toContain('CLI');
165
+ expect(routes(tree)).not.toContain('UNORGANIZED');
166
+ expect(problems(tree)).toEqual(
167
+ expect.arrayContaining([
168
+ ['duplicate_route', expect.stringContaining('both have the route "CLI"')],
169
+ ['duplicate_route', expect.stringContaining('takes the route "UNORGANIZED", which the CLI\'s own docs keep')],
170
+ ]),
171
+ );
172
+ expect(tree.getFolded('THEME')?.provider).toBe(CLI);
173
+ // A CLI topic whose own name has capitals keeps its route against a
174
+ // lowercase namespace, although namespaces go into the tree first.
175
+ const caps = buildDocsTree({namespaces: [ns('tokens')], docs: [], topics: [topic('Tokens', CLI)]});
176
+ expect(caps.get('Tokens')?.provider).toBe(CLI);
177
+ expect(caps.get('tokens')).toBeUndefined();
178
+ expect(problems(caps)).toEqual([
179
+ ['duplicate_route', expect.stringContaining('takes the route "tokens", which the CLI\'s own docs keep')],
180
+ ]);
181
+ });
182
+ it('builds three authored levels and a guide below them', () => {
183
+ const tree = buildDocsTree({
184
+ namespaces: [
185
+ ns('top'),
186
+ ns('middle', {placement: {parent: 'namespace:top', slot: 'items'}}),
187
+ ns('bottom', {placement: {parent: 'namespace:middle'}}),
188
+ ],
189
+ docs: [
190
+ guide('deep', {placement: {parent: 'namespace:bottom', order: 1}}),
191
+ ],
192
+ });
193
+ expect(problems(tree)).toEqual([]);
194
+ expect(routes(tree)).toEqual([
195
+ 'top',
196
+ 'top/middle',
197
+ 'top/middle/bottom',
198
+ 'top/middle/bottom/deep',
199
+ ]);
200
+ const deep = /** @type {any} */ (tree.get('top/middle/bottom/deep'));
201
+ expect(deep).toMatchObject({
202
+ id: 'astryx:artifact:v1/%40acme%2Fkit/generic/deep',
203
+ parent: 'top/middle/bottom',
204
+ slot: 'items',
205
+ order: 1,
206
+ generated: false,
207
+ });
208
+ expect(tree.ancestors(deep).map(node => node.route)).toEqual([
209
+ 'top',
210
+ 'top/middle',
211
+ 'top/middle/bottom',
212
+ ]);
213
+ expect(tree.roots().map(node => node.route)).toEqual(['top']);
214
+ });
215
+
216
+ it('adopts typed docs by group, with one generated level per kind', () => {
217
+ const tree = buildDocsTree({
218
+ namespaces: [
219
+ ns('api', {
220
+ slots: {kinds: {title: 'Reference', accepts: {kinds: ['namespace']}}},
221
+ adopts: [
222
+ {
223
+ source: {group: 'kit/api', kinds: ['function', 'enum']},
224
+ into: 'kinds',
225
+ groupBy: 'kind',
226
+ },
227
+ ],
228
+ }),
229
+ ],
230
+ docs: [
231
+ typed('enum', 'errorCodes', 'kit/api'),
232
+ typed('function', 'search', 'kit/api'),
233
+ typed('function', 'build', 'kit/api'),
234
+ typed('schema', 'config', 'kit/api'),
235
+ typed('function', 'other', 'kit/elsewhere'),
236
+ ],
237
+ });
238
+ expect(problems(tree)).toEqual([]);
239
+ expect(routes(tree)).toEqual([
240
+ 'api',
241
+ 'api/enums',
242
+ 'api/enums/error-codes',
243
+ 'api/functions',
244
+ 'api/functions/build',
245
+ 'api/functions/search',
246
+ ]);
247
+ const functions = /** @type {any} */ (tree.get('api/functions'));
248
+ expect(functions).toMatchObject({
249
+ id: null,
250
+ kind: 'namespace',
251
+ title: KIND_GROUPS.function.title,
252
+ generated: true,
253
+ parent: 'api',
254
+ slot: 'kinds',
255
+ });
256
+ // Kind levels follow the rule's kind order; docs inside sort by title.
257
+ expect(tree.get('api')?.slots[0].children).toEqual([
258
+ 'api/functions',
259
+ 'api/enums',
260
+ ]);
261
+ expect(functions.slots[0].children).toEqual([
262
+ 'api/functions/build',
263
+ 'api/functions/search',
264
+ ]);
265
+ });
266
+
267
+ it('adopts without grouping straight into the named slot', () => {
268
+ const tree = buildDocsTree({
269
+ namespaces: [
270
+ ns('commands', {
271
+ slots: {all: {title: 'All', accepts: {kinds: ['command']}}},
272
+ adopts: [{source: {group: 'kit/commands'}, into: 'all'}],
273
+ }),
274
+ ],
275
+ docs: [typed('command', 'theme add', 'kit/commands')],
276
+ });
277
+ expect(routes(tree)).toEqual(['commands', 'commands/theme-add']);
278
+ expect(tree.get('commands')?.slots[0].children).toEqual([
279
+ 'commands/theme-add',
280
+ ]);
281
+ });
282
+
283
+ it('prefers explicit placement over adoption, and never falls back', () => {
284
+ const namespaces = [
285
+ ns('guides'),
286
+ ns('api', {
287
+ slots: {all: {title: 'All', accepts: {kinds: ['generic']}}},
288
+ adopts: [{source: {group: 'kit/api'}, into: 'all'}],
289
+ }),
290
+ ];
291
+ const placed = buildDocsTree({
292
+ namespaces,
293
+ docs: [
294
+ guide('intro', {
295
+ group: 'kit/api',
296
+ placement: {parent: 'namespace:guides'},
297
+ }),
298
+ ],
299
+ });
300
+ expect(routes(placed)).toContain('guides/intro');
301
+ expect(routes(placed)).not.toContain('api/intro');
302
+
303
+ const broken = buildDocsTree({
304
+ namespaces,
305
+ docs: [
306
+ guide('intro', {
307
+ group: 'kit/api',
308
+ placement: {parent: 'namespace:nope'},
309
+ }),
310
+ ],
311
+ });
312
+ expect(routes(broken).some(route => route.endsWith('/intro'))).toBe(false);
313
+ expect(problems(broken)).toEqual([
314
+ [
315
+ 'invalid_placement',
316
+ 'placement.parent "namespace:nope" names no namespace; @acme/kit declares "api", "guides".',
317
+ ],
318
+ ]);
319
+ });
320
+
321
+ it('reports every way a placement can fail', () => {
322
+ const tree = buildDocsTree({
323
+ namespaces: [
324
+ ns('home', {
325
+ slots: {
326
+ one: {title: 'One', accepts: {kinds: ['generic']}},
327
+ two: {title: 'Two', accepts: {kinds: ['command']}},
328
+ },
329
+ }),
330
+ ],
331
+ docs: [
332
+ guide('no-slot', {placement: {parent: 'namespace:home'}}),
333
+ guide('bad-slot', {
334
+ placement: {parent: 'namespace:home', slot: 'three'},
335
+ }),
336
+ guide('wrong-kind', {
337
+ placement: {parent: 'namespace:home', slot: 'two'},
338
+ }),
339
+ guide('other-package', {
340
+ placement: {parent: '@other/pkg/namespace/home'},
341
+ }),
342
+ guide('not-a-ref', {placement: {parent: 'home'}}),
343
+ ],
344
+ });
345
+ expect(routes(tree)).toEqual(['home']);
346
+ expect(
347
+ problems(tree)
348
+ .map(([, message]) => message)
349
+ .sort(),
350
+ ).toEqual(
351
+ [
352
+ 'placement names no slot, and namespace "home" has 2 (one, two). Name one with placement.slot.',
353
+ 'placement.slot "three" is not a slot of namespace "home"; it declares one, two.',
354
+ 'slot "two" of namespace "home" does not accept generic docs; it accepts command.',
355
+ 'placement.parent "@other/pkg/namespace/home" belongs to @other/pkg. A doc can only be placed in a namespace of its own package (@acme/kit).',
356
+ 'placement.parent "home" is not a namespace reference. Write "namespace:<name>".',
357
+ ].sort(),
358
+ );
359
+ expect(tree.diagnostics.every(d => d.code === 'invalid_placement')).toBe(
360
+ true,
361
+ );
362
+ expect(tree.diagnostics.every(d => d.severity === 'error')).toBe(true);
363
+ });
364
+
365
+ it('fails a doc two namespaces adopt, naming both', () => {
366
+ const rule = {source: {group: 'kit/api'}, into: 'items'};
367
+ const tree = buildDocsTree({
368
+ namespaces: [ns('a', {adopts: [rule]}), ns('b', {adopts: [rule]})],
369
+ docs: [guide('shared', {group: 'kit/api'})],
370
+ });
371
+ expect(routes(tree)).toEqual(['a', 'b']);
372
+ expect(problems(tree)).toEqual([
373
+ [
374
+ 'overlapping_adoption',
375
+ '@acme/kit/generic/shared is adopted by "a" and "b"; exactly one namespace may adopt a doc.',
376
+ ],
377
+ ]);
378
+ });
379
+
380
+ it('fails two docs at one route, keeping the first by identity', () => {
381
+ const tree = buildDocsTree({
382
+ namespaces: [ns('home')],
383
+ docs: [
384
+ guide('fooBar', {placement: {parent: 'namespace:home'}}),
385
+ guide('foo_bar', {placement: {parent: 'namespace:home'}}),
386
+ ],
387
+ });
388
+ expect(routes(tree)).toEqual(['home', 'home/foo-bar']);
389
+ expect(tree.get('home/foo-bar')?.id).toBe(
390
+ 'astryx:artifact:v1/%40acme%2Fkit/generic/fooBar',
391
+ );
392
+ expect(problems(tree)).toEqual([
393
+ [
394
+ 'duplicate_route',
395
+ 'astryx:artifact:v1/%40acme%2Fkit/generic/foo_bar and astryx:artifact:v1/%40acme%2Fkit/generic/fooBar both have the route "home/foo-bar". Rename or move one of them.',
396
+ ],
397
+ ]);
398
+ });
399
+
400
+ it('fails a namespace cycle and everything under it', () => {
401
+ const tree = buildDocsTree({
402
+ namespaces: [
403
+ ns('a', {placement: {parent: 'namespace:b'}}),
404
+ ns('b', {placement: {parent: 'namespace:a'}}),
405
+ ],
406
+ docs: [guide('lost', {placement: {parent: 'namespace:a'}})],
407
+ });
408
+ expect(routes(tree)).toEqual([]);
409
+ expect(problems(tree).map(([code]) => code)).toEqual([
410
+ 'invalid_placement',
411
+ 'invalid_placement',
412
+ 'invalid_placement',
413
+ ]);
414
+ });
415
+
416
+ it('fails a duplicate or unsafe namespace name', () => {
417
+ const tree = buildDocsTree({
418
+ namespaces: [
419
+ ns('twice'),
420
+ {...ns('twice'), source: `${P}/other.doc.mjs`},
421
+ ns('Bad Name'),
422
+ ],
423
+ docs: [],
424
+ });
425
+ expect(routes(tree)).toEqual(['twice']);
426
+ expect(tree.diagnostics.map(d => d.code)).toEqual([
427
+ 'invalid_namespace',
428
+ 'invalid_namespace',
429
+ ]);
430
+ });
431
+
432
+ it('leaves a doc with no placement and no adoption out, in phase 1', () => {
433
+ const tree = buildDocsTree({
434
+ namespaces: [ns('home')],
435
+ docs: [guide('loose'), typed('function', 'free', 'kit/nowhere')],
436
+ });
437
+ expect(routes(tree)).toEqual(['home']);
438
+ expect(tree.diagnostics).toEqual([]);
439
+ });
440
+
441
+ it('is the same tree whatever order its inputs arrive in', () => {
442
+ const namespaces = [
443
+ ns('top'),
444
+ ns('child', {placement: {parent: 'namespace:top'}}),
445
+ ];
446
+ const docs = [
447
+ guide('b', {placement: {parent: 'namespace:child'}}),
448
+ guide('a', {placement: {parent: 'namespace:child'}}),
449
+ ];
450
+ const one = buildDocsTree({namespaces, docs});
451
+ const two = buildDocsTree({
452
+ namespaces: [...namespaces].reverse(),
453
+ docs: [...docs].reverse(),
454
+ });
455
+ expect(JSON.stringify([...two.nodes])).toBe(JSON.stringify([...one.nodes]));
456
+ });
457
+
458
+ it('orders children by order, then title', () => {
459
+ const tree = buildDocsTree({
460
+ namespaces: [ns('home')],
461
+ docs: [
462
+ guide('z', {
463
+ title: 'Zed',
464
+ placement: {parent: 'namespace:home', order: 1},
465
+ }),
466
+ guide('b', {title: 'Beta', placement: {parent: 'namespace:home'}}),
467
+ guide('a', {title: 'Alpha', placement: {parent: 'namespace:home'}}),
468
+ ],
469
+ });
470
+ expect(tree.get('home')?.slots[0].children).toEqual([
471
+ 'home/z',
472
+ 'home/a',
473
+ 'home/b',
474
+ ]);
475
+ });
476
+ });
477
+
478
+ describe('routeSegment', () => {
479
+ it('joins lowercase words with hyphens, whatever the name looks like', () => {
480
+ expect(routeSegment('integrationPackCheck')).toBe('integration-pack-check');
481
+ expect(routeSegment('doctor integration validate')).toBe(
482
+ 'doctor-integration-validate',
483
+ );
484
+ expect(routeSegment('error-codes')).toBe('error-codes');
485
+ expect(routeSegment('isError')).toBe('is-error');
486
+ expect(routeSegment('!!')).toBe('');
487
+ });
488
+ });
489
+
490
+ describe("the CLI's own docs tree", () => {
491
+ it(
492
+ 'builds with no diagnostic, rooted at cli',
493
+ async () => {
494
+ const tree = await loadDocsTree({fresh: true});
495
+ expect(tree.diagnostics).toEqual([]);
496
+ expect(tree.roots().map(node => node.route)).toEqual(['cli']);
497
+ expect(
498
+ tree.get('cli')?.slots.map(slot => [slot.name, slot.children]),
499
+ ).toEqual([
500
+ ['guides', ['cli/integrations', 'cli/writing-docs']],
501
+ ['reference', ['cli/commands', 'cli/api']],
502
+ ]);
503
+ expect(tree.get('cli/api')?.slots[0].children).toEqual([
504
+ 'cli/api/functions',
505
+ 'cli/api/schemas',
506
+ 'cli/api/enums',
507
+ ]);
508
+ },
509
+ SLOW,
510
+ );
511
+
512
+ it(
513
+ 'gives every CLI typed doc in a cli group one route, by its kind',
514
+ async () => {
515
+ const tree = await loadDocsTree();
516
+ const {loaded} = await loadCliSelfDocs();
517
+ const inTree = loaded.filter(({doc}) =>
518
+ String(doc.namespace).startsWith('cli/'),
519
+ );
520
+ expect(inTree.length).toBeGreaterThan(0);
521
+ const expected = inTree.map(({doc}) =>
522
+ doc.type === 'command'
523
+ ? `cli/commands/${routeSegment(doc.name)}`
524
+ : `cli/api/${KIND_GROUPS[doc.type].segment}/${routeSegment(doc.name)}`,
525
+ );
526
+ expect(expected.filter(route => tree.get(route) == null)).toEqual([]);
527
+ // A doc in the authoring group stays a section of `astryx docs authoring`.
528
+ const authoring = loaded.filter(({doc}) => doc.namespace === 'authoring');
529
+ expect(authoring.length).toBeGreaterThan(0);
530
+ const ids = new Set([...tree.nodes.values()].map(node => node.id));
531
+ expect(
532
+ authoring.filter(({doc}) =>
533
+ ids.has(createDocId(CLI_PROVIDER_ID, doc.type, doc.name)),
534
+ ),
535
+ ).toEqual([]);
536
+ },
537
+ SLOW,
538
+ );
539
+
540
+ it(
541
+ 'gives every function @astryxdesign/cli/api exports a route',
542
+ async () => {
543
+ const tree = await loadDocsTree();
544
+ const exported = Object.entries(api)
545
+ .filter(
546
+ ([, value]) =>
547
+ typeof value === 'function' &&
548
+ Object.getOwnPropertyDescriptor(value, 'prototype')?.writable !==
549
+ false,
550
+ )
551
+ .map(([name]) => `cli/api/functions/${routeSegment(name)}`);
552
+ expect(exported.length).toBeGreaterThan(0);
553
+ expect(exported.filter(route => tree.get(route) == null)).toEqual([]);
554
+ },
555
+ SLOW,
556
+ );
557
+
558
+ it(
559
+ 'reads only namespace and generic docs from the tree directory, each named after its file',
560
+ async () => {
561
+ const inputs = await loadTreeInputs({selfDocs: false});
562
+ expect(inputs.diagnostics).toEqual([]);
563
+ expect(inputs.namespaces.map(n => n.doc.name).sort()).toEqual([
564
+ 'api',
565
+ 'cli',
566
+ 'commands',
567
+ ]);
568
+ expect(inputs.docs.map(d => [d.name, d.placement?.parent])).toEqual([
569
+ ['integrations', 'namespace:cli'],
570
+ ['writing-docs', 'namespace:cli'],
571
+ ]);
572
+ },
573
+ SLOW,
574
+ );
575
+ });
576
+
577
+ describe('doc identity', () => {
578
+ it('builds each id from the provider id, never the package name', () => {
579
+ const provider = '@acme/tree-provider';
580
+ const tree = buildDocsTree({
581
+ namespaces: [{...ns('home'), providerId: provider}],
582
+ docs: [
583
+ {
584
+ ...guide('intro', {placement: {parent: 'namespace:home'}}),
585
+ providerId: provider,
586
+ },
587
+ ],
588
+ });
589
+ expect(problems(tree)).toEqual([]);
590
+ expect(tree.get('home')?.id).toBe(
591
+ 'astryx:artifact:v1/%40acme%2Ftree-provider/namespace/home',
592
+ );
593
+ expect(tree.get('home/intro')).toMatchObject({
594
+ id: 'astryx:artifact:v1/%40acme%2Ftree-provider/generic/intro',
595
+ provider: P,
596
+ });
597
+ });
598
+ });
@@ -87,7 +87,7 @@ function installPackage(
87
87
  fs.mkdirSync(componentsDir, {recursive: true});
88
88
  fs.writeFileSync(
89
89
  path.join(componentsDir, `${componentName}.doc.mjs`),
90
- `export const docs = {name: '${componentName}', usage: {description: 'A widget'}};\n`,
90
+ `export default {type: 'component', name: '${componentName}', usage: {description: 'A widget'}, props: []};\n`,
91
91
  );
92
92
  fs.writeFileSync(
93
93
  path.join(componentsDir, `${componentName}.tsx`),
@@ -0,0 +1,45 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in foundation/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /**
5
+ * The lowest version a semver range admits: the least of its `||`
6
+ * alternatives. Null when an alternative has no lower bound, or the range is
7
+ * not one this can read, so the caller treats it as admitting every version.
8
+ * @param {unknown} range
9
+ * @returns {string | null}
10
+ */
11
+ export function lowestAdmitted(range: unknown): string | null;
12
+ /**
13
+ * Why a package that ships a namespace doc or a placed guide would lose its
14
+ * docs on an older CLI, or null when its declared CLI range admits only CLIs
15
+ * that read the docs tree. Published 0.6.3 hides every topic of a package
16
+ * with a namespace doc; builds of main before the docs tree also do so for a
17
+ * placed guide.
18
+ * @param {any} pkg package.json
19
+ * @returns {string | null}
20
+ */
21
+ export function docsTreeCliProblem(pkg: any): string | null;
22
+ /**
23
+ * Why a package with a template that sets `replaces` would lose its templates
24
+ * and doc topics on an older CLI (spec:AST-035), or null when its declared CLI
25
+ * range admits only CLIs that read the field.
26
+ * @param {any} pkg package.json
27
+ * @returns {string | null}
28
+ */
29
+ export function replacesCliProblem(pkg: any): string | null;
30
+ /**
31
+ * The package.json with a CLI peer that reads namespace docs: optional, unless
32
+ * the package already says otherwise.
33
+ * @param {any} pkg package.json
34
+ * @returns {any}
35
+ */
36
+ export function withDocsTreeCli(pkg: any): any;
37
+ export const CLI_PACKAGE: "@astryxdesign/cli";
38
+ /**
39
+ * The first CLI release that reads an integration's docs tree (namespace docs
40
+ * and placed guides) and its templates' `replaces`. A release before it can
41
+ * hide every doc topic a package with a namespace doc or a placed guide
42
+ * ships, with no warning, and it rejects `replaces` and withholds the
43
+ * package's templates and doc topics.
44
+ */
45
+ export const DOCS_TREE_CLI: "0.7.0";