@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,202 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file The words build's page ranker reads: a template's terms and families,
5
+ * an idea's modifiers, containers and phrase pairs, and search's synonyms.
6
+ *
7
+ * Family words come from the templates' own ids — a word in the ids of two or
8
+ * more templates of one family, plus the family's name when a template carries
9
+ * it — never from a list kept here or from synonyms, so an integration's
10
+ * templates join the vocabulary by being named like their family, and "a
11
+ * landing page" is not a hero because "landing" is a synonym of "hero".
12
+ */
13
+
14
+ import {stem, STOPWORDS, SYNONYMS} from '../../search/search.mjs';
15
+
16
+ /**
17
+ * Words search drops that name layout: "side navigation", "side by side".
18
+ */
19
+ const LAYOUT_WORDS = new Set(['side']);
20
+
21
+ /**
22
+ * Where an idea's head ends: its first clause, before the parts it lists.
23
+ */
24
+ export const HEAD_END =
25
+ /:|,|;|\(|\s[-\u2013\u2014]\s|\b(?:with|showing|listing|for|that|where|plus|including|containing|featuring|which|to|from|of)\b/i;
26
+
27
+ /**
28
+ * Frames a page can live in, above whatever is underneath. A container phrase
29
+ * counts only when it names one of these.
30
+ */
31
+ const OVERLAY_FRAMES = new Set([
32
+ 'modal',
33
+ 'dialog',
34
+ 'drawer',
35
+ 'sheet',
36
+ 'popup',
37
+ 'overlay',
38
+ ]);
39
+
40
+ /**
41
+ * The family whose templates are app chrome rather than page content: the
42
+ * explicit `Shell -` category (architecture:template-authoring/INV7).
43
+ */
44
+ export const SHELL_FAMILY = 'Shell';
45
+
46
+ /** A container phrase: "in a modal", "inside the side panel". */
47
+ export const CONTAINER_PHRASE =
48
+ /\b(?:in|inside|within)\s+(?:a|an|the)\s+([a-z-]+)(?:\s+([a-z-]+))?/gi;
49
+
50
+ /**
51
+ * Where a phrase ends, for finding the word that heads it: punctuation and
52
+ * the words that join phrases.
53
+ */
54
+ export const PHRASE_END =
55
+ /[^a-z0-9\s-]+|\s(?:and|or|with|for|of|to|in|on|at|by|from|the|a|an|plus)\s/;
56
+
57
+ /** @param {string} t */
58
+ const isContentWord = t =>
59
+ t.length >= 2 && (LAYOUT_WORDS.has(t) || !STOPWORDS.has(t));
60
+
61
+ /**
62
+ * Content terms of a text: lowercase alphanumeric words, stopwords removed,
63
+ * stemmed.
64
+ * @param {string} text
65
+ * @returns {string[]}
66
+ */
67
+ export function terms(text) {
68
+ return (
69
+ String(text)
70
+ .toLowerCase()
71
+ .match(/[a-z0-9]+/g) ?? []
72
+ )
73
+ .filter(isContentWord)
74
+ .map(stem);
75
+ }
76
+
77
+ /**
78
+ * The terms of an idea that only ever modify another word, each with the words
79
+ * it modifies: in each phrase the last content word is its head, and the ones
80
+ * before it describe the next.
81
+ * @param {string} query
82
+ * @returns {Map<string, Set<string>>}
83
+ */
84
+ export function modifiersOf(query) {
85
+ const heads = new Set();
86
+ /** @type {Map<string, Set<string>>} */
87
+ const modifiers = new Map();
88
+ for (const phrase of String(query).toLowerCase().split(PHRASE_END)) {
89
+ const words = terms(phrase);
90
+ words.forEach((w, i) => {
91
+ if (i === words.length - 1) heads.add(w);
92
+ else modifiers.set(w, (modifiers.get(w) ?? new Set()).add(words[i + 1]));
93
+ });
94
+ }
95
+ for (const h of heads) modifiers.delete(h);
96
+ return modifiers;
97
+ }
98
+
99
+ /**
100
+ * Adjacent term pairs a text names, "a b", for telling a template that names
101
+ * a whole compound ("executive summary") from one that names only its parts.
102
+ * @param {string[]} texts
103
+ * @returns {Set<string>}
104
+ */
105
+ export function pairsOf(texts) {
106
+ const pairs = new Set();
107
+ for (const text of texts) {
108
+ for (const phrase of String(text).toLowerCase().split(PHRASE_END)) {
109
+ const words = terms(phrase);
110
+ for (let i = 1; i < words.length; i++)
111
+ pairs.add(`${words[i - 1]} ${words[i]}`);
112
+ }
113
+ }
114
+ return pairs;
115
+ }
116
+
117
+ /**
118
+ * The terms of an idea that name its frame: "modal" in "in a modal". A card, a
119
+ * table or an existing page after "in a" is where a part goes, not a frame.
120
+ * @param {string} query
121
+ * @returns {Set<string>}
122
+ */
123
+ export function containersOf(query) {
124
+ const found = new Set();
125
+ for (const m of String(query).matchAll(CONTAINER_PHRASE)) {
126
+ for (const t of terms(`${m[1]} ${m[2] ?? ''}`)) {
127
+ if (OVERLAY_FRAMES.has(t)) found.add(t);
128
+ }
129
+ }
130
+ return found;
131
+ }
132
+
133
+ /** @param {string} text */
134
+ export const normalized = text =>
135
+ (
136
+ String(text)
137
+ .toLowerCase()
138
+ .match(/[a-z0-9]+/g) ?? []
139
+ ).join(' ');
140
+
141
+ /** Stemmed synonym lookup built from search's vocabulary, both directions. */
142
+ export const SYNONYMS_OF = (() => {
143
+ /** @type {Map<string, Set<string>>} */
144
+ const index = new Map();
145
+ /** @param {string} a @param {string} b */
146
+ const add = (a, b) => {
147
+ const set = index.get(a) ?? new Set();
148
+ set.add(b);
149
+ index.set(a, set);
150
+ };
151
+ for (const [key, values] of Object.entries(SYNONYMS)) {
152
+ const k = stem(key);
153
+ for (const value of values) {
154
+ const v = stem(value);
155
+ add(k, v);
156
+ add(v, k);
157
+ for (const other of values) if (other !== value) add(v, stem(other));
158
+ }
159
+ }
160
+ return index;
161
+ })();
162
+
163
+ /**
164
+ * @typedef {import('../_adapter.mjs').PageTemplate} PageTemplate
165
+ * @typedef {import('../_adapter.mjs').ComponentWords} ComponentWords
166
+ * @typedef {{name: string, score: number, hits: number, familyNamed: boolean, containerMatched: boolean, family: string, base: boolean, matched: Set<string>}} RankedPage
167
+ * @typedef {'page' | 'part' | 'edit'} IdeaKind
168
+ */
169
+
170
+ /** @param {PageTemplate} page */
171
+ const familyOf = page => (page.category.split(' - ')[0] ?? '').trim();
172
+
173
+ /**
174
+ * Family words, from the templates' own ids: a word in the ids of two or more
175
+ * templates of one family, plus the family's name when a template carries it.
176
+ * Each word maps to its family.
177
+ * @param {PageTemplate[]} pages
178
+ * @returns {Map<string, string>}
179
+ */
180
+ export function familyWordsOf(pages) {
181
+ /** @type {Map<string, Map<string, number>>} */
182
+ const idWords = new Map();
183
+ for (const page of pages) {
184
+ const family = familyOf(page);
185
+ const counts = idWords.get(family) ?? new Map();
186
+ for (const w of new Set(page.name.split('-'))) {
187
+ if (w.length >= 3) counts.set(stem(w), (counts.get(stem(w)) ?? 0) + 1);
188
+ }
189
+ idWords.set(family, counts);
190
+ }
191
+ /** @type {Map<string, string>} */
192
+ const familyOfWord = new Map();
193
+ for (const [family, counts] of idWords) {
194
+ const heads = [...counts].filter(([, c]) => c >= 2).map(([w]) => w);
195
+ const name = normalized(family).split(' ').pop() ?? '';
196
+ if (name.length >= 3 && counts.has(stem(name))) heads.push(stem(name));
197
+ for (const head of heads) {
198
+ if (!familyOfWord.has(head)) familyOfWord.set(head, family);
199
+ }
200
+ }
201
+ return familyOfWord;
202
+ }
@@ -0,0 +1,20 @@
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 BUILD_TREE: import("../../foundation/route/route.mjs").Tree<BuildRequest, BuildContext, import("./build.type.mjs").BuildKitResponse>;
5
+ export type BuildRequest = {
6
+ query: string;
7
+ options: NonNullable<Parameters<typeof kitSignals>[1]>;
8
+ };
9
+ export type BuildContext = {
10
+ /**
11
+ * the root's weights
12
+ */
13
+ weights: import("../../foundation/route/route.mjs").ClassifierWeights;
14
+ /**
15
+ * the block picker's weights; null: build names its own blocks
16
+ */
17
+ blocks: import("./blocks/blocks.mjs").BlockPickerWeights | null;
18
+ };
19
+ export type BuildAnswer = import("./build.type.mjs").BuildKitResponse;
20
+ import { kitSignals } from './kit/signals.mjs';
@@ -0,0 +1,52 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file How build answers: a root classifier sends each request to the
5
+ * searcher that should answer it. Its weights (tree.weights.json) have one
6
+ * class today, so every request goes to the main searcher: the kit's signals
7
+ * (kit/signals.mjs), the block picker's blocks (blocks/blocks.mjs), and the
8
+ * kit around them (kit/answer.mjs).
9
+ * @input {query, options} and the data build's adapter gathered: the root's
10
+ * weights and the block picker's
11
+ * @output the build.kit response
12
+ * @position api/build; build.mjs runs it with the shared route helper
13
+ */
14
+
15
+ import {routeKit} from '../../foundation/route/route.mjs';
16
+ import {kitAnswer} from './kit/answer.mjs';
17
+ import {kitSignals} from './kit/signals.mjs';
18
+ import {pickBlocks} from './blocks/blocks.mjs';
19
+
20
+ /**
21
+ * @typedef {{query: string, options: NonNullable<Parameters<typeof kitSignals>[1]>}} BuildRequest
22
+ * @typedef {object} BuildContext
23
+ * @property {import('../../foundation/route/route.mjs').ClassifierWeights} weights the root's weights
24
+ * @property {import('./blocks/blocks.mjs').BlockPickerWeights | null} blocks the block picker's weights; null: build names its own blocks
25
+ * @typedef {import('./build.type.mjs').BuildKitResponse} BuildAnswer
26
+ */
27
+
28
+ /** @type {import('../../foundation/route/route.mjs').RouteKit<BuildRequest, BuildContext, BuildAnswer>} */
29
+ const {defineSearcher, defineClassifier, defineTree} = routeKit;
30
+
31
+ const main = defineSearcher({
32
+ id: 'main',
33
+ summary:
34
+ 'Ranks page templates, blocks, and components by the words of the request, starts from the best page template, and lists its blocks in the order the block picker ranks them.',
35
+ run: async (request, ctx) => {
36
+ const signals = await kitSignals(request.query, request.options);
37
+ if (!ctx.blocks) return kitAnswer(signals);
38
+ return kitAnswer(signals, pickBlocks(request.query, signals, ctx.blocks));
39
+ },
40
+ });
41
+
42
+ const root = defineClassifier({
43
+ id: 'root',
44
+ summary: 'Classifies the request and hands it to the searcher for its class.',
45
+ searchers: {},
46
+ fallback: 'main',
47
+ floor: 1,
48
+ features: () => ['#bias'],
49
+ weights: ctx => ctx.weights,
50
+ });
51
+
52
+ export const BUILD_TREE = defineTree('root', [root, main]);
@@ -0,0 +1,177 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file build's tree end to end, offline: the network and subprocesses throw,
5
+ * and build still answers. The block picker changes only the kit's blocks:
6
+ * the start, its alternatives, and every other field stay build's own, the
7
+ * block count is build's own, and the blocks it names are pinned per phrase.
8
+ */
9
+
10
+ import * as path from 'node:path';
11
+ import {fileURLToPath} from 'node:url';
12
+ import {afterAll, beforeAll, describe, expect, it, vi} from 'vitest';
13
+
14
+ // vi.mock calls are hoisted above the imports; the stub maker is hoisted with them.
15
+ const {blocked} = vi.hoisted(() => ({
16
+ /** @param {string} name */
17
+ blocked: name => async (/** @type {() => Promise<any>} */ load) => {
18
+ const real = await load();
19
+ /** @type {Record<string, unknown>} */
20
+ const stub = {};
21
+ for (const [key, value] of Object.entries(real)) {
22
+ stub[key] =
23
+ typeof value === 'function'
24
+ ? () => {
25
+ throw new Error(`build reached ${name}.${key}`);
26
+ }
27
+ : value;
28
+ }
29
+ return {...stub, default: stub};
30
+ },
31
+ }));
32
+ vi.mock('node:http', blocked('http'));
33
+ vi.mock('node:https', blocked('https'));
34
+ vi.mock('node:net', blocked('net'));
35
+ vi.mock('node:dns', blocked('dns'));
36
+ vi.mock('node:child_process', blocked('child_process'));
37
+ vi.mock('http', blocked('http'));
38
+ vi.mock('https', blocked('https'));
39
+ vi.mock('net', blocked('net'));
40
+ vi.mock('dns', blocked('dns'));
41
+ vi.mock('child_process', blocked('child_process'));
42
+
43
+ const {build} = await import('./build.mjs');
44
+ const {buildKit} = await import('./kit/kit.mjs');
45
+ const {loadBlockPicker, withListingCache} = await import('./_adapter.mjs');
46
+
47
+ const REPO = path.resolve(
48
+ path.dirname(fileURLToPath(import.meta.url)),
49
+ '../../../..',
50
+ );
51
+
52
+ /**
53
+ * Hand-written phrases and the blocks the picker names for each. The rest of
54
+ * the answer carries search results, which change whenever the docs do, so it
55
+ * is compared with build's own kit rather than pinned.
56
+ * @type {[string, string[]][]}
57
+ */
58
+ const PINNED = [
59
+ [
60
+ 'analytics dashboard with a KPI row and a sortable table',
61
+ [
62
+ 'PaginationPageSize',
63
+ 'GridDashboardLayout',
64
+ 'TableBulkActionsTable',
65
+ 'TableGridDividersTable',
66
+ 'TableRowExpansionTable',
67
+ ],
68
+ ],
69
+ [
70
+ 'settings page with notification toggles and a save bar',
71
+ [
72
+ 'ToggleButtonColor',
73
+ 'ToggleButtonGroup',
74
+ 'ToggleButtonGroupShowcase',
75
+ 'ToggleButtonGroupVertical',
76
+ 'ToggleButtonIconSwap',
77
+ ],
78
+ ],
79
+ [
80
+ 'empty state for a question bank with an import button',
81
+ [
82
+ 'EmptyStateActions',
83
+ 'EmptyStateCompact',
84
+ 'EmptyStateContainer',
85
+ 'EmptyStateShowcase',
86
+ 'ToolbarTableFilter',
87
+ ],
88
+ ],
89
+ [
90
+ 'login form with single sign-on buttons',
91
+ [
92
+ 'CardWithInnerLayout',
93
+ 'BottomSheetMobileKeyboard',
94
+ 'ButtonGroupBasic',
95
+ 'CollapsibleControlledAccordion',
96
+ 'BottomSheetSwitcherReviewFlow',
97
+ ],
98
+ ],
99
+ [
100
+ 'calendar for booking appointments',
101
+ [
102
+ 'CalendarRangeWithValue',
103
+ 'CalendarSingle',
104
+ 'CalendarTwoMonths',
105
+ 'CalendarConstraints',
106
+ 'CalendarShowcase',
107
+ ],
108
+ ],
109
+ [
110
+ 'kanban board for sprint tasks with a filter bar',
111
+ ['ToolbarTableFilter', 'PowerSearchShowcase'],
112
+ ],
113
+ ];
114
+
115
+ /** @type {typeof globalThis.fetch} */
116
+ let realFetch;
117
+ beforeAll(() => {
118
+ realFetch = globalThis.fetch;
119
+ globalThis.fetch = () => {
120
+ throw new Error('build reached fetch');
121
+ };
122
+ });
123
+ afterAll(() => {
124
+ globalThis.fetch = realFetch;
125
+ });
126
+
127
+ describe('build tree', () => {
128
+ it('keeps the blocks a request names in build’s slots', async () => {
129
+ const phrase = 'calendar for booking appointments';
130
+ const answer = await build(phrase, {cwd: REPO});
131
+ const own = await withListingCache(() => buildKit(phrase, {cwd: REPO}));
132
+ if (answer?.type !== 'build.kit' || own.type !== 'build.kit')
133
+ throw new Error('expected a kit');
134
+ own.data.blocks.forEach((b, k) => {
135
+ if (b.name.startsWith('Calendar'))
136
+ expect(answer.data.blocks[k].name).toBe(b.name);
137
+ });
138
+ });
139
+
140
+ it('runs with the network and subprocesses blocked', async () => {
141
+ const cp = await import('node:child_process');
142
+ const https = await import('node:https');
143
+ expect(() => cp.execFileSync('true')).toThrow(/build reached/);
144
+ expect(() => https.request('https://example.com')).toThrow(/build reached/);
145
+ expect(() => globalThis.fetch('https://example.com')).toThrow(
146
+ /build reached/,
147
+ );
148
+ });
149
+
150
+ it('loads the block picker weights as plain numbers', () => {
151
+ expect(loadBlockPicker()).not.toBeNull();
152
+ });
153
+
154
+ for (const [phrase, blocks] of PINNED) {
155
+ it(`answers "${phrase}" offline with the picker's blocks`, async () => {
156
+ const answer = await build(phrase, {cwd: REPO});
157
+ const own = await withListingCache(() => buildKit(phrase, {cwd: REPO}));
158
+ if (answer?.type !== 'build.kit' || own.type !== 'build.kit')
159
+ throw new Error('expected a kit');
160
+ // Only the blocks move: the start, its alternatives, and every other
161
+ // field are build's own.
162
+ expect({...answer.data, blocks: null}).toEqual({
163
+ ...own.data,
164
+ blocks: null,
165
+ });
166
+ expect(answer.data.start?.alternatives).toEqual(
167
+ own.data.start?.alternatives,
168
+ );
169
+ // The picker only re-orders build's own blocks: each keeps build's
170
+ // entry (its real score and reason), none is added, none is dropped.
171
+ expect(answer.data.blocks.length).toBe(own.data.blocks.length);
172
+ for (const b of answer.data.blocks)
173
+ expect(own.data.blocks.find(o => o.name === b.name)).toEqual(b);
174
+ expect(answer.data.blocks.map(b => b.name)).toEqual(blocks);
175
+ });
176
+ }
177
+ });
@@ -0,0 +1 @@
1
+ {"classes": ["main"], "tokens": {"#bias": [0]}}
@@ -461,8 +461,22 @@ async function componentRead(name, options = {}) {
461
461
  return componentDetailBlocks(dirName, cwd, owner.package);
462
462
  }
463
463
  const loaded = await loadComponentDoc(owner.docPath, docOpts);
464
- // A family member an integration lists under its own name reads as that
465
- // member, the way a Core sub-component does below.
464
+ // A family member reads as that member, scoped the way the unscoped
465
+ // lookup scopes it: an integration member by its own entry, a Core
466
+ // sub-component (HStack in the Stack doc) by scopeSubComponent.
467
+ const sub =
468
+ scoped.kind === 'core'
469
+ ? scopeSubComponent(loaded, dirName, coreDir)
470
+ : null;
471
+ if (sub) {
472
+ if (props) {
473
+ return componentDetailProps(
474
+ {props: sub.matchingComponent.props},
475
+ owner.package,
476
+ );
477
+ }
478
+ return componentDetail(sub.scoped, owner, dirName, coreDir);
479
+ }
466
480
  const docs =
467
481
  scoped.kind === 'integration'
468
482
  ? scopeIntegrationMember(loaded, owner, dirName)
@@ -135,6 +135,21 @@ describe('component list — components whose docs live in a family folder', ()
135
135
  });
