@astryxdesign/cli 0.7.2 → 0.7.3-canary.02f5fe4

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 (373) hide show
  1. package/README.md +104 -71
  2. package/api/build/_adapter.d.mts +58 -0
  3. package/api/build/_adapter.mjs +127 -3
  4. package/api/build/blocks/blocks.d.mts +92 -0
  5. package/api/build/blocks/blocks.mjs +528 -0
  6. package/api/build/blocks/blocks.test.mjs +238 -0
  7. package/api/build/blocks/blocks.weights.json +1 -0
  8. package/api/build/build.mjs +25 -6
  9. package/api/build/build.type.mjs +1 -1
  10. package/api/build/kit/answer.d.mts +18 -0
  11. package/api/build/kit/answer.mjs +195 -0
  12. package/api/build/kit/answer.test.mjs +108 -0
  13. package/api/build/kit/kind.d.mts +25 -0
  14. package/api/build/kit/kind.mjs +170 -0
  15. package/api/build/kit/kit.d.mts +2 -4
  16. package/api/build/kit/kit.mjs +8 -398
  17. package/api/build/kit/rank.d.mts +3 -32
  18. package/api/build/kit/rank.mjs +25 -342
  19. package/api/build/kit/rank.test.mjs +2 -1
  20. package/api/build/kit/signals.d.mts +79 -0
  21. package/api/build/kit/signals.mjs +295 -0
  22. package/api/build/kit/start.d.mts +32 -0
  23. package/api/build/kit/start.mjs +163 -0
  24. package/api/build/kit/words.d.mts +72 -0
  25. package/api/build/kit/words.mjs +202 -0
  26. package/api/build/tree.d.mts +20 -0
  27. package/api/build/tree.mjs +53 -0
  28. package/api/build/tree.test.mjs +185 -0
  29. package/api/build/tree.weights.json +1 -0
  30. package/api/component/_adapter.mjs +39 -5
  31. package/api/component/component-replacement.test.mjs +1 -0
  32. package/api/component/component.mjs +17 -3
  33. package/api/component/component.test.mjs +15 -0
  34. package/api/component/doc-links.test.mjs +2 -2
  35. package/api/discover/_adapter.d.mts +1 -2
  36. package/api/discover/_adapter.mjs +42 -17
  37. package/api/discover/_adapter.test.mjs +42 -0
  38. package/api/discover/discover.mjs +0 -1
  39. package/api/docs/_adapter.d.mts +2 -2
  40. package/api/docs/_adapter.mjs +82 -23
  41. package/api/docs/docs-homes.test.mjs +127 -0
  42. package/api/docs/docs.doc.mjs +3 -3
  43. package/api/docs/docs.test.mjs +35 -19
  44. package/api/docs/integration-tree.test.mjs +20 -20
  45. package/api/docs/integrationDocs.test.mjs +1 -1
  46. package/api/docs/namespace-section.test.mjs +6 -6
  47. package/api/docs/reference-blocks.test.mjs +2 -2
  48. package/api/doctor/_adapter.d.mts +33 -13
  49. package/api/doctor/_adapter.mjs +55 -25
  50. package/api/doctor/doctor.d.mts +23 -5
  51. package/api/doctor/doctor.mjs +183 -40
  52. package/api/doctor/doctor.test.mjs +95 -12
  53. package/api/hook/hook.mjs +1 -1
  54. package/api/init/init.doc.mjs +20 -6
  55. package/api/init/init.test.mjs +18 -2
  56. package/api/init/init.type.d.mts +3 -3
  57. package/api/init/init.type.mjs +3 -3
  58. package/api/init/remove/remove.mjs +17 -4
  59. package/api/init/remove/remove.test.mjs +114 -2
  60. package/api/init/run/run.mjs +69 -6
  61. package/api/integration/_adapter.d.mts +8 -0
  62. package/api/integration/_adapter.mjs +21 -1
  63. package/api/integration/authoring-checks.mjs +83 -9
  64. package/api/integration/authoring-checks.test.mjs +220 -0
  65. package/api/integration/pack-check.mjs +123 -4
  66. package/api/integration/pack-check.test.mjs +87 -0
  67. package/api/integration/validate-integration-case-disk.test.mjs +111 -0
  68. package/api/integration/validate-integration-fixes.test.mjs +6 -5
  69. package/api/integration/validate-integration-gaps.test.mjs +1975 -0
  70. package/api/integration/validate-integration.d.mts +15 -2
  71. package/api/integration/validate-integration.mjs +298 -39
  72. package/api/integration/validate-integration.type.d.mts +90 -1
  73. package/api/integration/validate-integration.type.mjs +67 -2
  74. package/api/integration/validateIntegration.doc.mjs +1 -1
  75. package/api/search/ORDERING.md +28 -0
  76. package/api/search/_adapter.d.mts +82 -2
  77. package/api/search/_adapter.mjs +157 -10
  78. package/api/search/_adapter.test.mjs +173 -0
  79. package/api/search/_candidate-score.d.mts +40 -0
  80. package/api/search/_candidate-score.mjs +238 -0
  81. package/api/search/_cards.json +6098 -0
  82. package/api/search/_compound.d.mts +50 -0
  83. package/api/search/_compound.mjs +170 -0
  84. package/api/search/_doc-searcher.d.mts +4 -0
  85. package/api/search/_doc-searcher.mjs +23 -0
  86. package/api/search/_doc-searcher.test.mjs +36 -0
  87. package/api/search/_doc-searcher.weights.json +1 -0
  88. package/api/search/_doc-text.d.mts +30 -0
  89. package/api/search/_doc-text.mjs +87 -0
  90. package/api/search/_entries.d.mts +63 -0
  91. package/api/search/_entries.mjs +232 -0
  92. package/api/search/_entries.test.mjs +48 -0
  93. package/api/search/_features.d.mts +35 -0
  94. package/api/search/_features.mjs +155 -0
  95. package/api/search/_gather-components.d.mts +26 -0
  96. package/api/search/_gather-components.mjs +284 -0
  97. package/api/search/_gather-docs.d.mts +20 -0
  98. package/api/search/_gather-docs.mjs +254 -0
  99. package/api/search/_gather-hooks.d.mts +12 -0
  100. package/api/search/_gather-hooks.mjs +52 -0
  101. package/api/search/_gather-templates.d.mts +25 -0
  102. package/api/search/_gather-templates.mjs +107 -0
  103. package/api/search/_lexical.d.mts +135 -0
  104. package/api/search/_lexical.mjs +351 -0
  105. package/api/search/_main-searcher.d.mts +19 -0
  106. package/api/search/_main-searcher.mjs +115 -0
  107. package/api/search/_main-searcher.test.mjs +106 -0
  108. package/api/search/_main-searcher.weights.json +1 -0
  109. package/api/search/_module-doc.d.mts +32 -0
  110. package/api/search/_module-doc.mjs +78 -0
  111. package/api/search/_order.d.mts +32 -0
  112. package/api/search/_order.mjs +162 -0
  113. package/api/search/_order.test.mjs +72 -0
  114. package/api/search/_priority.d.mts +50 -0
  115. package/api/search/_priority.mjs +139 -0
  116. package/api/search/_query-words.d.mts +25 -0
  117. package/api/search/_query-words.mjs +92 -0
  118. package/api/search/_query.d.mts +65 -0
  119. package/api/search/_query.mjs +176 -0
  120. package/api/search/_result.d.mts +97 -0
  121. package/api/search/_result.mjs +150 -0
  122. package/api/search/_score.d.mts +60 -0
  123. package/api/search/_score.mjs +283 -0
  124. package/api/search/_synonyms.d.mts +21 -0
  125. package/api/search/_synonyms.mjs +67 -0
  126. package/api/search/_template-searcher.d.mts +4 -0
  127. package/api/search/_template-searcher.mjs +23 -0
  128. package/api/search/_template-searcher.test.mjs +40 -0
  129. package/api/search/_template-searcher.weights.json +1 -0
  130. package/api/search/_titles.d.mts +33 -0
  131. package/api/search/_titles.mjs +65 -0
  132. package/api/search/_words.d.mts +64 -0
  133. package/api/search/_words.mjs +157 -0
  134. package/api/search/search.d.mts +22 -305
  135. package/api/search/search.doc.mjs +8 -6
  136. package/api/search/search.mjs +56 -2482
  137. package/api/search/search.test.mjs +14 -14
  138. package/api/search/search.type.d.mts +6 -1
  139. package/api/search/search.type.mjs +6 -1
  140. package/api/search/tree.d.mts +15 -0
  141. package/api/search/tree.mjs +40 -0
  142. package/api/search/tree.test.mjs +66 -0
  143. package/api/search/tree.weights.json +1 -0
  144. package/api/template/template.mjs +5 -1
  145. package/api/theme/build/build.packed-old-core.test.mjs +1 -0
  146. package/api/upgrade/_adapter.d.mts +57 -9
  147. package/api/upgrade/_adapter.mjs +198 -28
  148. package/api/upgrade/registry/registry.d.mts +10 -4
  149. package/api/upgrade/registry/registry.mjs +33 -16
  150. package/api/upgrade/registry/registry.test.mjs +85 -0
  151. package/api/upgrade/run/run.d.mts +4 -2
  152. package/api/upgrade/run/run.mjs +13 -2
  153. package/api/upgrade/run/run.test.mjs +417 -3
  154. package/api/upgrade/upgrade.doc.mjs +10 -5
  155. package/api/upgrade/upgrade.mjs +13 -2
  156. package/api/upgrade/upgrade.test.mjs +194 -0
  157. package/api/upgrade/upgrade.type.d.mts +5 -3
  158. package/api/upgrade/upgrade.type.mjs +8 -6
  159. package/assets/codemods/transforms/v0.7.0/__tests__/validate-data-token-ownership.test.mjs +11 -0
  160. package/assets/codemods/transforms/v0.7.0/validate-data-token-ownership.mjs +3 -3
  161. package/assets/docs/author-a-theme.doc.mjs +2 -2
  162. package/assets/docs/is-my-code-good.doc.mjs +287 -0
  163. package/assets/docs/tree/add-a-component.doc.mjs +1 -1
  164. package/assets/docs/tree/add-a-theme.doc.mjs +1 -1
  165. package/assets/docs/tree/add-a-topic.doc.mjs +2 -2
  166. package/assets/docs/tree/agent-guidance.doc.mjs +1 -1
  167. package/assets/docs/tree/api.doc.mjs +6 -0
  168. package/assets/docs/tree/{integrations.doc.mjs → authoring.doc.mjs} +15 -6
  169. package/assets/docs/tree/block-template.doc.mjs +1 -1
  170. package/assets/docs/tree/build-the-template.doc.mjs +1 -1
  171. package/assets/docs/tree/building-blocks.doc.mjs +2 -2
  172. package/assets/docs/tree/check-the-props.doc.mjs +1 -1
  173. package/assets/docs/tree/check-your-docs.doc.mjs +2 -2
  174. package/assets/docs/tree/checks.doc.mjs +3 -3
  175. package/assets/docs/tree/ci-and-containers.doc.mjs +13 -0
  176. package/assets/docs/tree/cli-command.doc.mjs +1 -1
  177. package/assets/docs/tree/cli.doc.mjs +1 -1
  178. package/assets/docs/tree/codemods.doc.mjs +1 -1
  179. package/assets/docs/tree/component-family.doc.mjs +1 -1
  180. package/assets/docs/tree/component-imports.doc.mjs +1 -1
  181. package/assets/docs/tree/components.doc.mjs +1 -1
  182. package/assets/docs/tree/configuration.doc.mjs +1 -1
  183. package/assets/docs/tree/debug-and-gap-reports.doc.mjs +1 -1
  184. package/assets/docs/tree/define-the-theme.doc.mjs +1 -1
  185. package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
  186. package/assets/docs/tree/docs.doc.mjs +1 -1
  187. package/assets/docs/tree/document-the-template.doc.mjs +1 -1
  188. package/assets/docs/tree/document-the-theme.doc.mjs +1 -1
  189. package/assets/docs/tree/export-template-assets.doc.mjs +1 -1
  190. package/assets/docs/tree/extend-or-replace.doc.mjs +1 -1
  191. package/assets/docs/{authoring.doc.mjs → tree/field-reference.doc.mjs} +4 -3
  192. package/assets/docs/tree/fonts-and-assets.doc.mjs +1 -1
  193. package/assets/docs/tree/generate-a-palette.doc.mjs +1 -1
  194. package/assets/docs/tree/global-integrations.doc.mjs +2 -2
  195. package/assets/docs/tree/grade-template-with-agent.doc.mjs +4 -4
  196. package/assets/docs/tree/help.doc.mjs +2 -2
  197. package/assets/docs/tree/host-entry.doc.mjs +128 -0
  198. package/assets/docs/tree/links.doc.mjs +1 -1
  199. package/assets/docs/tree/package-and-test.doc.mjs +1 -1
  200. package/assets/docs/tree/page-template.doc.mjs +1 -1
  201. package/assets/docs/tree/publishing.doc.mjs +1 -1
  202. package/assets/docs/tree/quick-start.doc.mjs +1 -1
  203. package/assets/docs/tree/replace-a-core-component.doc.mjs +1 -1
  204. package/assets/docs/tree/replace-a-core-template.doc.mjs +1 -1
  205. package/assets/docs/tree/sections-and-placement.doc.mjs +1 -1
  206. package/assets/docs/tree/see-it-in-an-app.doc.mjs +1 -1
  207. package/assets/docs/tree/ship.doc.mjs +2 -2
  208. package/assets/docs/tree/short-and-findable.doc.mjs +1 -1
  209. package/assets/docs/tree/single-component.doc.mjs +1 -1
  210. package/assets/docs/tree/start-a-template.doc.mjs +1 -1
  211. package/assets/docs/tree/subcomponent.doc.mjs +1 -1
  212. package/assets/docs/tree/template-assets.doc.mjs +1 -1
  213. package/assets/docs/tree/template-doc-overview.doc.mjs +2 -2
  214. package/assets/docs/tree/template-fonts.doc.mjs +1 -1
  215. package/assets/docs/tree/template-grading-rubric.doc.mjs +17 -38
  216. package/assets/docs/tree/template-icons.doc.mjs +1 -1
  217. package/assets/docs/tree/template-images-media.doc.mjs +1 -1
  218. package/assets/docs/tree/template-styles.doc.mjs +1 -1
  219. package/assets/docs/tree/templates.doc.mjs +1 -1
  220. package/assets/docs/tree/test-in-an-app.doc.mjs +1 -1
  221. package/assets/docs/tree/test-template-in-app.doc.mjs +1 -1
  222. package/assets/docs/tree/themes.doc.mjs +1 -1
  223. package/assets/docs/tree/tokens-data.doc.mjs +2 -2
  224. package/assets/docs/tree/troubleshooting.doc.mjs +1 -1
  225. package/assets/docs/tree/upgrading.doc.mjs +1 -1
  226. package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +1 -1
  227. package/assets/docs/tree/verify-packed-template.doc.mjs +1 -1
  228. package/assets/docs/tree/versioning.doc.mjs +5 -1
  229. package/assets/docs/tree/write-good-templates.doc.mjs +1 -1
  230. package/assets/docs/tree/write-the-template-file.doc.mjs +1 -1
  231. package/assets/docs/use-integrations.doc.mjs +120 -0
  232. package/assets/docs/working-with-ai.doc.mjs +6 -2
  233. package/assets/templates/themes/butter/butterTheme.ts +4 -2
  234. package/assets/templates/themes/stone/stoneTheme.ts +4 -2
  235. package/assets/templates/themes/y2k/y2kTheme.ts +4 -2
  236. package/authoring/debug/parse.d.mts +1 -1
  237. package/authoring/doctypes/schema/schema.doc.mjs +1 -1
  238. package/authoring/integration/integration.doc.mjs +1 -1
  239. package/authoring/integration/type.ts +1 -1
  240. package/clients/cli/bin/astryx.mjs +7 -78
  241. package/clients/cli/command-result-coverage.test.mjs +6 -5
  242. package/clients/cli/commands/build-theme.color-scheme.test.mjs +1 -1
  243. package/clients/cli/commands/{component/index.mjs → component.mjs} +37 -37
  244. package/clients/cli/commands/component.test.mjs +6 -6
  245. package/clients/cli/commands/docs.doc.mjs +2 -2
  246. package/clients/cli/commands/docs.mjs +1 -1
  247. package/clients/cli/commands/docs.test.mjs +14 -14
  248. package/clients/cli/commands/doctor-integration.test.mjs +49 -1
  249. package/clients/cli/commands/doctor.doc.mjs +1 -1
  250. package/clients/cli/commands/doctor.mjs +40 -0
  251. package/clients/cli/commands/doctor.test.mjs +65 -0
  252. package/clients/cli/commands/{hook/index.mjs → hook.mjs} +22 -22
  253. package/clients/cli/commands/import-hint-correctness.test.mjs +1 -1
  254. package/clients/cli/commands/init.behavior.test.mjs +230 -1
  255. package/clients/cli/commands/init.doc.mjs +28 -9
  256. package/clients/cli/commands/init.flag-help.test.mjs +1 -0
  257. package/clients/cli/commands/init.mjs +2 -2
  258. package/clients/cli/commands/integration-check.test.mjs +48 -0
  259. package/clients/cli/commands/integration.doc.mjs +1 -1
  260. package/clients/cli/commands/manifest.doc.mjs +1 -1
  261. package/clients/cli/commands/manifest.mjs +67 -0
  262. package/clients/cli/commands/outside-project.test.mjs +101 -1
  263. package/clients/cli/commands/postinstall.doc.mjs +23 -0
  264. package/clients/cli/commands/postinstall.mjs +31 -0
  265. package/clients/cli/commands/setup-nudge.test.mjs +78 -0
  266. package/clients/cli/commands/{build-theme.mjs → theme.mjs} +1 -1
  267. package/clients/cli/commands/upgrade.doc.mjs +4 -2
  268. package/clients/cli/commands/upgrade.mjs +9 -2
  269. package/clients/cli/commands/upgrade.subfolder-path.test.mjs +97 -0
  270. package/clients/cli/index.mjs +54 -94
  271. package/clients/cli/lib/component-format.mjs +1 -1
  272. package/clients/cli/lib/define-command.mjs +12 -7
  273. package/clients/cli/lib/exit-codes.test.mjs +5 -0
  274. package/clients/cli/lib/manifest.test.mjs +12 -0
  275. package/clients/cli/lib/welcome-box.mjs +50 -0
  276. package/clients/cli/run.mjs +108 -0
  277. package/foundation/agent-docs/agent-doc-state.d.mts +32 -7
  278. package/foundation/agent-docs/agent-doc-state.mjs +58 -9
  279. package/foundation/agent-docs/agent-docs.d.mts +100 -12
  280. package/foundation/agent-docs/agent-docs.mjs +1017 -105
  281. package/foundation/agent-docs/agent-docs.path-safety.test.mjs +1239 -17
  282. package/foundation/agent-docs/agent-docs.test.mjs +683 -39
  283. package/foundation/agent-docs/computer-agent-text.d.mts +39 -2
  284. package/foundation/agent-docs/computer-agent-text.mjs +135 -8
  285. package/foundation/agent-docs/computer-agent-text.test.mjs +99 -2
  286. package/foundation/config/project.mjs +20 -22
  287. package/foundation/config/project.test.mjs +56 -0
  288. package/foundation/discovery/authoring-self-docs.d.mts +7 -0
  289. package/foundation/discovery/authoring-self-docs.mjs +16 -4
  290. package/foundation/discovery/authoring-self-docs.test.mjs +15 -3
  291. package/foundation/discovery/authoring-surface.d.mts +1 -1
  292. package/foundation/discovery/authoring-surface.mjs +2 -2
  293. package/foundation/discovery/authoring-surface.test.mjs +1 -1
  294. package/foundation/discovery/cli-self-docs.d.mts +2 -1
  295. package/foundation/discovery/cli-self-docs.mjs +4 -3
  296. package/foundation/discovery/cli-self-docs.test.mjs +2 -2
  297. package/foundation/discovery/component-discovery.d.mts +37 -0
  298. package/foundation/discovery/component-discovery.mjs +80 -5
  299. package/foundation/discovery/component-loader.d.mts +1 -1
  300. package/foundation/discovery/docs-discovery.mjs +14 -1
  301. package/foundation/discovery/module-exports.d.mts +41 -0
  302. package/foundation/discovery/module-exports.mjs +253 -0
  303. package/foundation/discovery/runner-allowlist.d.mts +2 -4
  304. package/foundation/discovery/runner-allowlist.mjs +2 -4
  305. package/foundation/discovery/runner-commands.d.mts +77 -0
  306. package/foundation/discovery/runner-commands.mjs +294 -0
  307. package/foundation/discovery/runner-commands.test.mjs +128 -0
  308. package/foundation/discovery/stray-pointers.test.mjs +1 -1
  309. package/foundation/discovery/template-adapter.mjs +18 -1
  310. package/foundation/discovery/template-needs.mjs +1 -44
  311. package/foundation/discovery/theme-discovery.mjs +147 -3
  312. package/foundation/doc-compiler/doc-loads.test.mjs +33 -9
  313. package/foundation/doc-compiler/read.mjs +59 -1
  314. package/foundation/doc-compiler/read.test.mjs +11 -2
  315. package/foundation/doc-compiler/split-topic-keywords.test.mjs +3 -2
  316. package/foundation/doc-compiler/tree.d.mts +32 -6
  317. package/foundation/doc-compiler/tree.mjs +77 -43
  318. package/foundation/doc-compiler/tree.test.mjs +7 -7
  319. package/foundation/fs/paths.d.mts +70 -8
  320. package/foundation/fs/paths.mjs +556 -38
  321. package/foundation/fs/paths.test.mjs +430 -54
  322. package/foundation/fs/read-record.d.mts +18 -0
  323. package/foundation/fs/read-record.mjs +131 -0
  324. package/foundation/fs/read-record.test.mjs +77 -0
  325. package/foundation/integrations/autolink.d.mts +2 -1
  326. package/foundation/integrations/autolink.mjs +4 -13
  327. package/foundation/integrations/cli-peer-version.d.mts +36 -0
  328. package/foundation/integrations/cli-peer-version.mjs +132 -0
  329. package/foundation/integrations/cli-peer-version.test.mjs +354 -0
  330. package/foundation/integrations/computer-integrations.d.mts +6 -3
  331. package/foundation/integrations/computer-integrations.mjs +38 -9
  332. package/foundation/integrations/contribution-inventory.d.mts +15 -20
  333. package/foundation/integrations/contribution-inventory.mjs +41 -5
  334. package/foundation/integrations/exports-subpaths.d.mts +57 -0
  335. package/foundation/integrations/exports-subpaths.mjs +113 -0
  336. package/foundation/integrations/integrations.d.mts +27 -1
  337. package/foundation/integrations/integrations.mjs +61 -2
  338. package/foundation/integrations/public-imports.d.mts +118 -0
  339. package/foundation/integrations/public-imports.mjs +1769 -0
  340. package/foundation/integrations/public-imports.test.mjs +1793 -0
  341. package/foundation/integrations/validate-contributions.d.mts +115 -0
  342. package/foundation/integrations/validate-contributions.mjs +1168 -13
  343. package/foundation/release/cache.test.mjs +24 -21
  344. package/foundation/release/global-state.d.mts +8 -4
  345. package/foundation/release/global-state.mjs +4 -2
  346. package/foundation/release/handoff.d.mts +23 -4
  347. package/foundation/release/handoff.mjs +175 -8
  348. package/foundation/release/notices.d.mts +62 -8
  349. package/foundation/release/notices.mjs +136 -25
  350. package/foundation/release/notices.test.mjs +21 -0
  351. package/foundation/release/run-context.mjs +14 -0
  352. package/foundation/response/response-types.doc.mjs +1 -1
  353. package/foundation/route/route.d.mts +285 -0
  354. package/foundation/route/route.mjs +324 -0
  355. package/foundation/route/route.test.mjs +153 -0
  356. package/foundation/runtime/hosted-runtime.test.mjs +403 -0
  357. package/foundation/runtime/locked-versions.d.mts +32 -0
  358. package/foundation/runtime/locked-versions.mjs +65 -0
  359. package/foundation/runtime/package-root.d.mts +91 -0
  360. package/foundation/runtime/package-root.mjs +174 -0
  361. package/foundation/runtime/package-set.d.mts +62 -0
  362. package/foundation/runtime/package-set.mjs +130 -0
  363. package/foundation/runtime/package-set.test.mjs +432 -0
  364. package/foundation/text/string-utils.d.mts +2 -1
  365. package/foundation/text/string-utils.mjs +16 -5
  366. package/foundation/text/suggest-order.d.mts +72 -0
  367. package/foundation/text/suggest-order.mjs +363 -0
  368. package/foundation/text/suggest-order.test.mjs +259 -0
  369. package/host/door-conformance.test.mjs +274 -0
  370. package/host/index.d.ts +116 -0
  371. package/host/index.mjs +295 -0
  372. package/host/index.test.mjs +302 -0
  373. package/package.json +14 -9
