@astryxdesign/cli 0.6.3-canary.db4e378 → 0.6.3-canary.db52d98

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 (397) hide show
  1. package/README.md +38 -15
  2. package/api/blog/blog.doc.mjs +1 -0
  3. package/api/build/_adapter.d.mts +50 -0
  4. package/api/build/_adapter.mjs +60 -0
  5. package/api/build/build.doc.mjs +14 -7
  6. package/api/build/build.test.mjs +184 -6
  7. package/api/build/build.type.d.mts +57 -2
  8. package/api/build/build.type.mjs +30 -7
  9. package/api/build/help/help.d.mts +4 -0
  10. package/api/build/help/help.mjs +24 -11
  11. package/api/build/kit/kit.d.mts +4 -1
  12. package/api/build/kit/kit.mjs +165 -49
  13. package/api/build/kit/rank.d.mts +44 -0
  14. package/api/build/kit/rank.mjs +432 -0
  15. package/api/build/kit/rank.test.mjs +196 -0
  16. package/api/component/_adapter.d.mts +6 -12
  17. package/api/component/_adapter.mjs +20 -10
  18. package/api/component/component.doc.mjs +1 -0
  19. package/api/component/component.mjs +60 -12
  20. package/api/component/list/list.mjs +3 -2
  21. package/api/discover/discover.doc.mjs +1 -0
  22. package/api/docs/_adapter.d.mts +252 -34
  23. package/api/docs/_adapter.mjs +923 -132
  24. package/api/docs/detail/detail.mjs +11 -3
  25. package/api/docs/detail/section/section.mjs +24 -13
  26. package/api/docs/detail/section/section.test.mjs +15 -6
  27. package/api/docs/docs.d.mts +5 -3
  28. package/api/docs/docs.doc.mjs +39 -17
  29. package/api/docs/docs.mjs +44 -8
  30. package/api/docs/docs.test.mjs +158 -4
  31. package/api/docs/docs.type.d.mts +181 -2
  32. package/api/docs/docs.type.mjs +117 -3
  33. package/api/docs/index/index.mjs +11 -3
  34. package/api/docs/index/index.test.mjs +1 -1
  35. package/api/docs/integration-tree.test.mjs +555 -0
  36. package/api/docs/integrationDocs.test.mjs +14 -14
  37. package/api/docs/list/list.mjs +28 -12
  38. package/api/docs/node/node.d.mts +43 -0
  39. package/api/docs/node/node.mjs +192 -0
  40. package/api/docs/reference-blocks.test.mjs +406 -0
  41. package/api/doctor/doctor.d.mts +54 -4
  42. package/api/doctor/doctor.doc.mjs +1 -0
  43. package/api/doctor/doctor.mjs +331 -16
  44. package/api/doctor/doctor.test.mjs +420 -7
  45. package/api/gap-report/gap-report.doc.mjs +1 -0
  46. package/api/hook/_adapter.mjs +19 -5
  47. package/api/hook/hook.doc.mjs +1 -0
  48. package/api/hook/list/list.d.mts +1 -1
  49. package/api/hook/list/list.mjs +69 -17
  50. package/api/index.d.mts +1 -1
  51. package/api/index.mjs +1 -0
  52. package/api/init/init.doc.mjs +2 -1
  53. package/api/integration/add-contribution.d.mts +2 -1
  54. package/api/integration/add-contribution.mjs +114 -12
  55. package/api/integration/add-contribution.test.mjs +174 -3
  56. package/api/integration/add-theme.mjs +34 -64
  57. package/api/integration/add-theme.test.mjs +105 -21
  58. package/api/integration/authoring-checks.mjs +138 -28
  59. package/api/integration/authoring-checks.test.mjs +179 -7
  60. package/api/integration/authoring-checks.type.mjs +6 -1
  61. package/api/integration/integration-authoring.type.d.mts +2 -0
  62. package/api/integration/integration-authoring.type.mjs +2 -0
  63. package/api/integration/integration-block-exports.test.mjs +10 -6
  64. package/api/integration/integrationAdd.doc.mjs +7 -0
  65. package/api/integration/integrationAddAgentDoc.doc.mjs +1 -0
  66. package/api/integration/integrationAddCodemod.doc.mjs +1 -0
  67. package/api/integration/integrationAddComponent.doc.mjs +1 -0
  68. package/api/integration/integrationAddDoc.doc.mjs +8 -1
  69. package/api/integration/integrationAddTemplate.doc.mjs +1 -0
  70. package/api/integration/integrationAddTheme.doc.mjs +6 -5
  71. package/api/integration/integrationComponentConflicts.doc.mjs +1 -0
  72. package/api/integration/integrationDocConflicts.doc.mjs +2 -1
  73. package/api/integration/integrationPackCheck.doc.mjs +2 -1
  74. package/api/integration/integrationTemplateConflicts.doc.d.mts +3 -0
  75. package/api/integration/integrationTemplateConflicts.doc.mjs +4 -0
  76. package/api/integration/pack-check.mjs +34 -0
  77. package/api/integration/pack-check.test.mjs +138 -47
  78. package/api/integration/pack-check.type.d.mts +26 -2
  79. package/api/integration/pack-check.type.mjs +14 -1
  80. package/api/integration/summarizeIssues.doc.mjs +1 -0
  81. package/api/integration/template-conflict-compatibility.test.mjs +73 -0
  82. package/api/integration/validate-integration-fixes.test.mjs +1389 -0
  83. package/api/integration/validate-integration.mjs +50 -102
  84. package/api/integration/validate-integration.test.mjs +88 -26
  85. package/api/integration/validate-unread-theme-folders.test.mjs +110 -0
  86. package/api/integration/validateIntegration.doc.mjs +2 -1
  87. package/api/json/assertResponse.doc.mjs +1 -0
  88. package/api/json/index.ts +2 -0
  89. package/api/json/isError.doc.mjs +1 -0
  90. package/api/json/parseResponse.doc.mjs +1 -0
  91. package/api/layout/_adapter.mjs +20 -5
  92. package/api/layout/layoutCheck.doc.mjs +1 -0
  93. package/api/layout/layoutExpand.doc.mjs +1 -0
  94. package/api/layout/layoutGrammar.doc.mjs +1 -0
  95. package/api/search/search.d.mts +59 -1
  96. package/api/search/search.doc.mjs +5 -3
  97. package/api/search/search.mjs +458 -77
  98. package/api/search/search.test.mjs +91 -2
  99. package/api/search/search.type.d.mts +13 -1
  100. package/api/search/search.type.mjs +4 -1
  101. package/api/swizzle/swizzle.doc.mjs +1 -0
  102. package/api/template/list/list.mjs +1 -0
  103. package/api/template/table-floating-bulk-actions.test.mjs +66 -0
  104. package/api/template/template-integration.test.mjs +1072 -3
  105. package/api/template/template-suffix.test.mjs +41 -21
  106. package/api/template/template.doc.mjs +28 -7
  107. package/api/template/template.mjs +45 -8
  108. package/api/template/template.type.d.mts +6 -8
  109. package/api/template/template.type.mjs +3 -2
  110. package/api/theme/_adapter.d.mts +2 -3
  111. package/api/theme/_adapter.mjs +4 -5
  112. package/api/theme/add/add.binary.test.mjs +10 -17
  113. package/api/theme/add/add.test.mjs +14 -1
  114. package/api/theme/generateTonalPalette.doc.mjs +1 -0
  115. package/api/theme/integration-themes.test.mjs +39 -28
  116. package/api/theme/list/list.test.mjs +19 -20
  117. package/api/theme/listThemes.doc.mjs +6 -5
  118. package/api/theme/template/template.test.mjs +5 -0
  119. package/api/theme/themeAdd.doc.mjs +4 -3
  120. package/api/theme/themeBuild.doc.mjs +1 -0
  121. package/api/theme/themeList.doc.mjs +6 -3
  122. package/api/theme/themeListAvailable.doc.mjs +5 -3
  123. package/api/theme/themePaletteGenerate.doc.mjs +1 -0
  124. package/api/theme/themeTargets.doc.mjs +1 -0
  125. package/api/theme/themeTemplate.doc.mjs +2 -1
  126. package/api/upgrade/_adapter.d.mts +32 -5
  127. package/api/upgrade/_adapter.mjs +97 -69
  128. package/api/upgrade/provider-agreement.test.mjs +152 -0
  129. package/api/upgrade/run/run.mjs +356 -59
  130. package/api/upgrade/upgrade.doc.mjs +6 -1
  131. package/api/upgrade/upgrade.type.d.mts +34 -0
  132. package/api/upgrade/upgrade.type.mjs +15 -0
  133. package/assets/codemods/__tests__/runner.test.mjs +330 -8
  134. package/assets/codemods/integration-discovery.mjs +8 -2
  135. package/assets/codemods/integration-discovery.test.mjs +15 -0
  136. package/assets/codemods/integration-runner.mjs +56 -4
  137. package/assets/codemods/integration-runner.protection.test.mjs +153 -0
  138. package/assets/codemods/run-codemod.mjs +177 -34
  139. package/assets/codemods/runner.mjs +350 -102
  140. package/assets/codemods/transforms/next/__tests__/migrate-native-picker-to-presentation.test.mjs +63 -0
  141. package/assets/codemods/transforms/next/__tests__/migrate-theme-catalog-to-descriptors.test.mjs +220 -0
  142. package/assets/codemods/transforms/next/index.mjs +19 -1
  143. package/assets/codemods/transforms/next/migrate-native-picker-to-presentation.mjs +148 -0
  144. package/assets/codemods/transforms/next/migrate-theme-catalog-to-descriptors.mjs +141 -0
  145. package/assets/docs/getting-started.doc.mjs +2 -2
  146. package/assets/docs/layout.doc.dense.mjs +2 -2
  147. package/assets/docs/layout.doc.mjs +1 -1
  148. package/assets/docs/principles.doc.mjs +6 -6
  149. package/assets/docs/styling-libraries.doc.mjs +3 -3
  150. package/assets/docs/styling.doc.mjs +4 -4
  151. package/assets/docs/theme.doc.mjs +5 -5
  152. package/assets/docs/tokens.doc.mjs +1 -1
  153. package/assets/docs/tree/api.doc.mjs +30 -0
  154. package/assets/docs/tree/cli.doc.mjs +23 -0
  155. package/assets/docs/tree/commands.doc.mjs +25 -0
  156. package/assets/docs/{cli-integrations.doc.mjs → tree/integrations.doc.mjs} +66 -38
  157. package/assets/docs/tree/integrations.test.mjs +62 -0
  158. package/assets/docs/tree/writing-docs.doc.mjs +286 -0
  159. package/assets/docs/working-with-ai.doc.mjs +3 -3
  160. package/assets/templates/blocks/components/CheckboxList/CheckboxListSelectAllPattern.doc.mjs +1 -1
  161. package/assets/templates/blocks/components/CheckboxList/CheckboxListSelectAllPattern.tsx +1 -3
  162. package/assets/templates/blocks/components/DateInput/DateInputDateRange.tsx +1 -1
  163. package/assets/templates/blocks/components/Item/ItemDocumentTabs.doc.mjs +14 -0
  164. package/assets/templates/blocks/components/Item/ItemDocumentTabs.tsx +100 -0
  165. package/assets/templates/blocks/components/Table/TableBulkActionsTable.doc.mjs +14 -0
  166. package/assets/templates/blocks/components/Table/TableBulkActionsTable.tsx +71 -0
  167. package/assets/templates/blocks/components/Table/TableFloatingBulkActionsTable.doc.mjs +19 -0
  168. package/assets/templates/blocks/components/Table/TableFloatingBulkActionsTable.tsx +139 -0
  169. package/assets/templates/blocks/components/TimeInput/TimeInputConstrained.tsx +1 -0
  170. package/assets/templates/themes/butter/butterTheme.doc.mjs +11 -0
  171. package/assets/templates/themes/chocolate/chocolateTheme.doc.mjs +11 -0
  172. package/assets/templates/themes/gothic/gothicTheme.doc.mjs +11 -0
  173. package/assets/templates/themes/matcha/matchaTheme.doc.mjs +11 -0
  174. package/assets/templates/themes/neutral/neutralTheme.doc.mjs +11 -0
  175. package/assets/templates/themes/stone/stoneTheme.doc.mjs +11 -0
  176. package/assets/templates/themes/y2k/y2kTheme.doc.mjs +11 -0
  177. package/authoring/codemod/codemod.doc.mjs +1 -1
  178. package/authoring/codemod/type.ts +12 -0
  179. package/authoring/config/config.doc.mjs +1 -1
  180. package/authoring/debug/debug.doc.d.mts +11 -0
  181. package/authoring/debug/debug.doc.mjs +182 -0
  182. package/authoring/debug/parse.d.mts +3 -3
  183. package/authoring/doctypes/_schema.d.mts +119 -117
  184. package/authoring/doctypes/_schema.mjs +54 -3
  185. package/authoring/doctypes/base/graph-fields.doc.mjs +8 -6
  186. package/authoring/doctypes/base/type.ts +6 -5
  187. package/authoring/doctypes/command/command.doc.mjs +1 -1
  188. package/authoring/doctypes/command/type.ts +2 -2
  189. package/authoring/doctypes/component/type.ts +2 -2
  190. package/authoring/doctypes/doctypes-new.test.mjs +48 -6
  191. package/authoring/doctypes/enum/enum.doc.mjs +1 -1
  192. package/authoring/doctypes/enum/type.ts +1 -1
  193. package/authoring/doctypes/function/function.doc.mjs +3 -2
  194. package/authoring/doctypes/function/type.ts +3 -2
  195. package/authoring/doctypes/hook/type.ts +2 -2
  196. package/authoring/doctypes/load-contract.test.mjs +28 -2
  197. package/authoring/doctypes/namespace/namespace.doc.mjs +6 -10
  198. package/authoring/doctypes/namespace/parse.test.mjs +23 -25
  199. package/authoring/doctypes/namespace/type.ts +5 -2
  200. package/authoring/doctypes/parse.d.mts +4 -2
  201. package/authoring/doctypes/parse.mjs +10 -5
  202. package/authoring/doctypes/reference/reference.doc.mjs +35 -6
  203. package/authoring/doctypes/reference/type.ts +30 -13
  204. package/authoring/doctypes/schema/schema.doc.mjs +1 -1
  205. package/authoring/doctypes/schema/type.ts +1 -2
  206. package/authoring/doctypes/template/parse.d.mts +2 -0
  207. package/authoring/doctypes/template/parse.mjs +4 -0
  208. package/authoring/doctypes/template/parse.test.mjs +18 -0
  209. package/authoring/doctypes/template/template.doc.mjs +9 -3
  210. package/authoring/doctypes/template/type.ts +8 -0
  211. package/authoring/doctypes/theme/parse.d.mts +35 -0
  212. package/authoring/doctypes/theme/parse.mjs +76 -0
  213. package/authoring/doctypes/theme/theme.doc.d.mts +9 -0
  214. package/authoring/doctypes/theme/theme.doc.mjs +79 -0
  215. package/authoring/doctypes/theme/type.ts +42 -0
  216. package/authoring/doctypes/types.ts +2 -1
  217. package/authoring/gap-report/gap-report.doc.d.mts +12 -0
  218. package/authoring/gap-report/gap-report.doc.mjs +183 -0
  219. package/authoring/identity/identity.doc.mjs +2 -2
  220. package/authoring/index.d.mts +1 -0
  221. package/authoring/index.d.ts +7 -4
  222. package/authoring/index.mjs +2 -1
  223. package/authoring/integration/integration.doc.mjs +2 -2
  224. package/authoring/integration/type.ts +4 -10
  225. package/clients/cli/__tests__/cliManifest.test.ts +27 -29
  226. package/clients/cli/commands/blog.doc.mjs +1 -1
  227. package/clients/cli/commands/build-theme.adaptations.test.mjs +100 -1
  228. package/clients/cli/commands/build-theme.mjs +11 -45
  229. package/clients/cli/commands/build.doc.mjs +12 -7
  230. package/clients/cli/commands/build.mjs +105 -67
  231. package/clients/cli/commands/build.text-fields.test.mjs +41 -0
  232. package/clients/cli/commands/component/index.mjs +1 -1
  233. package/clients/cli/commands/component-ownership.test.mjs +3 -3
  234. package/clients/cli/commands/component.doc.mjs +1 -1
  235. package/clients/cli/commands/discover.broken-integration.test.mjs +7 -8
  236. package/clients/cli/commands/discover.doc.mjs +1 -1
  237. package/clients/cli/commands/discover.mjs +1 -1
  238. package/clients/cli/commands/docs.doc.mjs +21 -9
  239. package/clients/cli/commands/docs.mjs +184 -70
  240. package/clients/cli/commands/docs.test.mjs +120 -16
  241. package/clients/cli/commands/doctor-integration-components.doc.mjs +1 -1
  242. package/clients/cli/commands/doctor-integration-docs.doc.mjs +3 -3
  243. package/clients/cli/commands/doctor-integration-templates.doc.mjs +15 -8
  244. package/clients/cli/commands/doctor-integration-validate.doc.mjs +1 -1
  245. package/clients/cli/commands/doctor-integration.doc.mjs +1 -1
  246. package/clients/cli/commands/doctor-integration.test.mjs +90 -8
  247. package/clients/cli/commands/doctor.doc.mjs +1 -1
  248. package/clients/cli/commands/doctor.mjs +53 -16
  249. package/clients/cli/commands/gap-report.doc.mjs +1 -1
  250. package/clients/cli/commands/hook.doc.mjs +1 -1
  251. package/clients/cli/commands/init.doc.mjs +1 -1
  252. package/clients/cli/commands/integration-add.controls.test.mjs +1 -1
  253. package/clients/cli/commands/integration-add.doc.mjs +11 -1
  254. package/clients/cli/commands/integration-pack.doc.mjs +1 -1
  255. package/clients/cli/commands/integration-real-world.test.mjs +3 -9
  256. package/clients/cli/commands/integration.doc.mjs +1 -1
  257. package/clients/cli/commands/integration.mjs +1 -0
  258. package/clients/cli/commands/layout-check.doc.mjs +1 -1
  259. package/clients/cli/commands/layout-expand.doc.mjs +1 -1
  260. package/clients/cli/commands/layout-grammar.doc.mjs +1 -1
  261. package/clients/cli/commands/layout.doc.mjs +1 -1
  262. package/clients/cli/commands/manifest.doc.mjs +1 -1
  263. package/clients/cli/commands/search.doc.mjs +1 -1
  264. package/clients/cli/commands/search.mjs +11 -2
  265. package/clients/cli/commands/setup-nudge.test.mjs +6 -0
  266. package/clients/cli/commands/swizzle.doc.mjs +1 -1
  267. package/clients/cli/commands/template.doc.mjs +24 -7
  268. package/clients/cli/commands/template.mjs +4 -91
  269. package/clients/cli/commands/text-json-parity.test.mjs +719 -0
  270. package/clients/cli/commands/theme-add.doc.mjs +2 -2
  271. package/clients/cli/commands/theme-build.doc.mjs +1 -1
  272. package/clients/cli/commands/theme-list.doc.mjs +2 -2
  273. package/clients/cli/commands/theme-palette-generate.doc.mjs +3 -3
  274. package/clients/cli/commands/theme-palette.doc.mjs +1 -1
  275. package/clients/cli/commands/theme-targets.behavior.test.mjs +4 -3
  276. package/clients/cli/commands/theme-targets.doc.mjs +1 -1
  277. package/clients/cli/commands/theme-template.doc.mjs +1 -1
  278. package/clients/cli/commands/theme.doc.mjs +1 -1
  279. package/clients/cli/commands/upgrade.doc.mjs +3 -2
  280. package/clients/cli/commands/upgrade.file-protection.test.mjs +228 -0
  281. package/clients/cli/commands/upgrade.mjs +29 -7
  282. package/clients/cli/formatters/index.mjs +2 -0
  283. package/clients/cli/formatters/index.test.mjs +6 -0
  284. package/clients/cli/index.mjs +8 -0
  285. package/clients/cli/lib/hook-format.mjs +14 -5
  286. package/clients/cli/lib/manifest.mjs +9 -2
  287. package/foundation/agent-docs/agent-docs.d.mts +3 -2
  288. package/foundation/agent-docs/agent-docs.mjs +13 -9
  289. package/foundation/agent-docs/agent-docs.test.mjs +19 -1
  290. package/foundation/config/project-themes.test.mjs +11 -19
  291. package/foundation/config/project.d.mts +20 -11
  292. package/foundation/config/project.mjs +123 -80
  293. package/foundation/config/project.test.mjs +144 -21
  294. package/foundation/discovery/authoring-self-docs.d.mts +18 -0
  295. package/foundation/discovery/authoring-self-docs.mjs +37 -15
  296. package/foundation/discovery/authoring-self-docs.test.mjs +25 -9
  297. package/foundation/discovery/authoring-surface.d.mts +74 -0
  298. package/foundation/discovery/authoring-surface.mjs +525 -0
  299. package/foundation/discovery/authoring-surface.test.mjs +392 -0
  300. package/foundation/discovery/cli-self-docs.d.mts +119 -0
  301. package/foundation/discovery/cli-self-docs.mjs +490 -0
  302. package/foundation/discovery/cli-self-docs.test.mjs +375 -0
  303. package/foundation/discovery/component-discovery.d.mts +38 -0
  304. package/foundation/discovery/component-discovery.mjs +48 -0
  305. package/foundation/discovery/component-loader.d.mts +35 -38
  306. package/foundation/discovery/component-loader.mjs +53 -222
  307. package/foundation/discovery/docs-discovery.d.mts +112 -11
  308. package/foundation/discovery/docs-discovery.mjs +231 -36
  309. package/foundation/discovery/docs-discovery.test.mjs +111 -32
  310. package/foundation/discovery/docs-output-budget.d.mts +2 -2
  311. package/foundation/discovery/docs-output-budget.mjs +1 -1
  312. package/foundation/discovery/docs-section-key.d.mts +28 -10
  313. package/foundation/discovery/docs-section-key.mjs +134 -33
  314. package/foundation/discovery/docs-section-key.test.mjs +41 -19
  315. package/foundation/discovery/template-adapter.d.mts +99 -6
  316. package/foundation/discovery/template-adapter.mjs +480 -57
  317. package/foundation/discovery/template-adapter.test.mjs +57 -0
  318. package/foundation/discovery/template-conflict-release.d.mts +13 -0
  319. package/foundation/discovery/template-conflict-release.mjs +40 -0
  320. package/foundation/discovery/template-conflict-release.test.mjs +40 -0
  321. package/foundation/discovery/theme-discovery.d.mts +67 -7
  322. package/foundation/discovery/theme-discovery.mjs +916 -186
  323. package/foundation/discovery/theme-discovery.test.mjs +613 -219
  324. package/foundation/doc-compiler/bundle.d.mts +47 -0
  325. package/foundation/doc-compiler/bundle.mjs +278 -0
  326. package/foundation/doc-compiler/bundle.test.mjs +266 -0
  327. package/foundation/doc-compiler/compile.d.mts +220 -39
  328. package/foundation/doc-compiler/compile.mjs +311 -15
  329. package/foundation/doc-compiler/diagnostics.d.mts +126 -0
  330. package/foundation/doc-compiler/diagnostics.mjs +305 -0
  331. package/foundation/doc-compiler/doc-compiler.test.mjs +28 -1
  332. package/foundation/doc-compiler/doc-loads.test.mjs +1642 -0
  333. package/foundation/doc-compiler/import.d.mts +24 -0
  334. package/foundation/doc-compiler/import.mjs +59 -0
  335. package/foundation/doc-compiler/inputs.d.mts +102 -0
  336. package/foundation/doc-compiler/inputs.mjs +291 -0
  337. package/foundation/doc-compiler/inputs.test.mjs +299 -0
  338. package/foundation/doc-compiler/ir.d.mts +13 -0
  339. package/foundation/doc-compiler/ir.mjs +196 -12
  340. package/foundation/doc-compiler/lenses.d.mts +8 -5
  341. package/foundation/doc-compiler/lenses.mjs +50 -4
  342. package/foundation/doc-compiler/links.d.mts +162 -0
  343. package/foundation/doc-compiler/links.mjs +294 -0
  344. package/foundation/doc-compiler/links.test.mjs +192 -0
  345. package/foundation/doc-compiler/lower-doc.test.mjs +495 -0
  346. package/foundation/doc-compiler/overlays.d.mts +37 -0
  347. package/foundation/doc-compiler/overlays.mjs +206 -0
  348. package/foundation/doc-compiler/parse-readable.d.mts +9 -0
  349. package/foundation/doc-compiler/parse-readable.mjs +29 -0
  350. package/foundation/doc-compiler/read.d.mts +127 -0
  351. package/foundation/doc-compiler/read.mjs +325 -0
  352. package/foundation/doc-compiler/read.test.mjs +313 -0
  353. package/foundation/doc-compiler/source.d.mts +33 -0
  354. package/foundation/doc-compiler/source.mjs +128 -0
  355. package/foundation/doc-compiler/tree.d.mts +288 -0
  356. package/foundation/doc-compiler/tree.mjs +876 -0
  357. package/foundation/doc-compiler/tree.test.mjs +598 -0
  358. package/foundation/fs/file-protection.d.mts +33 -0
  359. package/foundation/fs/file-protection.mjs +825 -0
  360. package/foundation/fs/file-protection.test.mjs +250 -0
  361. package/foundation/integrations/autolink.d.mts +58 -1
  362. package/foundation/integrations/autolink.mjs +143 -57
  363. package/foundation/integrations/autolink.test.mjs +1 -1
  364. package/foundation/integrations/cli-requirement.d.mts +45 -0
  365. package/foundation/integrations/cli-requirement.mjs +154 -0
  366. package/foundation/integrations/cli-requirement.test.mjs +84 -0
  367. package/foundation/integrations/contribution-fixes.d.mts +145 -0
  368. package/foundation/integrations/contribution-fixes.mjs +1284 -0
  369. package/foundation/integrations/contribution-inventory.d.mts +3 -2
  370. package/foundation/integrations/contribution-inventory.mjs +27 -24
  371. package/foundation/integrations/contribution-inventory.test.mjs +86 -27
  372. package/foundation/integrations/integration-warnings.d.mts +9 -2
  373. package/foundation/integrations/integration-warnings.mjs +51 -26
  374. package/foundation/integrations/integration-warnings.test.mjs +74 -1
  375. package/foundation/integrations/integrations.d.mts +3 -0
  376. package/foundation/integrations/integrations.mjs +11 -97
  377. package/foundation/integrations/provider-ledger.test.mjs +275 -0
  378. package/foundation/integrations/provider-resolution.d.mts +152 -0
  379. package/foundation/integrations/provider-resolution.mjs +576 -0
  380. package/foundation/integrations/provider-resolution.test.mjs +369 -0
  381. package/foundation/integrations/theme-descriptor.d.mts +8 -0
  382. package/foundation/integrations/theme-descriptor.mjs +44 -0
  383. package/foundation/integrations/validate-contributions.mjs +121 -29
  384. package/foundation/response/error-codes.d.mts +3 -1
  385. package/foundation/response/error-codes.d.ts +2 -0
  386. package/foundation/response/error-codes.doc.mjs +12 -2
  387. package/foundation/response/error-codes.mjs +7 -1
  388. package/foundation/response/error-codes.test.mjs +55 -11
  389. package/foundation/response/response-types.doc.d.mts +5 -1
  390. package/foundation/response/response-types.doc.mjs +20 -11
  391. package/foundation/response/response.doc.mjs +1 -1
  392. package/foundation/text/string-utils.d.mts +8 -0
  393. package/foundation/text/string-utils.mjs +40 -10
  394. package/foundation/xle/expand.mjs +1 -1
  395. package/foundation/xle/xle.test.mjs +13 -0
  396. package/package.json +10 -9
  397. package/assets/templates/themes/manifest.json +0 -95
