@astryxdesign/cli 0.7.2-canary.fdd384f → 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 (450) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/README.md +169 -149
  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/kit/kit.mjs +3 -11
  7. package/api/component/_adapter.d.mts +31 -0
  8. package/api/component/_adapter.mjs +119 -11
  9. package/api/component/component.doc.mjs +6 -1
  10. package/api/component/component.mjs +60 -10
  11. package/api/component/doc-links.test.mjs +386 -0
  12. package/api/discover/_adapter.d.mts +132 -12
  13. package/api/discover/_adapter.mjs +488 -27
  14. package/api/discover/_adapter.test.mjs +599 -17
  15. package/api/discover/_catalog-view.d.mts +16 -5
  16. package/api/discover/_catalog-view.mjs +32 -8
  17. package/api/discover/detail/doc/doc.mjs +9 -1
  18. package/api/discover/discover.d.mts +11 -0
  19. package/api/discover/discover.doc.mjs +9 -7
  20. package/api/discover/discover.mjs +121 -19
  21. package/api/discover/discover.type.d.mts +20 -7
  22. package/api/discover/discover.type.mjs +17 -7
  23. package/api/discover/list/list.d.mts +4 -3
  24. package/api/discover/list/list.mjs +17 -10
  25. package/api/docs/_adapter.d.mts +63 -0
  26. package/api/docs/_adapter.mjs +312 -18
  27. package/api/docs/docs.doc.mjs +2 -1
  28. package/api/docs/docs.mjs +2 -1
  29. package/api/docs/docs.test.mjs +5 -2
  30. package/api/docs/integration-tree.test.mjs +22 -0
  31. package/api/docs/list/list.mjs +12 -2
  32. package/api/docs/reference-blocks.test.mjs +1 -3
  33. package/api/doctor/_adapter.d.mts +108 -0
  34. package/api/doctor/_adapter.mjs +223 -4
  35. package/api/doctor/doctor.d.mts +199 -0
  36. package/api/doctor/doctor.doc.mjs +3 -2
  37. package/api/doctor/doctor.mjs +530 -34
  38. package/api/doctor/doctor.test.mjs +180 -14
  39. package/api/doctor/release-checks.test.mjs +156 -0
  40. package/api/doctor/theme-checks.mjs +16 -3
  41. package/api/doctor/theme-checks.test.mjs +223 -7
  42. package/api/error.d.mts +10 -0
  43. package/api/error.mjs +25 -0
  44. package/api/gap-report/gap-report.d.mts +3 -0
  45. package/api/gap-report/gap-report.doc.mjs +11 -3
  46. package/api/gap-report/gap-report.mjs +152 -48
  47. package/api/gap-report/gap-report.test.mjs +233 -0
  48. package/api/gap-report/gap-report.type.d.mts +41 -0
  49. package/api/gap-report/gap-report.type.mjs +18 -0
  50. package/api/hook/_adapter.mjs +3 -11
  51. package/api/hook/hook.doc.mjs +4 -0
  52. package/api/hook/list/list.d.mts +1 -1
  53. package/api/init/_adapter.d.mts +22 -0
  54. package/api/init/_adapter.mjs +59 -0
  55. package/api/init/init.d.mts +7 -5
  56. package/api/init/init.doc.mjs +28 -7
  57. package/api/init/init.mjs +28 -4
  58. package/api/init/init.type.d.mts +48 -3
  59. package/api/init/init.type.mjs +37 -3
  60. package/api/init/run/run.d.mts +1 -1
  61. package/api/init/run/run.mjs +15 -4
  62. package/api/init/user/user.d.mts +14 -0
  63. package/api/init/user/user.mjs +87 -0
  64. package/api/init/user/user.test.mjs +232 -0
  65. package/api/integration/_adapter.d.mts +33 -0
  66. package/api/integration/_adapter.mjs +58 -0
  67. package/api/integration/authoring-checks.mjs +100 -5
  68. package/api/integration/authoring-checks.test.mjs +769 -0
  69. package/api/integration/authoring-checks.type.d.mts +134 -0
  70. package/api/integration/authoring-checks.type.mjs +60 -1
  71. package/api/integration/integrationCheck.doc.mjs +1 -1
  72. package/api/integration/integrationComponentConflicts.doc.mjs +11 -3
  73. package/api/integration/integrationDocConflicts.doc.mjs +22 -5
  74. package/api/integration/pack-check.mjs +303 -13
  75. package/api/integration/pack-check.test.mjs +371 -10
  76. package/api/integration/validate-integration-fixes.test.mjs +67 -0
  77. package/api/integration/validate-integration.d.mts +8 -0
  78. package/api/integration/validate-integration.mjs +55 -9
  79. package/api/integration/validate-integration.test.mjs +77 -8
  80. package/api/integration/validate-integration.type.d.mts +8 -0
  81. package/api/integration/validate-integration.type.mjs +3 -1
  82. package/api/integration/validate-unread-theme-folders.test.mjs +3 -2
  83. package/api/integration/validateIntegration.doc.mjs +4 -2
  84. package/api/search/_adapter.d.mts +11 -0
  85. package/api/search/_adapter.mjs +25 -0
  86. package/api/search/search.d.mts +73 -13
  87. package/api/search/search.doc.mjs +8 -3
  88. package/api/search/search.domain-priority.test.mjs +108 -18
  89. package/api/search/search.mjs +541 -52
  90. package/api/search/search.test.mjs +143 -1
  91. package/api/search/search.type.d.mts +14 -1
  92. package/api/search/search.type.mjs +6 -1
  93. package/api/swizzle/_adapter.mjs +2 -2
  94. package/api/swizzle/copy/copy.mjs +1 -1
  95. package/api/template/template-integration.test.mjs +1 -1
  96. package/api/theme/_adapter.d.mts +16 -0
  97. package/api/theme/_adapter.mjs +32 -5
  98. package/api/theme/build/build.d.mts +2 -1
  99. package/api/theme/build/build.mjs +70 -14
  100. package/api/theme/build/build.packed-old-core.test.mjs +20 -3
  101. package/api/theme/build/font-warning.mjs +5 -3
  102. package/api/theme/build/font-warning.test.mjs +2 -1
  103. package/api/theme/remove/remove.test.mjs +2 -2
  104. package/api/theme/targets/targets.mjs +3 -10
  105. package/api/theme/themeTargets.doc.mjs +4 -0
  106. package/api/theme/use/use.test.mjs +1 -1
  107. package/api/upgrade/_adapter.d.mts +40 -2
  108. package/api/upgrade/_adapter.mjs +118 -5
  109. package/api/upgrade/run/cli-behind-core.test.mjs +438 -0
  110. package/api/upgrade/run/run.mjs +35 -2
  111. package/api/upgrade/status/status.d.mts +21 -5
  112. package/api/upgrade/status/status.mjs +45 -8
  113. package/api/upgrade/upgrade.doc.mjs +6 -3
  114. package/api/upgrade/upgrade.mjs +5 -1
  115. package/api/upgrade/upgrade.type.d.mts +23 -0
  116. package/api/upgrade/upgrade.type.mjs +16 -1
  117. package/assets/codemods/__tests__/registry.test.mjs +1 -0
  118. package/assets/codemods/registry.mjs +1 -0
  119. package/assets/codemods/runner.mjs +7 -1
  120. package/assets/codemods/transforms/next/index.mjs +1 -11
  121. package/assets/codemods/transforms/v0.7.2/index.mjs +20 -0
  122. package/assets/discover/official-packages.json +367 -0
  123. package/assets/docs/author-a-theme.doc.mjs +2 -2
  124. package/assets/docs/browser-support.doc.mjs +1 -1
  125. package/assets/docs/getting-started.doc.mjs +23 -3
  126. package/assets/docs/styling-overview.doc.mjs +1 -1
  127. package/assets/docs/theme.doc.mjs +1 -1
  128. package/assets/docs/tree/add-a-component.doc.mjs +12 -2
  129. package/assets/docs/tree/add-a-theme.doc.mjs +13 -3
  130. package/assets/docs/tree/add-a-topic.doc.mjs +54 -9
  131. package/assets/docs/tree/agent-guidance.doc.mjs +19 -5
  132. package/assets/docs/tree/block-template.doc.mjs +6 -18
  133. package/assets/docs/tree/build-the-template.doc.mjs +1 -1
  134. package/assets/docs/tree/building-blocks.doc.mjs +4 -0
  135. package/assets/docs/tree/check-the-props.doc.mjs +8 -3
  136. package/assets/docs/tree/check-your-docs.doc.mjs +30 -13
  137. package/assets/docs/tree/checks.doc.mjs +17 -17
  138. package/assets/docs/tree/ci-and-containers.doc.mjs +88 -0
  139. package/assets/docs/tree/cli-command.doc.mjs +142 -0
  140. package/assets/docs/tree/cli.doc.mjs +6 -0
  141. package/assets/docs/tree/codemods.doc.mjs +17 -7
  142. package/assets/docs/tree/commands.doc.mjs +7 -1
  143. package/assets/docs/tree/component-family.doc.mjs +3 -5
  144. package/assets/docs/tree/component-imports.doc.mjs +13 -3
  145. package/assets/docs/tree/components.doc.mjs +7 -1
  146. package/assets/docs/tree/computer-agent-guide.doc.mjs +160 -0
  147. package/assets/docs/tree/computer-integrations.doc.mjs +93 -0
  148. package/assets/docs/tree/configuration.doc.mjs +7 -3
  149. package/assets/docs/tree/debug-and-gap-reports.doc.mjs +41 -22
  150. package/assets/docs/tree/define-the-theme.doc.mjs +6 -5
  151. package/assets/docs/tree/describe-the-component.doc.mjs +2 -2
  152. package/assets/docs/tree/docs.doc.mjs +9 -3
  153. package/assets/docs/tree/document-the-template.doc.mjs +1 -1
  154. package/assets/docs/tree/document-the-theme.doc.mjs +3 -3
  155. package/assets/docs/tree/export-template-assets.doc.mjs +1 -1
  156. package/assets/docs/tree/extend-or-replace.doc.mjs +19 -9
  157. package/assets/docs/tree/fonts-and-assets.doc.mjs +2 -2
  158. package/assets/docs/tree/generate-a-palette.doc.mjs +7 -5
  159. package/assets/docs/tree/global-integrations.doc.mjs +124 -0
  160. package/assets/docs/tree/global.doc.mjs +131 -0
  161. package/assets/docs/tree/grade-template-with-agent.doc.mjs +5 -5
  162. package/assets/docs/tree/install-once.doc.mjs +105 -0
  163. package/assets/docs/tree/integrations.doc.mjs +13 -1
  164. package/assets/docs/tree/layout-spacing.doc.mjs +2 -3
  165. package/assets/docs/tree/layout.doc.mjs +6 -6
  166. package/assets/docs/tree/links.doc.mjs +6 -5
  167. package/assets/docs/tree/migration-setup.doc.mjs +3 -2
  168. package/assets/docs/tree/migration-verification.doc.mjs +0 -1
  169. package/assets/docs/tree/migration.doc.mjs +0 -1
  170. package/assets/docs/tree/monorepos.doc.mjs +84 -0
  171. package/assets/docs/tree/package-and-test.doc.mjs +1 -1
  172. package/assets/docs/tree/page-template.doc.mjs +3 -7
  173. package/assets/docs/tree/problems.doc.mjs +205 -0
  174. package/assets/docs/tree/project-global-or-both.doc.mjs +131 -0
  175. package/assets/docs/tree/publishing.doc.mjs +6 -6
  176. package/assets/docs/tree/quick-start.doc.mjs +52 -26
  177. package/assets/docs/tree/reference.doc.mjs +192 -0
  178. package/assets/docs/tree/replace-a-core-component.doc.mjs +15 -7
  179. package/assets/docs/tree/replace-a-core-template.doc.mjs +9 -11
  180. package/assets/docs/tree/scaffold.doc.mjs +1 -1
  181. package/assets/docs/tree/sections-and-placement.doc.mjs +9 -11
  182. package/assets/docs/tree/see-it-in-an-app.doc.mjs +17 -5
  183. package/assets/docs/tree/short-and-findable.doc.mjs +9 -8
  184. package/assets/docs/tree/single-component.doc.mjs +14 -6
  185. package/assets/docs/tree/start-a-template.doc.mjs +18 -8
  186. package/assets/docs/tree/subcomponent.doc.mjs +6 -6
  187. package/assets/docs/tree/template-assets.doc.mjs +2 -2
  188. package/assets/docs/tree/template-doc-overview.doc.mjs +6 -17
  189. package/assets/docs/tree/template-fonts.doc.mjs +1 -1
  190. package/assets/docs/tree/template-icons.doc.mjs +1 -1
  191. package/assets/docs/tree/template-images-media.doc.mjs +1 -1
  192. package/assets/docs/tree/template-styles.doc.mjs +2 -2
  193. package/assets/docs/tree/templates.doc.mjs +7 -1
  194. package/assets/docs/tree/test-in-an-app.doc.mjs +7 -7
  195. package/assets/docs/tree/test-template-in-app.doc.mjs +5 -5
  196. package/assets/docs/tree/themes.doc.mjs +4 -0
  197. package/assets/docs/tree/tokens-and-setup.doc.mjs +19 -15
  198. package/assets/docs/tree/troubleshooting.doc.mjs +22 -14
  199. package/assets/docs/tree/uninstall.doc.mjs +109 -0
  200. package/assets/docs/tree/updates.doc.mjs +92 -0
  201. package/assets/docs/tree/upgrading.doc.mjs +6 -6
  202. package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +14 -2
  203. package/assets/docs/tree/verify-packed-template.doc.mjs +3 -3
  204. package/assets/docs/tree/versioning.doc.mjs +9 -9
  205. package/assets/docs/tree/who-answers.doc.mjs +119 -0
  206. package/assets/docs/tree/write-the-template-file.doc.mjs +5 -5
  207. package/assets/docs/working-with-ai.doc.mjs +23 -9
  208. package/authoring/codemod/codemod.doc.mjs +7 -3
  209. package/authoring/config/config.doc.mjs +9 -6
  210. package/authoring/config/type.ts +4 -2
  211. package/authoring/debug/parse.d.mts +5 -5
  212. package/authoring/discover/discover.doc.mjs +7 -1
  213. package/authoring/doctypes/_schema.d.mts +8 -3
  214. package/authoring/doctypes/_schema.mjs +1 -0
  215. package/authoring/doctypes/base/graph-fields.doc.mjs +6 -4
  216. package/authoring/doctypes/command/command.doc.mjs +20 -12
  217. package/authoring/doctypes/command/type.ts +14 -0
  218. package/authoring/doctypes/component/component.doc.mjs +29 -26
  219. package/authoring/doctypes/component/type.ts +15 -5
  220. package/authoring/doctypes/enum/enum.doc.mjs +7 -4
  221. package/authoring/doctypes/function/function.doc.mjs +9 -8
  222. package/authoring/doctypes/hook/hook.doc.mjs +5 -4
  223. package/authoring/doctypes/hook/type.ts +4 -0
  224. package/authoring/doctypes/namespace/namespace.doc.mjs +7 -6
  225. package/authoring/doctypes/reference/reference.doc.mjs +12 -11
  226. package/authoring/doctypes/schema/schema.doc.mjs +9 -9
  227. package/authoring/doctypes/template/template.doc.mjs +2 -2
  228. package/authoring/doctypes/theme/theme.doc.mjs +2 -2
  229. package/authoring/gap-report/gap-report.doc.mjs +2 -2
  230. package/authoring/identity/identity.doc.mjs +3 -3
  231. package/authoring/index.d.ts +1 -0
  232. package/authoring/integration/integration.doc.mjs +22 -5
  233. package/authoring/integration/parse.test.mjs +11 -0
  234. package/authoring/integration/schema.d.mts +14 -3
  235. package/authoring/integration/schema.mjs +33 -2
  236. package/authoring/integration/type.ts +8 -0
  237. package/clients/cli/bin/astryx.mjs +20 -3
  238. package/clients/cli/cli-command.test.mjs +166 -0
  239. package/clients/cli/commands/blog.doc.mjs +1 -0
  240. package/clients/cli/commands/build-theme.batch-report.test.mjs +14 -2
  241. package/clients/cli/commands/build-theme.font-warning.test.mjs +3 -3
  242. package/clients/cli/commands/build-theme.mjs +25 -12
  243. package/clients/cli/commands/build.doc.mjs +1 -0
  244. package/clients/cli/commands/build.exit-codes-doc.test.mjs +4 -0
  245. package/clients/cli/commands/cli-postinstall.test.mjs +49 -1
  246. package/clients/cli/commands/component.doc.mjs +1 -0
  247. package/clients/cli/commands/discover.broken-integration.test.mjs +11 -7
  248. package/clients/cli/commands/discover.doc.mjs +9 -1
  249. package/clients/cli/commands/discover.mjs +144 -93
  250. package/clients/cli/commands/discover.no-source.test.mjs +448 -33
  251. package/clients/cli/commands/discover.sources.test.mjs +20 -1
  252. package/clients/cli/commands/docs.doc.mjs +1 -0
  253. package/clients/cli/commands/docs.mjs +49 -19
  254. package/clients/cli/commands/docs.test.mjs +2 -2
  255. package/clients/cli/commands/doctor-integration-components.doc.mjs +7 -3
  256. package/clients/cli/commands/doctor-integration-docs.doc.mjs +20 -5
  257. package/clients/cli/commands/doctor-integration-templates.doc.mjs +1 -0
  258. package/clients/cli/commands/doctor-integration-validate.doc.mjs +13 -2
  259. package/clients/cli/commands/doctor-integration.test.mjs +52 -4
  260. package/clients/cli/commands/doctor.doc.mjs +12 -2
  261. package/clients/cli/commands/doctor.mjs +102 -7
  262. package/clients/cli/commands/doctor.test.mjs +10 -4
  263. package/clients/cli/commands/gap-report.doc.mjs +15 -2
  264. package/clients/cli/commands/gap-report.mjs +24 -2
  265. package/clients/cli/commands/gap-report.test.mjs +79 -0
  266. package/clients/cli/commands/hook.doc.mjs +1 -0
  267. package/clients/cli/commands/init.behavior.test.mjs +5 -0
  268. package/clients/cli/commands/init.doc.mjs +29 -3
  269. package/clients/cli/commands/integration-add.doc.mjs +7 -0
  270. package/clients/cli/commands/integration-authoring.test.mjs +49 -3
  271. package/clients/cli/commands/integration-check.doc.mjs +10 -2
  272. package/clients/cli/commands/integration-check.test.mjs +175 -1
  273. package/clients/cli/commands/integration-pack.doc.mjs +8 -1
  274. package/clients/cli/commands/integration-verify.doc.mjs +8 -1
  275. package/clients/cli/commands/integration.mjs +15 -9
  276. package/clients/cli/commands/json-contract.test.mjs +11 -2
  277. package/clients/cli/commands/manifest.doc.mjs +1 -0
  278. package/clients/cli/commands/outside-project.test.mjs +774 -0
  279. package/clients/cli/commands/search.doc.mjs +1 -0
  280. package/clients/cli/commands/search.mjs +16 -11
  281. package/clients/cli/commands/search.test.mjs +429 -296
  282. package/clients/cli/commands/setup-nudge.test.mjs +102 -2
  283. package/clients/cli/commands/swizzle.doc.mjs +7 -0
  284. package/clients/cli/commands/swizzle.mjs +5 -3
  285. package/clients/cli/commands/swizzle.routing.test.mjs +19 -2
  286. package/clients/cli/commands/template.doc.mjs +1 -0
  287. package/clients/cli/commands/theme-add-lifecycle.test.mjs +3 -3
  288. package/clients/cli/commands/theme-add.doc.mjs +8 -1
  289. package/clients/cli/commands/theme-build.doc.mjs +1 -0
  290. package/clients/cli/commands/theme-eject.doc.mjs +1 -0
  291. package/clients/cli/commands/theme-list.behavior.test.mjs +6 -0
  292. package/clients/cli/commands/theme-list.doc.mjs +1 -0
  293. package/clients/cli/commands/theme-palette-generate.doc.mjs +1 -0
  294. package/clients/cli/commands/theme-remove.doc.mjs +7 -0
  295. package/clients/cli/commands/theme-targets.doc.mjs +1 -0
  296. package/clients/cli/commands/theme-template.doc.mjs +1 -0
  297. package/clients/cli/commands/theme-use.doc.mjs +7 -0
  298. package/clients/cli/commands/upgrade.config-ordering.test.mjs +2 -1
  299. package/clients/cli/commands/upgrade.doc.mjs +22 -1
  300. package/clients/cli/commands/upgrade.file-protection.test.mjs +3 -2
  301. package/clients/cli/commands/upgrade.hook-output.test.mjs +2 -1
  302. package/clients/cli/commands/upgrade.older-cli.test.mjs +160 -0
  303. package/clients/cli/index.mjs +111 -16
  304. package/clients/cli/lib/cli-error.mjs +4 -1
  305. package/clients/cli/lib/component-format.test.mjs +1 -1
  306. package/clients/cli/lib/define-command.mjs +2 -2
  307. package/clients/cli/lib/json-shim.mjs +3 -1
  308. package/clients/cli/lib/manifest.d.ts +9 -0
  309. package/clients/cli/lib/manifest.mjs +17 -1
  310. package/clients/cli/lib/manifest.test.mjs +25 -0
  311. package/foundation/agent-docs/agent-doc-state.d.mts +35 -6
  312. package/foundation/agent-docs/agent-doc-state.mjs +66 -6
  313. package/foundation/agent-docs/agent-docs.d.mts +4 -3
  314. package/foundation/agent-docs/agent-docs.mjs +11 -9
  315. package/foundation/agent-docs/computer-agent-text.d.mts +133 -0
  316. package/foundation/agent-docs/computer-agent-text.mjs +623 -0
  317. package/foundation/agent-docs/computer-agent-text.test.mjs +602 -0
  318. package/foundation/config/project.d.mts +35 -3
  319. package/foundation/config/project.mjs +145 -8
  320. package/foundation/config/project.test.mjs +66 -0
  321. package/foundation/config/theme-state.mjs +5 -4
  322. package/foundation/discovery/authoring-self-docs.mjs +1 -1
  323. package/foundation/discovery/cli-self-docs.mjs +20 -3
  324. package/foundation/discovery/cli-self-docs.test.mjs +3 -2
  325. package/foundation/discovery/component-replacement.mjs +2 -1
  326. package/foundation/discovery/copied-passages.d.mts +216 -0
  327. package/foundation/discovery/copied-passages.mjs +720 -0
  328. package/foundation/discovery/copied-passages.test.mjs +124 -0
  329. package/foundation/discovery/doc-consistency.d.mts +34 -0
  330. package/foundation/discovery/doc-consistency.mjs +513 -0
  331. package/foundation/discovery/doc-consistency.test.mjs +347 -0
  332. package/foundation/discovery/doc-fluff.d.mts +112 -0
  333. package/foundation/discovery/doc-fluff.mjs +328 -0
  334. package/foundation/discovery/doc-fluff.test.mjs +142 -0
  335. package/foundation/discovery/doc-types.d.mts +1 -1
  336. package/foundation/discovery/doc-types.mjs +15 -8
  337. package/foundation/discovery/doc-types.test.mjs +94 -2
  338. package/foundation/discovery/docs-discovery.d.mts +7 -0
  339. package/foundation/discovery/docs-discovery.mjs +26 -2
  340. package/foundation/discovery/project-typescript.d.mts +11 -2
  341. package/foundation/discovery/project-typescript.mjs +32 -4
  342. package/foundation/discovery/prop-drift.d.mts +26 -7
  343. package/foundation/discovery/prop-drift.mjs +173 -8
  344. package/foundation/discovery/prop-drift.test.mjs +120 -2
  345. package/foundation/discovery/runner-allowlist.d.mts +40 -0
  346. package/foundation/discovery/runner-allowlist.mjs +53 -0
  347. package/foundation/discovery/stray-pointers.d.mts +52 -0
  348. package/foundation/discovery/stray-pointers.mjs +293 -0
  349. package/foundation/discovery/stray-pointers.test.mjs +86 -0
  350. package/foundation/discovery/template-needs.mjs +5 -2
  351. package/foundation/discovery/theme-imports.mjs +8 -4
  352. package/foundation/discovery/theme-imports.test.mjs +1 -1
  353. package/foundation/discovery/type-text.mjs +1 -1
  354. package/foundation/discovery/type-text.test.mjs +1 -1
  355. package/foundation/doc-compiler/doc-loads.test.mjs +42 -2
  356. package/foundation/doc-compiler/inputs.mjs +4 -1
  357. package/foundation/doc-compiler/links.d.mts +32 -0
  358. package/foundation/doc-compiler/links.mjs +163 -26
  359. package/foundation/doc-compiler/links.test.mjs +97 -4
  360. package/foundation/doc-compiler/read.d.mts +1 -1
  361. package/foundation/doc-compiler/tree.mjs +30 -1
  362. package/foundation/doc-compiler/tree.test.mjs +8 -1
  363. package/foundation/env/cli-command.d.mts +83 -0
  364. package/foundation/env/cli-command.mjs +152 -0
  365. package/foundation/env/cli-command.test.mjs +129 -0
  366. package/foundation/env/node-version.test.mjs +15 -3
  367. package/foundation/env/package-manager.d.mts +93 -9
  368. package/foundation/env/package-manager.global.test.mjs +66 -0
  369. package/foundation/env/package-manager.mjs +184 -13
  370. package/foundation/env/package-manager.test.mjs +341 -15
  371. package/foundation/env/project-scope.d.mts +9 -0
  372. package/foundation/env/project-scope.mjs +33 -0
  373. package/foundation/env/search-path.d.mts +42 -0
  374. package/foundation/env/search-path.mjs +205 -0
  375. package/foundation/env/search-path.test.mjs +122 -0
  376. package/foundation/env/user-dirs.d.mts +39 -0
  377. package/foundation/env/user-dirs.mjs +93 -0
  378. package/foundation/env/user-dirs.test.mjs +72 -0
  379. package/foundation/env/user-settings.d.mts +46 -0
  380. package/foundation/env/user-settings.mjs +106 -0
  381. package/foundation/env/user-settings.test.mjs +64 -0
  382. package/foundation/fs/file-protection.mjs +15 -1
  383. package/foundation/fs/module-loader.d.mts +2 -1
  384. package/foundation/fs/module-loader.mjs +60 -4
  385. package/foundation/fs/module-loader.stdout.test.mjs +3 -2
  386. package/foundation/fs/module-loader.test.mjs +194 -0
  387. package/foundation/fs/path-compare.d.mts +33 -0
  388. package/foundation/fs/path-compare.mjs +73 -0
  389. package/foundation/fs/paths.d.mts +57 -1
  390. package/foundation/fs/paths.mjs +131 -3
  391. package/foundation/fs/paths.test.mjs +10 -1
  392. package/foundation/integrations/computer-integrations.d.mts +78 -0
  393. package/foundation/integrations/computer-integrations.mjs +316 -0
  394. package/foundation/integrations/computer-integrations.test.mjs +292 -0
  395. package/foundation/integrations/contribution-inventory.d.mts +11 -0
  396. package/foundation/integrations/contribution-inventory.mjs +50 -2
  397. package/foundation/integrations/integration-warnings.mjs +3 -2
  398. package/foundation/integrations/integration-warnings.test.mjs +8 -9
  399. package/foundation/integrations/integrations.d.mts +25 -1
  400. package/foundation/integrations/integrations.mjs +46 -3
  401. package/foundation/integrations/issue.ts +6 -0
  402. package/foundation/integrations/validate-contributions.d.mts +2 -0
  403. package/foundation/integrations/validate-contributions.mjs +41 -1
  404. package/foundation/release/agent-text-worker.d.mts +4 -0
  405. package/foundation/release/agent-text-worker.mjs +52 -0
  406. package/foundation/release/cache.d.mts +310 -0
  407. package/foundation/release/cache.mjs +1216 -0
  408. package/foundation/release/cache.test.mjs +920 -0
  409. package/foundation/release/choose.d.mts +207 -0
  410. package/foundation/release/choose.mjs +677 -0
  411. package/foundation/release/choose.test.mjs +427 -0
  412. package/foundation/release/core-worker.d.mts +4 -0
  413. package/foundation/release/core-worker.mjs +38 -0
  414. package/foundation/release/global-state.d.mts +259 -0
  415. package/foundation/release/global-state.mjs +472 -0
  416. package/foundation/release/global-state.test.mjs +381 -0
  417. package/foundation/release/handoff.d.mts +60 -0
  418. package/foundation/release/handoff.e2e.test.mjs +278 -0
  419. package/foundation/release/handoff.mjs +357 -0
  420. package/foundation/release/lock-lease.d.mts +23 -0
  421. package/foundation/release/lock-lease.mjs +80 -0
  422. package/foundation/release/notices.d.mts +253 -0
  423. package/foundation/release/notices.mjs +596 -0
  424. package/foundation/release/notices.test.mjs +565 -0
  425. package/foundation/release/npm-guard.d.mts +4 -0
  426. package/foundation/release/npm-guard.mjs +126 -0
  427. package/foundation/release/project.d.mts +141 -0
  428. package/foundation/release/project.mjs +466 -0
  429. package/foundation/release/project.test.mjs +288 -0
  430. package/foundation/release/refresh-worker.d.mts +4 -0
  431. package/foundation/release/refresh-worker.mjs +27 -0
  432. package/foundation/release/run-context.d.mts +47 -0
  433. package/foundation/release/run-context.mjs +149 -0
  434. package/foundation/release/update-notice-env.d.mts +28 -0
  435. package/foundation/release/update-notice-env.mjs +39 -0
  436. package/foundation/response/base.d.ts +26 -3
  437. package/foundation/response/error-codes.d.mts +3 -1
  438. package/foundation/response/error-codes.doc.mjs +12 -0
  439. package/foundation/response/error-codes.mjs +17 -0
  440. package/foundation/response/error-codes.test.mjs +2 -0
  441. package/foundation/response/json-contract.test.mjs +7 -2
  442. package/foundation/response/json.mjs +4 -1
  443. package/foundation/response/response-types.doc.mjs +17 -11
  444. package/foundation/response/response-types.doc.test.mjs +46 -0
  445. package/foundation/response/response.doc.mjs +3 -3
  446. package/foundation/xle/browser-imports.test.mjs +74 -0
  447. package/package.json +11 -11
  448. package/scripts/postinstall.mjs +24 -5
  449. /package/assets/codemods/transforms/{next → v0.7.2}/__tests__/migrate-list-edge-compensation-to-edge.test.mjs +0 -0
  450. /package/assets/codemods/transforms/{next → v0.7.2}/migrate-list-edge-compensation-to-edge.mjs +0 -0