136
136
  }, SLOW);
137
137
 
138
+ it('answers a Core-scoped lookup of a member with that member, as the unscoped lookup does', async () => {
139
+ for (const name of ['HStack', 'VStack']) {
140
+ const unscoped = await component(name, {cwd});
141
+ const scoped = await component(name, {
142
+ cwd,
143
+ package: '@astryxdesign/core',
144
+ });
145
+ expect(scoped.data.name).toBe(name);
146
+ expect(scoped.data.parentDoc).toBe('Stack');
147
+ expect(scoped.data.import).toBe(`@astryxdesign/core/${name}`);
148
+ expect(scoped.data.props).toEqual(unscoped.data.props);
149
+ expect(scoped.data.props.length).toBeGreaterThan(0);
150
+ }
151
+ }, SLOW);
152
+
138
153
  it('prints each one\'s own import path in the list, the detail, and search', async () => {
139
154
  // The path `component <Name>` prints is the one docs and examples teach:
140
155
  // the component's own entry point, never its family's (/Stack, /Text) or
@@ -25,6 +25,7 @@ import {
25
25
  availableComputerIntegrations,
26
26
  } from '../../foundation/config/project.mjs';
27
27
  import {isOutsideProject} from '../../foundation/env/project-scope.mjs';
28
+ import {hostedRun} from '../../foundation/runtime/package-root.mjs';
28
29
  import {
29
30
  scanAllPackages,
30
31
  findComponentInPackages,
@@ -242,6 +243,7 @@ export async function describeInstalled(project, scanned) {
242
243
  * @returns {boolean}
243
244
  */
244
245
  export function hasSources(project) {
246
+ if (doorSources().length > 0) return true;
245
247
  if (project == null) return false;
246
248
  if (typeof project.config?.discover === 'function') return true;
247
249
  return project.loadedIntegrations.some(
@@ -249,6 +251,19 @@ export function hasSources(project) {
249
251
  );
250
252
  }
251
253
 
254
+ /** Where a discover source a host passed comes from (spec AST-081 FR1). */
255
+ const DOOR_SOURCE = 'the host';
256
+
257
+ /**
258
+ * The discover sources the host that started this run passed, if any.
259
+ * @returns {readonly DiscoverSource[]}
260
+ */
261
+ function doorSources() {
262
+ return /** @type {readonly DiscoverSource[]} */ (
263
+ hostedRun()?.discoverSources ?? []
264
+ );
265
+ }
266
+
252
267
  /** The source name of a package installed for the computer. */
253
268
  const COMPUTER_SOURCE = 'installed for this computer';
254
269
 
@@ -479,7 +494,8 @@ export function defaultCacheDir() {
479
494
 
480
495
  /**
481
496
  * @param {string} cacheDir
482
- * @param {string} from
497
+ * @param {string} from the source's key: its `from` label, with the host for
498
+ * a host's source
483
499
  * @param {{package?: string, version?: string}} request
484
500
  */
485
501
  function savedCopyPath(cacheDir, from, request) {
@@ -557,8 +573,9 @@ async function callSource(source, request, label, timeoutMs) {
557
573
  }
558
574
 
559
575
  /**
560
- * Call the project's own discover sources — the project's `discover` first,
561
- * then each integration's in load order — once per distinct function. A source that
576
+ * Call the discover sources — the project's `discover` first, then each
577
+ * integration's in load order, then each a host passed, named as the host's
578
+ * (spec AST-081 FR1, AST-051 FR6) — once per distinct function. A source that
562
579
  * throws, times out, or returns an invalid catalog falls back to its saved copy;
563
580
  * with none, it is reported and adds nothing. The others always count.
564
581
  *
@@ -572,20 +589,30 @@ async function callOwnSources(project, request = {}, options = {}) {
572
589
  const {cacheDir = defaultCacheDir(), timeoutMs = SOURCE_TIMEOUT_MS} = options;
573
590
  /** @type {CatalogResult} */
574
591
  const result = {sources: [], packages: []};
575
- if (project == null) return result;
592
+ const fromDoor = doorSources();
593
+ if (project == null && fromDoor.length === 0) return result;
576
594
 
577
- /** @type {Array<{from: string, source?: DiscoverSource, error?: string}>} */
595
+ /** @type {Array<{from: string, key?: string, source?: DiscoverSource, error?: string}>} */
578
596
  const origins = [];
579
- if (typeof project.config?.discover === 'function') {
597
+ if (typeof project?.config?.discover === 'function') {
580
598
  origins.push({from: 'astryx.config', source: project.config.discover});
581
599
  }
582
- for (const integration of project.loadedIntegrations) {
600
+ for (const integration of project?.loadedIntegrations ?? []) {
583
601
  if (integration.__discoverError) {
584
602
  origins.push({from: integration.name, error: integration.__discoverError});
585
603
  } else if (integration.__discover) {
586
604
  origins.push({from: integration.name, source: integration.__discover});
587
605
  }
588
606
  }
607
+ // Every tool that carries the CLI reports its sources as the host's, so
608
+ // their saved copies are kept per host, by the root that holds its CLI
609
+ // (spec:AST-051 FR8).
610
+ const host = hostedRun()?.startingRoot ?? '';
611
+ fromDoor.forEach((source, index) => {
612
+ const from =
613
+ fromDoor.length === 1 ? DOOR_SOURCE : `${DOOR_SOURCE} (${index + 1})`;
614
+ origins.push({from, key: `${from}\n${host}`, source});
615
+ });
589
616
 
590
617
  /** @type {Set<unknown>} */
591
618
  const called = new Set();
@@ -594,7 +621,7 @@ async function callOwnSources(project, request = {}, options = {}) {
594
621
  const ask = request.package
595
622
  ? {package: request.package, ...(request.version ? {version: request.version} : {})}
596
623
  : {};
597
- for (const {from, source, error} of origins) {
624
+ for (const {from, key = from, source, error} of origins) {
598
625
  if (source == null) {
599
626
  result.sources.push({name: from, from, status: 'failed', error});
600
627
  continue;
@@ -603,7 +630,7 @@ async function callOwnSources(project, request = {}, options = {}) {
603
630
  called.add(source);
604
631
 
605
632
  const label = `the discover source from ${from}`;
606
- const file = savedCopyPath(cacheDir, from, ask);
633
+ const file = savedCopyPath(cacheDir, key, ask);
607
634
  /** @type {import('../../authoring/discover/type').DiscoverCatalog} */
608
635
  let catalog;
609
636
  try {
@@ -21,6 +21,10 @@ import {
21
21
  withDeclaredOfficial,
22
22
  } from './_adapter.mjs';
23
23
  import {parseDiscoverCatalog} from '../../authoring/discover/parse.mjs';
24
+ import {
25
+ _resetHostedRun,
26
+ activateHostedRun,
27
+ } from '../../foundation/runtime/package-root.mjs';
24
28
 
25
29
  /** No official packages, so a test sees only its own sources after it. */
26
30
  const NO_OFFICIAL = {packages: []};
@@ -230,6 +234,44 @@ describe('callSources', () => {
230
234
  expect(otherRequest.sources[0].status).toBe('failed');
231
235
  });
232
236
 
237
+ it("keeps a host's saved copies per host, so one tool never answers with another's", async () => {
238
+ let online = true;
239
+ const source = async () => {
240
+ if (!online) throw new Error('offline');
241
+ return catalog('Acme catalog', [pkg('@acme/one')]);
242
+ };
243
+ const options = {cacheDir, official: NO_OFFICIAL};
244
+ try {
245
+ activateHostedRun({
246
+ discoverSources: [source],
247
+ startingRoot: '/tools/acme',
248
+ });
249
+ await callSources(undefined, {}, options);
250
+ online = false;
251
+ const sameHost = await callSources(undefined, {}, options);
252
+ expect(sameHost.sources[0]).toMatchObject({
253
+ name: 'Acme catalog',
254
+ from: 'the host',
255
+ status: 'saved',
256
+ });
257
+
258
+ _resetHostedRun();
259
+ activateHostedRun({
260
+ discoverSources: [source],
261
+ startingRoot: '/tools/other',
262
+ });
263
+ const otherHost = await callSources(undefined, {}, options);
264
+ expect(otherHost.sources[0]).toMatchObject({
265
+ from: 'the host',
266
+ status: 'failed',
267
+ error: 'offline',
268
+ });
269
+ expect(otherHost.packages).toEqual([]);
270
+ } finally {
271
+ _resetHostedRun();
272
+ }
273
+ });
274
+
233
275
  it('keeps the saved copy in the cache directory, not in the project', async () => {
234
276
  await callSources(
235
277
  project({