@@ -10,24 +10,53 @@
10
10
  * @output Catalog access, the compiler input for a topic, and the compiled
11
11
  * node for it: lowered (overlaid, extensions merged, keys stamped) or linked
12
12
  * (token references resolved too), memoized per catalog.
13
- * @position Sits beside docs.mjs (api/docs/). Loads authored files and hands
14
- * them to foundation/doc-compiler, so no leaf, doctor check or search loads,
15
- * merges, or resolves docs on its own. Discovery itself lives in
13
+ * @position Sits beside docs.mjs (api/docs/). Hands topics to
14
+ * foundation/doc-compiler (which loads their files) and memoizes the nodes
15
+ * per catalog, so no leaf, doctor check or search loads, merges, or resolves
16
+ * docs on its own. Discovery itself lives in
16
17
  * foundation/discovery/docs-discovery, which the catalog comes from.
17
18
  */
18
19
 
19
- import * as fs from 'node:fs';
20
- import * as path from 'node:path';
21
- import {pathToFileURL} from 'node:url';
22
20
  import {Project} from '../../foundation/config/project.mjs';
23
21
  import {DocsCatalog} from '../../foundation/discovery/docs-discovery.mjs';
24
22
  import {
25
23
  linkReferenceTopic,
26
24
  lowerReferenceTopic,
27
25
  } from '../../foundation/doc-compiler/compile.mjs';
