@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
@@ -32,6 +32,27 @@
32
32
  * @returns {Promise<PageTemplate[]>}
33
33
  */
34
34
  export function loadPageTemplates(cwd: string): Promise<PageTemplate[]>;
35
+ /**
36
+ * A block the kit can name, as search names a block result.
37
+ * @typedef {object} BlockTemplate
38
+ * @property {string} name The block's own id, as search reports it.
39
+ * @property {string} package The npm package that owns this block.
40
+ * @property {string} displayName Human-facing name.
41
+ * @property {string} description What the block is.
42
+ * @property {string} command `astryx template <id> --type block`, as search prints it.
43
+ */
44
+ /**
45
+ * Every ready page template and every block the project can scaffold, from one
46
+ * discovery: pages as {@link loadPageTemplates} returns them, blocks as
47
+ * {@link BlockTemplate}. Discovery failures leave both empty.
48
+ *
49
+ * @param {string} cwd
50
+ * @returns {Promise<{pages: PageTemplate[], blocks: BlockTemplate[]}>}
51
+ */
52
+ export function loadTemplates(cwd: string): Promise<{
53
+ pages: PageTemplate[];
54
+ blocks: BlockTemplate[];
55
+ }>;
35
56
  /**
36
57
  * The components the project can use, Core's and its integrations', each with
37
58
  * the keywords its own doc declares: what the ranker reads to tell a part of a
@@ -48,6 +69,18 @@ export function loadComponents(cwd: string): Promise<ComponentWords[]>;
48
69
  * @returns {import('./kit/weights.mjs').WeightsFile | null}
49
70
  */
50
71
  export function loadWeights(): import("./kit/weights.mjs").WeightsFile | null;
72
+ /**
73
+ * The classifier weights of build's root (tree.weights.json), read once.
74
+ * @returns {import('../../foundation/route/route.mjs').ClassifierWeights}
75
+ */
76
+ export function loadTreeWeights(): import("../../foundation/route/route.mjs").ClassifierWeights;
77
+ /**
78
+ * The block picker's weights (blocks/blocks.weights.json), read once; null
79
+ * when the file is absent, unreadable, or not the shape the picker reads, in
80
+ * which case build names its own blocks.
81
+ * @returns {import('./blocks/blocks.mjs').BlockPickerWeights | null}
82
+ */
83
+ export function loadBlockPicker(): import("./blocks/blocks.mjs").BlockPickerWeights | null;
51
84
  /**
52
85
  * Whether a parsed weights file has the shape the kit reads: every row has a
53
86
  * weight per candidate, every bias a number per candidate, and three blend
@@ -125,3 +158,28 @@ export type ComponentWords = {
125
158
  */
126
159
  keywords: string[];
127
160
  };
161
+ /**
162
+ * A block the kit can name, as search names a block result.
163
+ */
164
+ export type BlockTemplate = {
165
+ /**
166
+ * The block's own id, as search reports it.
167
+ */
168
+ name: string;
169
+ /**
170
+ * The npm package that owns this block.
171
+ */
172
+ package: string;
173
+ /**
174
+ * Human-facing name.
175
+ */
176
+ displayName: string;
177
+ /**
178
+ * What the block is.
179
+ */
180
+ description: string;
181
+ /**
182
+ * `astryx template <id> --type block`, as search prints it.
183
+ */
184
+ command: string;
185
+ };
@@ -9,7 +9,9 @@
9
9
  * contribute.
10
10
  * @output Ready page templates as `{name, displayName, description, category,
11
11
  * keywords, command}`, where `command` is the `astryx template` command that
12
- * selects exactly that template; components as `{name, keywords}`.
12
+ * selects exactly that template; blocks as search names them; components as
13
+ * `{name, keywords}`; the kit's matcher weights, the root's classifier
14
+ * weights, and the block picker's weights.
13
15
  * @position Beside build.mjs (api/build/). The kit leaf reads templates and
14
16
  * components only through here, because a subject's `_adapter.mjs` is its
15
17
  * only environment access. This adds no discovery of its own: templates come
@@ -63,13 +65,35 @@ export {withListingCache} from '../../foundation/fs/listing-cache.mjs';
63
65
  * @returns {Promise<PageTemplate[]>}
