@astryxdesign/cli 0.7.2-canary.fe2c655 → 0.7.2

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 (509) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/README.md +175 -154
  3. package/api/build/_adapter.d.mts +8 -0
  4. package/api/build/_adapter.mjs +12 -1
  5. package/api/build/build.doc.mjs +4 -0
  6. package/api/build/help/help.mjs +1 -1
  7. package/api/build/kit/kit.mjs +3 -11
  8. package/api/component/_adapter.d.mts +72 -1
  9. package/api/component/_adapter.mjs +212 -18
  10. package/api/component/component.doc.mjs +6 -1
  11. package/api/component/component.mjs +82 -15
  12. package/api/component/component.test.mjs +80 -0
  13. package/api/component/doc-links.test.mjs +386 -0
  14. package/api/component/integration-export-name.test.mjs +644 -0
  15. package/api/component/list/list.mjs +42 -4
  16. package/api/discover/_adapter.d.mts +132 -12
  17. package/api/discover/_adapter.mjs +488 -27
  18. package/api/discover/_adapter.test.mjs +599 -17
  19. package/api/discover/_catalog-view.d.mts +16 -5
  20. package/api/discover/_catalog-view.mjs +32 -8
  21. package/api/discover/detail/doc/doc.mjs +9 -1
  22. package/api/discover/discover.d.mts +11 -0
  23. package/api/discover/discover.doc.mjs +9 -7
  24. package/api/discover/discover.mjs +121 -19
  25. package/api/discover/discover.type.d.mts +20 -7
  26. package/api/discover/discover.type.mjs +17 -7
  27. package/api/discover/list/list.d.mts +4 -3
  28. package/api/discover/list/list.mjs +17 -10
  29. package/api/docs/_adapter.d.mts +74 -0
  30. package/api/docs/_adapter.mjs +553 -16
  31. package/api/docs/docs.doc.mjs +2 -1
  32. package/api/docs/docs.mjs +2 -1
  33. package/api/docs/docs.test.mjs +5 -2
  34. package/api/docs/integration-tree.test.mjs +22 -0
  35. package/api/docs/integrationDocs.test.mjs +124 -0
  36. package/api/docs/list/list.mjs +12 -2
  37. package/api/docs/reference-blocks.test.mjs +1 -3
  38. package/api/doctor/_adapter.d.mts +108 -0
  39. package/api/doctor/_adapter.mjs +223 -4
  40. package/api/doctor/doctor.d.mts +199 -0
  41. package/api/doctor/doctor.doc.mjs +3 -2
  42. package/api/doctor/doctor.mjs +530 -34
  43. package/api/doctor/doctor.test.mjs +180 -14
  44. package/api/doctor/release-checks.test.mjs +156 -0
  45. package/api/doctor/theme-checks.mjs +17 -4
  46. package/api/doctor/theme-checks.test.mjs +223 -7
  47. package/api/error.d.mts +10 -0
  48. package/api/error.mjs +25 -0
  49. package/api/gap-report/gap-report.d.mts +3 -0
  50. package/api/gap-report/gap-report.doc.mjs +11 -3
  51. package/api/gap-report/gap-report.mjs +219 -52
  52. package/api/gap-report/gap-report.test.mjs +233 -0
  53. package/api/gap-report/gap-report.type.d.mts +41 -0
  54. package/api/gap-report/gap-report.type.mjs +18 -0
  55. package/api/hook/_adapter.mjs +3 -11
  56. package/api/hook/hook.doc.mjs +4 -0
  57. package/api/hook/list/list.d.mts +1 -1
  58. package/api/index.d.mts +2 -0
  59. package/api/index.mjs +2 -0
  60. package/api/init/_adapter.d.mts +22 -0
  61. package/api/init/_adapter.mjs +59 -0
  62. package/api/init/init.d.mts +7 -5
  63. package/api/init/init.doc.mjs +28 -7
  64. package/api/init/init.mjs +28 -4
  65. package/api/init/init.type.d.mts +48 -3
  66. package/api/init/init.type.mjs +37 -3
  67. package/api/init/run/run.d.mts +1 -1
  68. package/api/init/run/run.mjs +17 -6
  69. package/api/init/user/user.d.mts +14 -0
  70. package/api/init/user/user.mjs +87 -0
  71. package/api/init/user/user.test.mjs +232 -0
  72. package/api/integration/_adapter.d.mts +45 -0
  73. package/api/integration/_adapter.mjs +112 -0
  74. package/api/integration/add-contribution.component-names.test.mjs +66 -0
  75. package/api/integration/add-contribution.mjs +8 -4
  76. package/api/integration/add-contribution.test.mjs +4 -2
  77. package/api/integration/authoring-checks.mjs +110 -6
  78. package/api/integration/authoring-checks.test.mjs +808 -0
  79. package/api/integration/authoring-checks.type.d.mts +143 -0
  80. package/api/integration/authoring-checks.type.mjs +74 -2
  81. package/api/integration/check/check.d.mts +96 -0
  82. package/api/integration/check/check.mjs +357 -0
  83. package/api/integration/check/check.test.mjs +489 -0
  84. package/api/integration/integration-check.type.d.mts +118 -0
  85. package/api/integration/integration-check.type.mjs +84 -0
  86. package/api/integration/integrationCheck.doc.d.mts +10 -0
  87. package/api/integration/integrationCheck.doc.mjs +73 -0
  88. package/api/integration/integrationComponentConflicts.doc.mjs +45 -6
  89. package/api/integration/integrationDocConflicts.doc.mjs +22 -5
  90. package/api/integration/pack-check.mjs +364 -120
  91. package/api/integration/pack-check.test.mjs +730 -10
  92. package/api/integration/validate-integration-fixes.test.mjs +67 -0
  93. package/api/integration/validate-integration.d.mts +8 -0
  94. package/api/integration/validate-integration.mjs +63 -9
  95. package/api/integration/validate-integration.test.mjs +88 -8
  96. package/api/integration/validate-integration.type.d.mts +16 -0
  97. package/api/integration/validate-integration.type.mjs +14 -1
  98. package/api/integration/validate-unread-theme-folders.test.mjs +3 -2
  99. package/api/integration/validateIntegration.doc.mjs +19 -3
  100. package/api/search/_adapter.d.mts +11 -0
  101. package/api/search/_adapter.mjs +25 -0
  102. package/api/search/search.d.mts +82 -13
  103. package/api/search/search.doc.mjs +8 -3
  104. package/api/search/search.domain-priority.test.mjs +108 -18
  105. package/api/search/search.integration-check.test.mjs +24 -0
  106. package/api/search/search.mjs +645 -64
  107. package/api/search/search.test.mjs +179 -1
  108. package/api/search/search.type.d.mts +14 -1
  109. package/api/search/search.type.mjs +6 -1
  110. package/api/swizzle/_adapter.mjs +2 -2
  111. package/api/swizzle/copy/copy.mjs +29 -15
  112. package/api/template/template-integration.test.mjs +1 -1
  113. package/api/template/template.test.mjs +120 -3
  114. package/api/theme/_adapter.d.mts +16 -0
  115. package/api/theme/_adapter.mjs +32 -5
  116. package/api/theme/build/build.d.mts +2 -1
  117. package/api/theme/build/build.family.test.mjs +64 -0
  118. package/api/theme/build/build.mjs +250 -38
  119. package/api/theme/build/build.packed-old-core.test.mjs +20 -3
  120. package/api/theme/build/font-warning.mjs +5 -3
  121. package/api/theme/build/font-warning.test.mjs +2 -1
  122. package/api/theme/remove/remove.test.mjs +2 -2
  123. package/api/theme/targets/targets.mjs +3 -10
  124. package/api/theme/themeBuild.doc.mjs +4 -4
  125. package/api/theme/themeTargets.doc.mjs +4 -0
  126. package/api/theme/use/use.test.mjs +1 -1
  127. package/api/upgrade/_adapter.d.mts +40 -2
  128. package/api/upgrade/_adapter.mjs +118 -5
  129. package/api/upgrade/run/cli-behind-core.test.mjs +438 -0
  130. package/api/upgrade/run/run.mjs +35 -2
  131. package/api/upgrade/status/status.d.mts +21 -5
  132. package/api/upgrade/status/status.mjs +45 -8
  133. package/api/upgrade/upgrade.doc.mjs +6 -3
  134. package/api/upgrade/upgrade.mjs +5 -1
  135. package/api/upgrade/upgrade.type.d.mts +23 -0
  136. package/api/upgrade/upgrade.type.mjs +16 -1
  137. package/assets/codemods/__tests__/registry.test.mjs +1 -0
  138. package/assets/codemods/registry.mjs +1 -0
  139. package/assets/codemods/runner.mjs +7 -1
  140. package/assets/codemods/transforms/v0.7.2/__tests__/migrate-list-edge-compensation-to-edge.test.mjs +120 -0
  141. package/assets/codemods/transforms/v0.7.2/index.mjs +20 -0
  142. package/assets/codemods/transforms/v0.7.2/migrate-list-edge-compensation-to-edge.mjs +198 -0
  143. package/assets/discover/official-packages.json +367 -0
  144. package/assets/docs/README.md +10 -0
  145. package/assets/docs/author-a-theme.doc.mjs +3 -3
  146. package/assets/docs/browser-support.doc.mjs +1 -1
  147. package/assets/docs/getting-started.doc.mjs +26 -6
  148. package/assets/docs/styling-overview.doc.mjs +1 -1
  149. package/assets/docs/theme.doc.dense.mjs +1 -1
  150. package/assets/docs/theme.doc.mjs +2 -2
  151. package/assets/docs/theme.doc.zh.mjs +1 -1
  152. package/assets/docs/tree/add-a-component.doc.mjs +22 -2
  153. package/assets/docs/tree/add-a-theme.doc.mjs +13 -3
  154. package/assets/docs/tree/add-a-topic.doc.mjs +54 -9
  155. package/assets/docs/tree/agent-guidance.doc.mjs +19 -5
  156. package/assets/docs/tree/block-template.doc.mjs +8 -20
  157. package/assets/docs/tree/build-the-template.doc.mjs +1 -1
  158. package/assets/docs/tree/building-blocks.doc.mjs +4 -0
  159. package/assets/docs/tree/check-the-props.doc.mjs +201 -0
  160. package/assets/docs/tree/check-your-docs.doc.mjs +69 -11
  161. package/assets/docs/tree/checks.doc.mjs +32 -24
  162. package/assets/docs/tree/ci-and-containers.doc.mjs +88 -0
  163. package/assets/docs/tree/cli-command.doc.mjs +142 -0
  164. package/assets/docs/tree/cli.doc.mjs +6 -0
  165. package/assets/docs/tree/codemods.doc.mjs +17 -7
  166. package/assets/docs/tree/commands.doc.mjs +7 -1
  167. package/assets/docs/tree/component-family.doc.mjs +8 -6
  168. package/assets/docs/tree/component-imports.doc.mjs +21 -3
  169. package/assets/docs/tree/components.doc.mjs +7 -1
  170. package/assets/docs/tree/computer-agent-guide.doc.mjs +160 -0
  171. package/assets/docs/tree/computer-integrations.doc.mjs +93 -0
  172. package/assets/docs/tree/configuration.doc.mjs +7 -3
  173. package/assets/docs/tree/debug-and-gap-reports.doc.mjs +41 -22
  174. package/assets/docs/tree/define-the-theme.doc.mjs +6 -5
  175. package/assets/docs/tree/describe-the-component.doc.mjs +3 -3
  176. package/assets/docs/tree/docs.doc.mjs +9 -3
  177. package/assets/docs/tree/document-the-template.doc.mjs +1 -1
  178. package/assets/docs/tree/document-the-theme.doc.mjs +3 -3
  179. package/assets/docs/tree/export-template-assets.doc.mjs +1 -1
  180. package/assets/docs/tree/extend-or-replace.doc.mjs +19 -9
  181. package/assets/docs/tree/fonts-and-assets.doc.mjs +2 -2
  182. package/assets/docs/tree/generate-a-palette.doc.mjs +7 -5
  183. package/assets/docs/tree/global-integrations.doc.mjs +124 -0
  184. package/assets/docs/tree/global.doc.mjs +131 -0
  185. package/assets/docs/tree/grade-template-with-agent.doc.mjs +5 -5
  186. package/assets/docs/tree/install-once.doc.mjs +105 -0
  187. package/assets/docs/tree/integrations.doc.mjs +13 -1
  188. package/assets/docs/tree/layout-spacing.doc.mjs +2 -3
  189. package/assets/docs/tree/layout.doc.mjs +6 -6
  190. package/assets/docs/tree/links.doc.mjs +7 -6
  191. package/assets/docs/tree/migration-setup.doc.mjs +3 -2
  192. package/assets/docs/tree/migration-verification.doc.mjs +0 -1
  193. package/assets/docs/tree/migration.doc.mjs +0 -1
  194. package/assets/docs/tree/monorepos.doc.mjs +84 -0
  195. package/assets/docs/tree/package-and-test.doc.mjs +1 -1
  196. package/assets/docs/tree/page-template.doc.mjs +4 -8
  197. package/assets/docs/tree/problems.doc.mjs +205 -0
  198. package/assets/docs/tree/project-global-or-both.doc.mjs +131 -0
  199. package/assets/docs/tree/publishing.doc.mjs +6 -6
  200. package/assets/docs/tree/quick-start.doc.mjs +53 -27
  201. package/assets/docs/tree/reference.doc.mjs +192 -0
  202. package/assets/docs/tree/replace-a-core-component.doc.mjs +15 -7
  203. package/assets/docs/tree/replace-a-core-template.doc.mjs +10 -12
  204. package/assets/docs/tree/scaffold.doc.mjs +1 -1
  205. package/assets/docs/tree/sections-and-placement.doc.mjs +9 -11
  206. package/assets/docs/tree/see-it-in-an-app.doc.mjs +17 -5
  207. package/assets/docs/tree/short-and-findable.doc.mjs +9 -8
  208. package/assets/docs/tree/single-component.doc.mjs +15 -6
  209. package/assets/docs/tree/start-a-template.doc.mjs +18 -8
  210. package/assets/docs/tree/subcomponent.doc.mjs +6 -6
  211. package/assets/docs/tree/template-assets.doc.mjs +2 -2
  212. package/assets/docs/tree/template-doc-overview.doc.mjs +6 -17
  213. package/assets/docs/tree/template-fonts.doc.mjs +1 -1
  214. package/assets/docs/tree/template-icons.doc.mjs +1 -1
  215. package/assets/docs/tree/template-images-media.doc.mjs +1 -1
  216. package/assets/docs/tree/template-styles.doc.mjs +2 -2
  217. package/assets/docs/tree/templates.doc.mjs +7 -1
  218. package/assets/docs/tree/test-in-an-app.doc.mjs +7 -7
  219. package/assets/docs/tree/test-template-in-app.doc.mjs +5 -5
  220. package/assets/docs/tree/themes.doc.mjs +4 -0
  221. package/assets/docs/tree/tokens-and-setup.doc.mjs +19 -15
  222. package/assets/docs/tree/troubleshooting.doc.mjs +29 -13
  223. package/assets/docs/tree/type-scale.doc.mjs +3 -3
  224. package/assets/docs/tree/uninstall.doc.mjs +109 -0
  225. package/assets/docs/tree/updates.doc.mjs +92 -0
  226. package/assets/docs/tree/upgrading.doc.mjs +6 -6
  227. package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +14 -2
  228. package/assets/docs/tree/verify-packed-template.doc.mjs +3 -3
  229. package/assets/docs/tree/versioning.doc.mjs +10 -9
  230. package/assets/docs/tree/who-answers.doc.mjs +119 -0
  231. package/assets/docs/tree/write-the-template-file.doc.mjs +7 -6
  232. package/assets/docs/use-a-theme.doc.mjs +2 -2
  233. package/assets/docs/working-with-ai.doc.mjs +66 -13
  234. package/assets/theme.template.ts +1 -1
  235. package/authoring/codemod/codemod.doc.mjs +7 -3
  236. package/authoring/config/config.doc.mjs +9 -6
  237. package/authoring/config/type.ts +4 -2
  238. package/authoring/debug/parse.d.mts +5 -5
  239. package/authoring/discover/discover.doc.mjs +7 -1
  240. package/authoring/doctypes/_schema.d.mts +8 -3
  241. package/authoring/doctypes/_schema.mjs +1 -0
  242. package/authoring/doctypes/base/graph-fields.doc.mjs +6 -4
  243. package/authoring/doctypes/command/command.doc.mjs +20 -12
  244. package/authoring/doctypes/command/type.ts +14 -0
  245. package/authoring/doctypes/component/component.doc.mjs +30 -27
  246. package/authoring/doctypes/component/type.ts +21 -8
  247. package/authoring/doctypes/enum/enum.doc.mjs +7 -4
  248. package/authoring/doctypes/function/function.doc.mjs +9 -8
  249. package/authoring/doctypes/hook/hook.doc.mjs +5 -4
  250. package/authoring/doctypes/hook/type.ts +4 -0
  251. package/authoring/doctypes/namespace/namespace.doc.mjs +7 -6
  252. package/authoring/doctypes/reference/reference.doc.mjs +12 -11
  253. package/authoring/doctypes/schema/schema.doc.mjs +9 -9
  254. package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
  255. package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
  256. package/authoring/doctypes/template/parse.d.mts +6 -2
  257. package/authoring/doctypes/template/parse.mjs +3 -1
  258. package/authoring/doctypes/template/template.doc.mjs +3 -3
  259. package/authoring/doctypes/template/type.ts +22 -0
  260. package/authoring/doctypes/theme/theme.doc.mjs +2 -2
  261. package/authoring/gap-report/gap-report.doc.mjs +2 -2
  262. package/authoring/identity/identity.doc.mjs +3 -3
  263. package/authoring/index.d.ts +2 -0
  264. package/authoring/integration/integration.doc.mjs +30 -6
  265. package/authoring/integration/parse.test.mjs +21 -0
  266. package/authoring/integration/schema.d.mts +16 -3
  267. package/authoring/integration/schema.mjs +34 -2
  268. package/authoring/integration/type.ts +12 -0
  269. package/clients/cli/bin/astryx.mjs +20 -3
  270. package/clients/cli/cli-command.test.mjs +166 -0
  271. package/clients/cli/commands/blog.doc.mjs +1 -0
  272. package/clients/cli/commands/build-theme.batch-report.test.mjs +17 -3
  273. package/clients/cli/commands/build-theme.font-warning.test.mjs +3 -3
  274. package/clients/cli/commands/build-theme.mjs +26 -13
  275. package/clients/cli/commands/build-theme.path-safety.test.mjs +224 -6
  276. package/clients/cli/commands/build-theme.registry.test.mjs +5 -2
  277. package/clients/cli/commands/build.doc.mjs +1 -0
  278. package/clients/cli/commands/build.exit-codes-doc.test.mjs +4 -0
  279. package/clients/cli/commands/cli-postinstall.test.mjs +49 -1
  280. package/clients/cli/commands/component.doc.mjs +1 -0
  281. package/clients/cli/commands/discover.broken-integration.test.mjs +11 -7
  282. package/clients/cli/commands/discover.doc.mjs +9 -1
  283. package/clients/cli/commands/discover.mjs +144 -93
  284. package/clients/cli/commands/discover.no-source.test.mjs +448 -33
  285. package/clients/cli/commands/discover.sources.test.mjs +20 -1
  286. package/clients/cli/commands/docs.doc.mjs +1 -0
  287. package/clients/cli/commands/docs.mjs +49 -19
  288. package/clients/cli/commands/docs.test.mjs +2 -2
  289. package/clients/cli/commands/doctor-integration-components.doc.mjs +19 -4
  290. package/clients/cli/commands/doctor-integration-docs.doc.mjs +20 -5
  291. package/clients/cli/commands/doctor-integration-templates.doc.mjs +1 -0
  292. package/clients/cli/commands/doctor-integration-validate.doc.mjs +22 -2
  293. package/clients/cli/commands/doctor-integration.test.mjs +888 -3
  294. package/clients/cli/commands/doctor.doc.mjs +12 -2
  295. package/clients/cli/commands/doctor.mjs +292 -7
  296. package/clients/cli/commands/doctor.test.mjs +10 -4
  297. package/clients/cli/commands/gap-report.doc.mjs +15 -2
  298. package/clients/cli/commands/gap-report.mjs +24 -2
  299. package/clients/cli/commands/gap-report.test.mjs +79 -0
  300. package/clients/cli/commands/hook.doc.mjs +1 -0
  301. package/clients/cli/commands/init.behavior.test.mjs +5 -0
  302. package/clients/cli/commands/init.doc.mjs +29 -3
  303. package/clients/cli/commands/integration-add.doc.mjs +7 -0
  304. package/clients/cli/commands/integration-authoring.test.mjs +49 -3
  305. package/clients/cli/commands/integration-check.doc.mjs +93 -0
  306. package/clients/cli/commands/integration-check.test.mjs +441 -0
  307. package/clients/cli/commands/integration-pack.doc.mjs +8 -1
  308. package/clients/cli/commands/integration-verify.doc.mjs +8 -1
  309. package/clients/cli/commands/integration.doc.mjs +6 -2
  310. package/clients/cli/commands/integration.mjs +119 -7
  311. package/clients/cli/commands/json-contract.test.mjs +11 -2
  312. package/clients/cli/commands/manifest.doc.mjs +1 -0
  313. package/clients/cli/commands/outside-project.test.mjs +774 -0
  314. package/clients/cli/commands/search.doc.mjs +1 -0
  315. package/clients/cli/commands/search.mjs +16 -11
  316. package/clients/cli/commands/search.test.mjs +429 -296
  317. package/clients/cli/commands/setup-nudge.test.mjs +102 -2
  318. package/clients/cli/commands/swizzle.doc.mjs +7 -0
  319. package/clients/cli/commands/swizzle.mjs +5 -3
  320. package/clients/cli/commands/swizzle.routing.test.mjs +19 -2
  321. package/clients/cli/commands/template.doc.mjs +1 -0
  322. package/clients/cli/commands/text-json-parity.test.mjs +5 -0
  323. package/clients/cli/commands/theme-add-lifecycle.test.mjs +3 -3
  324. package/clients/cli/commands/theme-add.doc.mjs +8 -1
  325. package/clients/cli/commands/theme-build.doc.mjs +3 -2
  326. package/clients/cli/commands/theme-eject.doc.mjs +1 -0
  327. package/clients/cli/commands/theme-list.behavior.test.mjs +6 -0
  328. package/clients/cli/commands/theme-list.doc.mjs +1 -0
  329. package/clients/cli/commands/theme-palette-generate.doc.mjs +1 -0
  330. package/clients/cli/commands/theme-remove.doc.mjs +7 -0
  331. package/clients/cli/commands/theme-targets.doc.mjs +1 -0
  332. package/clients/cli/commands/theme-template.doc.mjs +1 -0
  333. package/clients/cli/commands/theme-use.doc.mjs +7 -0
  334. package/clients/cli/commands/upgrade.config-ordering.test.mjs +2 -1
  335. package/clients/cli/commands/upgrade.doc.mjs +22 -1
  336. package/clients/cli/commands/upgrade.file-protection.test.mjs +3 -2
  337. package/clients/cli/commands/upgrade.hook-output.test.mjs +2 -1
  338. package/clients/cli/commands/upgrade.older-cli.test.mjs +160 -0
  339. package/clients/cli/index.mjs +112 -16
  340. package/clients/cli/lib/cli-error.mjs +4 -1
  341. package/clients/cli/lib/component-format.test.mjs +1 -1
  342. package/clients/cli/lib/define-command.mjs +2 -2
  343. package/clients/cli/lib/json-shim.mjs +3 -1
  344. package/clients/cli/lib/manifest.d.ts +9 -0
  345. package/clients/cli/lib/manifest.mjs +17 -1
  346. package/clients/cli/lib/manifest.test.mjs +25 -0
  347. package/foundation/agent-docs/agent-doc-state.d.mts +35 -6
  348. package/foundation/agent-docs/agent-doc-state.mjs +66 -6
  349. package/foundation/agent-docs/agent-docs.d.mts +4 -3
  350. package/foundation/agent-docs/agent-docs.mjs +11 -9
  351. package/foundation/agent-docs/computer-agent-text.d.mts +133 -0
  352. package/foundation/agent-docs/computer-agent-text.mjs +623 -0
  353. package/foundation/agent-docs/computer-agent-text.test.mjs +602 -0
  354. package/foundation/config/project.d.mts +35 -3
  355. package/foundation/config/project.mjs +226 -7
  356. package/foundation/config/project.test.mjs +66 -0
  357. package/foundation/config/theme-state.mjs +5 -4
  358. package/foundation/discovery/authoring-self-docs.mjs +2 -1
  359. package/foundation/discovery/cli-self-docs.mjs +20 -3
  360. package/foundation/discovery/cli-self-docs.test.mjs +3 -2
  361. package/foundation/discovery/component-discovery.d.mts +190 -10
  362. package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
  363. package/foundation/discovery/component-discovery.mjs +864 -56
  364. package/foundation/discovery/component-discovery.test.mjs +96 -0
  365. package/foundation/discovery/component-replacement.mjs +20 -1
  366. package/foundation/discovery/component-replacement.test.mjs +40 -0
  367. package/foundation/discovery/copied-passages.d.mts +216 -0
  368. package/foundation/discovery/copied-passages.mjs +720 -0
  369. package/foundation/discovery/copied-passages.test.mjs +124 -0
  370. package/foundation/discovery/doc-consistency.d.mts +34 -0
  371. package/foundation/discovery/doc-consistency.mjs +513 -0
  372. package/foundation/discovery/doc-consistency.test.mjs +347 -0
  373. package/foundation/discovery/doc-fluff.d.mts +112 -0
  374. package/foundation/discovery/doc-fluff.mjs +328 -0
  375. package/foundation/discovery/doc-fluff.test.mjs +142 -0
  376. package/foundation/discovery/doc-types.d.mts +157 -0
  377. package/foundation/discovery/doc-types.mjs +1231 -0
  378. package/foundation/discovery/doc-types.test.mjs +744 -0
  379. package/foundation/discovery/docs-discovery.d.mts +88 -0
  380. package/foundation/discovery/docs-discovery.mjs +83 -2
  381. package/foundation/discovery/integration-doc-files.d.mts +22 -0
  382. package/foundation/discovery/integration-doc-files.mjs +231 -0
  383. package/foundation/discovery/integration-doc-files.test.mjs +246 -0
  384. package/foundation/discovery/module-exports.d.mts +27 -0
  385. package/foundation/discovery/module-exports.mjs +356 -31
  386. package/foundation/discovery/module-exports.test.mjs +172 -0
  387. package/foundation/discovery/package-tsconfig.d.mts +14 -0
  388. package/foundation/discovery/package-tsconfig.mjs +228 -0
  389. package/foundation/discovery/package-tsconfig.test.mjs +210 -0
  390. package/foundation/discovery/project-typescript.d.mts +32 -0
  391. package/foundation/discovery/project-typescript.mjs +89 -0
  392. package/foundation/discovery/prop-drift.d.mts +297 -0
  393. package/foundation/discovery/prop-drift.mjs +2428 -0
  394. package/foundation/discovery/prop-drift.test.mjs +2337 -0
  395. package/foundation/discovery/runner-allowlist.d.mts +40 -0
  396. package/foundation/discovery/runner-allowlist.mjs +53 -0
  397. package/foundation/discovery/stray-pointers.d.mts +52 -0
  398. package/foundation/discovery/stray-pointers.mjs +293 -0
  399. package/foundation/discovery/stray-pointers.test.mjs +86 -0
  400. package/foundation/discovery/template-adapter.mjs +74 -5
  401. package/foundation/discovery/template-needs.mjs +5 -2
  402. package/foundation/discovery/theme-imports.mjs +8 -4
  403. package/foundation/discovery/theme-imports.test.mjs +1 -1
  404. package/foundation/discovery/type-match.d.mts +53 -0
  405. package/foundation/discovery/type-match.mjs +660 -0
  406. package/foundation/discovery/type-match.test.mjs +169 -0
  407. package/foundation/discovery/type-text.d.mts +68 -0
  408. package/foundation/discovery/type-text.mjs +658 -0
  409. package/foundation/discovery/type-text.test.mjs +196 -0
  410. package/foundation/discovery/unresolved-references.d.mts +62 -0
  411. package/foundation/discovery/unresolved-references.mjs +790 -0
  412. package/foundation/discovery/unresolved-references.test.mjs +567 -0
  413. package/foundation/doc-compiler/doc-loads.test.mjs +51 -1
  414. package/foundation/doc-compiler/inputs.mjs +27 -1
  415. package/foundation/doc-compiler/inputs.test.mjs +0 -1
  416. package/foundation/doc-compiler/links.d.mts +41 -5
  417. package/foundation/doc-compiler/links.mjs +167 -28
  418. package/foundation/doc-compiler/links.test.mjs +97 -4
  419. package/foundation/doc-compiler/read.d.mts +1 -1
  420. package/foundation/doc-compiler/tree.mjs +30 -1
  421. package/foundation/doc-compiler/tree.test.mjs +8 -1
  422. package/foundation/env/cli-command.d.mts +83 -0
  423. package/foundation/env/cli-command.mjs +152 -0
  424. package/foundation/env/cli-command.test.mjs +129 -0
  425. package/foundation/env/node-version.test.mjs +15 -3
  426. package/foundation/env/package-manager.d.mts +93 -9
  427. package/foundation/env/package-manager.global.test.mjs +66 -0
  428. package/foundation/env/package-manager.mjs +184 -13
  429. package/foundation/env/package-manager.test.mjs +341 -15
  430. package/foundation/env/project-scope.d.mts +9 -0
  431. package/foundation/env/project-scope.mjs +33 -0
  432. package/foundation/env/search-path.d.mts +42 -0
  433. package/foundation/env/search-path.mjs +205 -0
  434. package/foundation/env/search-path.test.mjs +122 -0
  435. package/foundation/env/user-dirs.d.mts +39 -0
  436. package/foundation/env/user-dirs.mjs +93 -0
  437. package/foundation/env/user-dirs.test.mjs +72 -0
  438. package/foundation/env/user-settings.d.mts +46 -0
  439. package/foundation/env/user-settings.mjs +106 -0
  440. package/foundation/env/user-settings.test.mjs +64 -0
  441. package/foundation/fs/file-protection.mjs +15 -1
  442. package/foundation/fs/module-loader.d.mts +2 -1
  443. package/foundation/fs/module-loader.mjs +60 -4
  444. package/foundation/fs/module-loader.stdout.test.mjs +3 -2
  445. package/foundation/fs/module-loader.test.mjs +194 -0
  446. package/foundation/fs/path-compare.d.mts +33 -0
  447. package/foundation/fs/path-compare.mjs +73 -0
  448. package/foundation/fs/paths.d.mts +57 -1
  449. package/foundation/fs/paths.mjs +131 -3
  450. package/foundation/fs/paths.test.mjs +10 -1
  451. package/foundation/integrations/computer-integrations.d.mts +78 -0
  452. package/foundation/integrations/computer-integrations.mjs +316 -0
  453. package/foundation/integrations/computer-integrations.test.mjs +292 -0
  454. package/foundation/integrations/contribution-inventory.d.mts +11 -0
  455. package/foundation/integrations/contribution-inventory.mjs +56 -3
  456. package/foundation/integrations/integration-warnings.mjs +3 -2
  457. package/foundation/integrations/integration-warnings.test.mjs +8 -9
  458. package/foundation/integrations/integrations.d.mts +25 -1
  459. package/foundation/integrations/integrations.mjs +46 -3
  460. package/foundation/integrations/issue.ts +6 -0
  461. package/foundation/integrations/validate-contributions.d.mts +2 -0
  462. package/foundation/integrations/validate-contributions.mjs +41 -1
  463. package/foundation/release/agent-text-worker.d.mts +4 -0
  464. package/foundation/release/agent-text-worker.mjs +52 -0
  465. package/foundation/release/cache.d.mts +310 -0
  466. package/foundation/release/cache.mjs +1216 -0
  467. package/foundation/release/cache.test.mjs +920 -0
  468. package/foundation/release/choose.d.mts +207 -0
  469. package/foundation/release/choose.mjs +677 -0
  470. package/foundation/release/choose.test.mjs +427 -0
  471. package/foundation/release/core-worker.d.mts +4 -0
  472. package/foundation/release/core-worker.mjs +38 -0
  473. package/foundation/release/global-state.d.mts +259 -0
  474. package/foundation/release/global-state.mjs +472 -0
  475. package/foundation/release/global-state.test.mjs +381 -0
  476. package/foundation/release/handoff.d.mts +60 -0
  477. package/foundation/release/handoff.e2e.test.mjs +278 -0
  478. package/foundation/release/handoff.mjs +357 -0
  479. package/foundation/release/lock-lease.d.mts +23 -0
  480. package/foundation/release/lock-lease.mjs +80 -0
  481. package/foundation/release/notices.d.mts +253 -0
  482. package/foundation/release/notices.mjs +596 -0
  483. package/foundation/release/notices.test.mjs +565 -0
  484. package/foundation/release/npm-guard.d.mts +4 -0
  485. package/foundation/release/npm-guard.mjs +126 -0
  486. package/foundation/release/project.d.mts +141 -0
  487. package/foundation/release/project.mjs +466 -0
  488. package/foundation/release/project.test.mjs +288 -0
  489. package/foundation/release/refresh-worker.d.mts +4 -0
  490. package/foundation/release/refresh-worker.mjs +27 -0
  491. package/foundation/release/run-context.d.mts +47 -0
  492. package/foundation/release/run-context.mjs +149 -0
  493. package/foundation/release/update-notice-env.d.mts +28 -0
  494. package/foundation/release/update-notice-env.mjs +39 -0
  495. package/foundation/response/base.d.ts +26 -3
  496. package/foundation/response/error-codes.d.mts +3 -1
  497. package/foundation/response/error-codes.doc.mjs +12 -0
  498. package/foundation/response/error-codes.mjs +17 -0
  499. package/foundation/response/error-codes.test.mjs +2 -0
  500. package/foundation/response/json-contract.test.mjs +7 -2
  501. package/foundation/response/json.mjs +4 -1
  502. package/foundation/response/response-types.doc.mjs +22 -11
  503. package/foundation/response/response-types.doc.test.mjs +46 -0
  504. package/foundation/response/response.doc.mjs +3 -3
  505. package/foundation/xle/browser-imports.test.mjs +74 -0
  506. package/foundation/xle/registry.mjs +4 -0
  507. package/foundation/xle/registry.test.mjs +28 -0
  508. package/package.json +11 -11
  509. package/scripts/postinstall.mjs +24 -5
