@astryxdesign/cli 0.7.2 → 0.7.3-canary.06872b4

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 (212) hide show
  1. package/README.md +99 -66
  2. package/api/build/_adapter.d.mts +58 -0
  3. package/api/build/_adapter.mjs +115 -3
  4. package/api/build/blocks/blocks.d.mts +42 -0
  5. package/api/build/blocks/blocks.mjs +169 -0
  6. package/api/build/blocks/blocks.test.mjs +108 -0
  7. package/api/build/blocks/blocks.weights.json +1 -0
  8. package/api/build/build.mjs +25 -6
  9. package/api/build/kit/answer.d.mts +14 -0
  10. package/api/build/kit/answer.mjs +85 -0
  11. package/api/build/kit/answer.test.mjs +38 -0
  12. package/api/build/kit/kind.d.mts +25 -0
  13. package/api/build/kit/kind.mjs +170 -0
  14. package/api/build/kit/kit.d.mts +2 -4
  15. package/api/build/kit/kit.mjs +8 -398
  16. package/api/build/kit/rank.d.mts +3 -32
  17. package/api/build/kit/rank.mjs +25 -342
  18. package/api/build/kit/rank.test.mjs +2 -1
  19. package/api/build/kit/signals.d.mts +60 -0
  20. package/api/build/kit/signals.mjs +284 -0
  21. package/api/build/kit/start.d.mts +32 -0
  22. package/api/build/kit/start.mjs +163 -0
  23. package/api/build/kit/words.d.mts +72 -0
  24. package/api/build/kit/words.mjs +202 -0
  25. package/api/build/tree.d.mts +20 -0
  26. package/api/build/tree.mjs +52 -0
  27. package/api/build/tree.test.mjs +177 -0
  28. package/api/build/tree.weights.json +1 -0
  29. package/api/component/component.mjs +16 -2
  30. package/api/component/component.test.mjs +15 -0
  31. package/api/discover/_adapter.mjs +36 -9
  32. package/api/discover/_adapter.test.mjs +42 -0
  33. package/api/docs/_adapter.mjs +17 -5
  34. package/api/docs/docs.test.mjs +7 -1
  35. package/api/doctor/doctor.mjs +86 -12
  36. package/api/doctor/doctor.test.mjs +28 -3
  37. package/api/init/init.doc.mjs +20 -6
  38. package/api/init/init.test.mjs +18 -2
  39. package/api/init/init.type.d.mts +3 -3
  40. package/api/init/init.type.mjs +3 -3
  41. package/api/init/remove/remove.mjs +17 -4
  42. package/api/init/remove/remove.test.mjs +114 -2
  43. package/api/init/run/run.mjs +69 -6
  44. package/api/integration/authoring-checks.mjs +83 -9
  45. package/api/integration/pack-check.mjs +123 -4
  46. package/api/integration/pack-check.test.mjs +87 -0
  47. package/api/integration/validate-integration-case-disk.test.mjs +111 -0
  48. package/api/integration/validate-integration-gaps.test.mjs +1964 -0
  49. package/api/integration/validate-integration.d.mts +11 -2
  50. package/api/integration/validate-integration.mjs +226 -32
  51. package/api/integration/validate-integration.type.d.mts +58 -1
  52. package/api/integration/validate-integration.type.mjs +45 -2
  53. package/api/integration/validateIntegration.doc.mjs +1 -1
  54. package/api/search/ORDERING.md +28 -0
  55. package/api/search/_adapter.d.mts +66 -2
  56. package/api/search/_adapter.mjs +118 -10
  57. package/api/search/_candidate-score.d.mts +40 -0
  58. package/api/search/_candidate-score.mjs +238 -0
  59. package/api/search/_cards.json +6098 -0
  60. package/api/search/_compound.d.mts +50 -0
  61. package/api/search/_compound.mjs +170 -0
  62. package/api/search/_doc-searcher.d.mts +4 -0
  63. package/api/search/_doc-searcher.mjs +23 -0
  64. package/api/search/_doc-searcher.test.mjs +36 -0
  65. package/api/search/_doc-searcher.weights.json +1 -0
  66. package/api/search/_doc-text.d.mts +30 -0
  67. package/api/search/_doc-text.mjs +87 -0
  68. package/api/search/_entries.d.mts +52 -0
  69. package/api/search/_entries.mjs +158 -0
  70. package/api/search/_features.d.mts +35 -0
  71. package/api/search/_features.mjs +155 -0
  72. package/api/search/_gather-components.d.mts +26 -0
  73. package/api/search/_gather-components.mjs +284 -0
  74. package/api/search/_gather-docs.d.mts +20 -0
  75. package/api/search/_gather-docs.mjs +254 -0
  76. package/api/search/_gather-hooks.d.mts +12 -0
  77. package/api/search/_gather-hooks.mjs +52 -0
  78. package/api/search/_gather-templates.d.mts +25 -0
  79. package/api/search/_gather-templates.mjs +107 -0
  80. package/api/search/_lexical.d.mts +135 -0
  81. package/api/search/_lexical.mjs +351 -0
  82. package/api/search/_main-searcher.d.mts +19 -0
  83. package/api/search/_main-searcher.mjs +115 -0
  84. package/api/search/_main-searcher.test.mjs +106 -0
  85. package/api/search/_main-searcher.weights.json +1 -0
  86. package/api/search/_module-doc.d.mts +32 -0
  87. package/api/search/_module-doc.mjs +78 -0
  88. package/api/search/_order.d.mts +32 -0
  89. package/api/search/_order.mjs +162 -0
  90. package/api/search/_order.test.mjs +72 -0
  91. package/api/search/_priority.d.mts +50 -0
  92. package/api/search/_priority.mjs +139 -0
  93. package/api/search/_query-words.d.mts +25 -0
  94. package/api/search/_query-words.mjs +92 -0
  95. package/api/search/_query.d.mts +65 -0
  96. package/api/search/_query.mjs +176 -0
  97. package/api/search/_result.d.mts +97 -0
  98. package/api/search/_result.mjs +150 -0
  99. package/api/search/_score.d.mts +60 -0
  100. package/api/search/_score.mjs +283 -0
  101. package/api/search/_synonyms.d.mts +21 -0
  102. package/api/search/_synonyms.mjs +67 -0
  103. package/api/search/_template-searcher.d.mts +4 -0
  104. package/api/search/_template-searcher.mjs +23 -0
  105. package/api/search/_template-searcher.test.mjs +40 -0
  106. package/api/search/_template-searcher.weights.json +1 -0
  107. package/api/search/_titles.d.mts +33 -0
  108. package/api/search/_titles.mjs +65 -0
  109. package/api/search/_words.d.mts +64 -0
  110. package/api/search/_words.mjs +157 -0
  111. package/api/search/search.d.mts +22 -305
  112. package/api/search/search.doc.mjs +7 -5
  113. package/api/search/search.mjs +53 -2483
  114. package/api/search/search.type.d.mts +6 -1
  115. package/api/search/search.type.mjs +6 -1
  116. package/api/search/tree.d.mts +15 -0
  117. package/api/search/tree.mjs +40 -0
  118. package/api/search/tree.test.mjs +66 -0
  119. package/api/search/tree.weights.json +1 -0
  120. package/api/theme/build/build.packed-old-core.test.mjs +1 -0
  121. package/api/upgrade/_adapter.d.mts +57 -9
  122. package/api/upgrade/_adapter.mjs +198 -28
  123. package/api/upgrade/registry/registry.d.mts +10 -4
  124. package/api/upgrade/registry/registry.mjs +33 -16
  125. package/api/upgrade/registry/registry.test.mjs +85 -0
  126. package/api/upgrade/run/run.d.mts +4 -2
  127. package/api/upgrade/run/run.mjs +13 -2
  128. package/api/upgrade/run/run.test.mjs +417 -3
  129. package/api/upgrade/upgrade.doc.mjs +10 -5
  130. package/api/upgrade/upgrade.mjs +13 -2
  131. package/api/upgrade/upgrade.test.mjs +194 -0
  132. package/api/upgrade/upgrade.type.d.mts +5 -3
  133. package/api/upgrade/upgrade.type.mjs +8 -6
  134. package/assets/codemods/transforms/v0.7.0/__tests__/validate-data-token-ownership.test.mjs +11 -0
  135. package/assets/codemods/transforms/v0.7.0/validate-data-token-ownership.mjs +3 -3
  136. package/assets/docs/is-my-code-good.doc.mjs +287 -0
  137. package/assets/docs/tree/api.doc.mjs +6 -0
  138. package/assets/docs/tree/host-entry.doc.mjs +128 -0
  139. package/assets/docs/tree/template-grading-rubric.doc.mjs +16 -37
  140. package/assets/docs/tree/tokens-data.doc.mjs +2 -2
  141. package/assets/docs/working-with-ai.doc.mjs +6 -2
  142. package/assets/templates/themes/butter/butterTheme.ts +4 -2
  143. package/assets/templates/themes/stone/stoneTheme.ts +4 -2
  144. package/assets/templates/themes/y2k/y2kTheme.ts +4 -2
  145. package/authoring/debug/parse.d.mts +1 -1
  146. package/clients/cli/bin/astryx.mjs +7 -78
  147. package/clients/cli/commands/doctor.mjs +30 -0
  148. package/clients/cli/commands/doctor.test.mjs +65 -0
  149. package/clients/cli/commands/init.behavior.test.mjs +230 -1
  150. package/clients/cli/commands/init.doc.mjs +28 -9
  151. package/clients/cli/commands/init.flag-help.test.mjs +1 -0
  152. package/clients/cli/commands/init.mjs +2 -2
  153. package/clients/cli/commands/outside-project.test.mjs +2 -1
  154. package/clients/cli/commands/setup-nudge.test.mjs +61 -0
  155. package/clients/cli/commands/upgrade.doc.mjs +4 -2
  156. package/clients/cli/commands/upgrade.mjs +9 -2
  157. package/clients/cli/commands/upgrade.subfolder-path.test.mjs +97 -0
  158. package/clients/cli/index.mjs +19 -1
  159. package/clients/cli/run.mjs +108 -0
  160. package/foundation/agent-docs/agent-doc-state.d.mts +20 -7
  161. package/foundation/agent-docs/agent-doc-state.mjs +35 -8
  162. package/foundation/agent-docs/agent-docs.d.mts +100 -12
  163. package/foundation/agent-docs/agent-docs.mjs +1010 -99
  164. package/foundation/agent-docs/agent-docs.path-safety.test.mjs +1239 -17
  165. package/foundation/agent-docs/agent-docs.test.mjs +678 -35
  166. package/foundation/config/project.mjs +20 -22
  167. package/foundation/config/project.test.mjs +56 -0
  168. package/foundation/discovery/component-discovery.d.mts +37 -0
  169. package/foundation/discovery/component-discovery.mjs +80 -5
  170. package/foundation/discovery/component-loader.d.mts +1 -1
  171. package/foundation/discovery/docs-discovery.mjs +14 -1
  172. package/foundation/discovery/module-exports.d.mts +41 -0
  173. package/foundation/discovery/module-exports.mjs +253 -0
  174. package/foundation/discovery/template-adapter.mjs +18 -1
  175. package/foundation/discovery/template-needs.mjs +1 -44
  176. package/foundation/discovery/theme-discovery.mjs +147 -3
  177. package/foundation/doc-compiler/doc-loads.test.mjs +8 -0
  178. package/foundation/doc-compiler/read.mjs +59 -1
  179. package/foundation/doc-compiler/read.test.mjs +11 -2
  180. package/foundation/fs/paths.d.mts +70 -8
  181. package/foundation/fs/paths.mjs +556 -38
  182. package/foundation/fs/paths.test.mjs +430 -54
  183. package/foundation/integrations/autolink.d.mts +2 -1
  184. package/foundation/integrations/autolink.mjs +4 -13
  185. package/foundation/integrations/computer-integrations.d.mts +6 -3
  186. package/foundation/integrations/computer-integrations.mjs +38 -9
  187. package/foundation/integrations/contribution-inventory.d.mts +15 -20
  188. package/foundation/integrations/contribution-inventory.mjs +41 -5
  189. package/foundation/integrations/integrations.d.mts +27 -1
  190. package/foundation/integrations/integrations.mjs +61 -2
  191. package/foundation/integrations/validate-contributions.d.mts +115 -0
  192. package/foundation/integrations/validate-contributions.mjs +1168 -13
  193. package/foundation/release/handoff.d.mts +23 -4
  194. package/foundation/release/handoff.mjs +175 -8
  195. package/foundation/release/run-context.mjs +14 -0
  196. package/foundation/response/response-types.doc.mjs +1 -1
  197. package/foundation/route/route.d.mts +285 -0
  198. package/foundation/route/route.mjs +324 -0
  199. package/foundation/route/route.test.mjs +153 -0
  200. package/foundation/runtime/hosted-runtime.test.mjs +403 -0
  201. package/foundation/runtime/locked-versions.d.mts +32 -0
  202. package/foundation/runtime/locked-versions.mjs +65 -0
  203. package/foundation/runtime/package-root.d.mts +91 -0
  204. package/foundation/runtime/package-root.mjs +174 -0
  205. package/foundation/runtime/package-set.d.mts +62 -0
  206. package/foundation/runtime/package-set.mjs +130 -0
  207. package/foundation/runtime/package-set.test.mjs +432 -0
  208. package/host/door-conformance.test.mjs +274 -0
  209. package/host/index.d.ts +116 -0
  210. package/host/index.mjs +295 -0
  211. package/host/index.test.mjs +302 -0
  212. package/package.json +14 -9