64
66
  */
65
67
  export async function loadPageTemplates(cwd) {
68
+ return (await loadTemplates(cwd)).pages;
69
+ }
70
+
71
+ /**
72
+ * A block the kit can name, as search names a block result.
73
+ * @typedef {object} BlockTemplate
74
+ * @property {string} name The block's own id, as search reports it.
75
+ * @property {string} package The npm package that owns this block.
76
+ * @property {string} displayName Human-facing name.
77
+ * @property {string} description What the block is.
78
+ * @property {string} command `astryx template <id> --type block`, as search prints it.
79
+ */
80
+
81
+ /**
82
+ * Every ready page template and every block the project can scaffold, from one
83
+ * discovery: pages as {@link loadPageTemplates} returns them, blocks as
84
+ * {@link BlockTemplate}. Discovery failures leave both empty.
85
+ *
86
+ * @param {string} cwd
87
+ * @returns {Promise<{pages: PageTemplate[], blocks: BlockTemplate[]}>}
88
+ */
89
+ export async function loadTemplates(cwd) {
66
90
  let templates;
67
91
  try {
68
92
  templates = await discoverTemplates(cwd);
69
93
  } catch {
70
- return [];
94
+ return {pages: [], blocks: []};
71
95
  }
72
- return templates
96
+ const pages = templates
73
97
  .filter(t => t.type === 'page' && t.isReady !== false)
74
98
  .map(t => ({
75
99
  name: t.dirName,
@@ -83,6 +107,16 @@ export async function loadPageTemplates(cwd) {
83
107
  // owns the Core id it names, so that id selects it, not its own.
84
108
  command: `astryx template ${t.replaces ?? t.dirName} --type page`,
85
109
  }));
110
+ const blocks = templates
111
+ .filter(t => t.type === 'block')
112
+ .map(t => ({
113
+ name: t.dirName,
114
+ package: t.package ?? '@astryxdesign/core',
115
+ displayName: t.name,
116
+ description: t.description || '',
117
+ command: `astryx template ${t.replaces ?? t.dirName} --type block`,
118
+ }));
119
+ return {pages, blocks};
86
120
  }
87
121
 
88
122
  /**
@@ -126,6 +160,48 @@ export function loadWeights() {
126
160
  return weights ?? null;
127
161
  }
128
162
 
163
+ /** @type {import('../../foundation/route/route.mjs').ClassifierWeights | undefined} */
164
+ let treeWeights;
165
+
166
+ /**
167
+ * The classifier weights of build's root (tree.weights.json), read once.
168
+ * @returns {import('../../foundation/route/route.mjs').ClassifierWeights}
169
+ */
170
+ export function loadTreeWeights() {
171
+ treeWeights ??= JSON.parse(
172
+ fs.readFileSync(new URL('./tree.weights.json', import.meta.url), 'utf8'),
173
+ );
174
+ return /** @type {import('../../foundation/route/route.mjs').ClassifierWeights} */ (
175
+ treeWeights
176
+ );
177
+ }
178
+
179
+ /** @type {import('./blocks/blocks.mjs').BlockPickerWeights | null | undefined} */
180
+ let blockPicker;
181
+
182
+ /**
183
+ * The block picker's weights (blocks/blocks.weights.json), read once; null
184
+ * when the file is absent, unreadable, or not the shape the picker reads, in
185
+ * which case build names its own blocks.
186
+ * @returns {import('./blocks/blocks.mjs').BlockPickerWeights | null}
187
+ */
188
+ export function loadBlockPicker() {
189
+ if (blockPicker === undefined) {
190
+ try {
191
+ const file = JSON.parse(
192
+ fs.readFileSync(
193
+ new URL('./blocks/blocks.weights.json', import.meta.url),
194
+ 'utf8',
195
+ ),
196
+ );
197
+ blockPicker = isBlockPickerWeights(file) ? file : null;
198
+ } catch {
199
+ blockPicker = null;
200
+ }
201
+ }
202
+ return blockPicker ?? null;
203
+ }
204
+
129
205
  /**
130
206
  * Whether a parsed weights file has the shape the kit reads: every row has a
131
207
  * weight per candidate, every bias a number per candidate, and three blend
@@ -159,6 +235,42 @@ export function isWeightsFile(file) {
159
235
  );
160
236
  }
161
237
 
238
+ /**
239
+ * Whether a parsed block picker file has the shape the picker reads: a
240
+ * vocabulary weight per word, a bias per block, sparse rows of
241
+ * [block index, weight] pairs, and five combiner numbers.
242
+ * @param {any} w
243
+ * @returns {w is import('./blocks/blocks.mjs').BlockPickerWeights}
244
+ */
245
+ function isBlockPickerWeights(w) {
246
+ const n = Array.isArray(w?.classes) ? w.classes.length : 0;
247
+ return (
248
+ n > 0 &&
249
+ w.classes.every((/** @type {unknown} */ c) => typeof c === 'string') &&
250
+ Array.isArray(w.bias) &&
251
+ w.bias.length === n &&
252
+ w.bias.every((/** @type {unknown} */ b) => Number.isFinite(b)) &&
253
+ !!w.vocab &&
254
+ typeof w.vocab === 'object' &&
255
+ Object.values(w.vocab).every(v => Number.isFinite(v)) &&
256
+ !!w.words &&
257
+ typeof w.words === 'object' &&
258
+ Object.values(w.words).every(
259
+ row =>
260
+ Array.isArray(row) &&
261
+ row.length % 2 === 0 &&
262
+ row.every(
263
+ (/** @type {number} */ x, /** @type {number} */ i) =>
264
+ Number.isFinite(x) &&
265
+ (i % 2 === 1 || (Number.isInteger(x) && x >= 0 && x < n)),
266
+ ),
267
+ ) &&
268
+ Array.isArray(w.theta) &&
269
+ w.theta.length === 5 &&
270
+ w.theta.every((/** @type {unknown} */ x) => Number.isFinite(x))
271
+ );
272
+ }
273
+
162
274
  /**
163
275
  * Analyze what a start template needs that the project lacks. The kit leaf
164
276
  * calls this through the adapter rather than reading the file itself, because
@@ -0,0 +1,42 @@
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 blocks build names for a request, re-ranked by the picker.
6
+ * @param {string} request
7
+ * @param {{kitBlocks: string[], searched: BlockScore[], foreign: string[]}} signals
8
+ * build's signals: the kit's blocks, search's block results [id, score,
9
+ * matched, total] in order, and the blocks whose package is not Core's
10
+ * @param {BlockPickerWeights} weights
11
+ * @returns {string[]} the kit's blocks in a new order: a block the request
12
+ * names, one the weights do not know, or one an integration ships keeps its
13
+ * slot; the others, best first, fill the remaining slots
14
+ */
15
+ export function pickBlocks(request: string, signals: {
16
+ kitBlocks: string[];
17
+ searched: BlockScore[];
18
+ foreign: string[];
19
+ }, weights: BlockPickerWeights): string[];
20
+ export type BlockPickerWeights = {
21
+ /**
22
+ * word or word pair -> weight x1024
23
+ */
24
+ vocab: Record<string, number>;
25
+ /**
26
+ * the block ids the words score
27
+ */
28
+ classes: string[];
29
+ /**
30
+ * one per class, x64
31
+ */
32
+ bias: number[];
33
+ /**
34
+ * word -> [class index, weight x64, ...]
35
+ */
36
+ words: Record<string, number[]>;
37
+ /**
38
+ * the five combiner numbers
39
+ */
40
+ theta: number[];
41
+ };
42
+ export type BlockScore = import("../kit/signals.mjs").BlockScore;
@@ -0,0 +1,169 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file build's block picker: re-ranks the blocks build would name for a
5
+ * request with checked-in word weights, and keeps build's own count.
6
+ *
7
+ * @input The request, build's signals for it (`kitSignals`: the blocks the kit
8
+ * names, search's block results with their scores and coverage), the block
9
+ * ids the project can scaffold, and the picker weights (`blocks.weights.json`
10
+ * beside this file, read by the adapter): a vocabulary of words and word
11
+ * pairs with their weight (x1024), the block ids it scores, a bias per block
12
+ * and a sparse row of weights per word (x64), and five combiner numbers.
13
+ * @output `pickBlocks(...)`: block ids, best first, as many as build's own list.
14
+ * @position api/build/blocks; tree.mjs's main searcher calls it between
15
+ * kitSignals (kit/signals.mjs) and kitAnswer (kit/answer.mjs). Pure: no file, network, or environment access.
16
+ *
17
+ * How it scores. The request's words and adjacent word pairs, each weighted
18
+ * (1 + ln count) x its vocabulary weight and scaled to unit length, give every
19
+ * block a linear score (its bias plus the weighted words' rows). Only the
20
+ * blocks build already names are re-ranked: never another block, never more.
21
+ * Each one's final score combines five numbers: search's score / 100, the
22
+ * share of the request search matched, whether the kit names it, 1 / (1 + its
23
+ * place in build's list), and the block's log-share of the linear scores.
24
+ * Ties keep build's order. A block the request names (its name, or the
25
+ * component its name starts with, written out, joined, or plural), a block the
26
+ * weights do not know, and a block whose package is not Core's keep the slot
27
+ * build gave them; the picker re-orders the rest within the other slots.
28
+ */
29
+
30
+ import {pluralOf} from '../../search/_query.mjs';
31
+ import {wordsOf} from '../../search/_words.mjs';
32
+
33
+ /**
34
+ * @typedef {object} BlockPickerWeights
35
+ * @property {Record<string, number>} vocab word or word pair -> weight x1024
36
+ * @property {string[]} classes the block ids the words score
37
+ * @property {number[]} bias one per class, x64
38
+ * @property {Record<string, number[]>} words word -> [class index, weight x64, ...]
39
+ * @property {number[]} theta the five combiner numbers
40
+ *
41
+ * @typedef {import('../kit/signals.mjs').BlockScore} BlockScore
42
+ */
43
+
44
+ /** The linear score of a block the words do not score. */
45
+ const UNSCORED = -30;
46
+
47
+ /**
48
+ * The words and adjacent word pairs of a request, with their weights, in the
49
+ * order they first appear; only those in the vocabulary.
50
+ * @param {string} request
51
+ * @param {BlockPickerWeights} weights
52
+ * @returns {Map<string, number>}
53
+ */
54
+ function features(request, weights) {
55
+ const tokens =
56
+ String(request ?? '')
57
+ .toLowerCase()
58
+ .match(/[a-z0-9]{2,}/g) ?? [];
59
+ const all = [...tokens];
60
+ for (let i = 0; i + 1 < tokens.length; i++)
61
+ all.push(`${tokens[i]} ${tokens[i + 1]}`);
62
+ /** @type {Map<string, number>} */
63
+ const counts = new Map();
64
+ for (const f of all) {
65
+ if (Object.hasOwn(weights.vocab, f))
66
+ counts.set(f, (counts.get(f) ?? 0) + 1);
67
+ }
68
+ /** @type {Map<string, number>} */
69
+ const x = new Map();
70
+ for (const [f, c] of counts)
71
+ x.set(f, ((1 + Math.log(c)) * weights.vocab[f]) / 1024);
72
+ return x;
73
+ }
74
+
75
+ /**
76
+ * Every class's linear score for a request, and the log of the normalizer.
77
+ * @param {string} request
78
+ * @param {BlockPickerWeights} weights
79
+ * @returns {{z: number[], lse: number}}
80
+ */
81
+ function linearScores(request, weights) {
82
+ const x = features(request, weights);
83
+ let sq = 0;
84
+ for (const v of x.values()) sq += v * v;
85
+ const norm = Math.sqrt(sq);
86
+ const z = weights.bias.slice();
87
+ if (norm > 0) {
88
+ for (const [f, v] of x) {
89
+ const row = Object.hasOwn(weights.words, f) ? weights.words[f] : null;
90
+ if (!row) continue;
91
+ for (let k = 0; k < row.length; k += 2)
92
+ z[row[k]] += (v / norm) * row[k + 1];
93
+ }
94
+ }
95
+ for (let c = 0; c < z.length; c++) z[c] /= 64;
96
+ let max = -Infinity;
97
+ for (const v of z) if (v > max) max = v;
98
+ let sum = 0;
99
+ for (const v of z) sum += Math.exp(v - max);
100
+ return {z, lse: max + Math.log(sum)};
101
+ }
102
+
103
+ /**
104
+ * Whether a request names a block: a run of one to three adjacent words of
105
+ * the request, joined, is the block's name, the start of its name that names
106
+ * its component (at least four letters, whole words), or the plural of either.
107
+ * @param {string[]} words the request's lowercase words
108
+ * @param {string} block
109
+ * @returns {boolean}
110
+ */
111
+ function names(words, block) {
112
+ const parts = wordsOf(block);
113
+ /** @type {Set<string>} */
114
+ const forms = new Set();
115
+ for (let n = 1; n <= parts.length; n++) {
116
+ const form = parts.slice(0, n).join('');
117
+ if (form.length >= 4) forms.add(form);
118
+ }
119
+ for (let i = 0; i < words.length; i++) {
120
+ let run = '';
121
+ for (let n = 0; n < 3 && i + n < words.length; n++) {
122
+ run += words[i + n];
123
+ for (const form of forms)
124
+ if (run === form || pluralOf(run, form)) return true;
125
+ }
126
+ }
127
+ return false;
128
+ }
129
+
130
+ /**
131
+ * The blocks build names for a request, re-ranked by the picker.
132
+ * @param {string} request
133
+ * @param {{kitBlocks: string[], searched: BlockScore[], foreign: string[]}} signals
134
+ * build's signals: the kit's blocks, search's block results [id, score,
135
+ * matched, total] in order, and the blocks whose package is not Core's
136
+ * @param {BlockPickerWeights} weights
137
+ * @returns {string[]} the kit's blocks in a new order: a block the request
138
+ * names, one the weights do not know, or one an integration ships keeps its
139
+ * slot; the others, best first, fill the remaining slots
140
+ */
141
+ export function pickBlocks(request, signals, weights) {
142
+ const own = signals.kitBlocks;
143
+ if (own.length === 0) return [];
144
+ const {z, lse} = linearScores(request, weights);
145
+ const classIndex = new Map(weights.classes.map((c, i) => [c, i]));
146
+ /** @type {Map<string, BlockScore>} */
147
+ const found = new Map();
148
+ for (const s of signals.searched) if (!found.has(s[0])) found.set(s[0], s);
149
+ const foreign = new Set(signals.foreign);
150
+ const words = wordsOf(request);
151
+ const keeps = own.map(
152
+ b => !classIndex.has(b) || foreign.has(b) || names(words, b),
153
+ );
154
+ const [t0, t1, t2, t3, t4] = weights.theta;
155
+ const scored = own
156
+ .map((b, pos) => ({b, pos}))
157
+ .filter((_, k) => !keeps[k])
158
+ .map(({b, pos}) => {
159
+ const s = found.get(b);
160
+ const c = classIndex.get(b);
161
+ const f0 = s ? s[1] / 100 : 0;
162
+ const f1 = s ? (s[2] ?? 0) / Math.max(s[3] ?? 0, 1) : 0;
163
+ const f4 = (c === undefined ? UNSCORED : z[c]) - lse;
164
+ return {b, pos, s: t0 * f0 + t1 * f1 + t2 + t3 / (1 + pos) + t4 * f4};
165
+ })
166
+ .sort((a, b) => b.s - a.s || a.pos - b.pos);
167
+ let next = 0;
168
+ return own.map((b, k) => (keeps[k] ? b : scored[next++].b));
169
+ }
@@ -0,0 +1,108 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Unit tests for build's block picker on small hand-written weights: it
5
+ * only re-orders the blocks build names (never another block, never more), a
6
+ * block the request names keeps its slot, as does a block the weights do not
7
+ * know or an integration ships, and ties keep build's order.
8
+ */
9
+
10
+ import {describe, expect, it} from 'vitest';
11
+ import {pickBlocks} from './blocks.mjs';
12
+
13
+ /** @type {import('./blocks.mjs').BlockPickerWeights} */
14
+ const WORDS = {
15
+ vocab: {chart: 1024, table: 2048, sales: 1536, 'sales chart': 3072, row: 512},
16
+ classes: ['ChartBar', 'TableBasic', 'CardStat', 'ListPlain', 'CalendarBasic'],
17
+ bias: [10, -20, 0, -64, 0],
18
+ words: {
19
+ chart: [0, 90, 2, 30],
20
+ table: [1, 120, 3, -40],
21
+ 'sales chart': [0, 60, 2, 50],
22
+ row: [2, 80],
23
+ },
24
+ theta: [0.5, 0.25, 0.5, 0.4, 1.5],
25
+ };
26
+ /** @type {[string, number, number, number][]} */
27
+ const SEARCHED = [
28
+ ['TableBasic', 96, 1, 3],
29
+ ['ListPlain', 64, 1, 3],
30
+ ['ChartBar', 50, 1, 3],
31
+ ['CardStat', 40, 1, 3],
32
+ ];
33
+
34
+ /** @type {import('./blocks.mjs').BlockPickerWeights} */
35
+ const TIES = {
36
+ vocab: {alpha: 1024},
37
+ classes: ['X', 'Y', 'Z'],
38
+ bias: [0, 0, 0],
39
+ words: {},
40
+ theta: [0, 0, 0, 0, 1],
41
+ };
42
+
43
+ /**
44
+ * @param {string[]} kitBlocks
45
+ * @param {string[]} [foreign]
46
+ */
47
+ const signals = (kitBlocks, foreign = []) => ({
48
+ kitBlocks,
49
+ searched: SEARCHED,
50
+ foreign,
51
+ });
52
+
53
+ describe('pickBlocks', () => {
54
+ it('re-orders only the blocks build names, never adding one', () => {
55
+ const own = ['TableBasic', 'ListPlain', 'CardStat'];
56
+ const picks = pickBlocks('sales chart with a row', signals(own), WORDS);
57
+ expect([...picks].sort()).toEqual([...own].sort());
58
+ expect(picks).toEqual(['CardStat', 'TableBasic', 'ListPlain']);
59
+ });
60
+
61
+ it('never names a block the words score highly that build did not name', () => {
62
+ const picks = pickBlocks(
63
+ 'sales chart chart chart',
64
+ signals(['TableBasic', 'ListPlain']),
65
+ WORDS,
66
+ );
67
+ expect(picks).not.toContain('ChartBar');
68
+ expect(picks).toHaveLength(2);
69
+ });
70
+
71
+ it('keeps a block the request names in its slot', () => {
72
+ const own = ['TableBasic', 'CalendarBasic', 'CardStat'];
73
+ for (const request of [
74
+ 'sales chart with a calendar',
75
+ 'sales chart with two calendars',
76
+ 'sales chart, calendar basic',
77
+ ]) {
78
+ expect(pickBlocks(request, signals(own), WORDS)[1]).toBe('CalendarBasic');
79
+ }
80
+ });
81
+
82
+ it('keeps a block the weights do not know or an integration ships in its slot', () => {
83
+ const own = ['TableBasic', 'AcmeCallout', 'CardStat', 'ListPlain'];
84
+ const picks = pickBlocks(
85
+ 'sales chart with a row',
86
+ signals(own, ['ListPlain']),
87
+ WORDS,
88
+ );
89
+ expect(picks[1]).toBe('AcmeCallout');
90
+ expect(picks[3]).toBe('ListPlain');
91
+ expect([...picks].sort()).toEqual([...own].sort());
92
+ });
93
+
94
+ it('keeps build’s order on ties and when no word is known', () => {
95
+ const own = ['X', 'Y', 'Z'];
96
+ expect(
97
+ pickBlocks('??', {kitBlocks: own, searched: [], foreign: []}, TIES),
98
+ ).toEqual(own);
99
+ });
100
+
101
+ it('returns nothing when build names no block, and does not change its input', () => {
102
+ expect(pickBlocks('sales chart', signals([]), WORDS)).toEqual([]);
103
+ const own = ['TableBasic', 'ListPlain'];
104
+ const copy = [...own];
105
+ pickBlocks('sales chart', signals(own), WORDS);
106
+ expect(own).toEqual(copy);
107
+ });
108
+ });