26
+ import {
27
+ deepFreeze,
28
+ loadTopicFile,
29
+ loadTopicInput,
30
+ OVERLAY_LANGUAGES,
31
+ overlayLanguages,
32
+ } from '../../foundation/doc-compiler/read.mjs';
33
+ import {
34
+ buildDocsTree,
35
+ loadTreeInputs,
36
+ } from '../../foundation/doc-compiler/tree.mjs';
37
+ import {sortDiagnostics} from '../../foundation/doc-compiler/diagnostics.mjs';
38
+ import {
39
+ linkBlocks,
40
+ parseLinkTarget,
41
+ } from '../../foundation/doc-compiler/links.mjs';
42
+ import {
43
+ createDocId,
44
+ normalizeProviderId,
45
+ } from '../../foundation/identity/provider-identity.mjs';
46
+ import {
47
+ cliDocIndex,
48
+ cliDocSection,
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';
28
56
  import {AstryxError} from '../error.mjs';
29
57
  import {ERROR_CODES} from '../../foundation/response/error-codes.mjs';
30
- import {parseDoc} from '../../authoring/doctypes/parse.mjs';
58
+
59
+ export {OVERLAY_LANGUAGES, overlayLanguages};
31
60
 
32
61
  /**
33
62
  * The project's topics: the built-in ones plus whatever the configured
@@ -51,141 +80,613 @@ export async function loadDocsCatalog(cwd = process.cwd()) {
51
80
  }
52
81
  }
53
82
 
54
- /** The localized overlays a docs read can apply. */
55
- export const OVERLAY_LANGUAGES = ['zh', 'dense'];
83
+ /**
84
+ * The overlay a read applies: none for the authored language.
85
+ * @param {string | null | undefined} lang
86
+ * @returns {string | null}
87
+ */
88
+ function overlayLanguage(lang) {
89
+ return lang && lang !== 'en' ? lang : null;
90
+ }
91
+
92
+ /** @type {WeakMap<DocsCatalog, Map<string, Promise<import('../../foundation/doc-compiler/compile.mjs').CompiledReferenceNode>>>} */
93
+ const loweredByCatalog = new WeakMap();
56
94
 
57
95
  /**
58
- * Where the `lang` overlay of a doc file lives: `{topic}.doc.{lang}.mjs`.
59
- * @param {string} docPath
60
- * @param {string} lang
96
+ * One topic, lowered for `lang` with its links as written: overlaid,
97
+ * extensions merged, keys stamped. Memoized per catalog, so a read that
98
+ * references a topic twice loads it once. Every read of the catalog shares the
99
+ * memoized node, so it is frozen; the lenses hand readers copies.
100
+ * @param {DocsCatalog} catalog
101
+ * @param {import('../../foundation/discovery/docs-discovery.mjs').DocsTopicEntry} entry
102
+ * @param {string | null} [lang]
103
+ * @returns {Promise<import('../../foundation/doc-compiler/compile.mjs').CompiledReferenceNode>}
104
+ */
105
+ function lowerRawTopic(catalog, entry, lang = null) {
106
+ const overlay = overlayLanguage(lang);
107
+ let cache = loweredByCatalog.get(catalog);
108
+ if (!cache) {
109
+ cache = new Map();
110
+ loweredByCatalog.set(catalog, cache);
111
+ }
112
+ const key = `${entry.name.toLowerCase()}\u0000${overlay ?? ''}`;
113
+ let lowered = cache.get(key);
114
+ if (!lowered) {
115
+ lowered = loadTopicInput(entry, overlay).then(input =>
116
+ deepFreeze(lowerReferenceTopic(input)),
117
+ );
118
+ cache.set(key, lowered);
119
+ }
120
+ return lowered;
121
+ }
122
+
123
+ /**
124
+ * @typedef {import('../../foundation/doc-compiler/tree.mjs').DocsTree} DocsTree
125
+ * @typedef {import('../../foundation/doc-compiler/tree.mjs').TreeNode} TreeNode
126
+ * @typedef {import('../../foundation/doc-compiler/links.mjs').LinkProblem} LinkProblem
127
+ * @typedef {import('../../foundation/doc-compiler/links.mjs').LinkResolver} LinkResolver
128
+ * @typedef {import('../../foundation/doc-compiler/links.mjs').DocIncluder} DocIncluder
129
+ * @typedef {import('../../foundation/discovery/docs-discovery.mjs').DocsTopicEntry} DocsTopicEntry
130
+ */
131
+
132
+ /** @type {WeakMap<DocsCatalog, Map<string, Promise<{node: import('../../foundation/doc-compiler/compile.mjs').CompiledReferenceNode, problems: LinkProblem[]}>>>} */
133
+ const linkedByCatalog = new WeakMap();
134
+
135
+ /**
136
+ * The CLI's own topics alone, for a check that runs without a project.
137
+ * @returns {DocsCatalog}
138
+ */
139
+ export function builtinCatalog() {
140
+ return DocsCatalog.fromBuiltins();
141
+ }
142
+
143
+ /**
144
+ * The provider id a topic's (or an extension's) links resolve against: its
145
+ * owner's.
146
+ * @param {{providerId?: string, package: string}} entry
61
147
  * @returns {string}
62
148
  */
63
- function overlayPath(docPath, lang) {
64
- return path.join(
65
- path.dirname(docPath),
66
- `${path.basename(docPath, '.doc.mjs')}.doc.${lang}.mjs`,
67
- );
149
+ function providerOf(entry) {
150
+ return normalizeProviderId(entry.providerId ?? entry.package);
68
151
  }
69
152
 
70
153
  /**
71
- * The overlay languages a topic ships for its own file or any extension.
72
- * @param {import('../../foundation/discovery/docs-discovery.mjs').DocsTopicEntry} entry
73
- * @returns {string[]}
154
+ * One topic, lowered for `lang` with every link between docs resolved
155
+ * (spec:AST-047 FR9): an inline `{@link <target>}` reads as the command that
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.
159
+ * @param {DocsCatalog} catalog
160
+ * @param {DocsTopicEntry} entry
161
+ * @param {string | null} [lang]
162
+ * @returns {Promise<import('../../foundation/doc-compiler/compile.mjs').CompiledReferenceNode>}
74
163
  */
75
- export function overlayLanguages(entry) {
76
- const files = [entry.path, ...entry.extensions.map(ext => ext.path)];
77
- return OVERLAY_LANGUAGES.filter(lang =>
78
- files.some(file => fs.existsSync(overlayPath(file, lang))),
79
- );
164
+ export async function lowerTopic(catalog, entry, lang = null) {
165
+ return (await linkTopic(catalog, entry, lang)).node;
80
166
  }
81
167
 
82
168
  /**
83
- * The overlay a read applies: none for the authored language.
84
- * @param {string | null | undefined} lang
85
- * @returns {string | null}
169
+ * Each link in a topic that names no doc.
170
+ * @param {DocsCatalog} catalog
171
+ * @param {DocsTopicEntry} entry
172
+ * @returns {Promise<LinkProblem[]>}
86
173
  */
87
- function overlayLanguage(lang) {
88
- return lang && lang !== 'en' ? lang : null;
174
+ export async function topicLinkProblems(catalog, entry) {
175
+ return (await linkTopic(catalog, entry, null)).problems;
89
176
  }
90
177
 
91
178
  /**
92
- * Load one authored file and the overlay for `lang`. A failure is recorded on
93
- * the result, not thrown, so the compiler reports it in reading order.
94
- * @param {string} docPath
179
+ * @param {DocsCatalog} catalog
180
+ * @param {DocsTopicEntry} entry
95
181
  * @param {string | null} lang
96
- * @returns {Promise<import('../../foundation/doc-compiler/compile.mjs').AuthoredFile>}
97
182
  */
98
- async function loadAuthoredFile(docPath, lang) {
99
- const file = path.basename(docPath);
100
- let doc;
101
- try {
102
- const mod = await import(pathToFileURL(docPath).href);
103
- doc = parseDoc(mod.docs ?? mod.default, file);
104
- } catch (error) {
105
- return {file, error};
183
+ function linkTopic(catalog, entry, lang) {
184
+ let cache = linkedByCatalog.get(catalog);
185
+ if (!cache) {
186
+ cache = new Map();
187
+ linkedByCatalog.set(catalog, cache);
106
188
  }
107
- if (!lang) return {file, doc};
108
- const translationPath = overlayPath(docPath, lang);
109
- if (!fs.existsSync(translationPath)) return {file, doc};
110
- try {
111
- const translationMod = await import(pathToFileURL(translationPath).href);
112
- return {
113
- file,
114
- doc,
115
- overlay: translationMod.docsZh || translationMod.docsDense || null,
116
- };
117
- } catch (overlayError) {
118
- return {file, doc, overlayError};
189
+ const key = `${entry.name.toLowerCase()}\u0000${overlayLanguage(lang) ?? ''}`;
190
+ let linked = cache.get(key);
191
+ if (!linked) {
192
+ linked = (async () => {
193
+ const raw = await lowerRawTopic(catalog, entry, lang);
194
+ /** @type {Map<string, {resolve: LinkResolver, include: DocIncluder}>} */
195
+ const linkers = new Map();
196
+ /** @param {string} provider */
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);
205
+ }
206
+ return linker;
207
+ };
208
+ /** @type {LinkProblem[]} */
209
+ const problems = [];
210
+ const sections = [];
211
+ // Each section resolves its links against the provider that wrote it:
212
+ // an extension's sections against the extension's provider, never the
213
+ // base topic's.
214
+ for (const section of raw.doc.sections) {
215
+ const provider = raw.sectionProviders?.[section.id];
216
+ const {resolve, include} = await linkerFor(
217
+ provider == null ? providerOf(entry) : normalizeProviderId(provider),
218
+ );
219
+ const linked = await linkBlocks(
220
+ section.content,
221
+ resolve,
222
+ {section: section.id ?? section.title},
223
+ include,
224
+ );
225
+ problems.push(...linked.problems);
226
+ sections.push({...section, content: linked.content});
227
+ }
228
+ return {
229
+ node: deepFreeze({...raw, doc: {...raw.doc, sections}}),
230
+ problems,
231
+ };
232
+ })();
233
+ cache.set(key, linked);
119
234
  }
235
+ return linked;
120
236
  }
121
237
 
238
+ /** @type {WeakMap<DocsCatalog, Promise<DocsTree>>} */
239
+ const treesByCatalog = new WeakMap();
240
+
122
241
  /**
123
- * Everything the compiler needs for one topic, read from disk.
124
- * @param {import('../../foundation/discovery/docs-discovery.mjs').DocsTopicEntry} entry
125
- * @param {string | null} lang
126
- * @returns {Promise<import('../../foundation/doc-compiler/compile.mjs').ReferenceTopicInput>}
127
- */
128
- async function loadCompilerInput(entry, lang) {
129
- const extensions = [];
130
- for (const extension of entry.extensions) {
131
- extensions.push({
132
- ...(await loadAuthoredFile(extension.path, lang)),
133
- provider: extension.package,
242
+ * The project's docs tree: the CLI's own docs plus the namespace docs and
243
+ * placed guides the configured integrations ship (spec:AST-046), built once
244
+ * per catalog. Without integration docs it is the CLI's tree, built once per
245
+ * process.
246
+ * @param {DocsCatalog} catalog
247
+ * @param {{fresh?: boolean}} [options] `fresh`: reread the CLI's tree files
248
+ * @returns {Promise<DocsTree>}
249
+ */
250
+ export function projectTree(catalog, {fresh = false} = {}) {
251
+ let tree = treesByCatalog.get(catalog);
252
+ if (!tree || fresh) {
253
+ tree = buildProjectTree(catalog, fresh);
254
+ treesByCatalog.set(catalog, tree);
255
+ }
256
+ return tree;
257
+ }
258
+
259
+ /** @type {ReturnType<typeof loadTreeInputs> | undefined} */
260
+ let cliTreeInputs;
261
+
262
+ /**
263
+ * Every flat topic in the catalog, as the tree's Unorganized level reads it.
264
+ * @param {DocsCatalog} catalog
265
+ * @returns {Promise<import('../../foundation/doc-compiler/tree.mjs').TreeTopicInput[]>}
266
+ */
267
+ async function flatTopicInputs(catalog) {
268
+ /** @type {import('../../foundation/doc-compiler/tree.mjs').TreeTopicInput[]} */
269
+ const topics = [];
270
+ for (const entry of catalog.entries()) {
271
+ const aliases = catalog.aliasesOf(entry);
272
+ let {title, description} = entry;
273
+ if (title == null || description == null) {
274
+ try {
275
+ const file = await loadTopicFile(entry.path, null);
276
+ title ??= file.doc?.title;
277
+ description ??= file.doc?.description;
278
+ } catch {
279
+ // A topic that does not load is reported where it is read.
280
+ }
281
+ }
282
+ topics.push({
283
+ provider: entry.package,
284
+ providerId: entry.providerId ?? entry.package,
285
+ name: entry.name,
286
+ title: title ?? entry.name,
287
+ summary: description ?? '',
288
+ source: `${entry.package}:${entry.name}`,
289
+ ...(aliases.length > 0 ? {aliases} : {}),
134
290
  });
135
291
  }
292
+ return topics;
293
+ }
294
+
295
+ /**
296
+ * The CLI's tree, each configured integration's namespaces and guides, and
297
+ * every flat topic in the generated Unorganized level.
298
+ * @param {DocsCatalog} catalog
299
+ * @param {boolean} fresh
300
+ * @returns {Promise<DocsTree>}
301
+ */
302
+ async function buildProjectTree(catalog, fresh) {
303
+ const added = catalog.treeInputs;
304
+ if (fresh || !cliTreeInputs) cliTreeInputs = loadTreeInputs();
305
+ const cli = await cliTreeInputs;
306
+ const result = buildDocsTree({
307
+ namespaces: [...cli.namespaces, ...added.flatMap(each => each.namespaces)],
308
+ docs: [...cli.docs, ...added.flatMap(each => each.guides)],
309
+ topics: await flatTopicInputs(catalog),
310
+ });
136
311
  return {
137
- id: entry.name,
138
- provider: entry.package,
139
- replaces: entry.replaces ?? null,
140
- lang,
141
- base: await loadAuthoredFile(entry.path, lang),
142
- extensions,
312
+ ...result,
313
+ diagnostics: sortDiagnostics([...cli.diagnostics, ...result.diagnostics]),
143
314
  };
144
315
  }
145
316
 
146
- /** @type {WeakMap<DocsCatalog, Map<string, Promise<import('../../foundation/doc-compiler/compile.mjs').CompiledReferenceNode>>>} */
147
- const loweredByCatalog = new WeakMap();
317
+ /** @type {WeakMap<DocsTree, Map<string, TreeNode>>} */
318
+ const identitiesByTree = new WeakMap();
319
+
320
+ /** @param {string} provider @param {string} kind @param {string} name */
321
+ function identityKey(provider, kind, name) {
322
+ return `${provider}\u0000${kind}\u0000${kind === 'generic' ? name.toLowerCase() : name}`;
323
+ }
324
+
325
+ /**
326
+ * Every tree node with an identity, by provider id, kind, and name.
327
+ * @param {DocsTree} tree
328
+ */
329
+ function identitiesOf(tree) {
330
+ let index = identitiesByTree.get(tree);
331
+ if (!index) {
332
+ index = new Map();
333
+ for (const node of tree.nodes.values()) {
334
+ if (node.id == null) continue;
335
+ let provider = node.providerId;
336
+ try {
337
+ provider = normalizeProviderId(node.providerId);
338
+ } catch {
339
+ // Kept as given; a link names it the same way.
340
+ }
341
+ index.set(identityKey(provider, node.kind, node.name), node);
342
+ }
343
+ identitiesByTree.set(tree, index);
344
+ }
345
+ return index;
346
+ }
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;
148
355
 
149
356
  /**
150
- * One topic, lowered for `lang`: overlaid, extensions merged, keys stamped.
151
- * Memoized per catalog, so a read that references a topic twice loads it once.
152
- * Every read of the catalog shares the memoized node, so it is frozen; the
153
- * lenses hand readers copies.
357
+ * The CLI authoring doc a link names, while `astryx docs authoring` is the
358
+ * CLI's own topic.
154
359
  * @param {DocsCatalog} catalog
155
- * @param {import('../../foundation/discovery/docs-discovery.mjs').DocsTopicEntry} entry
156
- * @param {string | null} [lang]
157
- * @returns {Promise<import('../../foundation/doc-compiler/compile.mjs').CompiledReferenceNode>}
360
+ * @param {string} kind
361
+ * @param {string} name
362
+ * @returns {Promise<any | null>}
158
363
  */
159
- export function lowerTopic(catalog, entry, lang = null) {
160
- const overlay = overlayLanguage(lang);
161
- let cache = loweredByCatalog.get(catalog);
162
- if (!cache) {
163
- cache = new Map();
164
- loweredByCatalog.set(catalog, cache);
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
+
384
+ /**
385
+ * How a doc's links find their targets (spec:AST-047 FR9): a doc in the
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.
389
+ * @param {DocsCatalog} catalog
390
+ * @param {string} fromProvider the provider id of the doc the links sit in
391
+ * @returns {Promise<(target: string) => Promise<FoundDoc | {problem: string}>>}
392
+ */
393
+ async function docFinder(catalog, fromProvider) {
394
+ const identities = identitiesOf(await projectTree(catalog));
395
+ return async target => {
396
+ const parsed = parseLinkTarget(target);
397
+ if ('error' in parsed) return {problem: parsed.error};
398
+ let provider;
399
+ try {
400
+ provider = normalizeProviderId(parsed.provider ?? fromProvider);
401
+ } catch {
402
+ return {
403
+ problem: `"${target}" names "${parsed.provider}", which is not a provider id: an npm package name, or the \`providerId\` its manifest declares`,
404
+ };
405
+ }
406
+ const node = identities.get(identityKey(provider, parsed.kind, parsed.name));
407
+ if (node) {
408
+ return {
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},
439
+ };
440
+ }
441
+ if (parsed.kind === 'generic') {
442
+ const entry = catalog.resolve(parsed.name);
443
+ if (
444
+ entry &&
445
+ !entry.tree &&
446
+ (providerOf(entry) === provider ||
447
+ catalog.aliasesOf(entry).includes(parsed.name.toLowerCase()))
448
+ ) {
449
+ let title = entry.title ?? entry.name;
450
+ let summary = entry.description ?? '';
451
+ try {
452
+ const raw = await lowerRawTopic(catalog, entry);
453
+ title = raw.doc.title ?? title;
454
+ summary = raw.doc.description ?? summary;
455
+ } catch {
456
+ // The topic budget check reports a topic that does not load; the
457
+ // link still opens it.
458
+ }
459
+ return {
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,
470
+ };
471
+ }
472
+ }
473
+ return {
474
+ problem: `"${target}" names no doc. Find it with \`astryx search ${parsed.name} --type doc\`, then name it as \`[<provider>:]<kind>:<name>\`.`,
475
+ };
476
+ };
477
+ }
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
+ );
165
539
  }
166
- const key = `${entry.name.toLowerCase()}\u0000${overlay ?? ''}`;
167
- let lowered = cache.get(key);
168
- if (!lowered) {
169
- lowered = loadCompilerInput(entry, overlay).then(input =>
170
- deepFreeze(lowerReferenceTopic(input)),
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
+ ]),
171
567
  );
172
- cache.set(key, lowered);
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;
173
596
  }
174
- return lowered;
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};
175
607
  }
176
608
 
177
609
  /**
178
- * Freeze a value and everything in it.
179
- * @template T
180
- * @param {T} value
181
- * @returns {T}
610
+ * Every link in the project's docs that names no doc: in each topic, each
611
+ * guide the tree places, and each typed doc.
612
+ * @param {DocsCatalog} catalog
613
+ * @param {DocsTree} tree
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
618
+ * @returns {Promise<string[]>}
182
619
  */
183
- function deepFreeze(value) {
184
- if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
185
- Object.freeze(value);
186
- for (const child of Object.values(value)) deepFreeze(child);
620
+ export async function docsLinkProblems(
621
+ catalog,
622
+ tree,
623
+ {owner, references = false} = {},
624
+ ) {
625
+ /** @type {string[]} */
626
+ const problems = [];
627
+ /** @param {string} where @param {LinkProblem[]} found */
628
+ const note = (where, found) => {
629
+ for (const problem of found) {
630
+ if ((problem.include === true) !== references) continue;
631
+ problems.push(
632
+ `${where}${problem.section ? ` \u00a7 ${problem.section}` : ''}: ${problem.message}`,
633
+ );
634
+ }
635
+ };
636
+ for (const entry of catalog.entries()) {
637
+ // A package owns a topic it wrote, and the sections it adds to another
638
+ // package's topic.
639
+ if (
640
+ owner != null &&
641
+ entry.package !== owner &&
642
+ !entry.extensions.some(extension => extension.package === owner)
643
+ ) {
644
+ continue;
645
+ }
646
+ try {
647
+ note(entry.name, await topicLinkProblems(catalog, entry));
648
+ } catch {
649
+ // The topic budget check reports a topic that does not load.
650
+ }
187
651
  }
188
- return value;
652
+ for (const node of tree.nodes.values()) {
653
+ if (owner != null && node.provider !== owner) continue;
654
+ if (node.kind === 'generic' && node.ref?.topicFile) {
655
+ try {
656
+ note(node.route, await topicLinkProblems(catalog, guideEntry(node)));
657
+ } catch {
658
+ // As above.
659
+ }
660
+ } else if (node.ref?.selfDoc) {
661
+ note(node.route, (await nodeContent(catalog, tree, node)).problems);
662
+ }
663
+ }
664
+ return problems;
665
+ }
666
+
667
+ /**
668
+ * What \`astryx doctor integration docs\` checks in one integration's docs: the
669
+ * docs tree they build beside the CLI's (namespaces, placements, routes) and
670
+ * every link in them (spec:AST-046, spec:AST-047).
671
+ * @param {{name: string}} integration
672
+ * @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
673
+ * @returns {Promise<string[]>}
674
+ */
675
+ export async function packageDocsProblems(integration, discovered) {
676
+ const catalog = DocsCatalog.fromBuiltins();
677
+ for (const record of discovered.records) catalog.add(record);
678
+ catalog.addTreeInputs({
679
+ namespaces: discovered.namespaces.map(input => ({...input, rank: 1})),
680
+ guides: discovered.guides.map(input => ({...input, rank: 1})),
681
+ });
682
+ const tree = await projectTree(catalog);
683
+ const problems = tree.diagnostics
684
+ .filter(d => d.severity === 'error' && d.provider === integration.name)
685
+ .map(d => `${d.source ?? integration.name}: ${d.message}`);
686
+ problems.push(
687
+ ...(await docsLinkProblems(catalog, tree, {owner: integration.name})),
688
+ );
689
+ return problems;
189
690
  }
190
691
 
191
692
  /**
@@ -202,6 +703,28 @@ export function referenceTargets(catalog, lang) {
202
703
  };
203
704
  }
204
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
+
205
728
  /**
206
729
  * One topic, compiled for `lang`: lowered, then every token reference linked.
207
730
  * @param {DocsCatalog} catalog
@@ -217,40 +740,308 @@ export async function compileTopic(catalog, entry, lang = null) {
217
740
  }
218
741
 
219
742
  /**
220
- * Resolve `topic` against the project's catalog (throwing `ERR_UNKNOWN_TOPIC`
221
- * when unmatched) and lower it with any --dense/--zh overlay and any
222
- * integration extension applied. Shared by the leaves so topic normalization
223
- * and unknown-topic handling live in exactly one place.
224
- *
225
- * @param {string} topic
226
- * @param {object} [options]
227
- * @param {string} [options.lang]
228
- * @param {boolean} [options.zh]
229
- * @param {boolean} [options.dense]
230
- * @param {string} [options.cwd]
231
- * @returns {Promise<{
232
- * catalog: DocsCatalog,
233
- * node: import('../../foundation/doc-compiler/compile.mjs').CompiledReferenceNode,
234
- * lang: string | null,
235
- * }>}
743
+ * A guide the docs tree places, as a topic entry the topic readers open by its
744
+ * route. It is never a flat topic: `astryx docs <route>` is its only name.
745
+ * @param {import('../../foundation/doc-compiler/tree.mjs').TreeNode} node
746
+ * @returns {import('../../foundation/discovery/docs-discovery.mjs').DocsTopicEntry}
236
747
  */
237
- export async function resolveTopicDocs(topic, options = {}) {
238
- const {lang = null, zh = false, dense = false, cwd} = options;
239
- const effectiveLang = lang || (dense ? 'dense' : zh ? 'zh' : null);
240
- const catalog = await loadDocsCatalog(cwd);
748
+ export function guideEntry(node) {
749
+ return {
750
+ name: node.route,
751
+ route: node.route,
752
+ package: node.provider,
753
+ providerId: node.providerId,
754
+ path: node.ref.topicFile,
755
+ extensions: [],
756
+ tree: true,
757
+ parent: node.parent ?? undefined,
758
+ };
759
+ }
760
+
761
+ /** @type {WeakMap<DocsTree, ReturnType<typeof cliDocIndex>>} */
762
+ const typedDocIndexes = new WeakMap();
763
+
764
+ /**
765
+ * What a typed doc in the docs tree prints: its content, with every link to
766
+ * another doc resolved (spec:AST-047 FR9). A namespace has no content. The
767
+ * CLI's doc modules are discovery, so this lives in the adapter
768
+ * (architecture:cli-surface INV21).
769
+ * @param {DocsCatalog} catalog
770
+ * @param {DocsTree} tree
771
+ * @param {TreeNode} node
772
+ * @returns {Promise<{content: any[], problems: LinkProblem[]}>}
773
+ */
774
+ export async function nodeContent(catalog, tree, node) {
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
+ }
241
782
 
242
- // A public API caller could pass a non-string topic; `resolve` answers
243
- // undefined for one, which lands on the same stable code as an unknown name
244
- // rather than a raw TypeError (which downgrades to ERR_UNKNOWN).
245
- const entry = catalog.resolve(topic);
246
- if (!entry) {
247
- throw new AstryxError(
248
- `Unknown topic "${String(topic)}"`,
249
- catalog.names().map(t => ({name: t, reason: 'available topic'})),
250
- ERROR_CODES.ERR_UNKNOWN_TOPIC,
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) {
790
+ let index = typedDocIndexes.get(tree);
791
+ if (!index) {
792
+ index = cliDocIndex(
793
+ [...tree.nodes.values()].flatMap(each =>
794
+ each.ref?.selfDoc ? [each.ref.selfDoc] : [],
795
+ ),
251
796
  );
797
+ typedDocIndexes.set(tree, index);
252
798
  }
799
+ return index;
800
+ }
253
801
 
802
+ /**
803
+ * The command that opens the level a topic sits in when the tree cannot say:
804
+ * a guide's parent namespace, or the topic list.
805
+ * @param {import('../../foundation/discovery/docs-discovery.mjs').DocsTopicEntry} entry
806
+ * @returns {import('./docs.type.mjs').DocsCommand}
807
+ */
808
+ export function topicUp(entry) {
809
+ return entry.tree && entry.parent
810
+ ? `astryx docs ${entry.parent}`
811
+ : 'astryx docs';
812
+ }
813
+
814
+ /**
815
+ * The moves from a node's place in the tree (spec:AST-047 FR2, FR4): up to its
816
+ * parent (the topic list, at the top), and across to the nodes before and
817
+ * after it in its parent's slot.
818
+ * @param {DocsTree} tree
819
+ * @param {TreeNode} node
820
+ * @returns {import('./docs.type.mjs').DocsLinks}
821
+ */
822
+ export function placeLinks(tree, node) {
823
+ /** @type {import('./docs.type.mjs').DocsLinks} */
824
+ const links = {
825
+ up: node.parent == null ? 'astryx docs' : `astryx docs ${node.parent}`,
826
+ };
827
+ const parent = node.parent == null ? undefined : tree.get(node.parent);
828
+ const siblings =
829
+ parent?.slots.find(slot => slot.children.includes(node.route))?.children ??
830
+ [];
831
+ const at = siblings.indexOf(node.route);
832
+ if (at > 0) links.previous = `astryx docs ${siblings[at - 1]}`;
833
+ if (at !== -1 && at < siblings.length - 1) {
834
+ links.next = `astryx docs ${siblings[at + 1]}`;
835
+ }
836
+ return links;
837
+ }
838
+
839
+ /**
840
+ * The moves a topic read offers: from its place in the tree, where a guide
841
+ * sits in its namespace and a flat topic in the Unorganized level.
842
+ * @param {DocsCatalog} catalog
843
+ * @param {import('../../foundation/discovery/docs-discovery.mjs').DocsTopicEntry} entry
844
+ * @returns {Promise<import('./docs.type.mjs').DocsLinks>}
845
+ */
846
+ export async function topicLinks(catalog, entry) {
847
+ const tree = await projectTree(catalog);
848
+ const node = tree.get(entry.tree ? (entry.route ?? entry.name) : entry.name);
849
+ const placed =
850
+ node && (entry.tree ? node.kind === 'generic' : node.ref?.flatTopic === entry.name);
851
+ return placed ? placeLinks(tree, node) : {up: topicUp(entry)};
852
+ }
853
+
854
+ /**
855
+ * What a docs argument names: a topic (a flat one, or a guide the docs tree
856
+ * places), a namespace or typed doc in the tree, or nothing, as nameOwner
857
+ * decides.
858
+ * @param {unknown} topic
859
+ * @param {{cwd?: string}} [options]
860
+ * @returns {Promise<
861
+ * | {kind: 'topic', catalog: DocsCatalog, entry: import('../../foundation/discovery/docs-discovery.mjs').DocsTopicEntry}
862
+ * | {kind: 'node', catalog: DocsCatalog, tree: import('../../foundation/doc-compiler/tree.mjs').DocsTree, node: import('../../foundation/doc-compiler/tree.mjs').TreeNode}
863
+ * | {kind: 'unknown', catalog: DocsCatalog}
864
+ * >}
865
+ */
866
+ export async function resolveDocsArgument(topic, {cwd} = {}) {
867
+ const catalog = await loadDocsCatalog(cwd);
868
+ if (typeof topic !== 'string' || topic === '')
869
+ return {kind: 'unknown', catalog};
870
+ const tree = await projectTree(catalog);
871
+ const owner = nameOwner(tree, catalog, topic);
872
+ if (owner == null) return {kind: 'unknown', catalog};
873
+ if (owner.kind === 'topic') return {kind: 'topic', catalog, entry: owner.entry};
874
+ return treeArgument(catalog, tree, owner.node);
875
+ }
876
+
877
+ /**
878
+ * Who answers to a name a reader types (spec:AST-046 FR5, FR11). The tree
879
+ * decides first: the node at that route, compared without case, holds it. A
880
+ * name with no node of its own is a topic's other name (its `replaces`
881
+ * alias), and that topic answers, unless the tree gave the topic's own route
882
+ * to another doc. Reads, the topic list, and search all ask this, so they
883
+ * agree on every name.
884
+ * @param {DocsTree} tree
885
+ * @param {DocsCatalog} catalog
886
+ * @param {string} name
887
+ * @returns {{kind: 'topic', entry: import('../../foundation/discovery/docs-discovery.mjs').DocsTopicEntry} | {kind: 'node', node: TreeNode} | null}
888
+ */
889
+ export function nameOwner(tree, catalog, name) {
890
+ const node = tree.get(name) ?? tree.getFolded?.(name);
891
+ if (node != null) {
892
+ if (node.ref?.flatTopic == null) return {kind: 'node', node};
893
+ const entry = catalog.resolve(node.ref.flatTopic);
894
+ return entry == null ? null : {kind: 'topic', entry};
895
+ }
896
+ const entry = catalog.resolve(name);
897
+ if (entry == null) return null;
898
+ const owner = routeOwner(tree, entry);
899
+ return owner == null ? {kind: 'topic', entry} : {kind: 'node', node: owner};
900
+ }
901
+
902
+ /**
903
+ * Whether a topic answers to its own name: what the topic list and search
904
+ * offer must open that topic.
905
+ * @param {DocsTree} tree
906
+ * @param {DocsCatalog} catalog
907
+ * @param {import('../../foundation/discovery/docs-discovery.mjs').DocsTopicEntry} entry
908
+ * @returns {boolean}
909
+ */
910
+ export function holdsOwnName(tree, catalog, entry) {
911
+ const owner = nameOwner(tree, catalog, entry.name);
912
+ return (
913
+ owner?.kind === 'topic' &&
914
+ owner.entry.name === entry.name &&
915
+ owner.entry.package === entry.package
916
+ );
917
+ }
918
+
919
+ /**
920
+ * What a tree node opens as: a guide the tree places reads like a topic; a
921
+ * namespace or a typed doc is a node.
922
+ * @param {DocsCatalog} catalog
923
+ * @param {DocsTree} tree
924
+ * @param {TreeNode} node
925
+ * @returns {{kind: 'topic', catalog: DocsCatalog, entry: import('../../foundation/discovery/docs-discovery.mjs').DocsTopicEntry} | {kind: 'node', catalog: DocsCatalog, tree: DocsTree, node: TreeNode}}
926
+ */
927
+ function treeArgument(catalog, tree, node) {
928
+ if (node.kind === 'generic') {
929
+ return {kind: 'topic', catalog, entry: guideEntry(node)};
930
+ }
931
+ return {kind: 'node', catalog, tree, node};
932
+ }
933
+
934
+ /**
935
+ * The doc that took a flat topic's route in the docs tree, when it is not
936
+ * that topic (spec:AST-046 FR11): the CLI keeps its routes, such as `cli`
937
+ * and `unorganized`, and a namespace keeps its route over a topic of the same
938
+ * name. Null when the topic owns its route, or the tree has no node there.
939
+ * @param {DocsTree} tree
940
+ * @param {import('../../foundation/discovery/docs-discovery.mjs').DocsTopicEntry} entry
941
+ * @returns {TreeNode | null}
942
+ */
943
+ export function routeOwner(tree, entry) {
944
+ const node = tree.get(entry.name) ?? tree.getFolded?.(entry.name);
945
+ if (node == null) return null;
946
+ return node.ref?.flatTopic === entry.name && node.provider === entry.package
947
+ ? null
948
+ : node;
949
+ }
950
+
951
+ /**
952
+ * The error for a docs argument that names nothing. For a route, it suggests
953
+ * the children of the deepest namespace the route reaches; otherwise, every
954
+ * topic and every top-level namespace.
955
+ * @param {unknown} topic
956
+ * @param {DocsCatalog} catalog
957
+ * @returns {Promise<AstryxError>}
958
+ */
959
+ export async function unknownTopicError(topic, catalog) {
960
+ const tree = await projectTree(catalog);
961
+ /** @type {Array<{name: string, reason: string}>} */
962
+ let suggestions = [];
963
+ if (typeof topic === 'string' && topic.includes('/')) {
964
+ const parts = topic.split('/');
965
+ for (
966
+ let depth = parts.length - 1;
967
+ depth > 0 && suggestions.length === 0;
968
+ depth--
969
+ ) {
970
+ const near = tree.get(parts.slice(0, depth).join('/'));
971
+ if (near) {
972
+ suggestions = near.slots.flatMap(slot =>
973
+ slot.children.map(route => ({
974
+ name: route,
975
+ reason: tree.get(route)?.summary ?? '',
976
+ })),
977
+ );
978
+ }
979
+ }
980
+ }
981
+ if (suggestions.length === 0 && typeof topic === 'string') {
982
+ // The docs whose own name it is (`doctor` is cli/commands/doctor), or whose
983
+ // route it spells with hyphens (`cli-integrations`, the guide's name before
984
+ // it moved to cli/integrations).
985
+ const wanted = topic.toLowerCase();
986
+ suggestions = [...tree.nodes.values()]
987
+ .filter(
988
+ node =>
989
+ !node.ref?.flatTopic &&
990
+ (node.name.toLowerCase() === wanted ||
991
+ node.route.replaceAll('/', '-').toLowerCase() === wanted),
992
+ )
993
+ .map(node => ({name: node.route, reason: node.summary}));
994
+ }
995
+ if (suggestions.length === 0) {
996
+ suggestions = [
997
+ ...tree
998
+ .roots()
999
+ .map(root => ({name: root.route, reason: 'docs namespace'})),
1000
+ ...catalog.names().map(name => ({name, reason: 'available topic'})),
1001
+ ];
1002
+ }
1003
+ return new AstryxError(
1004
+ `Unknown topic "${String(topic)}"${notLoaded(catalog)}`,
1005
+ suggestions,
1006
+ ERROR_CODES.ERR_UNKNOWN_TOPIC,
1007
+ );
1008
+ }
1009
+
1010
+ /**
1011
+ * A sentence naming the packages whose docs did not load, or nothing: a doc
1012
+ * that fails to load withdraws its package's docs, so a reader who cannot
1013
+ * find one learns where to look.
1014
+ * @param {DocsCatalog} catalog
1015
+ * @returns {string}
1016
+ */
1017
+ export function notLoaded(catalog) {
1018
+ const packages = [...new Set(catalog.issues.map(issue => issue.package))];
1019
+ if (packages.length === 0) return '';
1020
+ return `. The docs of ${packages.join(', ')} did not load; run \`astryx doctor integration docs\` in that package to see why.`;
1021
+ }
1022
+
1023
+ /**
1024
+ * Resolve a topic (a flat one, or a guide the docs tree places by its route)
1025
+ * and lower it for the topic readers.
1026
+ * @param {unknown} topic
1027
+ * @param {{lang?: string | null, zh?: boolean, dense?: boolean, cwd?: string}} [options]
1028
+ */
1029
+ export async function resolveTopicDocs(topic, options = {}) {
1030
+ const {lang = null, zh = false, dense = false, cwd} = options;
1031
+ const effectiveLang = lang || (dense ? 'dense' : zh ? 'zh' : null);
1032
+ // A public API caller could pass a non-string topic; it lands on the same
1033
+ // stable code as an unknown name rather than a raw TypeError.
1034
+ const found = await resolveDocsArgument(topic, {cwd});
1035
+ if (found.kind !== 'topic') {
1036
+ throw found.kind === 'node'
1037
+ ? new AstryxError(
1038
+ `"${found.node.route}" is a ${found.node.kind === 'namespace' ? 'namespace' : `${found.node.kind} doc`} in the docs tree, not a topic. Read it with \`astryx docs ${found.node.route}\`.`,
1039
+ undefined,
1040
+ ERROR_CODES.ERR_UNKNOWN_TOPIC,
1041
+ )
1042
+ : await unknownTopicError(topic, found.catalog);
1043
+ }
1044
+ const {catalog, entry} = found;
254
1045
  const node = await lowerTopic(catalog, entry, effectiveLang);
255
- return {catalog, node, lang: effectiveLang};
1046
+ return {catalog, node, lang: effectiveLang, entry};
256
1047
  }