@@ -0,0 +1,363 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Order the suggestions a failed lookup prints, best first.
5
+ *
6
+ * A failed `component`, `template` or `docs` lookup already knows which
7
+ * entries to suggest; this module decides their order, so the entry the
8
+ * caller most likely meant comes first. It never makes a list longer.
9
+ *
10
+ * Names are compared as words: `DropdownMenuItem`, `dropdown-menu-item` and
11
+ * `Dropdown Menu Items` all read as `dropdown menu item`.
12
+ *
13
+ * @input the name the caller typed and the candidate entries
14
+ * @output the entries, reordered
15
+ * @position lib: shared by the component, template and docs lookup errors
16
+ */
17
+
18
+ import {levenshteinDistance} from './levenshtein.mjs';
19
+
20
+ /** The score a synonym match gets: an exact keyword's. */
21
+ const SYNONYM_SCORE = 90;
22
+ /** The score a name with exactly the typed words gets: an exact name's. */
23
+ const SAME_WORDS_SCORE = 100;
24
+
25
+ /**
26
+ * A word with one trailing plural `s` removed (`tabs` → `tab`), leaving short
27
+ * words and `ss` endings alone.
28
+ * @param {string} word
29
+ * @returns {string}
30
+ */
31
+ function singular(word) {
32
+ return word.length > 3 && word.endsWith('s') && !word.endsWith('ss')
33
+ ? word.slice(0, -1)
34
+ : word;
35
+ }
36
+
37
+ /**
38
+ * A name's words, lowercase and singular: camelCase, PascalCase, kebab-case,
39
+ * snake_case, slashes and spaces all split.
40
+ * @param {string} text
41
+ * @returns {string[]}
42
+ */
43
+ export function nameWords(text) {
44
+ /** @type {string[]} */
45
+ const words = [];
46
+ for (const part of String(text ?? '').split(/[^A-Za-z0-9]+/)) {
47
+ if (!part) continue;
48
+ const found =
49
+ part.match(/[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|[0-9]+/g) ?? [];
50
+ for (const w of found) words.push(singular(w.toLowerCase()));
51
+ }
52
+ return words;
53
+ }
54
+
55
+ /**
56
+ * How closely one candidate name matches what the caller typed; higher is
57
+ * closer. Tiers, best first: the same words; every typed word present; some
58
+ * typed word present; none. Within a tier, more of the typed words and more of
59
+ * the candidate's own words matched rank higher, and a name that starts with
60
+ * the typed name (or the reverse) gets a small lift.
61
+ * @param {string[]} typed - {@link nameWords} of the typed name
62
+ * @param {string} name
63
+ * @returns {number}
64
+ */
65
+ function affinityOf(typed, name) {
66
+ const words = nameWords(name);
67
+ if (typed.length === 0 || words.length === 0) return 0;
68
+ const typedJoined = typed.join('');
69
+ const joined = words.join('');
70
+ if (joined === typedJoined) return 40;
71
+ const have = new Set(words);
72
+ const want = new Set(typed);
73
+ const covered = typed.filter(w => have.has(w)).length / typed.length;
74
+ const precise = words.filter(w => want.has(w)).length / words.length;
75
+ const prefix =
76
+ joined.startsWith(typedJoined) || typedJoined.startsWith(joined) ? 1 : 0;
77
+ const tier = covered === 1 ? 2 : covered > 0 ? 1 : 0;
78
+ return tier * 10 + covered * 4 + precise * 2 + prefix + precise * 0.01;
79
+ }
80
+
81
+ /**
82
+ * The same items, closest to the typed name first; each item is as close as
83
+ * its closest name. Equal matches keep their original order.
84
+ * @template T
85
+ * @param {string} typedName
86
+ * @param {T[]} items
87
+ * @param {(item: T) => Array<string | undefined>} namesOf
88
+ * @returns {T[]}
89
+ */
90
+ export function orderByAffinity(typedName, items, namesOf) {
91
+ const typed = nameWords(typedName);
92
+ const scored = items.map((item, index) => {
93
+ let best = 0;
94
+ for (const name of namesOf(item)) {
95
+ if (name) best = Math.max(best, affinityOf(typed, name));
96
+ }
97
+ return {item, index, best};
98
+ });
99
+ scored.sort((a, b) => b.best - a.best || a.index - b.index);
100
+ return scored.map(s => s.item);
101
+ }
102
+
103
+ /**
104
+ * Docs route suggestions, closest to the typed route first. When every
105
+ * suggestion sits under one namespace the typed route also starts with, that
106
+ * shared part is left out of the comparison: it says nothing about which page
107
+ * was meant. Equal matches keep their order.
108
+ * @template {{name: string}} T
109
+ * @param {string} topic - the typed route
110
+ * @param {T[]} suggestions
111
+ * @returns {T[]}
112
+ */
113
+ export function orderRouteSuggestions(topic, suggestions) {
114
+ let shared = suggestions.length > 0 ? suggestions[0].name : '';
115
+ for (const {name} of suggestions) {
116
+ while (shared && !name.startsWith(shared)) shared = shared.slice(0, -1);
117
+ }
118
+ const namespace = shared.slice(0, shared.lastIndexOf('/') + 1);
119
+ return namespace && topic.startsWith(namespace)
120
+ ? orderByAffinity(topic.slice(namespace.length), suggestions, s => [
121
+ s.name.slice(namespace.length),
122
+ ])
123
+ : orderByAffinity(topic, suggestions, s => [s.name]);
124
+ }
125
+
126
+ /** Library names a synonym may start with (`MUI TextField`). */
127
+ const LIBRARY_WORDS = new Set([
128
+ 'mui',
129
+ 'material',
130
+ 'chakra',
131
+ 'ant',
132
+ 'antd',
133
+ 'radix',
134
+ 'bootstrap',
135
+ 'mantine',
136
+ 'shadcn',
137
+ 'headless',
138
+ 'aria',
139
+ 'react',
140
+ ]);
141
+
142
+ /**
143
+ * The components whose synonyms name what was typed. A synonym matches when
144
+ * its words, with or without a leading library name, are the typed words
145
+ * (`MUI TextField` matches `MUI TextField`, `TextField` and `text-field`).
146
+ * @param {string} needle
147
+ * @param {Record<string, {synonyms?: string[]}>} cards - component name → its search card
148
+ * @returns {Set<string>}
149
+ */
150
+ export function synonymMatches(needle, cards) {
151
+ const typed = nameWords(needle).join('');
152
+ /** @type {Set<string>} */
153
+ const found = new Set();
154
+ if (!typed) return found;
155
+ for (const [name, card] of Object.entries(cards ?? {})) {
156
+ for (const synonym of card?.synonyms ?? []) {
157
+ const words = nameWords(synonym);
158
+ const whole = words.join('');
159
+ while (words.length > 1 && LIBRARY_WORDS.has(words[0])) words.shift();
160
+ if (whole === typed || words.join('') === typed) {
161
+ found.add(name);
162
+ break;
163
+ }
164
+ }
165
+ }
166
+ return found;
167
+ }
168
+
169
+ /** Words that name a structural part of a component (`<Family>Footer`). */
170
+ const PART_WORDS = new Set([
171
+ 'header',
172
+ 'footer',
173
+ 'body',
174
+ 'content',
175
+ 'title',
176
+ 'heading',
177
+ 'description',
178
+ 'item',
179
+ 'trigger',
180
+ 'action',
181
+ 'close',
182
+ 'group',
183
+ 'cell',
184
+ 'row',
185
+ 'column',
186
+ 'label',
187
+ 'icon',
188
+ 'root',
189
+ 'wrapper',
190
+ ]);
191
+
192
+ /** Part words that name the same slot of a component family. */
193
+ const SAME_PART = /** @type {Record<string, string[]>} */ ({
194
+ item: ['item', 'option'],
195
+ option: ['option', 'item'],
196
+ header: ['header', 'heading', 'title'],
197
+ heading: ['heading', 'header', 'title'],
198
+ title: ['title', 'heading', 'header'],
199
+ });
200
+
201
+ /**
202
+ * A suggestion score for a component whose name shares the typed compound
203
+ * name's words, on the same scale as `searchComponents`, or null. It covers
204
+ * part names that do not exist: the family part named with other words
205
+ * (`PickerItem` → `PickerOption`), the part named without its family's
206
+ * first word (`RailItem` → `SideRailItem`), the part named without its family's
207
+ * middle word (`FactItem` → `FactListItem`), and the parent of a structural
208
+ * part that is not its own component (`SheetFooter` → `Sheet`, but not
209
+ * `SheetMusic`).
210
+ * @param {string[]} typed - {@link nameWords} of the typed name
211
+ * @param {string} name
212
+ * @returns {{score: number, reason: string} | null}
213
+ */
214
+ function compoundMatch(typed, name) {
215
+ if (typed.length < 2) return null;
216
+ const words = nameWords(name);
217
+ const n = words.length;
218
+ const t = typed.length;
219
+ const same = (/** @type {string[]} */ a, /** @type {string[]} */ b) =>
220
+ a.length === b.length && a.every((w, i) => w === b[i]);
221
+ if (
222
+ n >= 2 &&
223
+ same(words.slice(0, -1), typed.slice(0, -1)) &&
224
+ (SAME_PART[typed[t - 1]] ?? []).includes(words[n - 1])
225
+ ) {
226
+ return {score: 79, reason: 'same part of the family'};
227
+ }
228
+ if (n > t && same(words.slice(n - t), typed)) {
229
+ return {score: 78, reason: 'name ends with the words typed'};
230
+ }
231
+ if (n > t && words[0] === typed[0] && words[n - 1] === typed[t - 1]) {
232
+ return {score: 77, reason: 'same part of the family'};
233
+ }
234
+ if (
235
+ n < t &&
236
+ same(typed.slice(0, n), words) &&
237
+ typed.slice(n).every(w => PART_WORDS.has(w))
238
+ ) {
239
+ return {score: 72, reason: 'the family this part belongs to'};
240
+ }
241
+ return null;
242
+ }
243
+
244
+ /**
245
+ * Rank component suggestions for a name that resolved to nothing.
246
+ *
247
+ * Every component `searchComponents` scored keeps its score. A component whose
248
+ * name has exactly the typed words (`chat-messages` for `ChatMessage`) scores
249
+ * like an exact name; one of whose synonyms is the typed name (see
250
+ * {@link synonymMatches}) like an exact keyword; one whose name shares the
251
+ * typed compound name's words (see {@link compoundMatch}) just below a
252
+ * one-letter typo of a name. Equal scores put synonym matches first, then order by how many of the typed words the
253
+ * name carries, then by how early the keyword that matched sits in the
254
+ * component's keywords, then by edit distance, then by shared leading letters,
255
+ * then by name. The caller decides how many to print.
256
+ * @param {string} needle - the typed component name (no `XDS` prefix)
257
+ * @param {Array<{name: string, score: number, reason: string, keywordAt?: number}>} results - `searchComponents` output
258
+ * @param {string[]} allNames - every component name
259
+ * @param {Set<string>} [synonyms] - {@link synonymMatches} for the typed name
260
+ * @returns {Array<{name: string, score: number, reason: string}>}
261
+ */
262
+ export function rankComponentSuggestions(
263
+ needle,
264
+ results,
265
+ allNames,
266
+ synonyms = new Set(),
267
+ ) {
268
+ const term = needle.toLowerCase();
269
+ const typed = nameWords(needle);
270
+ /** @type {Map<string, {name: string, score: number, reason: string, keywordAt: number}>} */
271
+ const byName = new Map();
272
+ for (const r of results) {
273
+ byName.set(r.name, {
274
+ name: r.name,
275
+ score: r.score,
276
+ reason: r.reason,
277
+ keywordAt: r.keywordAt ?? Infinity,
278
+ });
279
+ }
280
+ // The typed words are a name's words (`chat-messages` is `ChatMessage`):
281
+ // that name is meant, ahead of every rule below.
282
+ const joined = typed.join('');
283
+ for (const name of allNames) {
284
+ if (joined && nameWords(name).join('') === joined) {
285
+ byName.set(name, {
286
+ name,
287
+ score: SAME_WORDS_SCORE,
288
+ reason: 'same name',
289
+ keywordAt: -1,
290
+ });
291
+ }
292
+ }
293
+ for (const name of synonyms) {
294
+ if (!allNames.includes(name)) continue;
295
+ const prior = byName.get(name);
296
+ if (!prior || prior.score < SYNONYM_SCORE) {
297
+ byName.set(name, {
298
+ name,
299
+ score: SYNONYM_SCORE,
300
+ reason: 'another name for it',
301
+ keywordAt: Infinity,
302
+ });
303
+ }
304
+ }
305
+ for (const name of allNames) {
306
+ const match = compoundMatch(typed, name);
307
+ if (!match) continue;
308
+ const prior = byName.get(name);
309
+ if (!prior || match.score > prior.score) {
310
+ byName.set(name, {
311
+ name,
312
+ score: match.score,
313
+ reason: match.reason,
314
+ keywordAt: Infinity,
315
+ });
316
+ }
317
+ }
318
+ /** @param {string} name */
319
+ const lead = name => {
320
+ const lower = name.toLowerCase();
321
+ let i = 0;
322
+ while (i < term.length && i < lower.length && term[i] === lower[i]) i++;
323
+ return i / Math.max(term.length, lower.length);
324
+ };
325
+ /**
326
+ * How many typed words the name carries, whole or as a word's start; the
327
+ * last typed word, the one a compound name is a kind of (`PlaceInput` is an
328
+ * input), counts a little more.
329
+ */
330
+ const shared = (/** @type {string} */ name) => {
331
+ const words = nameWords(name);
332
+ const has = (/** @type {string} */ t) =>
333
+ words.some(
334
+ w =>
335
+ w === t ||
336
+ (Math.min(w.length, t.length) >= 3 &&
337
+ (w.startsWith(t) || t.startsWith(w))),
338
+ );
339
+ const count = typed.filter(has).length;
340
+ return typed.length > 1 && has(typed[typed.length - 1])
341
+ ? count + 0.5
342
+ : count;
343
+ };
344
+ return [...byName.values()]
345
+ .map(r => ({
346
+ ...r,
347
+ synonym: synonyms.has(r.name) ? 1 : 0,
348
+ shared: shared(r.name),
349
+ distance: levenshteinDistance(term, r.name.toLowerCase()),
350
+ lead: lead(r.name),
351
+ }))
352
+ .sort(
353
+ (a, b) =>
354
+ b.score - a.score ||
355
+ b.synonym - a.synonym ||
356
+ b.shared - a.shared ||
357
+ a.keywordAt - b.keywordAt ||
358
+ a.distance - b.distance ||
359
+ b.lead - a.lead ||
360
+ a.name.localeCompare(b.name),
361
+ )
362
+ .map(({name, score, reason}) => ({name, score, reason}));
363
+ }
@@ -0,0 +1,259 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {describe, expect, it} from 'vitest';
4
+ import {
5
+ nameWords,
6
+ orderByAffinity,
7
+ orderRouteSuggestions,
8
+ rankComponentSuggestions,
9
+ synonymMatches,
10
+ } from './suggest-order.mjs';
11
+
12
+ describe('nameWords', () => {
13
+ it('reads every spelling of a name as the same words', () => {
14
+ expect(nameWords('DropdownMenuItems')).toEqual([
15
+ 'dropdown',
16
+ 'menu',
17
+ 'item',
18
+ ]);
19
+ expect(nameWords('dropdown-menu-item')).toEqual([
20
+ 'dropdown',
21
+ 'menu',
22
+ 'item',
23
+ ]);
24
+ expect(nameWords('Dropdown Menu Item')).toEqual([
25
+ 'dropdown',
26
+ 'menu',
27
+ 'item',
28
+ ]);
29
+ expect(nameWords('cli/api_HTMLParser')).toEqual([
30
+ 'cli',
31
+ 'api',
32
+ 'html',
33
+ 'parser',
34
+ ]);
35
+ });
36
+
37
+ it('keeps short words and double-s endings', () => {
38
+ expect(nameWords('Gas')).toEqual(['gas']);
39
+ expect(nameWords('Glass')).toEqual(['glass']);
40
+ });
41
+ });
42
+
43
+ describe('orderByAffinity', () => {
44
+ const names = (/** @type {string[]} */ list, /** @type {string} */ typed) =>
45
+ orderByAffinity(typed, list, n => [n]);
46
+
47
+ it('puts the same words first, then every typed word, then some', () => {
48
+ expect(
49
+ names(
50
+ ['zeta-panel', 'wide-panel-grid', 'panel-grid', 'grid-panel-x'],
51
+ 'PanelGrid',
52
+ ),
53
+ ).toEqual(['panel-grid', 'wide-panel-grid', 'grid-panel-x', 'zeta-panel']);
54
+ });
55
+
56
+ it('matches a title as well as an id', () => {
57
+ const items = [
58
+ {id: 'alpha', title: 'Something Else'},
59
+ {id: 'beta', title: 'Wave Editor'},
60
+ ];
61
+ expect(
62
+ orderByAffinity('Wave Editor', items, i => [i.id, i.title]).map(
63
+ i => i.id,
64
+ ),
65
+ ).toEqual(['beta', 'alpha']);
66
+ });
67
+
68
+ it('keeps the original order between equal matches and never changes the list', () => {
69
+ const list = ['c', 'b', 'a', 'route/b-thing', 'route/a-thing'];
70
+ const out = names(list, 'thing');
71
+ expect(out).toEqual(['route/b-thing', 'route/a-thing', 'c', 'b', 'a']);
72
+ expect([...out].sort()).toEqual([...list].sort());
73
+ });
74
+
75
+ it('ranks a route by its words', () => {
76
+ expect(
77
+ names(
78
+ ['basics/basics-color', 'basics/basics-size', 'basics/basics-usage'],
79
+ 'basics/usage',
80
+ ),
81
+ ).toEqual([
82
+ 'basics/basics-usage',
83
+ 'basics/basics-color',
84
+ 'basics/basics-size',
85
+ ]);
86
+ });
87
+ });
88
+
89
+ describe('rankComponentSuggestions', () => {
90
+ const all = [
91
+ 'Zebra',
92
+ 'Picker',
93
+ 'MultiPicker',
94
+ 'PickerOption',
95
+ 'Gadget',
96
+ 'SideRailItem',
97
+ 'Sheet',
98
+ ];
99
+
100
+ it('breaks a tie by where the typed name sits in the keywords', () => {
101
+ const results = [
102
+ {name: 'MultiPicker', score: 90, reason: 'keyword', keywordAt: 3},
103
+ {name: 'Picker', score: 90, reason: 'keyword', keywordAt: 0},
104
+ {name: 'Gadget', score: 90, reason: 'keyword', keywordAt: 5},
105
+ ];
106
+ expect(
107
+ rankComponentSuggestions('Pick', results, all).map(r => r.name),
108
+ ).toEqual(['Picker', 'MultiPicker', 'Gadget']);
109
+ });
110
+
111
+ it('breaks a remaining tie by edit distance, not by the alphabet', () => {
112
+ const results = [
113
+ {name: 'Gadget', score: 70, reason: 'keyword'},
114
+ {name: 'Sheet', score: 70, reason: 'keyword'},
115
+ ];
116
+ expect(
117
+ rankComponentSuggestions('Sheets', results, all).map(r => r.name),
118
+ ).toEqual(['Sheet', 'Gadget']);
119
+ });
120
+
121
+ it('finds the family part named with other words', () => {
122
+ const results = [{name: 'Picker', score: 70, reason: 'keyword'}];
123
+ expect(rankComponentSuggestions('PickerItem', results, all)[0].name).toBe(
124
+ 'PickerOption',
125
+ );
126
+ });
127
+
128
+ it('finds the part named without its first word', () => {
129
+ expect(rankComponentSuggestions('RailItem', [], all)[0].name).toBe(
130
+ 'SideRailItem',
131
+ );
132
+ });
133
+
134
+ it('finds the family of a part that is not its own component', () => {
135
+ expect(rankComponentSuggestions('SheetFooter', [], all)[0].name).toBe(
136
+ 'Sheet',
137
+ );
138
+ });
139
+
140
+ it('puts a name with exactly the typed words first, ahead of every rule', () => {
141
+ const results = [{name: 'Picker', score: 80, reason: 'similar name'}];
142
+ const ranked = rankComponentSuggestions(
143
+ 'pickers',
144
+ results,
145
+ all,
146
+ new Set(['Gadget']),
147
+ );
148
+ expect(ranked[0].name).toBe('Picker');
149
+ expect(rankComponentSuggestions('side-rail-item', [], all)[0].name).toBe(
150
+ 'SideRailItem',
151
+ );
152
+ });
153
+
154
+ it('names a family only for a structural part', () => {
155
+ expect(
156
+ rankComponentSuggestions('SheetMusic', [], all).map(r => r.name),
157
+ ).not.toContain('Sheet');
158
+ });
159
+
160
+ it('does not let a lifted entry borrow a weaker keyword position', () => {
161
+ const results = [
162
+ {name: 'Picker', score: 90, reason: 'keyword', keywordAt: 2},
163
+ {name: 'MultiPicker', score: 70, reason: 'keyword', keywordAt: 0},
164
+ ];
165
+ const ranked = rankComponentSuggestions(
166
+ 'Pick',
167
+ results,
168
+ all,
169
+ new Set(['Picker', 'MultiPicker']),
170
+ );
171
+ expect(ranked.map(r => r.name).slice(0, 2)).toEqual([
172
+ 'Picker',
173
+ 'MultiPicker',
174
+ ]);
175
+ });
176
+
177
+ it('never lowers a score search gave', () => {
178
+ const results = [{name: 'Picker', score: 100, reason: 'exact name'}];
179
+ const ranked = rankComponentSuggestions('PickerItem', results, all);
180
+ expect(ranked[0]).toEqual({
181
+ name: 'Picker',
182
+ score: 100,
183
+ reason: 'exact name',
184
+ });
185
+ });
186
+
187
+ it('suggests only catalog names', () => {
188
+ const ranked = rankComponentSuggestions('PickerWidgetItem', [], all);
189
+ for (const r of ranked) expect(all).toContain(r.name);
190
+ });
191
+ });
192
+
193
+ describe('synonymMatches', () => {
194
+ const cards = {
195
+ Picker: {synonyms: ['choice box', 'Acme ChoiceBox', 'MUI Choice Field']},
196
+ Sheet: {synonyms: ['side sheet', 'choice']},
197
+ };
198
+
199
+ it('matches a synonym in any spelling, without a leading library name', () => {
200
+ expect([...synonymMatches('ChoiceField', cards)]).toEqual(['Picker']);
201
+ expect([...synonymMatches('choice-box', cards)]).toEqual(['Picker']);
202
+ expect([...synonymMatches('SideSheets', cards)]).toEqual(['Sheet']);
203
+ expect([...synonymMatches('MUI Choice Field', cards)]).toEqual(['Picker']);
204
+ });
205
+
206
+ it('needs every word to match', () => {
207
+ expect([...synonymMatches('Side', cards)]).toEqual([]);
208
+ expect([...synonymMatches('', cards)]).toEqual([]);
209
+ });
210
+
211
+ it('puts a synonym match first among equal scores, and only real names', () => {
212
+ const all = ['Gadget', 'Picker'];
213
+ const results = [
214
+ {name: 'Gadget', score: 90, reason: 'keyword', keywordAt: 0},
215
+ ];
216
+ const ranked = rankComponentSuggestions(
217
+ 'ChoiceField',
218
+ results,
219
+ all,
220
+ new Set(['Picker', 'NotInCatalog']),
221
+ );
222
+ expect(ranked.map(r => r.name)).toEqual(['Picker', 'Gadget']);
223
+ });
224
+ });
225
+
226
+ describe('orderRouteSuggestions', () => {
227
+ const kids = [
228
+ 'guide/start',
229
+ 'guide/building-blocks',
230
+ 'guide/ship',
231
+ 'guide/help',
232
+ ].map(name => ({name}));
233
+ const names = (/** @type {{name: string}[]} */ list) => list.map(s => s.name);
234
+
235
+ it('keeps tree order when no child matches, at any depth', () => {
236
+ expect(names(orderRouteSuggestions('guide/theme', kids))).toEqual(
237
+ names(kids),
238
+ );
239
+ expect(names(orderRouteSuggestions('guide/theme/colors', kids))).toEqual(
240
+ names(kids),
241
+ );
242
+ });
243
+
244
+ it('puts the child the route names first', () => {
245
+ expect(names(orderRouteSuggestions('guide/ship-it', kids))[0]).toBe(
246
+ 'guide/ship',
247
+ );
248
+ expect(names(orderRouteSuggestions('guide/help/extra', kids))[0]).toBe(
249
+ 'guide/help',
250
+ );
251
+ });
252
+
253
+ it('compares whole routes when the suggestions share no namespace', () => {
254
+ const roots = [{name: 'cli'}, {name: 'guide'}, {name: 'theme'}];
255
+ expect(names(orderRouteSuggestions('theme/quick-start', roots))[0]).toBe(
256
+ 'theme',
257
+ );
258
+ });
259
+ });