@@ -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
+ * The strongest hit a run of two or more adjacent query words has when it is
7
+ * read as one joined word, and the query words it covers. Readers type a
8
+ * compound name as separate words (`table header cell` is TableHeaderCell,
9
+ * `number field` the keyword `numberfield`), and no single word of the run
10
+ * matches on its own.
11
+ *
12
+ * - 100: the run joins to a component, hook, or template name, or its plural
13
+ * (`table header cells` is TableHeaderCell), or a hook name without its
14
+ * `use` prefix (`table sticky columns` is useTableStickyColumns).
15
+ * - 90: the run joins to a one-word keyword (`number field` is
16
+ * `numberfield`).
17
+ * - 95: the run joins to most of a name, cut short (`dropdown menu radio` is
18
+ * DropdownMenuRadioItem); 92: it is a name's leading words, covering at least half
19
+ * of it (`top nav mega` is TopNavMegaMenu); 70: it is two or more adjacent
20
+ * words of a name that cover at least half of it (`header cell` in
21
+ * TableHeaderCell), the same coverage a word prefix needs. These three
22
+ * are for components and hooks only.
23
+ *
24
+ * A doc's name is a route or key, so docs are left out.
25
+ * @param {string[]} words - The query's words, stopwords included.
26
+ * @param {Candidate} candidate
27
+ * @returns {{score: number, reason: string, covered: Set<string>, alias?: 'partial' | 'keyword', aliasWords?: number} | null}
28
+ * `covered` holds the run's words and every stretch of them joined. `alias`
29
+ * marks a hit on part of a name or on a keyword, not on the whole name.
30
+ */
31
+ export function compoundHit(words: string[], candidate: Candidate): {
32
+ score: number;
33
+ reason: string;
34
+ covered: Set<string>;
35
+ alias?: "partial" | "keyword";
36
+ aliasWords?: number;
37
+ } | null;
38
+ /**
39
+ * The longest run of adjacent query words read as one name. Core's longest
40
+ * names run to six words; a longer run is a sentence, not a name, and reading
41
+ * every run of a long build idea costs time on every candidate.
42
+ */
43
+ export const MAX_RUN_WORDS: 6;
44
+ /**
45
+ * Each candidate's compound hit from its last scoreQuery, so domainPriority
46
+ * reads it without walking the query's runs again.
47
+ * @type {WeakMap<object, ReturnType<typeof compoundHit>>}
48
+ */
49
+ export const compoundOf: WeakMap<object, ReturnType<typeof compoundHit>>;
50
+ export type Candidate = import("./_lexical.mjs").Candidate;
@@ -0,0 +1,170 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Compound names: adjacent query words joined into a candidate's name
5
+ * or keyword (`icon button` and IconButton).
6
+ */
7
+
8
+ import {wordsOf, joined} from './_words.mjs';
9
+ import {pluralOf} from './_query.mjs';
10
+
11
+ /** @typedef {import('./_lexical.mjs').Candidate} Candidate */
12
+
13
+ /**
14
+ * The strongest hit a run of two or more adjacent query words has when it is
15
+ * read as one joined word, and the query words it covers. Readers type a
16
+ * compound name as separate words (`table header cell` is TableHeaderCell,
17
+ * `number field` the keyword `numberfield`), and no single word of the run
18
+ * matches on its own.
19
+ *
20
+ * - 100: the run joins to a component, hook, or template name, or its plural
21
+ * (`table header cells` is TableHeaderCell), or a hook name without its
22
+ * `use` prefix (`table sticky columns` is useTableStickyColumns).
23
+ * - 90: the run joins to a one-word keyword (`number field` is
24
+ * `numberfield`).
25
+ * - 95: the run joins to most of a name, cut short (`dropdown menu radio` is
26
+ * DropdownMenuRadioItem); 92: it is a name's leading words, covering at least half
27
+ * of it (`top nav mega` is TopNavMegaMenu); 70: it is two or more adjacent
28
+ * words of a name that cover at least half of it (`header cell` in
29
+ * TableHeaderCell), the same coverage a word prefix needs. These three
30
+ * are for components and hooks only.
31
+ *
32
+ * A doc's name is a route or key, so docs are left out.
33
+ * @param {string[]} words - The query's words, stopwords included.
34
+ * @param {Candidate} candidate
35
+ * @returns {{score: number, reason: string, covered: Set<string>, alias?: 'partial' | 'keyword', aliasWords?: number} | null}
36
+ * `covered` holds the run's words and every stretch of them joined. `alias`
37
+ * marks a hit on part of a name or on a keyword, not on the whole name.
38
+ */
39
+ export function compoundHit(words, candidate) {
40
+ if (candidate.domain === 'doc' || words.length < 2) return null;
41
+ const names = [candidate.name, ...(candidate.aliases ?? [])]
42
+ .filter(Boolean)
43
+ .map(name => ({name, nameJoined: joined(name), nameWords: wordsOf(name)}));
44
+ // Part of a name counts only for a component or hook. A template's name
45
+ // starts with the component it shows (`ProgressBarShowcase`), so part of it
46
+ // names the component, not the template.
47
+ const partial =
48
+ candidate.domain === 'component' || candidate.domain === 'hook';
49
+ // One-word lowercase keywords: a keyword an author declares for a reader
50
+ // to type, joined (`numberfield`). A capitalized one names another entry (a
51
+ // block's `componentsUsed`), which matches by its own name, and a keyword of
52
+ // several words is a phrase, matched as one.
53
+ const keywords = (candidate.keywords ?? []).filter(k =>
54
+ /^[a-z0-9]+$/.test(String(k)),
55
+ );
56
+ /** @type {{score: number, reason: string, covered: Set<string>, alias?: 'partial' | 'keyword', aliasWords?: number} | null} */
57
+ let best = null;
58
+ for (const {run, word} of queryRuns(words)) {
59
+ let score = 0;
60
+ let reason = '';
61
+ /** @type {'partial' | 'keyword' | null} */
62
+ let kind = null;
63
+ for (const {name, nameJoined, nameWords} of names) {
64
+ if (
65
+ nameJoined === word ||
66
+ pluralOf(word, nameJoined) ||
67
+ (candidate.domain === 'hook' && nameJoined === `use${word}`)
68
+ ) {
69
+ if (score < 100)
70
+ [score, reason, kind] = [100, `name ${name} spelled`, null];
71
+ } else if (!partial) {
72
+ continue;
73
+ } else if (
74
+ nameJoined.startsWith(word) &&
75
+ word.length / nameJoined.length >= 0.75
76
+ ) {
77
+ // Most of the name, cut short: `dropdown menu radio` is
78
+ // DropdownMenuRadioItem.
79
+ if (score < 95)
80
+ [score, reason, kind] = [95, `name ${name} spelled`, 'partial'];
81
+ } else if (
82
+ word.length / nameJoined.length >= 0.5 &&
83
+ run.every(
84
+ (w, j) => nameWords[j + (nameWords[0] === 'use' ? 1 : 0)] === w,
85
+ )
86
+ ) {
87
+ // The name's leading words: `top nav mega` is TopNavMegaMenu.
88
+ if (score < 92)
89
+ [score, reason, kind] = [
90
+ 92,
91
+ `name ${name} starts "${run.join(' ')}"`,
92
+ 'partial',
93
+ ];
94
+ } else if (
95
+ word.length / nameJoined.length >= 0.5 &&
96
+ nameWords.some((_, k) => run.every((w, j) => nameWords[k + j] === w))
97
+ ) {
98
+ if (score < 70)
99
+ [score, reason, kind] = [
100
+ 70,
101
+ `name ${name} holds "${run.join(' ')}"`,
102
+ 'partial',
103
+ ];
104
+ }
105
+ }
106
+ if (score < 90 && keywords.includes(word)) {
107
+ [score, reason, kind] = [
108
+ 90,
109
+ `keyword "${run.join(' ')}" spelled`,
110
+ 'keyword',
111
+ ];
112
+ }
113
+ if (score > (best?.score ?? 0)) {
114
+ // Each word of the run, and each identifier typed as several of its
115
+ // words joined (`dropdownMenu` in `dropdownMenu checkbox`).
116
+ const covered = new Set();
117
+ for (let a = 0; a < run.length; a++) {
118
+ for (let b = a + 1; b <= run.length; b++) {
119
+ covered.add(run.slice(a, b).join(''));
120
+ }
121
+ }
122
+ best = {
123
+ score,
124
+ reason,
125
+ covered,
126
+ ...(kind ? {alias: kind, aliasWords: run.length} : {}),
127
+ };
128
+ }
129
+ }
130
+ return best;
131
+ }
132
+
133
+ /**
134
+ * The longest run of adjacent query words read as one name. Core's longest
135
+ * names run to six words; a longer run is a sentence, not a name, and reading
136
+ * every run of a long build idea costs time on every candidate.
137
+ */
138
+ export const MAX_RUN_WORDS = 6;
139
+
140
+ /**
141
+ * Each run of two to {@link MAX_RUN_WORDS} adjacent query words, longest
142
+ * first, with the run joined into one word. Computed once per query.
143
+ * @type {WeakMap<string[], {run: string[], word: string}[]>}
144
+ */
145
+ const runsOf = new WeakMap();
146
+
147
+ /**
148
+ * @param {string[]} words
149
+ * @returns {{run: string[], word: string}[]}
150
+ */
151
+ function queryRuns(words) {
152
+ let runs = runsOf.get(words);
153
+ if (runs) return runs;
154
+ runs = [];
155
+ for (let len = Math.min(words.length, MAX_RUN_WORDS); len >= 2; len--) {
156
+ for (let i = 0; i + len <= words.length; i++) {
157
+ const run = words.slice(i, i + len);
158
+ runs.push({run, word: run.join('')});
159
+ }
160
+ }
161
+ runsOf.set(words, runs);
162
+ return runs;
163
+ }
164
+
165
+ /**
166
+ * Each candidate's compound hit from its last scoreQuery, so domainPriority
167
+ * reads it without walking the query's runs again.
168
+ * @type {WeakMap<object, ReturnType<typeof compoundHit>>}
169
+ */
170
+ export const compoundOf = new WeakMap();
@@ -0,0 +1,4 @@
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
+ export const doc: import("../../foundation/route/route.mjs").Searcher<import("./tree.mjs").SearchRequest, import("./_adapter.mjs").SearchCatalog, import("./search.type.mjs").SearchResponse>;
@@ -0,0 +1,23 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file The doc searcher: the main searcher's signals with its own weights,
5
+ * in _doc-searcher.weights.json.
6
+ * @input {query, options} and the gathered catalog with its static data
7
+ * @output the search response, the lexical response's shape and size
8
+ * @position api/search; a topic child of the root dispatcher in tree.mjs
9
+ */
10
+
11
+ import {routeKit} from '../../foundation/route/route.mjs';
12
+ import {asWeights, rankWith} from './_main-searcher.mjs';
13
+
14
+ /** @type {import('./tree.mjs').SearchKit} */
15
+ const {defineSearcher} = routeKit;
16
+
17
+ export const doc = defineSearcher({
18
+ id: 'doc',
19
+ summary:
20
+ 'Ranks entries for queries that ask for a doc, with weights of its own.',
21
+ run: (request, catalog, weights) =>
22
+ rankWith(request, catalog, asWeights(weights) ?? catalog.data.doc),
23
+ });
@@ -0,0 +1,36 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Tests for the doc searcher (_doc-searcher.mjs): with its own weights
5
+ * it ranks a doc first for a query that asks for one. Run against the real
6
+ * @astryxdesign/core.
7
+ */
8
+
9
+ import {describe, it, expect} from 'vitest';
10
+ import * as path from 'node:path';
11
+ import {fileURLToPath} from 'node:url';
12
+ import {gatherCandidates, loadSearchData} from './_adapter.mjs';
13
+ import {doc} from './_doc-searcher.mjs';
14
+
15
+ const REPO = path.resolve(
16
+ path.dirname(fileURLToPath(import.meta.url)),
17
+ '../../../..',
18
+ );
19
+
20
+ describe('doc searcher', () => {
21
+ it('ranks a doc first for a doc query', async () => {
22
+ const catalog = {
23
+ ...(await gatherCandidates({cwd: REPO})),
24
+ data: loadSearchData(),
25
+ };
26
+ const answer = /** @type {any} */ (
27
+ await doc.run(
28
+ {query: 'how do I install a theme', options: {cwd: REPO}},
29
+ catalog,
30
+ null,
31
+ )
32
+ );
33
+ expect(answer.data.results[0].domain).toBe('doc');
34
+ expect(answer.data.results.length).toBeLessThanOrEqual(20);
35
+ });
36
+ });
@@ -0,0 +1 @@
1
+ {"w":[0.255034,-0.032567,0.309242,0.050403,-0.358275,0.280939,0.461358,-0.613439,0.397626,0.070136,0.168831,0.032204,0.147137,-0.010509,-0.139086,0.247925,0.0,-0.013425,0.0,0.271127,-0.15666,0.161205,0.275391,-0.017229,-0.003888,0.018954,0.006559,0.010396,-0.003214,0.027251,-0.005917,0.093235,0.101059,0.006095,-0.013116,-0.036481,0.209443,0.12958,0.032606,0.036493,0.082651,0.0,0.0,0.0,0.067563,0.110925,0.145737,0.046132,-0.014063,0.127665,0.014997,0.180173,0.312727,0.079946,0.015853,-0.006495,0.009776,0.005509,0.006095],"mu":[0.122953,0.004932,0.176043,0.017021,0.147234,0.050213,0.178723,0.617872,0.005957,0.170087,0.638276,0.058177,0.049519,0.001321,0.051274,0.176439,0.061188,0.001341,0.0,0.009766,0.004823,0.073882,0.034193,0.000289,0.000827,0.000398,0.002652,0.001028,2.6e-05,0.008501,0.002364,0.111253,0.047342,0.000627,0.073136,0.02573,0.390793,0.145756,0.002861,0.044053,0.014124,0.0,0.0,0.0,0.007995,0.002104,0.029954,0.009344,0.000122,0.021697,0.001864,0.11116,0.041329,0.00039,0.000531,1e-06,0.000703,4.6e-05,6e-05],"sd":[0.272558,0.037015,0.37131,0.129352,0.35434,0.218385,0.383122,0.485909,0.076955,0.825557,1.798503,0.476936,0.124531,0.036157,0.2159,0.232289,1e-06,0.036574,1e-06,0.079222,0.057495,0.229243,0.146561,0.014114,0.017094,0.010761,0.028927,0.011091,0.003965,0.198705,0.104828,0.662804,0.448885,0.080939,0.547129,0.361981,1.544968,0.799909,0.170388,0.407639,0.250897,1e-06,1e-06,1e-06,0.048112,0.025129,0.109037,0.046651,0.006134,0.063975,0.011564,0.248543,0.049449,0.008334,0.023035,0.001195,0.026489,0.006758,0.007742]}
@@ -0,0 +1,30 @@
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 words one section says: its prose, headings, and list items.
6
+ * @param {any} section
7
+ * @returns {string[]}
8
+ */
9
+ export function sectionProse(section: any): string[];
10
+ /**
11
+ * The identifiers a doc part names in code ticks (`token-ref`,
12
+ * `ERR_UNKNOWN_SECTION`). Each is a keyword: a reader who types one exactly
13
+ * wants the part that defines or explains it.
14
+ * @param {any} part - a section, or `{content}` of a typed doc
15
+ * @returns {string[]}
16
+ */
17
+ export function codeTerms(part: any): string[];
18
+ /**
19
+ * The headings inside a section. Each names a subsection, so a query that
20
+ * names one should find the section as surely as one that names its title.
21
+ * @param {any} section
22
+ * @returns {string[]}
23
+ */
24
+ export function headings(section: any): string[];
25
+ /**
26
+ * A table cell as plain words, without its code ticks.
27
+ * @param {unknown} cell
28
+ * @returns {string}
29
+ */
30
+ export function plain(cell: unknown): string;
@@ -0,0 +1,87 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file The text search reads from a doc section: its prose, code terms,
5
+ * headings, and table cells as plain words. Imported only by the adapter's
6
+ * gatherers.
7
+ */
8
+
9
+ import {unlinkText} from '../../foundation/doc-compiler/links.mjs';
10
+
11
+ /**
12
+ * The words one section says: its prose, headings, and list items.
13
+ * @param {any} section
14
+ * @returns {string[]}
15
+ */
16
+ export function sectionProse(section) {
17
+ /** @type {string[]} */
18
+ const prose = [];
19
+ if (section?.title) prose.push(section.title);
20
+ for (const block of section?.content || []) {
21
+ if ((block.type === 'prose' || block.type === 'heading') && block.text) {
22
+ prose.push(block.text);
23
+ } else if (block.type === 'list' && Array.isArray(block.items)) {
24
+ for (const item of block.items) {
25
+ const text = typeof item === 'string' ? item : item?.text;
26
+ if (typeof text === 'string') prose.push(text);
27
+ }
28
+ } else if (block.type === 'table' && Array.isArray(block.rows)) {
29
+ for (const row of block.rows) prose.push(row.map(plain).join(' '));
30
+ } else if (block.type === 'code' && typeof block.code === 'string') {
31
+ prose.push([block.label, block.code].filter(Boolean).join(' '));
32
+ }
33
+ }
34
+ return prose;
35
+ }
36
+
37
+ /**
38
+ * The identifiers a doc part names in code ticks (`token-ref`,
39
+ * `ERR_UNKNOWN_SECTION`). Each is a keyword: a reader who types one exactly
40
+ * wants the part that defines or explains it.
41
+ * @param {any} part - a section, or `{content}` of a typed doc
42
+ * @returns {string[]}
43
+ */
44
+ export function codeTerms(part) {
45
+ /** @type {Set<string>} */
46
+ const terms = new Set();
47
+ /** @param {unknown} text */
48
+ const scan = text => {
49
+ for (const m of String(text ?? '').matchAll(/`([^`\s]{2,40})`/g)) {
50
+ terms.add(m[1]);
51
+ }
52
+ };
53
+ for (const block of part?.content || []) {
54
+ if (block.type === 'prose') scan(block.text);
55
+ else if (block.type === 'list' && Array.isArray(block.items)) {
56
+ for (const item of block.items) {
57
+ scan(typeof item === 'string' ? item : item?.text);
58
+ }
59
+ } else if (block.type === 'table' && Array.isArray(block.rows)) {
60
+ for (const row of block.rows) for (const cell of row) scan(cell);
61
+ }
62
+ }
63
+ return [...terms];
64
+ }
65
+
66
+ /**
67
+ * The headings inside a section. Each names a subsection, so a query that
68
+ * names one should find the section as surely as one that names its title.
69
+ * @param {any} section
70
+ * @returns {string[]}
71
+ */
72
+ export function headings(section) {
73
+ return (section?.content || [])
74
+ .filter(
75
+ (/** @type {any} */ block) => block.type === 'heading' && block.text,
76
+ )
77
+ .map((/** @type {any} */ block) => String(block.text));
78
+ }
79
+
80
+ /**
81
+ * A table cell as plain words, without its code ticks.
82
+ * @param {unknown} cell
83
+ * @returns {string}
84
+ */
85
+ export function plain(cell) {
86
+ return unlinkText(String(cell ?? '')).replaceAll('`', '');
87
+ }
@@ -0,0 +1,52 @@
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 entries search can answer with and their text, by field. Each doc
6
+ * section is its own entry. Built once per gathered catalog.
7
+ * @param {any[]} all the gathered candidates
8
+ * @param {Record<string, {what?: string, synonyms?: string[], props?: string[]}>} cards
9
+ * @returns {EntryTable}
10
+ */
11
+ export function entryTable(all: any[], cards: Record<string, {
12
+ what?: string;
13
+ synonyms?: string[];
14
+ props?: string[];
15
+ }>): EntryTable;
16
+ export function entryOf(table: {
17
+ index: Map<string, number>;
18
+ }, res: {
19
+ domain: string;
20
+ name: string;
21
+ package?: string;
22
+ section?: string;
23
+ }): number | undefined;
24
+ export function entryId(c: any): string;
25
+ export type EntryTable = {
26
+ /**
27
+ * every returnable `<domain>:<name>`, in code-point order
28
+ */
29
+ ids: string[];
30
+ /**
31
+ * id -> position
32
+ */
33
+ index: Map<string, number>;
34
+ /**
35
+ * one candidate per id, for its result
36
+ */
37
+ candidate: any[];
38
+ /**
39
+ * domain index per id
40
+ */
41
+ domain: number[];
42
+ /**
43
+ * each id's name words
44
+ */
45
+ name: string[][];
46
+ fields: Record<"name" | "desc" | "card", {
47
+ post: Map<string, [number, number][]>;
48
+ idf: Map<string, number>;
49
+ len: number[];
50
+ avg: number;
51
+ }>;
52
+ };
@@ -0,0 +1,158 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file The entry table search's searchers rank over: every returnable entry
5
+ * with its name words and the BM25 postings of its name, docs text, and card.
6
+ * Built once per gathered catalog.
7
+ */
8
+
9
+ import {DOMAINS, words, fold, nameWords} from './_query-words.mjs';
10
+
11
+ /** @param {string[]} list @returns {Map<string, number>} */
12
+ const counts = list => {
13
+ const m = new Map();
14
+ for (const w of list) m.set(w, (m.get(w) ?? 0) + 1);
15
+ return m;
16
+ };
17
+
18
+ /**
19
+ * @typedef {object} EntryTable
20
+ * @property {string[]} ids every returnable `<domain>:<name>`, in code-point order
21
+ * @property {Map<string, number>} index id -> position
22
+ * @property {any[]} candidate one candidate per id, for its result
23
+ * @property {number[]} domain domain index per id
24
+ * @property {string[][]} name each id's name words
25
+ * @property {Record<'name'|'desc'|'card', {post: Map<string, [number, number][]>, idf: Map<string, number>, len: number[], avg: number}>} fields
26
+ */
27
+
28
+ const tables = new WeakMap();
29
+
30
+ /**
31
+ * The entry a lexical result answers as.
32
+ * @param {{index: Map<string, number>}} table
33
+ * @param {{domain: string, name: string, package?: string, section?: string}} res
34
+ * @returns {number | undefined}
35
+ */
36
+ export const entryOf = (table, res) => {
37
+ const id = `${res.domain}:${res.name}${res.section ? `#${res.section}` : ''}`;
38
+ return table.index.get(`${id}|${res.package}`) ?? table.index.get(id);
39
+ };
40
+
41
+ /**
42
+ * The id a candidate answers as: a doc section is its own entry, beside its
43
+ * topic. @param {any} c
44
+ */
45
+ export const entryId = c =>
46
+ c.domain === 'doc'
47
+ ? `doc:${c._topic ?? c.name}${c._section ? `#${c._section}` : ''}`
48
+ : `${c.domain}:${c._resultName ?? c.name}`;
49
+
50
+ /**
51
+ * The entries search can answer with and their text, by field. Each doc
52
+ * section is its own entry. Built once per gathered catalog.
53
+ * @param {any[]} all the gathered candidates
54
+ * @param {Record<string, {what?: string, synonyms?: string[], props?: string[]}>} cards
55
+ * @returns {EntryTable}
56
+ */
57
+ export function entryTable(all, cards) {
58
+ const hit = tables.get(all);
59
+ if (hit) return hit;
60
+ /** @type {Map<string, any[]>} */
61
+ const by = new Map();
62
+ /** @type {Map<string, string>} each id's first package */
63
+ const owner = new Map();
64
+ for (const c of all) {
65
+ // Two packages may each give an entry the same name (an integration's
66
+ // Dialog beside Core's): the later package's entry keeps its own id.
67
+ const plain = entryId(c);
68
+ if (!owner.has(plain)) owner.set(plain, c._package ?? '');
69
+ const id =
70
+ (c._package ?? '') === owner.get(plain)
71
+ ? plain
72
+ : `${plain}|${c._package}`;
73
+ if (!by.has(id)) by.set(id, []);
74
+ /** @type {any[]} */ (by.get(id)).push(c);
75
+ }
76
+ const ids = [...by.keys()].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
77
+ /** @type {Record<'name'|'desc'|'card', Map<string, number>[]>} */
78
+ const docs = {name: [], desc: [], card: []};
79
+ const table = /** @type {EntryTable} */ (
80
+ /** @type {unknown} */ ({
81
+ ids,
82
+ index: new Map(ids.map((id, k) => [id, k])),
83
+ candidate: [],
84
+ domain: [],
85
+ name: [],
86
+ fields: {},
87
+ })
88
+ );
89
+ for (const id of ids) {
90
+ const group = /** @type {any[]} */ (by.get(id));
91
+ const first = group.find(c => !c._section) ?? group[0];
92
+ const plain = id.split('|')[0].split('#')[0];
93
+ const [domain, name] = [
94
+ plain.slice(0, plain.indexOf(':')),
95
+ plain.slice(plain.indexOf(':') + 1),
96
+ ];
97
+ const nw = nameWords(name).map(fold);
98
+ table.candidate.push(first);
99
+ table.domain.push(DOMAINS.indexOf(domain));
100
+ table.name.push(nw);
101
+ const text = [];
102
+ for (const c of group) {
103
+ text.push(
104
+ c.description ?? '',
105
+ ...(c.keywords ?? []),
106
+ ...(c.aliases ?? []),
107
+ ...(c.titles ?? []),
108
+ );
109
+ text.push(
110
+ ...(c.guidance ?? []).slice(0, 10),
111
+ ...(c.prose ?? []).slice(0, 4),
112
+ );
113
+ }
114
+ const card =
115
+ domain === 'component' || domain === 'hook' ? cards[name] : undefined;
116
+ docs.name.push(counts([...nw, nw.join('')]));
117
+ docs.desc.push(counts(words(text.join(' ')).map(fold)));
118
+ docs.card.push(
119
+ counts(
120
+ card
121
+ ? words(
122
+ [
123
+ card.what ?? '',
124
+ ...(card.synonyms ?? []),
125
+ ...(card.props ?? []),
126
+ ].join(' '),
127
+ ).map(fold)
128
+ : [],
129
+ ),
130
+ );
131
+ }
132
+ const n = ids.length;
133
+ for (const f of /** @type {const} */ (['name', 'desc', 'card'])) {
134
+ /** @type {Map<string, [number, number][]>} */
135
+ const post = new Map();
136
+ /** @type {Map<string, number>} */
137
+ const df = new Map();
138
+ /** @type {number[]} */
139
+ const len = [];
140
+ docs[f].forEach((m, k) => {
141
+ let l = 1;
142
+ for (const [w, c] of m) {
143
+ l += c;
144
+ df.set(w, (df.get(w) ?? 0) + 1);
145
+ const list = post.get(w) ?? [];
146
+ list.push([k, c]);
147
+ post.set(w, list);
148
+ }
149
+ len.push(l);
150
+ });
151
+ const idf = new Map();
152
+ for (const [w, d] of df)
153
+ idf.set(w, Math.log(1 + (n - d + 0.5) / (d + 0.5)));
154
+ table.fields[f] = {post, idf, len, avg: len.reduce((a, b) => a + b, 0) / n};
155
+ }
156
+ tables.set(all, table);
157
+ return table;
158
+ }
@@ -0,0 +1,35 @@
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
+ * Every entry's signals for one query, as rows of numbers.
6
+ * @param {EntryTable} table
7
+ * @param {string} query
8
+ * @param {{domain: string, name: string, score?: number}[]} lexical the lexical ranking, best first
9
+ * @param {number[] | null} route the router's class probabilities
10
+ * @returns {Float64Array[]}
11
+ */
12
+ export function signals(table: EntryTable, query: string, lexical: {
13
+ domain: string;
14
+ name: string;
15
+ score?: number;
16
+ }[], route: number[] | null): Float64Array[];
17
+ /**
18
+ * Ranks the entries by a searcher's weights: the weighted sum of each entry's
19
+ * standardized signals and per-domain copies; ties go to the earlier id.
20
+ * @param {EntryTable} table
21
+ * @param {Float64Array[]} rows
22
+ * @param {{w: number[], mu: number[], sd: number[]}} weights
23
+ * @returns {number[]} entry positions, best first
24
+ */
25
+ export function rank(table: EntryTable, rows: Float64Array[], weights: {
26
+ w: number[];
27
+ mu: number[];
28
+ sd: number[];
29
+ }): number[];
30
+ /** @typedef {import('./_entries.mjs').EntryTable} EntryTable */
31
+ /** The signal names, in weights-file order (before the per-domain copies). */
32
+ export const SIGNALS: string[];
33
+ /** The signals that also get one copy per domain. */
34
+ export const PER_DOMAIN: string[];
35
+ export type EntryTable = import("./_entries.mjs").EntryTable;