@@ -57,9 +57,10 @@
57
57
  * Between domains, ranking adds one rule ahead of the score: domain priority
58
58
  * (see {@link domainPriority}). A component, hook, template, or theme whose
59
59
  * name or keyword a query word hits ranks ahead of every doc the reader did not
60
- * ask for by name or title, so `font size` finds `Text` before a typography
61
- * topic that declares the phrase. A doc the query names, or whose title the
62
- * query holds, keeps its place.
60
+ * ask for by name, title, or a declared phrase, so `font size` finds `Text`
61
+ * before a token page whose heading holds the phrase. A doc the query names,
62
+ * whose title the query holds, or that declares the whole query as a keyword
63
+ * keeps its place.
63
64
  *
64
65
  * Description and guidance are separate tiers on purpose. A component's own
65
66
  * one-line description saying "notification" is a claim about what it IS; the
@@ -81,10 +82,9 @@
81
82
 
82
83
  import {withListingCache} from '../../foundation/fs/listing-cache.mjs';
83
84
  import {readDocView} from '../../foundation/doc-compiler/read.mjs';
84
- import {
85
- discoverExternalPackages,
86
- findCoreDir,
87
- } from '../../foundation/fs/paths.mjs';
85
+ import {discoverExternalPackages} from '../../foundation/fs/paths.mjs';
86
+ import {requireSearchCoreDir} from './_adapter.mjs';
87
+ import {isOutsideProject} from '../../foundation/env/project-scope.mjs';
88
88
  import {
89
89
  discoverComponents,
90
90
  discoverValidIntegrationComponents,
@@ -125,9 +125,10 @@ import {
125
125
  sectionKey,
126
126
  sectionSummary,
127
127
  } from '../../foundation/discovery/docs-section-key.mjs';
128
- import {AstryxError} from '../error.mjs';
128
+ import {AstryxError, coreDirOrThrow} from '../error.mjs';
129
129
  import {ERROR_CODES} from '../../foundation/response/error-codes.mjs';
130
130
  import {setResultCoverage} from './coverage.mjs';
131
+ import {linkSearchResponse} from '../component/_adapter.mjs';
131
132
 
132
133
  /**
133
134
  * A search candidate gathered from one content domain. Extra underscore-
@@ -138,6 +139,9 @@ import {setResultCoverage} from './coverage.mjs';
138
139
  * @property {string[]} [aliases] - Other names the candidate answers to, scored
139
140
  * with the same name signals: a theme's display name.
140
141
  * @property {string[]} [keywords]
142
+ * @property {string[]} [_declared] - A doc's own declared keywords: what its
143
+ * author lists in `keywords`, without the code terms and table cells its
144
+ * other keywords add.
141
145
  * @property {string[]} [weakKeywords]
142
146
  * @property {string} [description]
143
147
  * @property {string[]} [prose]
@@ -255,9 +259,34 @@ function wordForms(w) {
255
259
  if (w.length > 3 && w.endsWith('s')) forms.add(w.slice(0, -1));
256
260
  if (w.length > 4 && w.endsWith('es')) forms.add(w.slice(0, -2));
257
261
  if (w.length > 4 && w.endsWith('ies')) forms.add(w.slice(0, -3) + 'y');
262
+ // A gerund keeps its verb: "spacing" lost a final e ("space"), and
263
+ // "dragging" doubled a consonant ("drag"). A past participle does not: it
264
+ // usually describes a state ("archived notes"), not the action a catalog
265
+ // word names.
266
+ if (w.length >= 6 && w.endsWith('ing')) {
267
+ const base = w.slice(0, -3);
268
+ forms.add(base);
269
+ forms.add(`${base}e`);
270
+ if (/([b-df-hj-np-tv-z])\1$/.test(base)) forms.add(base.slice(0, -1));
271
+ }
258
272
  return forms;
259
273
  }
260
274
 
275
+ /**
276
+ * The singulars a lowercase word may have when it ends in a plural suffix:
277
+ * "tokens" is "token", "boxes" is "box", "entries" is "entry".
278
+ * @param {string} w
279
+ * @returns {string[]}
280
+ */
281
+ function singulars(w) {
282
+ /** @type {string[]} */
283
+ const out = [];
284
+ if (w.length > 3 && w.endsWith('s')) out.push(w.slice(0, -1));
285
+ if (w.length > 4 && w.endsWith('es')) out.push(w.slice(0, -2));
286
+ if (w.length > 4 && w.endsWith('ies')) out.push(w.slice(0, -3) + 'y');
287
+ return out;
288
+ }
289
+
261
290
  /**
262
291
  * Whether two lowercase words are the same word, up to plural and stem form.
263
292
  * @param {string} a
@@ -298,9 +327,201 @@ function wordsOf(text) {
298
327
  function startsAWordOf(term, text) {
299
328
  if (term.length < 4) return false;
300
329
  if (term.length / String(text).length < 0.5) return false;
301
- return wordsOf(text).some(w => w.startsWith(term) || sameWord(term, w));
330
+ // The term or its singular may start the word: "tokens" starts
331
+ // "tokenizer" through "token". A singular needs five letters, so "stats"
332
+ // does not start "status".
333
+ const starts = [term, ...singulars(term).filter(f => f.length >= 5)];
334
+ return wordsOf(text).some(
335
+ w => starts.some(f => w.startsWith(f)) || sameWord(term, w),
336
+ );
337
+ }
338
+
339
+ /**
340
+ * A query with its camelCase and PascalCase identifiers spelled as words, so
341
+ * `SidebarToggle` reads "sidebar toggle" and `useTablePaginationState` "use
342
+ * table pagination state". Plain words, and identifiers joined with `_` or `-`, are
343
+ * unchanged: a joined token that matches nothing stays unmatched.
344
+ * @param {string} query - The query as typed, before lowercasing.
345
+ * @returns {string} Lowercased.
346
+ */
347
+ export function spellIdentifiers(query) {
348
+ return String(query)
349
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
350
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1 $2')
351
+ .toLowerCase();
352
+ }
353
+
354
+ /**
355
+ * A name or keyword with its separators removed, lowercased: `Side Nav`,
356
+ * `side-nav`, and `SideNav` all join to `sidenav`.
357
+ * @param {string} text
358
+ * @returns {string}
359
+ */
360
+ function joined(text) {
361
+ return String(text)
362
+ .toLowerCase()
363
+ .replace(/[^a-z0-9]+/g, '');
364
+ }
365
+
366
+ /**
367
+ * The strongest hit a run of two or more adjacent query words has when it is
368
+ * read as one joined word, and the query words it covers. Readers type a
369
+ * compound name as separate words (`table header cell` is TableHeaderCell,
370
+ * `number field` the keyword `numberfield`), and no single word of the run
371
+ * matches on its own.
372
+ *
373
+ * - 100: the run joins to a component, hook, or template name, or its plural
374
+ * (`table header cells` is TableHeaderCell), or a hook name without its
375
+ * `use` prefix (`table sticky columns` is useTableStickyColumns).
376
+ * - 90: the run joins to a one-word keyword (`number field` is
377
+ * `numberfield`).
378
+ * - 95: the run joins to most of a name, cut short (`dropdown menu radio` is
379
+ * DropdownMenuRadioItem); 92: it is a name's leading words, covering at least half
380
+ * of it (`top nav mega` is TopNavMegaMenu); 70: it is two or more adjacent
381
+ * words of a name that cover at least half of it (`header cell` in
382
+ * TableHeaderCell), the same coverage a word prefix needs. These three
383
+ * are for components and hooks only.
384
+ *
385
+ * A doc's name is a route or key, so docs are left out.
386
+ * @param {string[]} words - The query's words, stopwords included.
387
+ * @param {Candidate} candidate
388
+ * @returns {{score: number, reason: string, covered: Set<string>, alias?: 'partial' | 'keyword', aliasWords?: number} | null}
389
+ * `covered` holds the run's words and every stretch of them joined. `alias`
390
+ * marks a hit on part of a name or on a keyword, not on the whole name.
391
+ */
392
+ export function compoundHit(words, candidate) {
393
+ if (candidate.domain === 'doc' || words.length < 2) return null;
394
+ const names = [candidate.name, ...(candidate.aliases ?? [])]
395
+ .filter(Boolean)
396
+ .map(name => ({name, nameJoined: joined(name), nameWords: wordsOf(name)}));
397
+ // Part of a name counts only for a component or hook. A template's name
398
+ // starts with the component it shows (`ProgressBarShowcase`), so part of it
399
+ // names the component, not the template.
400
+ const partial =
401
+ candidate.domain === 'component' || candidate.domain === 'hook';
402
+ // One-word lowercase keywords: a keyword an author declares for a reader
403
+ // to type, joined (`numberfield`). A capitalized one names another entry (a
404
+ // block's `componentsUsed`), which matches by its own name, and a keyword of
405
+ // several words is a phrase, matched as one.
406
+ const keywords = (candidate.keywords ?? []).filter(k =>
407
+ /^[a-z0-9]+$/.test(String(k)),
408
+ );
409
+ /** @type {{score: number, reason: string, covered: Set<string>, alias?: 'partial' | 'keyword', aliasWords?: number} | null} */
410
+ let best = null;
411
+ for (const {run, word} of queryRuns(words)) {
412
+ let score = 0;
413
+ let reason = '';
414
+ /** @type {'partial' | 'keyword' | null} */
415
+ let kind = null;
416
+ for (const {name, nameJoined, nameWords} of names) {
417
+ if (
418
+ nameJoined === word ||
419
+ pluralOf(word, nameJoined) ||
420
+ (candidate.domain === 'hook' && nameJoined === `use${word}`)
421
+ ) {
422
+ if (score < 100)
423
+ [score, reason, kind] = [100, `name ${name} spelled`, null];
424
+ } else if (!partial) {
425
+ continue;
426
+ } else if (
427
+ nameJoined.startsWith(word) &&
428
+ word.length / nameJoined.length >= 0.75
429
+ ) {
430
+ // Most of the name, cut short: `dropdown menu radio` is
431
+ // DropdownMenuRadioItem.
432
+ if (score < 95)
433
+ [score, reason, kind] = [95, `name ${name} spelled`, 'partial'];
434
+ } else if (
435
+ word.length / nameJoined.length >= 0.5 &&
436
+ run.every(
437
+ (w, j) => nameWords[j + (nameWords[0] === 'use' ? 1 : 0)] === w,
438
+ )
439
+ ) {
440
+ // The name's leading words: `top nav mega` is TopNavMegaMenu.
441
+ if (score < 92)
442
+ [score, reason, kind] = [
443
+ 92,
444
+ `name ${name} starts "${run.join(' ')}"`,
445
+ 'partial',
446
+ ];
447
+ } else if (
448
+ word.length / nameJoined.length >= 0.5 &&
449
+ nameWords.some((_, k) => run.every((w, j) => nameWords[k + j] === w))
450
+ ) {
451
+ if (score < 70)
452
+ [score, reason, kind] = [
453
+ 70,
454
+ `name ${name} holds "${run.join(' ')}"`,
455
+ 'partial',
456
+ ];
457
+ }
458
+ }
459
+ if (score < 90 && keywords.includes(word)) {
460
+ [score, reason, kind] = [
461
+ 90,
462
+ `keyword "${run.join(' ')}" spelled`,
463
+ 'keyword',
464
+ ];
465
+ }
466
+ if (score > (best?.score ?? 0)) {
467
+ // Each word of the run, and each identifier typed as several of its
468
+ // words joined (`dropdownMenu` in `dropdownMenu checkbox`).
469
+ const covered = new Set();
470
+ for (let a = 0; a < run.length; a++) {
471
+ for (let b = a + 1; b <= run.length; b++) {
472
+ covered.add(run.slice(a, b).join(''));
473
+ }
474
+ }
475
+ best = {
476
+ score,
477
+ reason,
478
+ covered,
479
+ ...(kind ? {alias: kind, aliasWords: run.length} : {}),
480
+ };
481
+ }
482
+ }
483
+ return best;
302
484
  }
303
485
 
486
+ /**
487
+ * The longest run of adjacent query words read as one name. Core's longest
488
+ * names run to six words; a longer run is a sentence, not a name, and reading
489
+ * every run of a long build idea costs time on every candidate.
490
+ */
491
+ const MAX_RUN_WORDS = 6;
492
+
493
+ /**
494
+ * Each run of two to {@link MAX_RUN_WORDS} adjacent query words, longest
495
+ * first, with the run joined into one word. Computed once per query.
496
+ * @type {WeakMap<string[], {run: string[], word: string}[]>}
497
+ */
498
+ const runsOf = new WeakMap();
499
+
500
+ /**
501
+ * @param {string[]} words
502
+ * @returns {{run: string[], word: string}[]}
503
+ */
504
+ function queryRuns(words) {
505
+ let runs = runsOf.get(words);
506
+ if (runs) return runs;
507
+ runs = [];
508
+ for (let len = Math.min(words.length, MAX_RUN_WORDS); len >= 2; len--) {
509
+ for (let i = 0; i + len <= words.length; i++) {
510
+ const run = words.slice(i, i + len);
511
+ runs.push({run, word: run.join('')});
512
+ }
513
+ }
514
+ runsOf.set(words, runs);
515
+ return runs;
516
+ }
517
+
518
+ /**
519
+ * Each candidate's compound hit from its last scoreQuery, so domainPriority
520
+ * reads it without walking the query's runs again.
521
+ * @type {WeakMap<object, ReturnType<typeof compoundHit>>}
522
+ */
523
+ const compoundOf = new WeakMap();
524
+
304
525
  /**
305
526
  * The fewest letters both words need before an edit distance counts as a
306
527
  * typo, by distance. Below them, one edit usually makes a different word.
@@ -513,19 +734,20 @@ const STRONG_TOKEN_SCORE = 70;
513
734
  * typography/font-setup, `side panel` is "Side panels");
514
735
  * - 1: a word of the query is the topic's name (`illustration` in a longer
515
736
  * question is the illustrations guide);
737
+ * - 1: a query of two or more words is one of the topic's own declared
738
+ * keywords, whatever the plural (`code review` on a guide that declares it);
516
739
  * - 1: the query holds one of the doc's titles whole (`resizable side panels`
517
740
  * names "Side panels"; `light dark mode button` names "Light/Dark Mode");
518
741
  * - 1: the doc matches every word of the query and one of them is a word of
519
742
  * its title (`switch to a dark theme` and "Use a theme").
520
743
  * A section answers to its topic's name.
521
744
  *
522
- * A doc that matched only by keyword, by a heading holding the query, or by
745
+ * A doc that matched only by a heading holding the query, by one word, or by
523
746
  * words spread through its text has no priority. Docs are split into many
524
- * small topics, and each declares its own keywords and headings, so a common
525
- * phrase such as `font size` hits a guide's keyword or heading exactly
526
- * (170-190) while the component the reader is after matches one word by
527
- * keyword (`Text`, 98). Ranked on text alone, every split adds another doc
528
- * above the component. Broad reference pages match every word of many queries
747
+ * small topics, each with its own headings, so a common phrase such as
748
+ * `font size` hits a heading exactly (170) while the component the reader is
749
+ * after matches one word by keyword (`Text`, 98). Ranked on text alone, every
750
+ * split adds another doc above the component. Broad reference pages match every word of many queries
529
751
  * in their text the same way. The score stays the text-match strength the
530
752
  * result reports, so callers that gate on it (`build`) see the same numbers.
531
753
  *
@@ -536,7 +758,13 @@ const STRONG_TOKEN_SCORE = 70;
536
758
  * candidate's scoreQuery result.
537
759
  * @returns {0 | 1 | 2}
538
760
  */
539
- export function domainPriority(term, tokens, candidate, hit) {
761
+ export function domainPriority(
762
+ term,
763
+ tokens,
764
+ candidate,
765
+ hit,
766
+ queryWords = phraseWords(term),
767
+ ) {
540
768
  const words = [term, ...tokens];
541
769
  if (candidate.domain === 'doc') {
542
770
  const topic = String(candidate._topic ?? candidate.name).toLowerCase();
@@ -551,6 +779,20 @@ export function domainPriority(term, tokens, candidate, hit) {
551
779
  if (named.some(n => samePhrase(term, n))) {
552
780
  return 2;
553
781
  }
782
+ // A query of two or more words that is one of the doc's own declared
783
+ // keywords, read as words whatever the plural, is the author naming the
784
+ // doc for that phrase: `code review` on a guide that declares it. Only
785
+ // what the author declared counts: the code terms and table cells a
786
+ // doc's other keywords add (`missing_field`) name what it mentions, not
787
+ // what it is for. One word is left out too, and a section declares
788
+ // nothing.
789
+ if (
790
+ phraseWords(term).length >= 2 &&
791
+ candidate._section == null &&
792
+ (candidate._declared ?? []).some(k => samePhrase(term, String(k)))
793
+ ) {
794
+ return 1;
795
+ }
554
796
  if (
555
797
  words.some(
556
798
  w =>
@@ -581,6 +823,12 @@ export function domainPriority(term, tokens, candidate, hit) {
581
823
  return 0;
582
824
  }
583
825
  if (hit.score >= FULL_COVERAGE_SCORE) return 1;
826
+ const compound = compoundOf.has(candidate)
827
+ ? compoundOf.get(candidate)
828
+ : compoundHit(queryWords, candidate);
829
+ if ((compound?.score ?? 0) >= STRONG_TOKEN_SCORE) {
830
+ return 1;
831
+ }
584
832
  const fuzzy = tokens.length <= 1;
585
833
  return words.some(
586
834
  w =>
@@ -696,14 +944,24 @@ export function titleInQuery(term, titles) {
696
944
  * @param {string} term - Lowercased full query.
697
945
  * @param {string[]} tokens - Content tokens from tokenizeQuery(term).
698
946
  * @param {Candidate} candidate
699
- * @returns {{score: number, reason: string, matched: number, total: number} | null}
947
+ * @param {string[]} [words] - The query's words for compound names (see
948
+ * compoundHit); empty when the query names an entry outright.
949
+ * @param {{plain?: boolean}} [opts] - `plain`: score without matches on part
950
+ * of a name or a joined keyword.
951
+ * @returns {{score: number, reason: string, matched: number, total: number, alias?: 'partial' | 'keyword', aliasWords?: number} | null}
700
952
  * `matched`/`total` are the query concepts this candidate answered, out of
701
953
  * the concepts the query had. Callers that must distinguish "matched one word
702
954
  * of three" from "matched all three" — `build`, which gates its pages group
703
955
  * on coverage — cannot recover that from the score, because a single strong
704
956
  * hit and a broad weak one land on the same number.
705
957
  */
706
- export function scoreQuery(term, tokens, candidate) {
958
+ export function scoreQuery(
959
+ term,
960
+ tokens,
961
+ candidate,
962
+ words = phraseWords(term),
963
+ {plain = false} = {},
964
+ ) {
707
965
  const total = Math.max(tokens.length, 1);
708
966
  // A whole-phrase hit answered the whole query by definition.
709
967
  const asFull = (/** @type {{score: number, reason: string}} */ hit) => ({
@@ -712,14 +970,24 @@ export function scoreQuery(term, tokens, candidate) {
712
970
  total,
713
971
  });
714
972
  // Typo tolerance is for one-word lookups. In a multi-word query a near miss
715
- // is usually a different word, not a typo.
716
- const fuzzy = tokens.length <= 1;
717
- const full = scoreCandidate(term, candidate, {fuzzy});
973
+ // is usually a different word, not a typo. One typed identifier is one
974
+ // word, whatever words it spells.
975
+ const fuzzy = tokens.length <= 1 || !/\s/.test(term);
976
+ const full = scoreCandidate(term, candidate, {
977
+ fuzzy,
978
+ joins: !plain && words.length > 0,
979
+ });
718
980
  // A query of several words keeps its phrase tiers below even when stopwords
719
981
  // leave one content word: "make an integration" is still the phrase an
720
982
  // author declares as a keyword, and "build an integration" still names a
721
983
  // title outright, though each tokenizes to `integration` alone.
722
- const phrase = phraseWords(term).length >= 2;
984
+ const phrase = phraseWords(term).length >= 2 || words.length >= 2;
985
+ let compound = compoundHit(words, candidate);
986
+ if (plain) {
987
+ if (compound?.alias) compound = null;
988
+ } else {
989
+ compoundOf.set(candidate, compound);
990
+ }
723
991
 
724
992
  /** 0–1 content tokens: whole-phrase fuzzy matching (typo tolerance for
725
993
  * single words), but if stopwords left exactly one DIFFERENT token (e.g.
@@ -744,7 +1012,11 @@ export function scoreQuery(term, tokens, candidate) {
744
1012
  // of Table-related templates each match "table" and "contents" separately
745
1013
  // and accumulate a higher raw score (#5239).
746
1014
  if (full && full.score >= 90) {
747
- return asFull({score: full.score + 100, reason: full.reason});
1015
+ return asFull({
1016
+ score: full.score + 100,
1017
+ reason: full.reason,
1018
+ ...(full.alias ? {alias: full.alias, aliasWords: words.length} : {}),
1019
+ });
748
1020
  }
749
1021
 
750
1022
  // The whole query standing as a phrase in a doc's title or one of its
@@ -770,8 +1042,25 @@ export function scoreQuery(term, tokens, candidate) {
770
1042
  let tokenSum = 0;
771
1043
  /** @type {string[]} */
772
1044
  const hitTerms = [];
1045
+ // Adjacent words that join into a name or keyword answer each word they
1046
+ // join, as strongly as the joined word matches (see compoundHit).
1047
+ // Whether a counted word matched only through part of a name or a keyword
1048
+ // read joined, not through the entry's own name or a word of it.
1049
+ /** @type {'partial' | 'keyword' | false} */
1050
+ let alias = false;
1051
+ let aliasWords = 0;
773
1052
  for (const tok of tokens) {
774
- const h = bestForToken(tok, candidate, {fuzzy});
1053
+ let h = bestForToken(tok, candidate, {fuzzy});
1054
+ if (
1055
+ compound?.covered.has(joined(tok)) &&
1056
+ compound.score > (h?.score ?? 0)
1057
+ ) {
1058
+ h = compound;
1059
+ if (compound.alias && compound.score >= MIN_TOKEN_SCORE) {
1060
+ alias = alias === 'partial' ? alias : compound.alias;
1061
+ aliasWords = compound.aliasWords ?? 0;
1062
+ }
1063
+ }
775
1064
  if (h && h.score >= MIN_TOKEN_SCORE) {
776
1065
  if (h.score > strongest) strongest = h.score;
777
1066
  matched++;
@@ -821,6 +1110,7 @@ export function scoreQuery(term, tokens, candidate) {
821
1110
  reason,
822
1111
  matched,
823
1112
  total,
1113
+ ...(alias ? {alias, aliasWords} : {}),
824
1114
  };
825
1115
  }
826
1116
 
@@ -846,7 +1136,13 @@ export function scoreQuery(term, tokens, candidate) {
846
1136
  strongest + Math.min(matched - 1, 3) * 12 + coverage * 15,
847
1137
  );
848
1138
  if (full && full.score >= tokenScore) return asFull(full);
849
- return {score: tokenScore, reason, matched, total};
1139
+ return {
1140
+ score: tokenScore,
1141
+ reason,
1142
+ matched,
1143
+ total,
1144
+ ...(alias ? {alias, aliasWords} : {}),
1145
+ };
850
1146
  }
851
1147
 
852
1148
  /**
@@ -865,8 +1161,9 @@ export function scoreQuery(term, tokens, candidate) {
865
1161
  * @param {string} [candidate.description]
866
1162
  * @param {string[]} [candidate.prose] - Extra free-text blobs (doc section text, best practices).
867
1163
  * @param {string[]} [candidate.guidance] - Usage guidance (features, best practices) — scored a tier below description.
868
- * @param {{fuzzy?: boolean}} [opts] - `fuzzy`: allow edit-distance (typo) matches. Default true; multi-word queries pass false.
869
- * @returns {{score: number, reason: string} | null}
1164
+ * @param {{fuzzy?: boolean, joins?: boolean}} [opts] - `fuzzy`: allow edit-distance (typo) matches. Default true; multi-word queries pass false. `joins`: read a spaced query joined into a name or keyword (default true); off when the query names an entry outright.
1165
+ * @returns {{score: number, reason: string, alias?: 'partial' | 'keyword'} | null}
1166
+ * `alias` marks a hit that read the query's words joined.
870
1167
  */
871
1168
  export function scoreCandidate(
872
1169
  term,
@@ -880,18 +1177,23 @@ export function scoreCandidate(
880
1177
  prose = [],
881
1178
  guidance = [],
882
1179
  },
883
- {fuzzy = true} = {},
1180
+ {fuzzy = true, joins = true} = {},
884
1181
  ) {
885
1182
  let best = 0;
886
1183
  let reason = '';
1184
+ /** @type {'partial' | 'keyword' | false} */
1185
+ let alias = false;
887
1186
  /**
888
1187
  * @param {number} score
889
1188
  * @param {string} why
1189
+ * @param {'partial' | 'keyword' | false} [byAlias] - the hit reads the
1190
+ * query's words joined, into part of a name or into a keyword
890
1191
  */
891
- const consider = (score, why) => {
1192
+ const consider = (score, why, byAlias = false) => {
892
1193
  if (score > best) {
893
1194
  best = score;
894
1195
  reason = why;
1196
+ alias = byAlias;
895
1197
  }
896
1198
  };
897
1199
 
@@ -915,6 +1217,16 @@ export function scoreCandidate(
915
1217
  nameLower === term.replace(/\s+/g, '');
916
1218
  if (nameLower === term || leafLower === term || spelled) {
917
1219
  consider(100, 'exact name');
1220
+ } else if (
1221
+ joins &&
1222
+ (domain === 'component' || domain === 'hook') &&
1223
+ /\s/.test(term) &&
1224
+ nameLower.startsWith(term.replace(/\s+/g, '')) &&
1225
+ term.replace(/\s+/g, '').length / nameLower.length >= 0.75
1226
+ ) {
1227
+ // Most of the name typed as words, cut short: `complex select` is
1228
+ // ComplexSelector, ahead of a one-word keyword elsewhere.
1229
+ consider(95, `name "${candidateName}" spelled`, 'partial');
918
1230
  } else if (pluralOf(nameLower, term) || pluralOf(term, nameLower)) {
919
1231
  // One point under the exact spelling, so the doc named `tokens` still
920
1232
  // outranks the Token component for `tokens`.
@@ -938,12 +1250,27 @@ export function scoreCandidate(
938
1250
  }
939
1251
 
940
1252
  // ── Keyword signals ─────────────────────────────────────────────
1253
+ const termJoined = joined(term);
941
1254
  for (const kw of keywords) {
942
1255
  const kwLower = String(kw).toLowerCase();
1256
+ // A one-word keyword typed as separate words is the keyword: `number
1257
+ // field` is `numberfield`. Only a
1258
+ // lowercase keyword, one an author declares for a reader to type: a
1259
+ // capitalized one names another entry (a block's `componentsUsed`). A
1260
+ // doc's keywords include code terms, matched as written.
943
1261
  if (kwLower === term) {
944
1262
  consider(90, `keyword "${kw}"`);
945
1263
  continue;
946
1264
  }
1265
+ if (
1266
+ joins &&
1267
+ domain !== 'doc' &&
1268
+ /\s/.test(term) &&
1269
+ String(kw) === termJoined
1270
+ ) {
1271
+ consider(90, `keyword "${kw}"`, 'keyword');
1272
+ continue;
1273
+ }
947
1274
  if (sameWord(term, kwLower)) {
948
1275
  consider(88, `keyword "${kw}"`);
949
1276
  continue;
@@ -975,9 +1302,12 @@ export function scoreCandidate(
975
1302
  // Match the term's stem as a whole word, tolerating plural/gerund suffixes
976
1303
  // so "chart" matches "charts" and "filter" matches "filtering".
977
1304
  if (term.length >= 3) {
978
- const root = stem(term);
979
- const escaped = root.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
980
- const re = new RegExp(`\\b${escaped}(s|es|ing|ed|ies)?\\b`);
1305
+ // Any form of the term, with any common suffix: `spacing` finds
1306
+ // "space" and "spaces", `tables` finds "table".
1307
+ const roots = [...new Set([stem(term), ...wordForms(term)])]
1308
+ .filter(f => f.length >= 3)
1309
+ .map(f => f.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
1310
+ const re = new RegExp(`\\b(?:${roots.join('|')})(s|es|ing|ed|ies)?\\b`);
981
1311
  if (description && re.test(description.toLowerCase())) {
982
1312
  consider(50, `description mentions "${term}"`);
983
1313
  } else {
@@ -1003,7 +1333,7 @@ export function scoreCandidate(
1003
1333
  }
1004
1334
  }
1005
1335
 
1006
- return best > 0 ? {score: best, reason} : null;
1336
+ return best > 0 ? {score: best, reason, ...(alias ? {alias} : {})} : null;
1007
1337
  }
1008
1338
 
1009
1339
  /**
@@ -1508,6 +1838,10 @@ async function gatherDocs(cwd) {
1508
1838
  ...defined,
1509
1839
  ...codeTerms({content}),
1510
1840
  ],
1841
+ _declared: [
1842
+ ...(Array.isArray(selfDoc?.keywords) ? selfDoc.keywords : []),
1843
+ ...(Array.isArray(node.keywords) ? node.keywords : []),
1844
+ ],
1511
1845
  description: node.summary || '',
1512
1846
  prose: sectionProse({title: node.title, content}),
1513
1847
  titles: [node.title],
@@ -1644,6 +1978,7 @@ function topicCandidates(
1644
1978
  ...(doc?.title || title ? [doc?.title || title] : []),
1645
1979
  ...(Array.isArray(doc?.keywords) ? doc.keywords : []),
1646
1980
  ],
1981
+ _declared: Array.isArray(doc?.keywords) ? doc.keywords : [],
1647
1982
  description: doc?.description || summary,
1648
1983
  prose: split
1649
1984
  ? sections.map(section => section.title).filter(Boolean)
@@ -1792,8 +2127,11 @@ async function gatherThemes(cwd) {
1792
2127
  * @param {string} reason
1793
2128
  * @param {number} matchedTerms - Query concepts this result answered.
1794
2129
  * @param {number} queryTerms - Query concepts there were to answer.
2130
+ * @param {boolean} [outside] - The search runs outside a project, where a
2131
+ * theme cannot be added to an app, so its command forks the source instead
2132
+ * (spec AST-081 FR3).
1795
2133
  */
1796
- function toResult(c, score, reason, matchedTerms, queryTerms) {
2134
+ function toResult(c, score, reason, matchedTerms, queryTerms, outside = false) {
1797
2135
  const base = {
1798
2136
  domain: c.domain,
1799
2137
  name: c._resultName ?? c.name,
@@ -1842,7 +2180,9 @@ function toResult(c, score, reason, matchedTerms, queryTerms) {
1842
2180
  result = {
1843
2181
  ...base,
1844
2182
  displayName: c._displayName,
1845
- command: `astryx theme add ${c.name}`,
2183
+ command: outside
2184
+ ? `astryx theme eject ${c.name}`
2185
+ : `astryx theme add ${c.name}`,
1846
2186
  };
1847
2187
  break;
1848
2188
  default:
@@ -1903,21 +2243,29 @@ async function searchOnce(query, options) {
1903
2243
  }
1904
2244
 
1905
2245
  const term = String(query).trim().toLowerCase();
1906
- const tokens = tokenizeQuery(term);
1907
-
1908
2246
  // `astryx docs` reads docs without @astryxdesign/core, and `astryx theme
1909
2247
  // list` reads themes without it (bundled themes need no project), so a
1910
2248
  // search of either must too. Every other domain reads core: asked for by
1911
2249
  // name, it is an error without core; an open search then covers the docs
1912
2250
  // and themes alone.
1913
2251
  const needsCore = !type || !CORELESS_DOMAINS.includes(type);
1914
- const coreDir = needsCore ? findCoreDir(cwd) : null;
1915
- if (type && needsCore && !coreDir) {
1916
- throw new AstryxError(
1917
- 'Could not find @astryxdesign/core package',
1918
- undefined,
1919
- ERROR_CODES.ERR_CORE_NOT_FOUND,
1920
- );
2252
+ // A domain that reads Core gets it as `component` does: the project's, else
2253
+ // the answering release's own, installed into the per-user cache on first
2254
+ // need (spec AST-081 FR3). Asked for by name, a Core that cannot be had is
2255
+ // the error; an open search answers from the rest and says what it skipped
2256
+ // and why, never silently.
2257
+ /** @type {string | null} */
2258
+ let coreDir = null;
2259
+ /** @type {string | null} */
2260
+ let coreSkipped = null;
2261
+ if (needsCore && type) {
2262
+ coreDir = coreDirOrThrow(requireSearchCoreDir, cwd);
2263
+ } else if (needsCore) {
2264
+ try {
2265
+ coreDir = requireSearchCoreDir(cwd);
2266
+ } catch (error) {
2267
+ coreSkipped = /** @type {Error} */ (error).message;
2268
+ }
1921
2269
  }
1922
2270
 
1923
2271
  // Gather candidates from each requested domain in parallel.
@@ -1936,14 +2284,101 @@ async function searchOnce(query, options) {
1936
2284
 
1937
2285
  const all = [...components, ...hooks, ...docTopics, ...templates, ...themes];
1938
2286
 
2287
+ // A query that is one identifier counts as the words it spells
2288
+ // (`SidebarToggle`), and still matches whole as typed. In a query of several
2289
+ // words, each identifier stays one term (`ChatComposer ChatLayout` names two
2290
+ // components), and its words still join into names (see compoundHit). Both
2291
+ // only help a query that names no entry outright: when the query is an
2292
+ // entry's name, typed whole or as its words, that entry answers it, and
2293
+ // neither reading may add near siblings beside it.
2294
+ const spelled = spellIdentifiers(String(query).trim());
2295
+ const exact = all.some(candidate =>
2296
+ [candidate.name, ...(candidate.aliases ?? [])].some(name => {
2297
+ const lower = String(name).toLowerCase();
2298
+ return (
2299
+ lower === term ||
2300
+ (candidate.domain !== 'doc' &&
2301
+ !/[\s_-]/.test(lower) &&
2302
+ lower === term.replace(/\s+/g, ''))
2303
+ );
2304
+ }),
2305
+ );
2306
+ // One identifier is read as its words only when, read whole, it matches
2307
+ // nothing: a prop or hook name a doc already mentions keeps the
2308
+ // entries that mention it.
2309
+ const split =
2310
+ !exact &&
2311
+ !/\s/.test(term) &&
2312
+ spelled !== term &&
2313
+ !all.some(candidate => scoreCandidate(term, candidate));
2314
+ const tokens = tokenizeQuery(split ? spelled : term);
2315
+ /** @type {string[]} */
2316
+ const words = exact
2317
+ ? []
2318
+ : phraseWords(split || /\s/.test(term) ? spelled : term);
2319
+ // The entries the query names: a component, hook, or template whose whole
2320
+ // name is adjacent query words joined (`icon button in a card header` names
2321
+ // IconButton), or one identifier typed as such. A name inside a longer named
2322
+ // run does not count (`icon` there is part of IconButton).
2323
+ const queryWords = phraseWords(spelled);
2324
+ const typedIdentifiers = new Set(
2325
+ (String(query).match(/[A-Za-z0-9]+/g) ?? [])
2326
+ .filter(w => /[A-Z]/.test(w))
2327
+ .map(w => w.toLowerCase()),
2328
+ );
2329
+ /** @type {Map<string, [number, number]>} each run joined, longest first */
2330
+ const spans = new Map();
2331
+ for (let len = Math.min(queryWords.length, MAX_RUN_WORDS); len >= 1; len--) {
2332
+ for (let a = 0; a + len <= queryWords.length; a++) {
2333
+ // One plain word names too much (`list`, `field`); a named entry is a
2334
+ // run of joined words, or an identifier typed as one.
2335
+ if (len === 1 && !typedIdentifiers.has(queryWords[a])) continue;
2336
+ const run = queryWords.slice(a, a + len).join('');
2337
+ if (!spans.has(run)) spans.set(run, [a, a + len]);
2338
+ }
2339
+ }
2340
+ /**
2341
+ * Where a query word is the candidate's whole name, or null: such an entry
2342
+ * keeps the rank its name earns, whatever else it matched.
2343
+ * @param {Candidate} candidate
2344
+ * @returns {[number, number] | null}
2345
+ */
2346
+ const wordNamed = candidate => {
2347
+ if (candidate.domain === 'doc') return null;
2348
+ for (const name of [candidate.name, ...(candidate.aliases ?? [])]) {
2349
+ const n = joined(name);
2350
+ const at = queryWords.findIndex(w => w === n || w === `${n}s`);
2351
+ if (at >= 0) return [at, at + 1];
2352
+ }
2353
+ return null;
2354
+ };
2355
+ /** @param {Candidate} candidate @returns {[number, number] | null} */
2356
+ const namedSpan = candidate => {
2357
+ if (candidate.domain === 'doc') return null;
2358
+ /** @type {[number, number] | null} */
2359
+ let best = null;
2360
+ for (const name of [candidate.name, ...(candidate.aliases ?? [])]) {
2361
+ const n = joined(name);
2362
+ const span =
2363
+ spans.get(n) ??
2364
+ spans.get(`${n}s`) ??
2365
+ (/(?:s|x|z|ch|sh)$/.test(n) ? spans.get(`${n}es`) : undefined);
2366
+ if (span && (!best || span[1] - span[0] > best[1] - best[0])) {
2367
+ best = span;
2368
+ }
2369
+ }
2370
+ return best;
2371
+ };
2372
+
1939
2373
  // Score every candidate on its own merits. The consumer groups results by
1940
2374
  // role (page / block / component) and takes the top of each, so there's no
1941
2375
  // cross-role competition to engineer — a target page only needs to be the
1942
2376
  // strongest PAGE, not outrank every component.
1943
- /** @type {{result: any, priority: 0 | 1 | 2}[]} */
2377
+ /** @type {{result: any, priority: 0 | 1 | 2, key: number, alias: 'partial' | 'keyword' | false, aliasWords: number, candidate: Candidate, span: [number, number] | null, word: [number, number] | null}[]} */
1944
2378
  const ranked = [];
2379
+ const outside = themes.length > 0 && isOutsideProject(cwd);
1945
2380
  for (const candidate of all) {
1946
- const hit = scoreQuery(term, tokens, candidate);
2381
+ const hit = scoreQuery(term, tokens, candidate, words);
1947
2382
  if (hit)
1948
2383
  ranked.push({
1949
2384
  result: toResult(
@@ -1952,10 +2387,56 @@ async function searchOnce(query, options) {
1952
2387
  hit.reason,
1953
2388
  hit.matched,
1954
2389
  hit.total,
2390
+ outside,
1955
2391
  ),
1956
- priority: domainPriority(term, tokens, candidate, hit),
2392
+ priority: domainPriority(term, tokens, candidate, hit, words),
2393
+ key: hit.score,
2394
+ alias: hit.alias ?? false,
2395
+ aliasWords: hit.aliasWords ?? 0,
2396
+ candidate,
2397
+ span: namedSpan(candidate),
2398
+ word: wordNamed(candidate),
1957
2399
  });
1958
2400
  }
2401
+ // A match on a keyword read joined never outranks an entry the query names,
2402
+ // and neither does a match on part of another name unless it spans more
2403
+ // query words than any named run: `table header with sticky columns` keeps
2404
+ // TableHeader above useTableStickyColumns, which only holds the words
2405
+ // `sticky columns`. Only the order changes; each result keeps the score its
2406
+ // text earned.
2407
+ /** @param {[number, number]} inner @param {[number, number]} outer */
2408
+ const inside = (inner, outer) =>
2409
+ outer[0] <= inner[0] &&
2410
+ outer[1] >= inner[1] &&
2411
+ outer[1] - outer[0] > inner[1] - inner[0];
2412
+ const named = ranked.filter(r => {
2413
+ const span = r.span;
2414
+ return (
2415
+ span != null &&
2416
+ !ranked.some(o => o !== r && o.span != null && inside(span, o.span))
2417
+ );
2418
+ });
2419
+ /** @param {{key: number}[]} rows */
2420
+ const floorOf = rows =>
2421
+ rows.length ? Math.min(...rows.map(r => r.key)) : Infinity;
2422
+ const namedFloor = floorOf(named);
2423
+ const longestNamed = Math.max(
2424
+ 0,
2425
+ ...named.map(r => (r.span ? r.span[1] - r.span[0] : 0)),
2426
+ );
2427
+ for (const r of ranked) {
2428
+ if (named.includes(r)) continue;
2429
+ if (!r.alias || r.word) continue;
2430
+ // A match on part of a longer name that spans more words than any named
2431
+ // run reads more of the query than the named entry does (`dropdown menu
2432
+ // radio` and DropdownMenuRadioItem, against DropdownMenu), so it is not
2433
+ // held back. A joined keyword always is.
2434
+ if (r.alias === 'partial' && r.aliasWords > longestNamed) continue;
2435
+ // It keeps what it earns without those matches.
2436
+ const own =
2437
+ scoreQuery(term, tokens, r.candidate, words, {plain: true})?.score ?? 0;
2438
+ r.key = Math.max(own, Math.min(r.key, namedFloor - 0.5));
2439
+ }
1959
2440
 
1960
2441
  // Docs and everything else are each sorted by score desc, then domain
1961
2442
  // (stable order), then name. The two are merged: at each step the stronger
@@ -1964,11 +2445,11 @@ async function searchOnce(query, options) {
1964
2445
  /** @type {Record<string, number>} */
1965
2446
  const domainOrder = {component: 0, hook: 1, doc: 2, template: 3, theme: 4};
1966
2447
  /**
1967
- * @param {{result: any}} x
1968
- * @param {{result: any}} y
2448
+ * @param {{result: any, key: number}} x
2449
+ * @param {{result: any, key: number}} y
1969
2450
  */
1970
- const byScore = ({result: a}, {result: b}) =>
1971
- b.score - a.score ||
2451
+ const byScore = ({result: a, key: ka}, {result: b, key: kb}) =>
2452
+ kb - ka ||
1972
2453
  (domainOrder[a.domain] ?? 9) - (domainOrder[b.domain] ?? 9) ||
1973
2454
  a.name.localeCompare(b.name);
1974
2455
  const others = ranked.filter(r => r.result.domain !== 'doc').sort(byScore);
@@ -2015,11 +2496,19 @@ async function searchOnce(query, options) {
2015
2496
  ),
2016
2497
  },
2017
2498
  };
2499
+ if (coreSkipped) {
2500
+ response.meta = {
2501
+ partial: {
2502
+ skipped: ['component', 'hook', 'template'],
2503
+ reason: coreSkipped,
2504
+ },
2505
+ };
2506
+ }
2018
2507
  if (wants('component')) {
2019
2508
  searchedComponentsOf.set(
2020
2509
  response,
2021
2510
  components.map(c => ({name: c.name, keywords: c.keywords ?? []})),
2022
2511
  );
2023
2512
  }
2024
- return response;
2513
+ return linkSearchResponse(response, cwd);
2025
2514
  }