@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
@@ -0,0 +1,720 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * Copied passages: prose that two docs repeat word for word. One fact has one
5
+ * owner, so a passage repeated in a second doc goes stale in one of them the
6
+ * next time the other is edited.
7
+ *
8
+ * Only prose is compared: code, examples, names, titles, keywords, and links
9
+ * are left out, and a run must be at least {@link PASSAGE_WORDS} words long,
10
+ * so a reported passage is always a real copy, never a shared phrase.
11
+ */
12
+
13
+ import fs from 'node:fs';
14
+
15
+ import {builtinGuideFiles, loadTopicModule} from './docs-discovery.mjs';
16
+
17
+ /** The shortest run of identical words reported as a copy. */
18
+ export const PASSAGE_WORDS = 10;
19
+
20
+ /**
21
+ * A copy carries at least this many distinct content words, so a run of
22
+ * glue words ("and it is in the way that you can…") is a phrase, not a fact.
23
+ */
24
+ export const MIN_CONTENT_WORDS = 4;
25
+
26
+ /**
27
+ * A run that this many topics share is boilerplate, such as a lead-in every
28
+ * guide of a series opens with, not a copy of one topic's fact.
29
+ */
30
+ export const BOILERPLATE_TOPICS = 3;
31
+
32
+ /** English function words, which say nothing about what a passage states. */
33
+ const FUNCTION_WORDS = new Set(
34
+ (
35
+ 'a about above after again all also an and any are as at be because been before being below between both but by ' +
36
+ 'can could did do does doing down during each either else every few for from further had has have having he her here ' +
37
+ 'him his how i if in into is it its itself just may me might more most must my no nor not now of off on once only or ' +
38
+ 'other our out over own same she should so some such than that the their them then there these they this those through ' +
39
+ 'to too under until up us very was we were what when where which while who whom why will with would you your'
40
+ ).split(' '),
41
+ );
42
+
43
+ /**
44
+ * The number of distinct content words in a run: words that are not function
45
+ * words, with surrounding punctuation set aside.
46
+ *
47
+ * @param {{word: string}[]} words
48
+ * @param {number} start
49
+ * @param {number} length
50
+ */
51
+ export function contentWords(words, start, length) {
52
+ const found = new Set();
53
+ for (const {word} of words.slice(start, start + length)) {
54
+ const bare = word.replace(/^[^a-z0-9@]+|[^a-z0-9]+$/g, '');
55
+ if (bare && !FUNCTION_WORDS.has(bare)) found.add(bare);
56
+ }
57
+ return found.size;
58
+ }
59
+
60
+ /** Fields that hold identifiers or code rather than prose. */
61
+ const SKIP_KEYS = new Set([
62
+ 'aliases',
63
+ 'audience',
64
+ 'category',
65
+ 'code',
66
+ 'example',
67
+ 'examples',
68
+ 'extends',
69
+ 'id',
70
+ 'import',
71
+ 'keywords',
72
+ 'label',
73
+ 'lang',
74
+ 'name',
75
+ 'placement',
76
+ 'projection',
77
+ 'props',
78
+ 'relationships',
79
+ 'replaces',
80
+ 'source',
81
+ 'title',
82
+ 'type',
83
+ ]);
84
+
85
+ /** Blocks whose content is code, or another doc's content included by name. */
86
+ const SKIP_BLOCKS = new Set(['code', 'example', 'reference']);
87
+
88
+ /**
89
+ * Pointers to other docs, which are not prose: a `{@link …}` and a written
90
+ * `astryx docs <name>` command. Two topics pointing at the same section say
91
+ * no fact twice.
92
+ */
93
+ const POINTERS = /\{@link [^}]*\}|`astryx docs [^`]*`/g;
94
+
95
+ /**
96
+ * The words a passage is compared by: lowercase, with links and Markdown
97
+ * emphasis set aside. A token with no letter or digit (the `.` after a link)
98
+ * is not a word.
99
+ *
100
+ * @param {string} text
101
+ * @returns {string[]}
102
+ */
103
+ export function passageWords(text) {
104
+ return text
105
+ .toLowerCase()
106
+ .replace(POINTERS, ' ')
107
+ .replace(/[`*]/g, '')
108
+ .split(/[^a-z0-9'@./-]+/)
109
+ .filter(word => /[a-z0-9]/.test(word));
110
+ }
111
+
112
+ /**
113
+ * Every word of a doc file, in order, with its 1-based line. Words are runs of
114
+ * letters and digits, so punctuation, code ticks, quotes and escapes in the
115
+ * source neither join nor split them.
116
+ *
117
+ * @param {string} source
118
+ * @returns {{word: string, line: number}[]}
119
+ */
120
+ export function fileWords(source) {
121
+ /** @type {{word: string, line: number}[]} */
122
+ const words = [];
123
+ source.split('\n').forEach((line, at) => {
124
+ for (const match of line.matchAll(/[A-Za-z0-9]+/g)) {
125
+ words.push({word: match[0].toLowerCase(), line: at + 1});
126
+ }
127
+ });
128
+ return words;
129
+ }
130
+
131
+ /**
132
+ * The words of a string the way {@link fileWords} reads a file, each with its
133
+ * offset in the string.
134
+ *
135
+ * @param {string} text
136
+ * @returns {{word: string, at: number}[]}
137
+ */
138
+ export function textWords(text) {
139
+ return [...text.matchAll(/[A-Za-z0-9]+/g)].map(match => ({
140
+ word: match[0].toLowerCase(),
141
+ at: /** @type {number} */ (match.index),
142
+ }));
143
+ }
144
+
145
+ /**
146
+ * Where a string's whole run of words starts among a file's words, looking
147
+ * from `from` on, or -1.
148
+ *
149
+ * @param {{word: string}[]} file
150
+ * @param {{word: string}[]} words
151
+ * @param {number} [from]
152
+ * @returns {number}
153
+ */
154
+ export function locateWords(file, words, from = 0) {
155
+ if (words.length === 0) return -1;
156
+ outer: for (let i = from; i + words.length <= file.length; i++) {
157
+ for (let k = 0; k < words.length; k++) {
158
+ if (file[i + k].word !== words[k].word) continue outer;
159
+ }
160
+ return i;
161
+ }
162
+ return -1;
163
+ }
164
+
165
+ /**
166
+ * The file line of the character at offset `at` of a string whose words start
167
+ * at `index` among the file's words.
168
+ *
169
+ * @param {{line: number}[]} file
170
+ * @param {number} index
171
+ * @param {{at: number}[]} words the string's words
172
+ * @param {number} at
173
+ * @returns {number | null}
174
+ */
175
+ export function lineAtOffset(file, index, words, at) {
176
+ let k = words.findIndex(word => word.at + 1 > at);
177
+ if (k < 0) k = words.length - 1;
178
+ return file[index + k]?.line ?? null;
179
+ }
180
+
181
+ /**
182
+ * A shared run with the boilerplate trimmed off its ends, judged window by
183
+ * window: every word a leading boilerplate window covers, and every word from
184
+ * the first trailing one on. A lead-in many topics open with thus never hides
185
+ * the copy after it. Null when less than {@link PASSAGE_WORDS} words are left.
186
+ *
187
+ * @param {{start: number, length: number}} run
188
+ * @param {(at: number) => boolean} boilerplate whether the window of
189
+ * {@link PASSAGE_WORDS} words starting at `at` is boilerplate
190
+ * @returns {{start: number, length: number} | null}
191
+ */
192
+ export function trimBoilerplate({start, length}, boilerplate) {
193
+ const lastWindow = start + length - PASSAGE_WORDS;
194
+ let lead = start;
195
+ while (lead <= lastWindow && boilerplate(lead)) lead += 1;
196
+ const begin = lead > start ? lead - 1 + PASSAGE_WORDS : start;
197
+ let tail = lastWindow;
198
+ while (tail >= begin && boilerplate(tail)) tail -= 1;
199
+ const end = tail < lastWindow ? tail + 1 : start + length;
200
+ return end - begin < PASSAGE_WORDS
201
+ ? null
202
+ : {start: begin, length: end - begin};
203
+ }
204
+
205
+ /**
206
+ * The words of one string as passageWords reads them, each with where it sits
207
+ * in the string as written, so a passage can be quoted with its case,
208
+ * punctuation and code ticks.
209
+ *
210
+ * @param {string} text
211
+ * @returns {{word: string, start: number, end: number}[]}
212
+ */
213
+ export function wordSpans(text) {
214
+ const masked = text
215
+ .toLowerCase()
216
+ .replace(POINTERS, pointer => ' '.repeat(pointer.length));
217
+ /** @type {{word: string, start: number, end: number}[]} */
218
+ const spans = [];
219
+ for (const match of masked.matchAll(/[a-z0-9'@./`*-]+/g)) {
220
+ const word = match[0].replace(/[`*]/g, '');
221
+ if (!/[a-z0-9]/.test(word)) continue;
222
+ const start = /** @type {number} */ (match.index);
223
+ spans.push({word, start, end: start + match[0].length});
224
+ }
225
+ return spans;
226
+ }
227
+
228
+ /**
229
+ * Each prose string of a doc, in order, with the title of the section it is
230
+ * in (null before the first section). Code, examples, reference blocks, and
231
+ * identifier fields are left out.
232
+ *
233
+ * @param {unknown} doc an authored doc object
234
+ * @returns {{text: string, section: string | null}[]}
235
+ */
236
+ export function proseStrings(doc) {
237
+ /** @type {{text: string, section: string | null}[]} */
238
+ const strings = [];
239
+ /**
240
+ * @param {unknown} value
241
+ * @param {string | null} section
242
+ */
243
+ const walk = (value, section) => {
244
+ if (typeof value === 'string') {
245
+ strings.push({text: value, section});
246
+ return;
247
+ }
248
+ if (Array.isArray(value)) {
249
+ for (const item of value) walk(item, section);
250
+ return;
251
+ }
252
+ if (value == null || typeof value !== 'object') return;
253
+ const record = /** @type {Record<string, unknown>} */ (value);
254
+ if (typeof record.type === 'string' && SKIP_BLOCKS.has(record.type)) return;
255
+ if (Array.isArray(record.sections)) {
256
+ for (const [key, field] of Object.entries(record)) {
257
+ if (key !== 'sections' && !SKIP_KEYS.has(key)) walk(field, section);
258
+ }
259
+ for (const each of record.sections) {
260
+ const title =
261
+ typeof each?.title === 'string' ? each.title : (section ?? null);
262
+ walk(each, title);
263
+ }
264
+ return;
265
+ }
266
+ for (const [key, field] of Object.entries(record)) {
267
+ if (!SKIP_KEYS.has(key)) walk(field, section);
268
+ }
269
+ };
270
+ walk(doc, null);
271
+ return strings;
272
+ }
273
+
274
+ /**
275
+ * Each prose word of a doc, in order, with the title of the section it is in
276
+ * (null before the first section) and the number of the string it came from.
277
+ * A passage never runs from one string into the next, so two fields that
278
+ * happen to sit side by side are not read as one sentence.
279
+ *
280
+ * @param {unknown} doc an authored doc object
281
+ * @returns {{word: string, section: string | null, block: number}[]}
282
+ */
283
+ export function proseWords(doc) {
284
+ /** @type {{word: string, section: string | null, block: number}[]} */
285
+ const words = [];
286
+ proseStrings(doc).forEach(({text, section}, at) => {
287
+ for (const word of passageWords(text)) {
288
+ words.push({word, section, block: at + 1});
289
+ }
290
+ });
291
+ return words;
292
+ }
293
+
294
+ /**
295
+ * Whether the `size` words from `at` all come from one string.
296
+ * @param {{block?: number}[]} words
297
+ * @param {number} at
298
+ * @param {number} size
299
+ */
300
+ function inOneString(words, at, size) {
301
+ return words[at].block === words[at + size - 1].block;
302
+ }
303
+
304
+ /**
305
+ * The runs of at least `size` words that both docs have, each inside one
306
+ * string of each doc, longest first, as its start in `mine` and its length.
307
+ *
308
+ * @param {{word: string, block?: number}[]} mine
309
+ * @param {{word: string, block?: number}[]} other
310
+ * @param {number} [size]
311
+ * @returns {{start: number, length: number}[]}
312
+ */
313
+ export function sharedPassages(mine, other, size = PASSAGE_WORDS) {
314
+ if (mine.length < size || other.length < size) return [];
315
+ const theirs = other.map(each => each.word);
316
+ /** @type {Map<string, number[]>} */
317
+ const starts = new Map();
318
+ for (let i = 0; i + size <= theirs.length; i++) {
319
+ if (!inOneString(other, i, size)) continue;
320
+ const key = theirs.slice(i, i + size).join(' ');
321
+ const list = starts.get(key);
322
+ if (list) list.push(i);
323
+ else starts.set(key, [i]);
324
+ }
325
+ const words = mine.map(each => each.word);
326
+ /** @type {{start: number, length: number}[]} */
327
+ const runs = [];
328
+ let i = 0;
329
+ while (i + size <= words.length) {
330
+ const found = inOneString(mine, i, size)
331
+ ? starts.get(words.slice(i, i + size).join(' '))
332
+ : undefined;
333
+ if (!found) {
334
+ i++;
335
+ continue;
336
+ }
337
+ let best = size;
338
+ for (const j of found) {
339
+ let length = size;
340
+ while (
341
+ i + length < words.length &&
342
+ j + length < theirs.length &&
343
+ mine[i + length].block === mine[i].block &&
344
+ other[j + length].block === other[j].block &&
345
+ words[i + length] === theirs[j + length]
346
+ ) {
347
+ length++;
348
+ }
349
+ best = Math.max(best, length);
350
+ }
351
+ runs.push({start: i, length: best});
352
+ i += best;
353
+ }
354
+ return runs.sort((a, b) => b.length - a.length);
355
+ }
356
+
357
+ /**
358
+ * @typedef {{name: string, title?: string, file?: string, doc?: unknown, words: {word: string, section: string | null, block: number}[], replaces: string | null, extendsTopic?: string | null}} GuideWords
359
+ */
360
+
361
+ /**
362
+ * Each run of {@link PASSAGE_WORDS} words inside one string of a doc, joined.
363
+ * @param {{word: string, block: number}[]} words
364
+ * @returns {Set<string>}
365
+ */
366
+ function windows(words) {
367
+ /** @type {Set<string>} */
368
+ const keys = new Set();
369
+ for (let i = 0; i + PASSAGE_WORDS <= words.length; i++) {
370
+ if (!inOneString(words, i, PASSAGE_WORDS)) continue;
371
+ keys.add(
372
+ words
373
+ .slice(i, i + PASSAGE_WORDS)
374
+ .map(each => each.word)
375
+ .join(' '),
376
+ );
377
+ }
378
+ return keys;
379
+ }
380
+
381
+ /**
382
+ * Guides with an index from each run of words to the guides that have it, so
383
+ * a topic is compared in full only with the guides it shares a run with.
384
+ * @typedef {{guides: GuideWords[], index: Map<string, number[]>}} IndexedGuides
385
+ */
386
+
387
+ /**
388
+ * @param {GuideWords[]} guides
389
+ * @returns {IndexedGuides}
390
+ */
391
+ function indexGuides(guides) {
392
+ /** @type {Map<string, number[]>} */
393
+ const index = new Map();
394
+ guides.forEach((guide, at) => {
395
+ for (const key of windows(guide.words)) {
396
+ const list = index.get(key);
397
+ if (list) list.push(at);
398
+ else index.set(key, [at]);
399
+ }
400
+ });
401
+ return {guides, index};
402
+ }
403
+
404
+ /**
405
+ * The indexes of the guides that share at least one run with `words`.
406
+ * @param {{word: string, block: number}[]} words
407
+ * @param {IndexedGuides} indexed
408
+ * @returns {number[]}
409
+ */
410
+ function candidates(words, indexed) {
411
+ /** @type {Set<number>} */
412
+ const found = new Set();
413
+ for (const key of windows(words)) {
414
+ for (const at of indexed.index.get(key) ?? []) found.add(at);
415
+ }
416
+ return [...found].sort((a, b) => a - b);
417
+ }
418
+
419
+ /** @type {Promise<IndexedGuides> | null} */
420
+ let coreGuides = null;
421
+
422
+ /**
423
+ * @param {string} file
424
+ * @returns {Promise<any>} the doc, or null for one that does not load (the
425
+ * load problem is reported elsewhere)
426
+ */
427
+ export async function loadQuietly(file) {
428
+ try {
429
+ return await loadTopicModule(file);
430
+ } catch {
431
+ return null;
432
+ }
433
+ }
434
+
435
+ /** @returns {Promise<IndexedGuides>} the CLI's own guides, read and indexed once */
436
+ export function loadCoreGuides() {
437
+ coreGuides ??= (async () => {
438
+ /** @type {GuideWords[]} */
439
+ const guides = [];
440
+ for (const file of builtinGuideFiles()) {
441
+ const doc = await loadQuietly(file);
442
+ if (typeof doc?.name === 'string') {
443
+ guides.push({name: doc.name, words: proseWords(doc), replaces: null});
444
+ }
445
+ }
446
+ return indexGuides(guides);
447
+ })();
448
+ return coreGuides;
449
+ }
450
+
451
+ /**
452
+ * One entry for each pair of topics that repeat a passage word for word: two
453
+ * of the package's own topics, or one of them and a Core topic, except the
454
+ * Core topic it replaces (a replacement takes that topic's place, so it may
455
+ * carry its text).
456
+ *
457
+ * @param {{records: {name: string, path: string, replaces?: string | null, extendsTopic?: string | null}[], guides?: {name: string, ref?: {topicFile?: string}}[]}} discovered
458
+ * the package's docs, as discoverIntegrationDocs returns them
459
+ * @returns {Promise<import('../../api/integration/authoring-checks.type.mjs').CopiedPassage[]>}
460
+ */
461
+ export async function findCopiedPassages(discovered) {
462
+ const topics = [
463
+ ...discovered.records.map(record => ({
464
+ name: record.name,
465
+ file: record.path,
466
+ replaces: record.replaces ?? null,
467
+ extendsTopic: record.extendsTopic ?? null,
468
+ })),
469
+ ...(discovered.guides ?? []).map(guide => ({
470
+ name: guide.name,
471
+ file: guide.ref?.topicFile,
472
+ replaces: null,
473
+ extendsTopic: null,
474
+ })),
475
+ ];
476
+ /** @type {GuideWords[]} */
477
+ const mine = [];
478
+ for (const each of topics) {
479
+ const doc = each.file ? await loadQuietly(each.file) : null;
480
+ if (doc) {
481
+ mine.push({
482
+ ...each,
483
+ title: typeof doc.title === 'string' ? doc.title : each.name,
484
+ doc,
485
+ words: proseWords(doc),
486
+ });
487
+ }
488
+ }
489
+ if (mine.length === 0) return [];
490
+ const core = await loadCoreGuides();
491
+ const coreNames = new Set(core.guides.map(guide => guide.name.toLowerCase()));
492
+ const packageNames = new Set(mine.map(each => each.name.toLowerCase()));
493
+ /**
494
+ * The page a topic's sections show on: its own, or the one it extends.
495
+ * @param {GuideWords} each
496
+ */
497
+ const pageOf = each => (each.extendsTopic ?? each.name).toLowerCase();
498
+ /** @type {import('../../api/integration/authoring-checks.type.mjs').CopiedPassage[]} */
499
+ const found = [];
500
+ /**
501
+ * How well a passage fits a topic: how many of its content words the
502
+ * topic's name, title, or the section the passage sits in also use.
503
+ * @param {GuideWords} topic
504
+ * @param {string[]} words the passage's words
505
+ * @param {string | null} section
506
+ */
507
+ const fit = (topic, words, section) => {
508
+ // A plural matches its singular: a "menus" topic fits a passage on a menu.
509
+ const stem = (/** @type {string} */ word) =>
510
+ word.length > 3 && word.endsWith('s') ? word.slice(0, -1) : word;
511
+ // Function words ("the", "with") fit every passage, so only content
512
+ // words count, as contentWords counts them.
513
+ const label = new Set(
514
+ passageWords(
515
+ `${topic.name.replace(/-/g, ' ')} ${topic.title ?? ''} ${section ?? ''}`,
516
+ )
517
+ .filter(word => !FUNCTION_WORDS.has(word))
518
+ .map(stem),
519
+ );
520
+ return new Set(words.map(stem).filter(word => label.has(word))).size;
521
+ };
522
+ /**
523
+ * The file line where a passage starts, found from the prose string it is
524
+ * in, or null.
525
+ * @param {GuideWords} topic
526
+ * @param {number} start
527
+ */
528
+ const lineOf = (topic, start) => {
529
+ if (!topic.file || !topic.doc) return null;
530
+ const text = proseStrings(topic.doc)[topic.words[start].block - 1]?.text;
531
+ if (!text) return null;
532
+ let source;
533
+ try {
534
+ source = fs.readFileSync(topic.file, 'utf8');
535
+ } catch {
536
+ return null;
537
+ }
538
+ const file = fileWords(source);
539
+ const words = textWords(text);
540
+ const index = locateWords(file, words);
541
+ if (index < 0) return null;
542
+ // The passage starts at the string's word the run's first word opens.
543
+ let first = start;
544
+ while (
545
+ first > 0 &&
546
+ topic.words[first - 1].block === topic.words[start].block
547
+ ) {
548
+ first -= 1;
549
+ }
550
+ const span = wordSpans(text)[start - first];
551
+ return span ? lineAtOffset(file, index, words, span.start) : null;
552
+ };
553
+ /**
554
+ * A passage as written: the words of the run, from the string it is in.
555
+ * @param {GuideWords} topic
556
+ * @param {number} start
557
+ * @param {number} length
558
+ * @returns {string | null}
559
+ */
560
+ const writtenText = (topic, start, length) => {
561
+ if (!topic.doc) return null;
562
+ const {block} = topic.words[start];
563
+ const text = proseStrings(topic.doc)[block - 1]?.text;
564
+ if (!text) return null;
565
+ let first = start;
566
+ while (first > 0 && topic.words[first - 1].block === block) first -= 1;
567
+ const spans = wordSpans(text);
568
+ const from = spans[start - first];
569
+ const to = spans[start - first + length - 1];
570
+ return from && to ? text.slice(from.start, to.end) : null;
571
+ };
572
+ /**
573
+ * One entry for a pair: the topic that should keep the text (a Core topic,
574
+ * or of two package topics the one the passage fits), and the exact change
575
+ * for the other.
576
+ * @param {GuideWords} first
577
+ * @param {GuideWords} second
578
+ * @param {'package' | 'core'} kind
579
+ */
580
+ const compare = (first, second, kind) => {
581
+ const filtered = (
582
+ /** @type {GuideWords} */ a,
583
+ /** @type {GuideWords} */ b,
584
+ ) =>
585
+ sharedPassages(a.words, b.words).flatMap(({start, length}) => {
586
+ // Boilerplate is judged window by window: the leading and trailing
587
+ // 10-word windows three or more topics share are trimmed, so a
588
+ // lead-in many topics open with does not hide the copy after it.
589
+ const run = trimBoilerplate(
590
+ {start, length},
591
+ at => topicsSharing(a.words, at) >= BOILERPLATE_TOPICS,
592
+ );
593
+ return run &&
594
+ contentWords(a.words, run.start, run.length) >= MIN_CONTENT_WORDS
595
+ ? [run]
596
+ : [];
597
+ });
598
+ let runs = filtered(first, second);
599
+ if (runs.length === 0) return;
600
+ let topic = first;
601
+ let owner = second;
602
+ let why = 'Core keeps the text it shares';
603
+ if (kind === 'package') {
604
+ const words = first.words
605
+ .slice(runs[0].start, runs[0].start + runs[0].length)
606
+ .map(each => each.word);
607
+ const otherRuns = filtered(second, first);
608
+ const firstFit = fit(first, words, first.words[runs[0].start].section);
609
+ const secondFit = otherRuns.length
610
+ ? fit(second, words, second.words[otherRuns[0].start].section)
611
+ : 0;
612
+ // The passage stays where it fits best; on a tie, in the topic whose
613
+ // name sorts first, so the advice is the same on every run.
614
+ const firstKeeps =
615
+ firstFit > secondFit ||
616
+ (firstFit === secondFit && first.name.localeCompare(second.name) < 0);
617
+ if (firstKeeps && otherRuns.length) {
618
+ topic = second;
619
+ owner = first;
620
+ runs = otherRuns;
621
+ }
622
+ why =
623
+ firstFit === secondFit
624
+ ? `"${owner.name}" keeps it: neither topic's name, title or section fits it better, so the one whose name sorts first does`
625
+ : `"${owner.name}" keeps it: its name, title or section fits the passage`;
626
+ }
627
+ const [{start, length}] = runs;
628
+ const written =
629
+ writtenText(topic, start, length) ??
630
+ topic.words
631
+ .slice(start, start + length)
632
+ .map(each => each.word)
633
+ .join(' ');
634
+ const excerpt =
635
+ written.length <= 240
636
+ ? written
637
+ : `${written.slice(0, 120)} … ${written.slice(-80)}`;
638
+ const section = topic.words[start].section;
639
+ const line = lineOf(topic, start);
640
+ const where = [
641
+ section ? `section "${section}"` : null,
642
+ line == null ? null : `line ${line}`,
643
+ ]
644
+ .filter(Boolean)
645
+ .join(', ');
646
+ const more =
647
+ runs.length > 1 ? `, and ${runs.length - 1} more passage(s)` : '';
648
+ const named =
649
+ kind === 'core' ? `the Core topic "${owner.name}"` : `"${owner.name}"`;
650
+ // An extension has no page of its own: its sections show on the page of
651
+ // the topic it extends, so a link to its text names that topic. Topic
652
+ // names resolve without regard to case, as the docs catalog reads them.
653
+ const page = pageOf(owner);
654
+ // The page is Core's only when the owner is a Core guide, or an extension
655
+ // of a Core topic that no package topic of the same name replaces.
656
+ const corePage =
657
+ kind === 'core' ||
658
+ (owner.extendsTopic != null &&
659
+ coreNames.has(page) &&
660
+ !packageNames.has(page));
661
+ const link = corePage
662
+ ? `{@link @astryxdesign/cli:generic:${page}}`
663
+ : `{@link generic:${page}}`;
664
+ // A Core topic one of the package's topics replaces opens that
665
+ // replacement, which does not carry the passage, so no link reaches it.
666
+ const replacer =
667
+ kind === 'core'
668
+ ? mine.find(each => each.replaces?.toLowerCase() === page)
669
+ : undefined;
670
+ // Two topics on one page (one extends the other, or both extend the same
671
+ // topic) would link the page to itself: the copy goes instead.
672
+ const fix = replacer
673
+ ? `"${replacer.name}" replaces the Core topic "${owner.name}", so a link to it opens "${replacer.name}", which does not carry this text: reword the passage in "${topic.name}" in your own words, or move it into "${replacer.name}".`
674
+ : pageOf(topic) === page
675
+ ? `"${topic.name}" and "${owner.name}" show on the same page ("${page}"): delete the passage from "${topic.name}", or, if its section overrides one there, drop the override so the original shows.`
676
+ : `In "${topic.name}", replace that whole passage, from its first word to its last, code ticks included, with ${link}.`;
677
+ found.push({
678
+ topic: topic.name,
679
+ section,
680
+ line,
681
+ otherTopic: owner.name,
682
+ otherOwner: kind,
683
+ owner: owner.name,
684
+ text: written,
685
+ words: length,
686
+ passages: runs.length,
687
+ message:
688
+ `"${topic.name}"${where ? ` (${where})` : ''} repeats ${length} words of ${named} word for word${more}: "${excerpt}". ` +
689
+ `${why}. ${fix}`,
690
+ });
691
+ };
692
+ const own = indexGuides(mine);
693
+ /**
694
+ * How many topics, the package's and Core's, have the run that starts here.
695
+ * @param {{word: string}[]} words
696
+ * @param {number} start
697
+ */
698
+ const topicsSharing = (words, start) => {
699
+ const key = words
700
+ .slice(start, start + PASSAGE_WORDS)
701
+ .map(each => each.word)
702
+ .join(' ');
703
+ return (
704
+ new Set(own.index.get(key) ?? []).size +
705
+ new Set(core.index.get(key) ?? []).size
706
+ );
707
+ };
708
+ mine.forEach((topic, i) => {
709
+ for (const at of candidates(topic.words, own)) {
710
+ if (at > i) compare(topic, mine[at], 'package');
711
+ }
712
+ for (const at of candidates(topic.words, core)) {
713
+ const other = core.guides[at];
714
+ if (topic.replaces?.toLowerCase() !== other.name.toLowerCase()) {
715
+ compare(topic, other, 'core');
716
+ }
717
+ }
718
+ });
719
+ return found;
720
+ }