@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,115 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file The main searcher: ranks every entry by a weighted sum of plain
5
+ * signals (see _features.mjs): the lexical ranker's score and rank, BM25 over
6
+ * each entry's name, docs text and source-built card, query-word coverage,
7
+ * whether the query holds the entry's name, and the router's guess of the
8
+ * domain the query wants. Its weights are static numbers in
9
+ * _main-searcher.weights.json.
10
+ * @input {query, options} and the gathered catalog with its static data
11
+ * @output the search response, the lexical response's shape and size
12
+ * @position api/search; a child of the root dispatcher in tree.mjs
13
+ */
14
+
15
+ import {routeKit} from '../../foundation/route/route.mjs';
16
+ import {rank, signals} from './_features.mjs';
17
+ import {entryTable} from './_entries.mjs';
18
+ import {orderMatches} from './_order.mjs';
19
+ import {routeProbs} from './_query-words.mjs';
20
+ import {namesWhole, rankLexical} from './_lexical.mjs';
21
+
22
+ /** @typedef {{w: number[], mu: number[], sd: number[]}} SearcherWeights */
23
+
24
+ /**
25
+ * A tuning override for a searcher, or null for its shipped weights.
26
+ * @param {unknown} weights
27
+ * @returns {SearcherWeights | null}
28
+ */
29
+ export const asWeights = weights =>
30
+ /** @type {SearcherWeights | null} */ (weights);
31
+
32
+ /**
33
+ * A name read loosely: lowercase, with dashes, underscores and slashes read as
34
+ * spaces.
35
+ * @param {string} text
36
+ * @returns {string}
37
+ */
38
+ const looseName = text =>
39
+ text
40
+ .toLowerCase()
41
+ .replace(/[-_/]+/g, ' ')
42
+ .replace(/\s+/g, ' ')
43
+ .trim();
44
+
45
+ /**
46
+ * True when the query, read loosely, is the candidate's name, another name of
47
+ * it, its title, or the last part of its route (`use-a-theme` typed as
48
+ * `use a theme`). Such a query names that entry too, though the lexical
49
+ * ranker's own gate (namesWhole) needs the name exactly.
50
+ * @param {import('./_lexical.mjs').Candidate} candidate
51
+ * @param {string} loose the query, read loosely
52
+ * @returns {boolean}
53
+ */
54
+ function namesLoosely(candidate, loose) {
55
+ if (!loose) return false;
56
+ const names = [
57
+ candidate.name,
58
+ ...(candidate.aliases ?? []),
59
+ candidate._title,
60
+ candidate._topic,
61
+ candidate._resultName,
62
+ candidate._commandName,
63
+ ];
64
+ return names.some(name => {
65
+ if (!name) return false;
66
+ const whole = looseName(String(name));
67
+ const last = looseName(String(name).split('/').pop() ?? '');
68
+ return whole === loose || last === loose;
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Ranks with one searcher's weights. Shared by the main searcher and every
74
+ * topic searcher, which differ only in their weights.
75
+ * @param {import('./tree.mjs').SearchRequest} request
76
+ * @param {import('./tree.mjs').SearchCatalog} catalog
77
+ * @param {SearcherWeights} weights
78
+ * @returns {import('./search.type.mjs').SearchResponse}
79
+ */
80
+ export function rankWith(request, catalog, weights) {
81
+ const {query, options} = request;
82
+ // A query that is an entry's whole name gets the lexical answer as is: that
83
+ // entry first, with no near siblings added beside it.
84
+ const term = String(query).trim().toLowerCase();
85
+ const loose = looseName(term);
86
+ if (catalog.all.some(c => namesWhole(c, term) || namesLoosely(c, loose))) {
87
+ return rankLexical(query, options, catalog);
88
+ }
89
+ const table = entryTable(catalog.all, catalog.data.cards);
90
+ const lexical = rankLexical(
91
+ query,
92
+ {...options, limit: Number.MAX_SAFE_INTEGER},
93
+ catalog,
94
+ );
95
+ const route = routeProbs(catalog.data.router, query);
96
+ const rows = signals(table, query, lexical.data.results, route);
97
+ return orderMatches(
98
+ table,
99
+ rank(table, rows, weights),
100
+ lexical,
101
+ query,
102
+ options.limit ?? 20,
103
+ );
104
+ }
105
+
106
+ /** @type {import('./tree.mjs').SearchKit} */
107
+ const {defineSearcher} = routeKit;
108
+
109
+ export const main = defineSearcher({
110
+ id: 'main',
111
+ summary:
112
+ 'Ranks every component, hook, doc, template, and theme by a weighted sum of plain word-match signals.',
113
+ run: (request, catalog, weights) =>
114
+ rankWith(request, catalog, asWeights(weights) ?? catalog.data.main),
115
+ });
@@ -0,0 +1,106 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Tests for the main searcher (_main-searcher.mjs) through search():
5
+ * it ranks by plain signals, answers the same every time, never grows the
6
+ * response past the lexical ranker's, and touches no network. Run against the
7
+ * real @astryxdesign/core.
8
+ */
9
+
10
+ import {describe, it, expect, vi, afterEach} from 'vitest';
11
+ import * as path from 'node:path';
12
+ import http from 'node:http';
13
+ import https from 'node:https';
14
+ import net from 'node:net';
15
+ import {fileURLToPath} from 'node:url';
16
+ import {search, searchCore} from './search.mjs';
17
+
18
+ const REPO = path.resolve(
19
+ path.dirname(fileURLToPath(import.meta.url)),
20
+ '../../../..',
21
+ );
22
+ const QUERIES = [
23
+ 'date chooser',
24
+ 'show a confirmation before deleting',
25
+ 'toast notice for a saved draft',
26
+ 'grid of rows you can sort by column',
27
+ 'how do I install a theme',
28
+ 'page of settings with a side menu',
29
+ 'Bookmarkz',
30
+ 'qzxv',
31
+ ];
32
+ /** @param {unknown} r */
33
+ const top = r =>
34
+ /** @type {any} */ (r).data.results.map(x => `${x.domain}:${x.name}`);
35
+
36
+ afterEach(() => vi.restoreAllMocks());
37
+
38
+ describe('main searcher', () => {
39
+ it('finds a component from what it does', async () => {
40
+ expect(
41
+ top(await search('show a confirmation before deleting', {cwd: REPO}))[0],
42
+ ).toBe('component:AlertDialog');
43
+ expect(top(await search('date chooser', {cwd: REPO}))[0]).toBe(
44
+ 'component:DateInput',
45
+ );
46
+ });
47
+
48
+ it('answers a whole-name query exactly as the lexical ranker does', async () => {
49
+ for (const q of ['Button', 'Dialog', 'useTheme', 'tokens']) {
50
+ expect(top(await search(q, {cwd: REPO}))).toEqual(
51
+ top(await searchCore(q, {cwd: REPO})),
52
+ );
53
+ }
54
+ });
55
+
56
+ it('answers a name typed with spaces for its dashes as the lexical ranker does', async () => {
57
+ for (const q of ['author a theme', 'tokens data']) {
58
+ expect(top(await search(q, {cwd: REPO}))).toEqual(
59
+ top(await searchCore(q, {cwd: REPO})),
60
+ );
61
+ }
62
+ });
63
+
64
+ it('answers the same every time', async () => {
65
+ for (const q of QUERIES.slice(0, 3)) {
66
+ expect(top(await search(q, {cwd: REPO}))).toEqual(
67
+ top(await search(q, {cwd: REPO})),
68
+ );
69
+ }
70
+ });
71
+
72
+ it(
73
+ 'never returns more results or more bytes than the lexical ranker',
74
+ {timeout: 120_000},
75
+ async () => {
76
+ let mine = 0;
77
+ let core = 0;
78
+ for (const q of QUERIES) {
79
+ const [a, b] = [
80
+ await search(q, {cwd: REPO}),
81
+ await searchCore(q, {cwd: REPO}),
82
+ ];
83
+ expect(a.data.results.length).toBeLessThanOrEqual(
84
+ b.data.results.length,
85
+ );
86
+ mine += JSON.stringify(a).length;
87
+ core += JSON.stringify(b).length;
88
+ }
89
+ expect(mine).toBeLessThanOrEqual(core);
90
+ },
91
+ );
92
+
93
+ it('touches no network', async () => {
94
+ const fail = () => {
95
+ throw new Error('search reached the network');
96
+ };
97
+ vi.spyOn(http, 'request').mockImplementation(fail);
98
+ vi.spyOn(https, 'request').mockImplementation(fail);
99
+ vi.spyOn(net, 'connect').mockImplementation(fail);
100
+ vi.spyOn(net.Socket.prototype, 'connect').mockImplementation(fail);
101
+ vi.spyOn(globalThis, 'fetch').mockImplementation(fail);
102
+ expect(
103
+ top(await search('toast notice for a saved draft', {cwd: REPO}))[0],
104
+ ).toBe('component:Toast');
105
+ });
106
+ });
@@ -0,0 +1 @@
1
+ {"w":[0.1414,-0.004648,0.300865,0.056056,0.392736,0.10546,-0.147371,-0.284355,0.209928,0.04428,0.047343,0.064332,0.189238,-0.023495,-0.108461,0.121128,0.0,0.004331,0.0008,0.088292,0.072215,0.1392,0.24194,0.008913,-0.000386,0.006744,-0.003374,-0.001714,-0.001373,0.046662,0.011763,0.051916,0.155163,0.00274,-0.018366,-0.017329,0.166314,0.186888,0.021662,0.066943,0.046315,0.0,0.0,0.0,0.222553,0.09503,0.121672,0.164462,-0.00524,0.050195,0.059338,0.325721,0.112113,0.05238,0.010799,0.002423,0.003703,0.000179,0.00274],"mu":[0.130015,0.004813,0.187932,0.01647,0.147234,0.050213,0.178723,0.617872,0.005957,0.150607,0.600003,0.113058,0.050851,0.000815,0.06067,0.179093,0.074597,0.001104,1.1e-05,0.016463,0.00663,0.038722,0.068087,0.000114,0.001394,0.000461,0.001084,0.001865,8e-06,0.021395,0.005404,0.028145,0.095511,0.000152,0.125196,0.036065,0.147049,0.290649,0.001044,0.087703,0.025355,0.0,0.0,0.0,0.014431,0.003255,0.013062,0.020055,4.8e-05,0.078997,0.004913,0.028451,0.066588,0.000143,0.000763,3e-05,0.000194,0.000102,1.5e-05],"sd":[0.268781,0.036771,0.372756,0.127248,0.35434,0.218385,0.383122,0.485909,0.076955,0.803588,1.602279,0.680163,0.11702,0.02842,0.22979,0.22327,1e-06,0.033186,0.003276,0.107253,0.069348,0.158982,0.208221,0.008485,0.025221,0.012654,0.015171,0.018277,0.002347,0.32287,0.162291,0.335994,0.644886,0.039907,0.762585,0.446869,0.840132,1.16098,0.094205,0.600728,0.32812,1e-06,1e-06,1e-06,0.071304,0.032858,0.067147,0.071859,0.003777,0.214543,0.045987,0.102079,0.082198,0.00474,0.027608,0.005436,0.013914,0.010108,0.003818]}
@@ -0,0 +1,32 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in api/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /** @typedef {import('./_gather-components.mjs').Candidate} Candidate */
5
+ /**
6
+ * A component or hook doc, compiled, or null when it cannot be read.
7
+ * @param {string} docPath
8
+ * @param {string} [exportName]
9
+ * @param {'components' | 'hooks'} [root]
10
+ * @returns {Promise<any>}
11
+ */
12
+ export function loadModuleDoc(docPath: string, exportName?: string, root?: "components" | "hooks"): Promise<any>;
13
+ /**
14
+ * Build component candidates from core's own tree: name + keywords +
15
+ * usage/description from the component's .doc.mjs.
16
+ * @param {string} coreDir
17
+ * @returns {Promise<Candidate[]>}
18
+ */
19
+ /**
20
+ * Usage guidance from a component doc: its feature list and its best-practice
21
+ * advice, flattened to plain strings.
22
+ *
23
+ * This is where a reader's vocabulary usually lives. `Banner` calls itself "a
24
+ * persistent message" and only its guidance names "form errors, system
25
+ * updates, maintenance notices" — so a search for the words people actually
26
+ * type finds nothing without it.
27
+ *
28
+ * @param {any} doc
29
+ * @returns {string[]}
30
+ */
31
+ export function guidanceFrom(doc: any): string[];
32
+ export type Candidate = import("./_gather-components.mjs").Candidate;
@@ -0,0 +1,78 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Reading a component or hook doc for search: the compiled doc and its
5
+ * usage guidance. Imported only by the adapter's gatherers.
6
+ */
7
+
8
+ import {readDocView} from '../../foundation/doc-compiler/read.mjs';
9
+
10
+ /** @typedef {import('./_gather-components.mjs').Candidate} Candidate */
11
+
12
+ /**
13
+ * A component or hook doc, compiled, or null when it cannot be read.
14
+ * @param {string} docPath
15
+ * @param {string} [exportName]
16
+ * @param {'components' | 'hooks'} [root]
17
+ * @returns {Promise<any>}
18
+ */
19
+ export async function loadModuleDoc(
20
+ docPath,
21
+ exportName = 'docs',
22
+ root = 'components',
23
+ ) {
24
+ try {
25
+ // Support both the stamped default export and the legacy named export.
26
+ return (
27
+ (await readDocView(docPath, {
28
+ root,
29
+ loader: 'native',
30
+ exports: ['default', exportName],
31
+ })) ?? null
32
+ );
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Build component candidates from core's own tree: name + keywords +
40
+ * usage/description from the component's .doc.mjs.
41
+ * @param {string} coreDir
42
+ * @returns {Promise<Candidate[]>}
43
+ */
44
+
45
+ /**
46
+ * Usage guidance from a component doc: its feature list and its best-practice
47
+ * advice, flattened to plain strings.
48
+ *
49
+ * This is where a reader's vocabulary usually lives. `Banner` calls itself "a
50
+ * persistent message" and only its guidance names "form errors, system
51
+ * updates, maintenance notices" — so a search for the words people actually
52
+ * type finds nothing without it.
53
+ *
54
+ * @param {any} doc
55
+ * @returns {string[]}
56
+ */
57
+ export function guidanceFrom(doc) {
58
+ if (!doc) return [];
59
+ const features = Array.isArray(doc.features) ? doc.features : [];
60
+ const practices = Array.isArray(doc.usage?.bestPractices)
61
+ ? doc.usage.bestPractices
62
+ : [];
63
+ return [...features, ...practices]
64
+ .map(entry =>
65
+ typeof entry === 'string'
66
+ ? entry
67
+ : [
68
+ entry?.title,
69
+ entry?.text,
70
+ entry?.description,
71
+ entry?.do,
72
+ entry?.dont,
73
+ ]
74
+ .filter(Boolean)
75
+ .join(' '),
76
+ )
77
+ .filter(Boolean);
78
+ }
@@ -0,0 +1,32 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in api/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /**
5
+ * The search response: the lexical matches, each band in the learned order.
6
+ * Nothing is added or dropped, so the count and the scores stay the lexical
7
+ * ranker's; only the order inside a band, and so which matches fill the
8
+ * limit, changes.
9
+ * @param {EntryTable} table
10
+ * @param {number[]} order entry positions, best first, by the searcher's weights
11
+ * @param {import('./search.type.mjs').SearchResponse} lexical every lexical match
12
+ * @param {string} query
13
+ * @param {number} limit
14
+ * @returns {import('./search.type.mjs').SearchResponse}
15
+ */
16
+ export function orderMatches(table: EntryTable, order: number[], lexical: import("./search.type.mjs").SearchResponse, query: string, limit: number): import("./search.type.mjs").SearchResponse;
17
+ /**
18
+ * An integration's matches that the lexical ranker scored the same keep its
19
+ * order among themselves: they have no cards, so the learned order would only
20
+ * guess between them.
21
+ * @param {Item[]} items
22
+ * @returns {Item[]}
23
+ */
24
+ export function integrationTies(items: Item[]): Item[];
25
+ export function bandOf(score: number): number;
26
+ export type Item = {
27
+ res: any;
28
+ i: number;
29
+ band: number;
30
+ learned: number;
31
+ };
32
+ export type EntryTable = import("./_entries.mjs").EntryTable;
@@ -0,0 +1,162 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file How a searcher orders the lexical ranker's matches. Search answers
5
+ * only with what the lexical ranker matched, each with its own score and
6
+ * reason. A match never moves past one of another score band (BANDS below)
7
+ * or another kind, doc or not; between runs the lexical order holds. A band
8
+ * can be wide: 55–99 holds keyword matches beside one-word description hits,
9
+ * and a synonym name match can score under 55, so inside a band a weaker match
10
+ * can lead a stronger one when the weights favor it. Inside such a
11
+ * run the learned order places the matches, with these exceptions: matches
12
+ * that scored the same keep their lexical order, as one block; an entry the
13
+ * query names leads; in a query of one or two words, the matches on every
14
+ * word keep their lexical order; and an integration's matches that scored the
15
+ * same keep theirs.
16
+ */
17
+
18
+ import {entryOf} from './_entries.mjs';
19
+ import {words} from './_query-words.mjs';
20
+ import {searchedComponentsOf} from './_result.mjs';
21
+
22
+ /** @typedef {import('./_entries.mjs').EntryTable} EntryTable */
23
+
24
+ /**
25
+ * Where each lexical band starts, highest first: the whole query as a name,
26
+ * as a keyword, as a phrase in a title, a title in the query, every word
27
+ * matched, a strong partial match, a name or keyword match, and below that,
28
+ * prose alone.
29
+ */
30
+ const BANDS = [195, 188, 170, 160, 151, 100, 55];
31
+ /** The lowest score of the every-word top tier. */
32
+ const FULL_COVERAGE = 151;
33
+
34
+ /** @param {number} score */
35
+ export const bandOf = score => {
36
+ const i = BANDS.findIndex(floor => score >= floor);
37
+ return i === -1 ? BANDS.length : i;
38
+ };
39
+
40
+ /**
41
+ * The search response: the lexical matches, each band in the learned order.
42
+ * Nothing is added or dropped, so the count and the scores stay the lexical
43
+ * ranker's; only the order inside a band, and so which matches fill the
44
+ * limit, changes.
45
+ * @param {EntryTable} table
46
+ * @param {number[]} order entry positions, best first, by the searcher's weights
47
+ * @param {import('./search.type.mjs').SearchResponse} lexical every lexical match
48
+ * @param {string} query
49
+ * @param {number} limit
50
+ * @returns {import('./search.type.mjs').SearchResponse}
51
+ */
52
+ export function orderMatches(table, order, lexical, query, limit) {
53
+ const all = lexical.data.results;
54
+ /** @type {Map<number, number>} */
55
+ const place = new Map(order.map((k, i) => [k, i]));
56
+ // Runs of consecutive lexical matches in one band and of one kind (doc or
57
+ // not) are the only places the learned order acts: a match never crosses
58
+ // into another band, or past a doc or a non-doc the lexical ranker placed.
59
+ let run = -1;
60
+ /** @type {string} */
61
+ let last = '';
62
+ const items = all.map((res, i) => {
63
+ // Each match's own band: the lexical merge can place a weaker match
64
+ // ahead of a stronger doc, and the two must never share a run.
65
+ const key = `${bandOf(res.score ?? 0)}:${res.domain === 'doc'}`;
66
+ if (key !== last) {
67
+ run++;
68
+ last = key;
69
+ }
70
+ const k = entryOf(table, res);
71
+ return {
72
+ res,
73
+ i,
74
+ band: run,
75
+ learned: k === undefined ? Infinity : (place.get(k) ?? Infinity),
76
+ };
77
+ });
78
+ const named = (/** @type {Item} */ it) =>
79
+ (it.res.score ?? 0) >= 95 &&
80
+ /^(exact name|plural of the name|name )/.test(it.res.reason ?? '');
81
+ // Matches the lexical ranker scored the same move as one block, in its
82
+ // order; the learned position of the block's first match places the block
83
+ // in its run (not its best member's, which would let one strong member lift
84
+ // a whole block of weak ones), and an entry the query names leads its run.
85
+ // Moving each match on its own breaks search's existing rules for tied
86
+ // matches (`file required` leads with Field, `ci` with the integration
87
+ // check) and grows answers, so ties move together; where the block lands is
88
+ // measured as spec:AST-083 FR6 requires.
89
+ /** @type {Map<string, number>} */
90
+ const block = new Map();
91
+ const blockOf = (/** @type {Item} */ it) => `${it.band}:${it.res.score}`;
92
+ for (const it of items) {
93
+ const at = named(it) ? -1 : it.learned;
94
+ const k = blockOf(it);
95
+ if (!block.has(k) || at === -1) block.set(k, at);
96
+ }
97
+ let ranked = [...items].sort(
98
+ (x, y) =>
99
+ x.band - y.band ||
100
+ (block.get(blockOf(x)) ?? 0) - (block.get(blockOf(y)) ?? 0) ||
101
+ x.i - y.i,
102
+ );
103
+ if (words(query).length <= 2) {
104
+ ranked = keepLexical(ranked, it => (it.res.score ?? 0) >= FULL_COVERAGE);
105
+ }
106
+ ranked = integrationTies(ranked);
107
+ const response = {
108
+ ...lexical,
109
+ data: {...lexical.data, results: ranked.slice(0, limit).map(it => it.res)},
110
+ };
111
+ const searched = searchedComponentsOf.get(lexical);
112
+ if (searched) searchedComponentsOf.set(response, searched);
113
+ return /** @type {import('./search.type.mjs').SearchResponse} */ (response);
114
+ }
115
+
116
+ /**
117
+ * @typedef {{res: any, i: number, band: number, learned: number}} Item
118
+ */
119
+
120
+ /**
121
+ * The matches `pick` selects keep the places they hold, filled in lexical
122
+ * order.
123
+ * @param {Item[]} items
124
+ * @param {(item: Item) => boolean} pick
125
+ * @returns {Item[]}
126
+ */
127
+ function keepLexical(items, pick) {
128
+ /** @type {number[]} */
129
+ const at = [];
130
+ items.forEach((it, j) => pick(it) && at.push(j));
131
+ if (at.length < 2) return items;
132
+ const sorted = at.map(j => items[j]).sort((a, b) => a.i - b.i);
133
+ const out = [...items];
134
+ at.forEach((j, n) => (out[j] = sorted[n]));
135
+ return out;
136
+ }
137
+
138
+ /**
139
+ * An integration's matches that the lexical ranker scored the same keep its
140
+ * order among themselves: they have no cards, so the learned order would only
141
+ * guess between them.
142
+ * @param {Item[]} items
143
+ * @returns {Item[]}
144
+ */
145
+ export function integrationTies(items) {
146
+ /** @type {Map<number, number[]>} */
147
+ const groups = new Map();
148
+ items.forEach((it, j) => {
149
+ const pkg = it.res.package ?? '';
150
+ if (!pkg || pkg.startsWith('@astryxdesign/')) return;
151
+ const key = it.res.score ?? 0;
152
+ if (!groups.has(key)) groups.set(key, []);
153
+ /** @type {number[]} */ (groups.get(key)).push(j);
154
+ });
155
+ const out = [...items];
156
+ for (const at of groups.values()) {
157
+ if (at.length < 2) continue;
158
+ const sorted = at.map(j => items[j]).sort((a, b) => a.i - b.i);
159
+ at.forEach((j, n) => (out[j] = sorted[n]));
160
+ }
161
+ return out;
162
+ }
@@ -0,0 +1,72 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Invariants of search's ordering, checked against the lexical ranker on
5
+ * the real catalog: search answers only with lexical matches, keeps each one's
6
+ * score and reason, and never moves a match past one of another band or kind.
7
+ */
8
+
9
+ import {describe, it, expect} from 'vitest';
10
+ import * as path from 'node:path';
11
+ import {fileURLToPath} from 'node:url';
12
+ import {search, searchCore} from './search.mjs';
13
+ import {bandOf} from './_order.mjs';
14
+
15
+ const cwd = path.resolve(
16
+ path.dirname(fileURLToPath(import.meta.url)),
17
+ '../../../..',
18
+ );
19
+
20
+ /** @param {any} r */
21
+ const keyOf = r => `${r.domain}:${r.name}:${r.command}`;
22
+ /** @param {any} r */
23
+ const runOf = r => `${bandOf(r.score ?? 0)}:${r.domain === 'doc'}`;
24
+
25
+ const QUERIES = [
26
+ 'account settings',
27
+ 'table of contents',
28
+ 'content',
29
+ 'dark mode',
30
+ 'illustration',
31
+ 'date picker in a card',
32
+ 'how do I add a theme',
33
+ 'button',
34
+ 'side panel that resizes',
35
+ ];
36
+
37
+ describe('search ordering invariants', () => {
38
+ for (const q of QUERIES) {
39
+ it(`keeps the lexical matches, bands and kinds for "${q}"`, async () => {
40
+ const ours = await search(q, {cwd, limit: 50});
41
+ const lexical = await searchCore(q, {
42
+ cwd,
43
+ limit: Number.MAX_SAFE_INTEGER,
44
+ });
45
+ expect(ours.data.matchCount).toBe(lexical.data.matchCount);
46
+ expect(ours.data.results.length).toBeLessThanOrEqual(
47
+ Math.min(50, lexical.data.matchCount),
48
+ );
49
+ /** @type {Map<string, number>} */
50
+ const at = new Map(lexical.data.results.map((r, i) => [keyOf(r), i]));
51
+ const byKey = new Map(lexical.data.results.map(r => [keyOf(r), r]));
52
+ for (const r of ours.data.results) {
53
+ const own = byKey.get(keyOf(r));
54
+ expect(own, `${keyOf(r)} is a lexical match`).toBeDefined();
55
+ expect(r.score).toBe(own?.score);
56
+ expect(r.reason).toBe(own?.reason);
57
+ }
58
+ // Two results of different runs (band or kind) keep the lexical order.
59
+ const rs = ours.data.results;
60
+ for (let i = 0; i < rs.length; i++) {
61
+ for (let j = i + 1; j < rs.length; j++) {
62
+ if (runOf(rs[i]) === runOf(rs[j])) continue;
63
+ expect(
64
+ /** @type {number} */ (at.get(keyOf(rs[i]))) <
65
+ /** @type {number} */ (at.get(keyOf(rs[j]))),
66
+ `${keyOf(rs[i])} above ${keyOf(rs[j])}`,
67
+ ).toBe(true);
68
+ }
69
+ }
70
+ }, 60_000);
71
+ }
72
+ });
@@ -0,0 +1,50 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in api/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /** @typedef {import('./_lexical.mjs').Candidate} Candidate */
5
+ /**
6
+ * A match's domain priority: 0 (none), 1, or 2 (a doc the whole query names).
7
+ * A component, hook, template, or theme with priority ranks ahead of any doc
8
+ * without it, whatever the two scores are. Docs among themselves rank by
9
+ * priority, then score; everything else ranks among itself by score.
10
+ *
11
+ * A component, hook, template, or theme has priority (1) when one of the
12
+ * query's words, or the whole query, hits its name or an authored keyword at
13
+ * {@link STRONG_TOKEN_SCORE} or above, or when it matches every word.
14
+ *
15
+ * A doc has priority when the reader asked for that doc:
16
+ * - 2: the whole query is the topic's name, its last route segment, or its
17
+ * own title, as words and whatever the plural (`font setup` is
18
+ * typography/font-setup, `side panel` is "Side panels");
19
+ * - 1: a word of the query is the topic's name (`illustration` in a longer
20
+ * question is the illustrations guide);
21
+ * - 1: a query of two or more words is one of the topic's own declared
22
+ * keywords, whatever the plural (`code review` on a guide that declares it);
23
+ * - 1: the query holds one of the doc's titles whole (`resizable side panels`
24
+ * names "Side panels"; `light dark mode button` names "Light/Dark Mode");
25
+ * - 1: the doc matches every word of the query and one of them is a word of
26
+ * its title (`switch to a dark theme` and "Use a theme").
27
+ * A section answers to its topic's name.
28
+ *
29
+ * A doc that matched only by a heading holding the query, by one word, or by
30
+ * words spread through its text has no priority. Docs are split into many
31
+ * small topics, each with its own headings, so a common phrase such as
32
+ * `font size` hits a heading exactly (170) while the component the reader is
33
+ * after matches one word by keyword (`Text`, 98). Ranked on text alone, every
34
+ * split adds another doc above the component. Broad reference pages match every word of many queries
35
+ * in their text the same way. The score stays the text-match strength the
36
+ * result reports, so callers that gate on it (`build`) see the same numbers.
37
+ *
38
+ * @param {string} term - Lowercased search term.
39
+ * @param {string[]} tokens - Content tokens from tokenizeQuery(term).
40
+ * @param {Candidate} candidate
41
+ * @param {{score: number, matched: number, total: number}} hit - The
42
+ * candidate's scoreQuery result.
43
+ * @returns {0 | 1 | 2}
44
+ */
45
+ export function domainPriority(term: string, tokens: string[], candidate: Candidate, hit: {
46
+ score: number;
47
+ matched: number;
48
+ total: number;
49
+ }, queryWords?: string[]): 0 | 1 | 2;
50
+ export type Candidate = import("./_lexical.mjs").Candidate;