@@ -19,8 +19,9 @@ import {Command} from 'commander';
19
19
  import {discoverCoreTemplates} from '../../../foundation/discovery/template-adapter.mjs';
20
20
  import {discoverOwnedComponents} from '../../../foundation/discovery/component-discovery.mjs';
21
21
  import {discoverBuiltinTopics} from '../../../foundation/discovery/docs-discovery.mjs';
22
- import {findCoreDir} from '../../../foundation/fs/paths.mjs';
22
+ import {findCoreDir, findProjectCoreDir} from '../../../foundation/fs/paths.mjs';
23
23
  import {registerDoctor} from './doctor.mjs';
24
+ import {expectRecordsCarry} from '../../../test-utils/text-projection.mjs';
24
25
 
25
26
  let tmpDir;
26
27
  let previousCwd;
@@ -184,7 +185,7 @@ describe('doctor integration — command', () => {
184
185
  ]);
185
186
 
186
187
  expect(logCalls.join('\n')).toContain(
187
- 'astryx doctor integration templates <package>',
188
+ ' doctor integration templates <package>',
188
189
  );
189
190
  expect(process.exitCode).toBeUndefined();
190
191
  });
@@ -209,6 +210,470 @@ describe('doctor integration — command', () => {
209
210
  expect(process.exitCode).toBeUndefined();
210
211
  });
211
212
 
213
+ it('validate prints the fix and the file an issue carries, and JSON carries the same issue', async () => {
214
+ writeDocIntegration({name: 'deploying'});
215
+ fs.writeFileSync(
216
+ path.join(tmpDir, 'docs', 'ghost.doc.zh.mjs'),
217
+ "export default {title: 'ghost'};\n",
218
+ );
219
+ process.chdir(tmpDir);
220
+
221
+ await createProgram().parseAsync([
222
+ 'node',
223
+ 'astryx',
224
+ 'doctor',
225
+ 'integration',
226
+ 'validate',
227
+ ]);
228
+ const output = logCalls.join('\n');
229
+ expect(output).toContain('orphan_doc_overlay');
230
+ expect(output).toMatch(
231
+ /fix:\s+Delete docs\/ghost\.doc\.zh\.mjs, or add the topic it translates/,
232
+ );
233
+ expect(output).toMatch(/file:\s+docs\/ghost\.doc\.zh\.mjs/);
234
+ expect(output).toMatch(/line:\s+1\b/);
235
+
236
+ logCalls = [];
237
+ await createProgram().parseAsync([
238
+ 'node',
239
+ 'astryx',
240
+ '--json',
241
+ 'doctor',
242
+ 'integration',
243
+ 'validate',
244
+ ]);
245
+ const parsed = JSON.parse(logCalls.join('\n'));
246
+ expect(parsed.data.issues).toEqual([
247
+ {
248
+ code: 'orphan_doc_overlay',
249
+ severity: 'warning',
250
+ message: expect.stringContaining('docs/ghost.doc.zh.mjs'),
251
+ fix: expect.stringContaining('Delete docs/ghost.doc.zh.mjs'),
252
+ file: 'docs/ghost.doc.zh.mjs',
253
+ line: 1,
254
+ },
255
+ ]);
256
+ });
257
+
258
+ it('validate warns about each doc field that does not match its authoring type, and still exits 0', async () => {
259
+ writeIntegration({id: 'hero', name: 'Hero', type: 'block'});
260
+ process.chdir(tmpDir);
261
+
262
+ await createProgram().parseAsync([
263
+ 'node',
264
+ 'astryx',
265
+ 'doctor',
266
+ 'integration',
267
+ 'validate',
268
+ ]);
269
+
270
+ const output = logCalls.join('\n');
271
+ expect(output).toContain('[ok] No integration issues found.');
272
+ expect(output).toMatch(
273
+ /Checking 1 doc\(s\) against the authoring types \(TypeScript \d+\.\d+/,
274
+ );
275
+ expect(output).toContain('templates/hero.doc.mjs');
276
+ expect(output).toMatch(/export:\s+default/);
277
+ expect(output).toMatch(/type:\s+IntegrationTemplateDoc/);
278
+ expect(output).toContain(
279
+ 'Add `aspectRatio`: IntegrationBlockTemplateDoc requires it.',
280
+ );
281
+ expect(output).toContain(
282
+ 'Add `displayName`: IntegrationBlockTemplateDoc requires it.',
283
+ );
284
+ expect(output).toContain(
285
+ '2 doc field(s) in 1 file(s) do not match the authoring types.',
286
+ );
287
+ expect(process.exitCode).toBeUndefined();
288
+ });
289
+
290
+ it('validate says there is no doc to type-check, rather than asking for TypeScript', async () => {
291
+ const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-no-docs-'));
292
+ try {
293
+ fs.writeFileSync(
294
+ path.join(outside, 'package.json'),
295
+ JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
296
+ );
297
+ fs.writeFileSync(
298
+ path.join(outside, 'astryx.integration.mjs'),
299
+ "export default {components: './components'};\n",
300
+ );
301
+ fs.mkdirSync(path.join(outside, 'components'));
302
+ fs.writeFileSync(
303
+ path.join(outside, 'components', 'Widget.tsx'),
304
+ 'export function Widget() { return null; }\n',
305
+ );
306
+ process.chdir(outside);
307
+
308
+ await createProgram().parseAsync([
309
+ 'node',
310
+ 'astryx',
311
+ 'doctor',
312
+ 'integration',
313
+ 'validate',
314
+ ]);
315
+
316
+ const output = logCalls.join('\n');
317
+ expect(output).toContain('[info] No doc files to type-check.');
318
+ expect(output).not.toContain('Docs not type-checked');
319
+ expect(output).not.toContain('against the authoring types');
320
+ } finally {
321
+ process.chdir(previousCwd);
322
+ fs.rmSync(outside, {recursive: true, force: true});
323
+ }
324
+ });
325
+
326
+ it('validate says the docs matched even when another check failed', async () => {
327
+ writeIntegration({id: 'hero', missingRoot: true});
328
+ fs.writeFileSync(
329
+ path.join(tmpDir, 'astryx.integration.mjs'),
330
+ "export default {templates: './missing', components: './components'};\n",
331
+ );
332
+ fs.mkdirSync(path.join(tmpDir, 'components'), {recursive: true});
333
+ fs.writeFileSync(
334
+ path.join(tmpDir, 'components', 'AcmeBadge.doc.mjs'),
335
+ "export default {type: 'component', name: 'AcmeBadge', displayName: 'Badge', usage: {description: 'A badge.'}, props: []};\n",
336
+ );
337
+ fs.writeFileSync(
338
+ path.join(tmpDir, 'components', 'AcmeBadge.tsx'),
339
+ 'export function AcmeBadge() { return null; }\n',
340
+ );
341
+ process.chdir(tmpDir);
342
+
343
+ await createProgram().parseAsync([
344
+ 'node',
345
+ 'astryx',
346
+ 'doctor',
347
+ 'integration',
348
+ 'validate',
349
+ ]);
350
+
351
+ const output = logCalls.join('\n');
352
+ expect(process.exitCode).toBe(1);
353
+ expect(output).toContain(
354
+ '[info] No doc field differs from its authoring type (1 checked).',
355
+ );
356
+ expect(output).not.toContain('[ok] Every doc');
357
+ });
358
+
359
+ it('validate prints every field of the doc type check that JSON carries', async () => {
360
+ writeIntegration({id: 'hero', name: 'Hero', type: 'block'});
361
+ fs.writeFileSync(
362
+ path.join(tmpDir, 'astryx.integration.mjs'),
363
+ "export default {templates: './templates', components: './components'};\n",
364
+ );
365
+ fs.mkdirSync(path.join(tmpDir, 'components'), {recursive: true});
366
+ fs.writeFileSync(
367
+ path.join(tmpDir, 'components', 'AcmeBadge.doc.mjs'),
368
+ "export default {type: 'component', name: 'AcmeBadge', displayName: 5, usage: {description: 'A badge.'}, props: [], keyword: ['x']};\n",
369
+ );
370
+ fs.writeFileSync(
371
+ path.join(tmpDir, 'components', 'AcmeBadge.tsx'),
372
+ 'export function AcmeBadge() { return null; }\n',
373
+ );
374
+ fs.writeFileSync(
375
+ path.join(tmpDir, 'components', 'shared.mjs'),
376
+ `export default {type: 'component', name: 'AcmeAgain', displayName: 'Again', usage: {description: 'x'}, props: []};\n`,
377
+ );
378
+ fs.writeFileSync(
379
+ path.join(tmpDir, 'components', 'AcmeAgain.doc.mjs'),
380
+ "export {default} from './shared.mjs';\n",
381
+ );
382
+ fs.writeFileSync(
383
+ path.join(tmpDir, 'components', 'AcmeAgain.tsx'),
384
+ 'export function AcmeAgain() { return null; }\n',
385
+ );
386
+ process.chdir(tmpDir);
387
+ const run = async (/** @type {string[]} */ flags) => {
388
+ logCalls.length = 0;
389
+ await createProgram().parseAsync([
390
+ 'node',
391
+ 'astryx',
392
+ ...flags,
393
+ 'doctor',
394
+ 'integration',
395
+ 'validate',
396
+ ]);
397
+ return logCalls.join('\n');
398
+ };
399
+
400
+ const report = JSON.parse(await run(['--json'])).data.docTypes;
401
+ const output = await run([]);
402
+
403
+ expect([...new Set(report.findings.map(f => f.problem))].sort()).toEqual([
404
+ 'missing_field',
405
+ 'unknown_field',
406
+ 'wrong_type',
407
+ ]);
408
+ expect(report.unchecked).toHaveLength(1);
409
+ expectRecordsCarry(output, report.findings, ['doc', 'field', 'problem']);
410
+ expectRecordsCarry(output, report.unchecked, ['doc', 'reason']);
411
+ expect(output).toContain(
412
+ `Checking ${report.docs} doc(s) against the authoring types (TypeScript ${report.typescript})`,
413
+ );
414
+ });
415
+
416
+ it('validate type-checks each doc component discovery serves, in __fixtures__, in a dot-folder or behind a symlink', async () => {
417
+ fs.writeFileSync(
418
+ path.join(tmpDir, 'package.json'),
419
+ JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
420
+ );
421
+ fs.writeFileSync(
422
+ path.join(tmpDir, 'astryx.integration.mjs'),
423
+ `export default {components: './components'};\n`,
424
+ );
425
+ for (const [rel, name] of [
426
+ ['components/__fixtures__/AcmeFixture.doc.mjs', 'AcmeFixture'],
427
+ ['components/.drafts/AcmeDraft.doc.mjs', 'AcmeDraft'],
428
+ ['shared/AcmeLinked.doc.mjs', 'AcmeLinked'],
429
+ ]) {
430
+ fs.mkdirSync(path.dirname(path.join(tmpDir, rel)), {recursive: true});
431
+ fs.writeFileSync(
432
+ path.join(tmpDir, rel),
433
+ `export default {type: 'component', name: '${name}', displayName: '${name}', usage: {description: 'Fixture.'}, props: [], keyword: []};\n`,
434
+ );
435
+ }
436
+ fs.symlinkSync(
437
+ path.join(tmpDir, 'shared', 'AcmeLinked.doc.mjs'),
438
+ path.join(tmpDir, 'components', 'AcmeLinked.doc.mjs'),
439
+ );
440
+ process.chdir(tmpDir);
441
+
442
+ await createProgram().parseAsync([
443
+ 'node',
444
+ 'astryx',
445
+ '--json',
446
+ 'doctor',
447
+ 'integration',
448
+ 'validate',
449
+ ]);
450
+
451
+ const {docTypes} = JSON.parse(logCalls.join('\n')).data;
452
+ expect(docTypes).toMatchObject({status: 'checked', docs: 3, unchecked: []});
453
+ expect(
454
+ docTypes.findings.map(
455
+ (/** @type {any} */ f) => `${f.doc} ${f.field} ${f.problem}`,
456
+ ),
457
+ ).toEqual([
458
+ 'components/__fixtures__/AcmeFixture.doc.mjs keyword unknown_field',
459
+ 'components/.drafts/AcmeDraft.doc.mjs keyword unknown_field',
460
+ 'components/AcmeLinked.doc.mjs keyword unknown_field',
461
+ ]);
462
+ });
463
+
464
+ it('validate type-checks a hidden doc and a second doc for one name, and counts a test fixture', async () => {
465
+ fs.writeFileSync(
466
+ path.join(tmpDir, 'package.json'),
467
+ JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
468
+ );
469
+ fs.writeFileSync(
470
+ path.join(tmpDir, 'astryx.integration.mjs'),
471
+ `export default {components: './components'};\n`,
472
+ );
473
+ for (const [rel, name, extra] of [
474
+ ['components/AcmeBadge.doc.mjs', 'AcmeBadge', ''],
475
+ ['components/again/AcmeBadge.doc.mjs', 'AcmeBadge', ', keyword: []'],
476
+ [
477
+ 'components/AcmeHidden.doc.mjs',
478
+ 'AcmeHidden',
479
+ ', hidden: true, keyword: []',
480
+ ],
481
+ [
482
+ 'components/__tests__/AcmeTested.doc.mjs',
483
+ 'AcmeTested',
484
+ ', keyword: []',
485
+ ],
486
+ ]) {
487
+ fs.mkdirSync(path.dirname(path.join(tmpDir, rel)), {recursive: true});
488
+ fs.writeFileSync(
489
+ path.join(tmpDir, rel),
490
+ `export default {type: 'component', name: '${name}', displayName: '${name}', usage: {description: 'Fixture.'}, props: []${extra}};\n`,
491
+ );
492
+ fs.writeFileSync(
493
+ path.join(tmpDir, rel.replace('.doc.mjs', '.tsx')),
494
+ 'export default function Fixture() { return null; }\n',
495
+ );
496
+ }
497
+ process.chdir(tmpDir);
498
+ const run = async (/** @type {string[]} */ flags) => {
499
+ logCalls.length = 0;
500
+ await createProgram().parseAsync([
501
+ 'node',
502
+ 'astryx',
503
+ ...flags,
504
+ 'doctor',
505
+ 'integration',
506
+ 'validate',
507
+ ]);
508
+ return logCalls.join('\n');
509
+ };
510
+
511
+ const {docTypes} = JSON.parse(await run(['--json'])).data;
512
+ const output = await run([]);
513
+
514
+ expect(docTypes).toMatchObject({docs: 3, fixtures: 1, unchecked: []});
515
+ expect(
516
+ docTypes.findings.map(
517
+ (/** @type {any} */ f) => `${f.doc} ${f.field} ${f.problem}`,
518
+ ),
519
+ ).toEqual([
520
+ 'components/AcmeHidden.doc.mjs keyword unknown_field',
521
+ 'components/again/AcmeBadge.doc.mjs keyword unknown_field',
522
+ ]);
523
+ expect(output).toContain('[info] 1 test fixture doc(s) not checked.');
524
+ expect(output).not.toContain('[ok] Every doc matches');
525
+ });
526
+
527
+ it('validate keeps the fixture count when test fixtures are the only docs', async () => {
528
+ fs.writeFileSync(
529
+ path.join(tmpDir, 'package.json'),
530
+ JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
531
+ );
532
+ fs.writeFileSync(
533
+ path.join(tmpDir, 'astryx.integration.mjs'),
534
+ `export default {components: './components'};\n`,
535
+ );
536
+ const rel = 'components/__tests__/AcmeTested.doc.mjs';
537
+ fs.mkdirSync(path.dirname(path.join(tmpDir, rel)), {recursive: true});
538
+ fs.writeFileSync(
539
+ path.join(tmpDir, rel),
540
+ `export default {type: 'component', name: 'AcmeTested', displayName: 'AcmeTested', usage: {description: 'Fixture.'}, props: []};\n`,
541
+ );
542
+ process.chdir(tmpDir);
543
+ const run = async (/** @type {string[]} */ flags) => {
544
+ logCalls.length = 0;
545
+ await createProgram().parseAsync([
546
+ 'node',
547
+ 'astryx',
548
+ ...flags,
549
+ 'doctor',
550
+ 'integration',
551
+ 'validate',
552
+ ]);
553
+ return logCalls.join('\n');
554
+ };
555
+
556
+ const {docTypes} = JSON.parse(await run(['--json'])).data;
557
+ const output = await run([]);
558
+
559
+ expect(docTypes).toMatchObject({docs: 0, fixtures: 1, findings: []});
560
+ expect(output).toContain('[info] No doc files to type-check.');
561
+ expect(output).toContain('[info] 1 test fixture doc(s) not checked.');
562
+ });
563
+
564
+ it("validate keeps main's issue for a root it cannot read, and lists a root with a folder it cannot read", async () => {
565
+ fs.writeFileSync(
566
+ path.join(tmpDir, 'package.json'),
567
+ JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
568
+ );
569
+ fs.writeFileSync(
570
+ path.join(tmpDir, 'astryx.integration.mjs'),
571
+ `export default {components: './components', docs: './docs'};\n`,
572
+ );
573
+ fs.mkdirSync(path.join(tmpDir, 'components', 'locked'), {recursive: true});
574
+ fs.mkdirSync(path.join(tmpDir, 'docs'));
575
+ const locked = [
576
+ path.join(tmpDir, 'components', 'locked'),
577
+ path.join(tmpDir, 'docs'),
578
+ ];
579
+ for (const dir of locked) fs.chmodSync(dir, 0o000);
580
+ process.chdir(tmpDir);
581
+ try {
582
+ await createProgram().parseAsync([
583
+ 'node',
584
+ 'astryx',
585
+ '--json',
586
+ 'doctor',
587
+ 'integration',
588
+ 'validate',
589
+ ]);
590
+ } finally {
591
+ for (const dir of locked) fs.chmodSync(dir, 0o755);
592
+ }
593
+
594
+ const {data} = JSON.parse(logCalls.join('\n'));
595
+ expect(data.issues.map((/** @type {any} */ i) => i.code).sort()).toEqual([
596
+ 'invalid_component',
597
+ 'unreadable_root',
598
+ ]);
599
+ expect(data.docTypes.unchecked).toEqual([
600
+ {
601
+ doc: 'components',
602
+ reason: expect.stringMatching(
603
+ /^the components root does not load, so no doc in it was checked: .*EACCES/,
604
+ ),
605
+ },
606
+ ]);
607
+ });
608
+
609
+ it('validate reports the doc type check as additive JSON', async () => {
610
+ writeIntegration({id: 'hero', name: 'Hero', type: 'block'});
611
+ process.chdir(tmpDir);
612
+
613
+ await createProgram().parseAsync([
614
+ 'node',
615
+ 'astryx',
616
+ '--json',
617
+ 'doctor',
618
+ 'integration',
619
+ 'validate',
620
+ ]);
621
+
622
+ const parsed = JSON.parse(logCalls.join('\n'));
623
+ expect(parsed.data.issues).toEqual([]);
624
+ expect(parsed.data.docTypes).toMatchObject({
625
+ status: 'checked',
626
+ docs: 1,
627
+ unchecked: [],
628
+ });
629
+ expect(parsed.data.docTypes.typescript).toMatch(/^\d+\.\d+/);
630
+ expect(parsed.data.docTypes.findings).toEqual([
631
+ expect.objectContaining({
632
+ doc: 'templates/hero.doc.mjs',
633
+ export: 'default',
634
+ type: 'IntegrationTemplateDoc',
635
+ field: 'aspectRatio',
636
+ problem: 'missing_field',
637
+ severity: 'warning',
638
+ }),
639
+ expect.objectContaining({field: 'displayName', problem: 'missing_field'}),
640
+ ]);
641
+ expect(process.exitCode).toBeUndefined();
642
+ });
643
+
644
+ it('validate says, in one visible line, that docs were not type-checked when the project has no TypeScript', async () => {
645
+ const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-no-ts-'));
646
+ const inside = tmpDir;
647
+ try {
648
+ // The fixture helpers write to tmpDir; point it outside the repository,
649
+ // where no TypeScript resolves, and restore it for afterEach.
650
+ tmpDir = outside;
651
+ writeIntegration({id: 'hero', name: 'Hero', type: 'block'});
652
+ tmpDir = inside;
653
+ process.chdir(outside);
654
+
655
+ await createProgram().parseAsync([
656
+ 'node',
657
+ 'astryx',
658
+ 'doctor',
659
+ 'integration',
660
+ 'validate',
661
+ ]);
662
+
663
+ const output = logCalls.join('\n');
664
+ expect(output).toContain(
665
+ '[skip] Docs not type-checked: TypeScript is not installed in this project',
666
+ );
667
+ expect(output).not.toMatch(
668
+ /Checking \d+ doc\(s\) against the authoring types/,
669
+ );
670
+ expect(process.exitCode).toBeUndefined();
671
+ } finally {
672
+ process.chdir(previousCwd);
673
+ fs.rmSync(outside, {recursive: true, force: true});
674
+ }
675
+ });
676
+
212
677
  it('templates warns on a Core id and writes the exact --package command', async () => {
213
678
  const [core] = await discoverCoreTemplates();
214
679
  expect(core).toBeDefined();
@@ -352,7 +817,8 @@ describe('doctor integration — command', () => {
352
817
  tmpDir = outside;
353
818
  try {
354
819
  writeComponentIntegration('AcmeCarousel');
355
- expect(findCoreDir(outside)).toBeNull();
820
+ // Doctor judges the project's own Core, never the one a copy carries.
821
+ expect(findProjectCoreDir(outside)).toBeNull();
356
822
  process.chdir(outside);
357
823
 
358
824
  await createProgram().parseAsync([
@@ -366,6 +832,10 @@ describe('doctor integration — command', () => {
366
832
  const printed = logCalls.join('\n');
367
833
  expect(printed).toContain('core_not_found');
368
834
  expect(printed).not.toContain('[ok]');
835
+ // No TypeScript out here either: the props comparison says it skipped.
836
+ expect(printed).toContain(
837
+ '[skip] Props not compared: TypeScript is not installed in this project',
838
+ );
369
839
  expect(process.exitCode).toBe(1);
370
840
  } finally {
371
841
  tmpDir = previousTmp;
@@ -396,6 +866,356 @@ describe('doctor integration — command', () => {
396
866
  expect(process.exitCode).toBeUndefined();
397
867
  });
398
868
 
869
+ it('components prints each documented prop that differs from the code, and still exits 0', async () => {
870
+ writeComponentIntegration('AcmeBadge');
871
+ fs.writeFileSync(
872
+ path.join(tmpDir, 'components', 'AcmeBadge.doc.mjs'),
873
+ `export default {type: 'component', name: 'AcmeBadge', displayName: 'AcmeBadge', description: 'Fixture component.', props: [{name: 'label', type: 'string', description: 'Text.'}]};\n`,
874
+ );
875
+ fs.writeFileSync(
876
+ path.join(tmpDir, 'components', 'AcmeBadge.tsx'),
877
+ 'export function AcmeBadge(props: {label: string}) { return null; }\n',
878
+ );
879
+ process.chdir(tmpDir);
880
+
881
+ await createProgram().parseAsync([
882
+ 'node',
883
+ 'astryx',
884
+ 'doctor',
885
+ 'integration',
886
+ 'components',
887
+ ]);
888
+
889
+ const output = logCalls.join('\n');
890
+ expect(output).toMatch(/^kind:\s+required_mismatch$/m);
891
+ expect(output).toContain(
892
+ '"label" is required in the code, but the doc does not mark it required.',
893
+ );
894
+ expect(output).toContain(
895
+ '1 prop(s) differ between the docs and the code in 1 of 1 component(s) compared (TypeScript',
896
+ );
897
+ expect(output).not.toContain('[ok] Documented prop');
898
+ expect(process.exitCode).toBeUndefined();
899
+ });
900
+
901
+ it('components compares a hidden doc and a family entry with no record, so their drift is never an [ok]', async () => {
902
+ writeComponentIntegration('AcmeMenu');
903
+ const dir = path.join(tmpDir, 'components');
904
+ fs.writeFileSync(
905
+ path.join(dir, 'AcmeMenu.doc.mjs'),
906
+ `export default {type: 'component', name: 'AcmeMenuRoot', displayName: 'AcmeMenuRoot', description: 'Fixture component.', hiddenComponents: ['AcmeMenuItem'], components: [{name: 'AcmeMenuItem', displayName: 'AcmeMenuItem', description: 'Fixture item.', props: [{name: 'label', type: 'string', description: 'Text.'}]}]};\n`,
907
+ );
908
+ fs.writeFileSync(
909
+ path.join(dir, 'AcmeMenu.tsx'),
910
+ 'export function AcmeMenuRoot() { return null; }\nexport function AcmeMenuItem(props: {value?: string}) { return null; }\n',
911
+ );
912
+ fs.writeFileSync(
913
+ path.join(dir, 'AcmeSecret.doc.mjs'),
914
+ `export default {type: 'component', name: 'AcmeSecret', displayName: 'AcmeSecret', description: 'Fixture component.', hidden: true, props: [{name: 'tone', type: 'string', description: 'Tone.'}]};\n`,
915
+ );
916
+ fs.writeFileSync(
917
+ path.join(dir, 'AcmeSecret.tsx'),
918
+ 'export function AcmeSecret(props: {size?: number}) { return null; }\n',
919
+ );
920
+ process.chdir(tmpDir);
921
+ const run = async (/** @type {string[]} */ flags) => {
922
+ logCalls.length = 0;
923
+ await createProgram().parseAsync([
924
+ 'node',
925
+ 'astryx',
926
+ ...flags,
927
+ 'doctor',
928
+ 'integration',
929
+ 'components',
930
+ ]);
931
+ return logCalls.join('\n');
932
+ };
933
+
934
+ const report = JSON.parse(await run(['--json'])).data.propDrift;
935
+ const output = await run([]);
936
+
937
+ expect(
938
+ report.findings.map(f => `${f.component}.${f.prop} ${f.kind}`),
939
+ ).toEqual([
940
+ 'AcmeMenuItem.label not_in_code',
941
+ 'AcmeMenuItem.value not_in_doc',
942
+ 'AcmeSecret.size not_in_doc',
943
+ 'AcmeSecret.tone not_in_code',
944
+ ]);
945
+ expect(output).not.toContain('[ok] Documented prop');
946
+ expect(process.exitCode).toBeUndefined();
947
+ });
948
+
949
+ it('components lists props a mapped type builds from keys it cannot trace, and says the [ok] leaves them out', async () => {
950
+ writeComponentIntegration('AcmeChip');
951
+ const dir = path.join(tmpDir, 'components');
952
+ fs.writeFileSync(
953
+ path.join(dir, 'AcmeChip.doc.mjs'),
954
+ `export default {type: 'component', name: 'AcmeChip', displayName: 'AcmeChip', description: 'Fixture.', props: [{name: 'label', type: 'string', description: 'Text.'}]};\n`,
955
+ );
956
+ fs.writeFileSync(
957
+ path.join(dir, 'AcmeChip.tsx'),
958
+ "export function AcmeChip(props: {label?: string} & {[K in `on${Capitalize<'open'>}`]?: () => void}) { return null; }\n",
959
+ );
960
+ process.chdir(tmpDir);
961
+ const run = async (/** @type {string[]} */ flags) => {
962
+ logCalls.length = 0;
963
+ await createProgram().parseAsync([
964
+ 'node',
965
+ 'astryx',
966
+ ...flags,
967
+ 'doctor',
968
+ 'integration',
969
+ 'components',
970
+ ]);
971
+ return logCalls.join('\n');
972
+ };
973
+
974
+ const {propsNotChecked} = JSON.parse(await run(['--json'])).data.propDrift;
975
+ const output = await run([]);
976
+
977
+ expect(propsNotChecked).toEqual([
978
+ expect.objectContaining({component: 'AcmeChip', props: ['onOpen']}),
979
+ ]);
980
+ expect(output).toContain(
981
+ '[info] Props not checked for a doc entry in 1 component(s):',
982
+ );
983
+ expect(output).toContain(
984
+ 'in 1 component(s), apart from the props not checked (TypeScript',
985
+ );
986
+ });
987
+
988
+ it('components compares the entries of a family doc whose own source is missing', async () => {
989
+ writeComponentIntegration('AcmeTabs');
990
+ const dir = path.join(tmpDir, 'components');
991
+ const entry = (/** @type {string} */ name, /** @type {string} */ prop) =>
992
+ `{name: '${name}', displayName: '${name}', description: 'Fixture.', props: [{name: '${prop}', type: 'string', description: 'Text.'}]}`;
993
+ fs.writeFileSync(
994
+ path.join(dir, 'AcmeTabs.doc.mjs'),
995
+ `export default {type: 'component', name: 'AcmeTabs', displayName: 'AcmeTabs', description: 'Fixture.', props: [], hiddenComponents: ['AcmeTabsPanel'], components: [${entry('AcmeTabsItem', 'label')}, ${entry('AcmeTabsPanel', 'ghost')}]};\n`,
996
+ );
997
+ fs.rmSync(path.join(dir, 'AcmeTabs.tsx'), {force: true});
998
+ // The package's entry exports the members, so discovery records the
999
+ // listed one, though the family's own file is missing.
1000
+ for (const name of ['AcmeTabsItem', 'AcmeTabsPanel']) {
1001
+ fs.writeFileSync(
1002
+ path.join(dir, `${name}.tsx`),
1003
+ `export function ${name}(props: {label?: string; open?: boolean}) { return null; }\n`,
1004
+ );
1005
+ fs.appendFileSync(
1006
+ path.join(tmpDir, 'index.ts'),
1007
+ `export {${name}} from './components/${name}';\n`,
1008
+ );
1009
+ }
1010
+ process.chdir(tmpDir);
1011
+
1012
+ await createProgram().parseAsync([
1013
+ 'node',
1014
+ 'astryx',
1015
+ '--json',
1016
+ 'doctor',
1017
+ 'integration',
1018
+ 'components',
1019
+ ]);
1020
+ const report = JSON.parse(logCalls.join('\n')).data.propDrift;
1021
+
1022
+ expect(
1023
+ report.findings.map(
1024
+ (/** @type {any} */ f) => `${f.component}.${f.prop} ${f.kind}`,
1025
+ ),
1026
+ ).toEqual([
1027
+ 'AcmeTabsItem.open not_in_doc',
1028
+ 'AcmeTabsPanel.ghost not_in_code',
1029
+ 'AcmeTabsPanel.label not_in_doc',
1030
+ 'AcmeTabsPanel.open not_in_doc',
1031
+ ]);
1032
+ expect(report.unchecked).toEqual([
1033
+ {
1034
+ component: 'AcmeTabs',
1035
+ doc: 'components/AcmeTabs.doc.mjs',
1036
+ reason: 'no source file for AcmeTabs',
1037
+ },
1038
+ ]);
1039
+ });
1040
+
1041
+ it('components counts two docs of one name as two components, and compares a .doc.ts beside a .doc.mjs', async () => {
1042
+ writeComponentIntegration('AcmeTwin');
1043
+ const dir = path.join(tmpDir, 'components');
1044
+ const doc = (/** @type {string} */ name, /** @type {string} */ prop) =>
1045
+ `export default {type: 'component', name: '${name}', displayName: '${name}', description: 'Fixture.', props: [{name: '${prop}', type: 'string', description: 'Text.'}]};\n`;
1046
+ fs.writeFileSync(
1047
+ path.join(dir, 'AcmeTwin.doc.mjs'),
1048
+ doc('AcmeTwin', 'tone'),
1049
+ );
1050
+ fs.writeFileSync(
1051
+ path.join(dir, 'AcmeTwin.doc.ts'),
1052
+ doc('AcmeTwin', 'size'),
1053
+ );
1054
+ fs.writeFileSync(
1055
+ path.join(dir, 'AcmeTwin.tsx'),
1056
+ 'export function AcmeTwin(props: {label?: string}) { return null; }\n',
1057
+ );
1058
+ for (const folder of ['a', 'b']) {
1059
+ fs.mkdirSync(path.join(dir, folder));
1060
+ fs.writeFileSync(
1061
+ path.join(dir, folder, 'AcmeX.doc.mjs'),
1062
+ doc('AcmeX', folder),
1063
+ );
1064
+ fs.writeFileSync(
1065
+ path.join(dir, folder, 'AcmeX.tsx'),
1066
+ 'export function AcmeX() { return null; }\n',
1067
+ );
1068
+ }
1069
+ process.chdir(tmpDir);
1070
+ const run = async (/** @type {string[]} */ flags) => {
1071
+ logCalls.length = 0;
1072
+ await createProgram().parseAsync([
1073
+ 'node',
1074
+ 'astryx',
1075
+ ...flags,
1076
+ 'doctor',
1077
+ 'integration',
1078
+ 'components',
1079
+ ]);
1080
+ return logCalls.join('\n');
1081
+ };
1082
+
1083
+ const report = JSON.parse(await run(['--json'])).data.propDrift;
1084
+ const output = await run([]);
1085
+
1086
+ expect(
1087
+ report.findings
1088
+ .filter(f => f.kind === 'not_in_code')
1089
+ .map(f => `${f.doc} ${f.prop}`)
1090
+ .sort(),
1091
+ ).toEqual([
1092
+ 'components/AcmeTwin.doc.mjs tone',
1093
+ 'components/AcmeTwin.doc.ts size',
1094
+ 'components/a/AcmeX.doc.mjs a',
1095
+ 'components/b/AcmeX.doc.mjs b',
1096
+ ]);
1097
+ expect(output).toMatch(/ in 4 of 4 component\(s\) compared /);
1098
+ });
1099
+
1100
+ it('components prints every field of the props comparison that JSON carries', async () => {
1101
+ writeComponentIntegration('AcmeBadge');
1102
+ fs.writeFileSync(
1103
+ path.join(tmpDir, 'components', 'AcmeBadge.doc.mjs'),
1104
+ `export default {type: 'component', name: 'AcmeBadge', displayName: 'AcmeBadge', description: 'Fixture component.', props: [${[
1105
+ "{name: 'label', type: 'string', description: 'Text.'}",
1106
+ "{name: 'elevation', type: 'number', description: 'Shadow.'}",
1107
+ "{name: 'size', type: 'string', description: 'Size.'}",
1108
+ "{name: 'meta', type: '{id: string; extra?: number}', description: 'Data.'}",
1109
+ "{name: 'gap', type: 'number', description: 'Gap.', default: '8'}",
1110
+ ].join(', ')}]};\n`,
1111
+ );
1112
+ // `key` is a prop a doc never needs, though the component declares it.
1113
+ fs.writeFileSync(
1114
+ path.join(tmpDir, 'components', 'AcmeBadge.tsx'),
1115
+ "export function AcmeBadge({gap = 4}: {label: string; tone?: 'neutral' | 'info'; size?: number; meta?: {id: string}; gap?: number; key?: string}) { return null; }\n",
1116
+ );
1117
+ fs.writeFileSync(
1118
+ path.join(tmpDir, 'components', 'AcmeLonely.doc.mjs'),
1119
+ `export default {type: 'component', name: 'AcmeLonely', displayName: 'AcmeLonely', description: 'Fixture component.', props: []};\n`,
1120
+ );
1121
+ fs.writeFileSync(
1122
+ path.join(tmpDir, 'components', 'AcmeLonely.tsx'),
1123
+ 'export function Other() { return null; }\n',
1124
+ );
1125
+ process.chdir(tmpDir);
1126
+ const run = async (/** @type {string[]} */ flags) => {
1127
+ logCalls.length = 0;
1128
+ await createProgram().parseAsync([
1129
+ 'node',
1130
+ 'astryx',
1131
+ ...flags,
1132
+ 'doctor',
1133
+ 'integration',
1134
+ 'components',
1135
+ ]);
1136
+ return logCalls.join('\n');
1137
+ };
1138
+
1139
+ const report = JSON.parse(await run(['--json'])).data.propDrift;
1140
+ const output = await run([]);
1141
+
1142
+ expect(report.findings.map(f => `${f.prop} ${f.kind}`)).toEqual([
1143
+ 'elevation not_in_code',
1144
+ 'gap default_mismatch',
1145
+ 'label required_mismatch',
1146
+ 'size type_mismatch',
1147
+ 'tone not_in_doc',
1148
+ ]);
1149
+ expectRecordsCarry(output, report.findings, ['component', 'prop', 'kind']);
1150
+ expectRecordsCarry(output, report.unchecked, ['component', 'reason']);
1151
+ expectRecordsCarry(output, report.typesNotCompared, [
1152
+ 'component',
1153
+ 'reason',
1154
+ 'props',
1155
+ ]);
1156
+ expect(report.typesNotCompared).toHaveLength(1);
1157
+ expect(output).toContain(
1158
+ `Documented prop types judged: ${report.typesCompared}; not compared: 1.`,
1159
+ );
1160
+ // The response-type reference, which the README's JSON table comes from,
1161
+ // names every field too.
1162
+ const {doc: responseTypes} =
1163
+ await import('../../../foundation/response/response-types.doc.mjs');
1164
+ const member = responseTypes.members.find(
1165
+ m => m.value === 'integration.component-conflicts',
1166
+ );
1167
+ for (const field of Object.keys(report)) {
1168
+ expect(member?.description).toMatch(new RegExp(`\\b${field}\\b`));
1169
+ }
1170
+ expect(output).toContain(
1171
+ `in 1 of ${report.components} component(s) compared (TypeScript ${report.typescript}).`,
1172
+ );
1173
+ });
1174
+
1175
+ it('components says documented props match when they do', async () => {
1176
+ writeComponentIntegration('AcmeOnlyWidget');
1177
+ process.chdir(tmpDir);
1178
+
1179
+ await createProgram().parseAsync([
1180
+ 'node',
1181
+ 'astryx',
1182
+ 'doctor',
1183
+ 'integration',
1184
+ 'components',
1185
+ ]);
1186
+
1187
+ const output = logCalls.join('\n');
1188
+ expect(output).toMatch(
1189
+ /\[ok\] Documented prop names and required flags match the code in 1 component\(s\) \(TypeScript \d/,
1190
+ );
1191
+ expect(output).toMatch(
1192
+ /\[info\] Documented prop types judged: \d+; not compared: 0\./,
1193
+ );
1194
+ });
1195
+
1196
+ it('components prints no [ok] for props it could not compare', async () => {
1197
+ writeComponentIntegration('AcmeLonely');
1198
+ fs.writeFileSync(
1199
+ path.join(tmpDir, 'components', 'AcmeLonely.tsx'),
1200
+ 'export function Other() { return null; }\n',
1201
+ );
1202
+ process.chdir(tmpDir);
1203
+
1204
+ await createProgram().parseAsync([
1205
+ 'node',
1206
+ 'astryx',
1207
+ 'doctor',
1208
+ 'integration',
1209
+ 'components',
1210
+ ]);
1211
+
1212
+ const output = logCalls.join('\n');
1213
+ expect(output).toContain(
1214
+ '[info] Props not compared for 1 documented component(s):',
1215
+ );
1216
+ expect(output).not.toContain('[ok] Documented prop');
1217
+ });
1218
+
399
1219
  it('components exposes conflicts as structured JSON warnings', async () => {
400
1220
  const coreDir = findCoreDir(tmpDir);
401
1221
  expect(coreDir).not.toBeNull();
@@ -425,6 +1245,71 @@ describe('doctor integration — command', () => {
425
1245
  expect(process.exitCode).toBeUndefined();
426
1246
  });
427
1247
 
1248
+ it('docs resolves a link to a component or a template, its own or Core’s, and still flags one that names no doc', async () => {
1249
+ const page = (await discoverCoreTemplates()).find(
1250
+ template => template.type === 'page',
1251
+ );
1252
+ expect(page).toBeDefined();
1253
+ fs.writeFileSync(
1254
+ path.join(tmpDir, 'package.json'),
1255
+ JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
1256
+ );
1257
+ fs.writeFileSync(
1258
+ path.join(tmpDir, 'astryx.integration.mjs'),
1259
+ "export default {components: './components', templates: './templates', docs: './docs'};\n",
1260
+ );
1261
+ for (const dir of ['components', 'templates', 'docs']) {
1262
+ fs.mkdirSync(path.join(tmpDir, dir), {recursive: true});
1263
+ }
1264
+ fs.writeFileSync(
1265
+ path.join(tmpDir, 'components', 'AcmeButton.doc.mjs'),
1266
+ "export default {type: 'component', name: 'AcmeButton', displayName: 'Acme Button', description: 'A button.', props: []};\n",
1267
+ );
1268
+ fs.writeFileSync(
1269
+ path.join(tmpDir, 'components', 'AcmeButton.tsx'),
1270
+ 'export function AcmeButton() { return null; }\n',
1271
+ );
1272
+ fs.writeFileSync(
1273
+ path.join(tmpDir, 'templates', 'acme-card.doc.mjs'),
1274
+ "export default {type: 'block', name: 'Acme card', description: 'A card.'};\n",
1275
+ );
1276
+ fs.writeFileSync(
1277
+ path.join(tmpDir, 'templates', 'acme-card.tsx'),
1278
+ 'export default function Fixture() { return null; }\n',
1279
+ );
1280
+ const text =
1281
+ 'Start with {@link component:AcmeButton} or {@link block:acme-card}, ' +
1282
+ 'built on {@link @astryxdesign/core:component:Button} and ' +
1283
+ `{@link @astryxdesign/core:page:${page?.dirName}}. Not {@link component:AcmeMissing}.`;
1284
+ fs.writeFileSync(
1285
+ path.join(tmpDir, 'docs', 'using-widgets.doc.mjs'),
1286
+ `export default ${JSON.stringify({
1287
+ type: 'generic',
1288
+ name: 'using-widgets',
1289
+ title: 'Using widgets',
1290
+ description: 'Fixture topic.',
1291
+ sections: [{title: 'Overview', content: [{type: 'prose', text}]}],
1292
+ })};\n`,
1293
+ );
1294
+ process.chdir(tmpDir);
1295
+
1296
+ await createProgram().parseAsync([
1297
+ 'node',
1298
+ 'astryx',
1299
+ '--json',
1300
+ 'doctor',
1301
+ 'integration',
1302
+ 'docs',
1303
+ ]);
1304
+
1305
+ const parsed = JSON.parse(logCalls.join('\n'));
1306
+ const unresolved = parsed.data.issues
1307
+ .filter(issue => issue.code === 'invalid_doc_graph')
1308
+ .map(issue => issue.message);
1309
+ expect(unresolved).toHaveLength(1);
1310
+ expect(unresolved[0]).toContain('"component:AcmeMissing" names no doc');
1311
+ });
1312
+
428
1313
  it('docs reports an intentional replacement as info', async () => {
429
1314
  const [coreTopic] = Object.keys(discoverBuiltinTopics());
430
1315
  writeDocIntegration({name: 'acme-setup', replaces: coreTopic});