@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
@@ -4,20 +4,27 @@
4
4
  * @file build API — dispatcher + barrel for the "assemble a page" assistant.
5
5
  *
6
6
  * `build()` with no query signals the workflow playbook (`build.help`). With a
7
- * query it runs the unified search and groups the results into a composition
8
- * KIT (`build.kit`). Each shape is projected by its own leaf; this file only
7
+ * query it runs build's tree (tree.mjs), whose searchers answer with a
8
+ * composition KIT (`build.kit`). Each shape is projected by its own leaf; this file only
9
9
  * routes to the leaves and re-exports them, keeping the SAME `build` export so
10
10
  * api/index.mjs and the CLI consumer stay unchanged.
11
11
  *
12
12
  * no query → build.help (api/build/help/help.mjs)
13
- * a query → build.kit (api/build/kit/kit.mjs)
13
+ * a query → build.kit (api/build/tree.mjs: kit/signals.mjs, the block
14
+ * picker in blocks/blocks.mjs, then kit/answer.mjs)
14
15
  */
15
16
 
16
17
  import {AstryxError} from '../error.mjs';
17
18
  import {ERROR_CODES} from '../../foundation/response/error-codes.mjs';
18
19
  import {buildHelp} from './help/help.mjs';
19
20
  import {buildKit} from './kit/kit.mjs';
20
- import {withListingCache} from './_adapter.mjs';
21
+ import {
22
+ loadBlockPicker,
23
+ loadTreeWeights,
24
+ withListingCache,
25
+ } from './_adapter.mjs';
26
+ import {runTree} from '../../foundation/route/route.mjs';
27
+ import {BUILD_TREE} from './tree.mjs';
21
28
 
22
29
  // Barrel: surface the leaves alongside the dispatcher for the fractal shape.
23
30
  export {buildHelp, buildKit};
@@ -34,7 +41,10 @@ export async function build(query, options = {}) {
34
41
  if (!query || !String(query).trim()) {
35
42
  return buildHelp();
36
43
  }
37
- if (options.limit != null && (!Number.isInteger(options.limit) || options.limit <= 0)) {
44
+ if (
45
+ options.limit != null &&
46
+ (!Number.isInteger(options.limit) || options.limit <= 0)
47
+ ) {
38
48
  throw new AstryxError(
39
49
  `Invalid limit "${options.limit}". Must be a positive integer.`,
40
50
  undefined,
@@ -43,5 +53,14 @@ export async function build(query, options = {}) {
43
53
  }
44
54
  // The kit resolves every candidate's source and docs by walking the same
45
55
  // tree; read each directory once for this call.
46
- return withListingCache(() => buildKit(query, options));
56
+ return withListingCache(
57
+ async () =>
58
+ /** @type {import('./build.type.mjs').BuildKitResponse} */ (
59
+ await runTree(
60
+ BUILD_TREE,
61
+ {query, options},
62
+ {weights: loadTreeWeights(), blocks: loadBlockPicker()},
63
+ )
64
+ ),
65
+ );
47
66
  }
@@ -0,0 +1,14 @@
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 kit for a request. `blocks` re-orders the blocks build names itself:
6
+ * each keeps build's own entry (its score and reason), and a name build did
7
+ * not give is dropped, so the kit never lists more blocks than build's own.
8
+ * Blocks of the same name from two packages are both kept.
9
+ *
10
+ * @param {import('./signals.mjs').KitSignals} signals
11
+ * @param {string[]} [blocks] block names; build's own when omitted
12
+ * @returns {import('../build.type.mjs').BuildKitResponse}
13
+ */
14
+ export function kitAnswer(signals: import("./signals.mjs").KitSignals, blocks?: string[]): import("../build.type.mjs").BuildKitResponse;
@@ -0,0 +1,85 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file The build.kit response, from a request's signals (signals.mjs) and the
5
+ * blocks to name. With build's own blocks it is the kit build has always
6
+ * given; another list changes only the blocks, never their count.
7
+ * @input {@link import('./signals.mjs').KitSignals} and block names
8
+ * @output the build.kit response
9
+ * @position api/build/kit, beside signals.mjs; the main searcher in tree.mjs
10
+ * calls it with the block picker's list.
11
+ */
12
+
13
+ import {FOUNDATION, FRAME} from './signals.mjs';
14
+
15
+ /**
16
+ * Fewer offerable results than this and the kit says how to look further.
17
+ *
18
+ * Three is the point below which a kit stops being a starting point. An agent
19
+ * that reads a near-empty kit does not conclude "my wording was wrong" — it
20
+ * concludes the package has nothing and falls back on its own memory of what
21
+ * Astryx contains, which is exactly the failure `build` exists to prevent.
22
+ */
23
+ const THIN_KIT = 3;
24
+
25
+ /**
26
+ * The kit for a request. `blocks` re-orders the blocks build names itself:
27
+ * each keeps build's own entry (its score and reason), and a name build did
28
+ * not give is dropped, so the kit never lists more blocks than build's own.
29
+ * Blocks of the same name from two packages are both kept.
30
+ *
31
+ * @param {import('./signals.mjs').KitSignals} signals
32
+ * @param {string[]} [blocks] block names; build's own when omitted
33
+ * @returns {import('../build.type.mjs').BuildKitResponse}
34
+ */
35
+ export function kitAnswer(signals, blocks = signals.kitBlocks) {
36
+ // Entries by name, in build's order: two packages can ship a block with the
37
+ // same name, and each name in `blocks` takes the next entry of that name, so
38
+ // both stay listed.
39
+ /** @type {Map<string, import('../build.type.mjs').BuildKitResponse['data']['blocks']>} */
40
+ const own = new Map();
41
+ for (const b of signals.blocks)
42
+ own.set(b.name, [...(own.get(b.name) ?? []), b]);
43
+ const named = blocks.flatMap(name => {
44
+ const entry = own.get(name)?.shift();
45
+ return entry ? [entry] : [];
46
+ });
47
+
48
+ // What to try when the kit comes back thin. Keyword search over a design
49
+ // system misses in a predictable way — the reader's words and the package's
50
+ // often do not overlap — so name the two commands that browse rather than
51
+ // search, and say plainly that this is not semantic matching.
52
+ //
53
+ // STRUCTURED, not prose: `commands` are bare subcommands, because the API
54
+ // cannot know how the caller invokes the CLI. Baking `astryx component
55
+ // --list` into the text hands a pnpm-workspace reader a command that does
56
+ // not resolve — the same defect `getCliInvocation` exists to prevent, and
57
+ // the renderer applies it. A JSON caller gets the parts, not a sentence.
58
+ const hint =
59
+ signals.pages.length + named.length + signals.domain.length < THIN_KIT
60
+ ? {
61
+ reason:
62
+ 'Few matches. This is keyword search, not semantic — try other wordings.',
63
+ commands: ['component --list', 'template --list'],
64
+ }
65
+ : undefined;
66
+
67
+ return {
68
+ type: 'build.kit',
69
+ data: {
70
+ query: signals.query,
71
+ // Distinguishes "search found nothing" from a weak-but-non-empty result
72
+ // set. Either way the kit still names a template to start from.
73
+ hasResults: signals.matchCount > 0,
74
+ matchCount: signals.matchCount,
75
+ directMatch: signals.directMatch,
76
+ start: signals.start,
77
+ pages: signals.pages,
78
+ blocks: named,
79
+ domain: signals.domain,
80
+ frame: FRAME,
81
+ foundation: FOUNDATION,
82
+ hint,
83
+ },
84
+ };
85
+ }
@@ -0,0 +1,38 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {describe, it, expect} from 'vitest';
4
+ import {kitAnswer} from './answer.mjs';
5
+
6
+ /** @param {any[]} blocks */
7
+ const signals = blocks =>
8
+ /** @type {any} */ ({
9
+ query: 'q',
10
+ matchCount: blocks.length,
11
+ directMatch: false,
12
+ start: null,
13
+ pages: [],
14
+ blocks,
15
+ kitBlocks: blocks.map(b => b.name),
16
+ domain: [],
17
+ });
18
+
19
+ describe('kitAnswer', () => {
20
+ const a = {name: 'Hero', package: '@a/one', score: 90, reason: 'name'};
21
+ const b = {name: 'Hero', package: '@b/two', score: 80, reason: 'name'};
22
+ const c = {name: 'Footer', package: '@a/one', score: 70, reason: 'keyword'};
23
+
24
+ it('keeps blocks of the same name from two packages', () => {
25
+ const kit = kitAnswer(signals([a, b, c]));
26
+ expect(kit.data.blocks).toEqual([a, b, c]);
27
+ });
28
+
29
+ it('keeps both when the picker re-orders them', () => {
30
+ const kit = kitAnswer(signals([a, b, c]), ['Footer', 'Hero', 'Hero']);
31
+ expect(kit.data.blocks).toEqual([c, a, b]);
32
+ });
33
+
34
+ it('never lists a block build did not give, or more than it gave', () => {
35
+ const kit = kitAnswer(signals([a, c]), ['Hero', 'Hero', 'Nav', 'Footer']);
36
+ expect(kit.data.blocks).toEqual([a, c]);
37
+ });
38
+ });
@@ -0,0 +1,25 @@
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
+ * What an idea asks for (spec:AST-048/FR3): a whole `page`; a `part` of one,
6
+ * when its head noun names one of the system's components and it lists fewer
7
+ * than PAGE_PIECES pieces; or an `edit` of a page the builder already has.
8
+ *
9
+ * @param {string} query
10
+ * @param {PageTemplate[]} pages
11
+ * @param {ComponentWords[]} components
12
+ * @returns {IdeaKind}
13
+ */
14
+ export function ideaKind(query: string, pages: PageTemplate[], components: ComponentWords[]): IdeaKind;
15
+ /**
16
+ * Whether an idea asks for a new page (see `asksNewPage`), from the idea and
17
+ * the project's page templates.
18
+ * @param {string} query
19
+ * @param {PageTemplate[]} pages
20
+ * @returns {boolean}
21
+ */
22
+ export function asksForNewPage(query: string, pages: PageTemplate[]): boolean;
23
+ export type PageTemplate = import("./words.mjs").PageTemplate;
24
+ export type ComponentWords = import("./words.mjs").ComponentWords;
25
+ export type IdeaKind = import("./words.mjs").IdeaKind;
@@ -0,0 +1,170 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Whether a build idea asks for a page, a part of a page, or a change to
5
+ * a page the builder already has.
6
+ *
7
+ * Parts (spec:AST-048/FR3). "A date range picker" asks for a part of a page,
8
+ * not a page, and the system's own components say what a part is: an idea
9
+ * whose head noun is a word of a component's name or keywords, Core's or an
10
+ * integration's, asks for that part, unless the noun is a family word ("a data
11
+ * table") or the idea lists a page's worth of pieces. Words search drops as
12
+ * stopwords ("page", "screen", "app", "view") name no component, so "a calendar
13
+ * page" asks for a page and "a calendar" for a part. A part starts from the
14
+ * base template of the family the idea names ("an empty state for a settings
15
+ * page"), else from the app shell. An idea that changes a page or part the
16
+ * builder already has starts from the app shell too: that page is the
17
+ * builder's to keep (FR9), and no template scaffolds it. "Existing" marks one
18
+ * when its phrase names a page family or a component ("add a column to the
19
+ * existing table"), unless the idea asks for a new page ("a new dashboard like
20
+ * the existing dashboard", "clone the existing dashboard as a new page"). A
21
+ * bare "existing" phrase that names a page ("existing reports dashboard with a
22
+ * date filter") reads as the builder's own page.
23
+ */
24
+
25
+ import {stem} from '../../search/search.mjs';
26
+ import {
27
+ HEAD_END,
28
+ CONTAINER_PHRASE,
29
+ PHRASE_END,
30
+ terms,
31
+ containersOf,
32
+ familyWordsOf,
33
+ } from './words.mjs';
34
+
35
+ /**
36
+ * @typedef {import('./words.mjs').PageTemplate} PageTemplate
37
+ * @typedef {import('./words.mjs').ComponentWords} ComponentWords
38
+ * @typedef {import('./words.mjs').IdeaKind} IdeaKind
39
+ */
40
+
41
+ /**
42
+ * How many listed pieces make an idea a page: a part is one thing with a detail
43
+ * or two ("a status pill with a tooltip"), while three pieces, two commas
44
+ * apart, describe a page.
45
+ */
46
+ const PAGE_PIECES = 3;
47
+
48
+ /**
49
+ * The word that marks a page or part the builder already has, when its phrase
50
+ * names one (see `changesExistingPage`).
51
+ */
52
+ const EXISTING = /\bexisting\b/i;
53
+
54
+ /**
55
+ * The word that marks an idea asking for a new page ("a new dashboard", "as a
56
+ * new page"), which is never a change to one the builder has.
57
+ */
58
+ const NEW = /\bnew\b/;
59
+
60
+ /**
61
+ * What an idea asks for (spec:AST-048/FR3): a whole `page`; a `part` of one,
62
+ * when its head noun names one of the system's components and it lists fewer
63
+ * than PAGE_PIECES pieces; or an `edit` of a page the builder already has.
64
+ *
65
+ * @param {string} query
66
+ * @param {PageTemplate[]} pages
67
+ * @param {ComponentWords[]} components
68
+ * @returns {IdeaKind}
69
+ */
70
+ export function ideaKind(query, pages, components) {
71
+ const text = String(query);
72
+ const familyWords = familyWordsOf(pages);
73
+ if (changesExistingPage(text, familyWords, components)) return 'edit';
74
+ // The head's last word, page words included: "calendar" is the noun of "a
75
+ // calendar", "page" the noun of "a calendar page". An overlay frame names
76
+ // where the thing lives, not the thing: "drafts" is the noun of "drafts in
77
+ // a modal".
78
+ const head = (text.split(HEAD_END)[0] ?? '').replace(CONTAINER_PHRASE, m =>
79
+ containersOf(m).size > 0 ? ' ' : m,
80
+ );
81
+ const words = (head.toLowerCase().match(/[a-z0-9]+/g) ?? []).filter(
82
+ w => w.length >= 2,
83
+ );
84
+ const noun = stem(words[words.length - 1] ?? '');
85
+ if (familyWords.has(noun)) return 'page';
86
+ const pieces = (text.match(/,/g) ?? []).length + 1;
87
+ if (pieces >= PAGE_PIECES) return 'page';
88
+ return components.some(c => componentTerms(c).includes(noun))
89
+ ? 'part'
90
+ : 'page';
91
+ }
92
+
93
+ /**
94
+ * Whether an idea changes a page or part the builder already has: "existing"
95
+ * in the same phrase as a word that names a family of page templates or one of
96
+ * the system's components ("the existing incidents table", "the existing
97
+ * banner"). A new page "inspired by the existing one", or "existing users" on a
98
+ * new page, names neither, and an idea that asks for a new page is no change
99
+ * whatever it names.
100
+ * @param {string} text
101
+ * @param {Map<string, string>} familyWords
102
+ * @param {ComponentWords[]} components
103
+ * @returns {boolean}
104
+ */
105
+ function changesExistingPage(text, familyWords, components) {
106
+ const componentWords = new Set(components.flatMap(nameTerms));
107
+ const phrases = text.toLowerCase().split(PHRASE_END);
108
+ if (asksNewPage(phrases, familyWords)) return false;
109
+ return phrases.some(phrase => {
110
+ const at = phrase.search(EXISTING);
111
+ return (
112
+ at >= 0 &&
113
+ terms(phrase.slice(at)).some(
114
+ t => familyWords.has(t) || componentWords.has(t),
115
+ )
116
+ );
117
+ });
118
+ }
119
+
120
+ /**
121
+ * Whether an idea asks for a new page: "new" before a word that names a family
122
+ * of page templates ("a new dashboard"), or before only words search drops,
123
+ * such as "page" ("as a new page"). "A new column" asks for a part.
124
+ * @param {string[]} phrases
125
+ * @param {Map<string, string>} familyWords
126
+ * @returns {boolean}
127
+ */
128
+ function asksNewPage(phrases, familyWords) {
129
+ return phrases.some(phrase => {
130
+ const at = phrase.search(NEW);
131
+ if (at < 0) return false;
132
+ const after = terms(phrase.slice(at + 'new'.length));
133
+ return after.length === 0 || familyWords.has(after[0]);
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Whether an idea asks for a new page (see `asksNewPage`), from the idea and
139
+ * the project's page templates.
140
+ * @param {string} query
141
+ * @param {PageTemplate[]} pages
142
+ * @returns {boolean}
143
+ */
144
+ export function asksForNewPage(query, pages) {
145
+ return asksNewPage(
146
+ String(query).toLowerCase().split(PHRASE_END),
147
+ familyWordsOf(pages),
148
+ );
149
+ }
150
+
151
+ /**
152
+ * The words of a component's name: "DateRangeInput" is date, range, input.
153
+ * @param {ComponentWords} component
154
+ * @returns {string[]}
155
+ */
156
+ function nameTerms(component) {
157
+ return terms(component.name.replace(/([a-z0-9])([A-Z])/g, '$1 $2'));
158
+ }
159
+
160
+ /**
161
+ * The terms a component answers to: the words of its name and of its keywords.
162
+ * @param {ComponentWords} component
163
+ * @returns {string[]}
164
+ */
165
+ function componentTerms(component) {
166
+ return [
167
+ ...nameTerms(component),
168
+ ...component.keywords.flatMap(keyword => terms(keyword)),
169
+ ];
170
+ }
@@ -2,7 +2,8 @@
2
2
  // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
3
 
4
4
  /**
5
- * The page template to start from, and the kit around it.
5
+ * The page template to start from, and the kit around it, with the blocks
6
+ * build names itself.
6
7
  *
7
8
  * @param {string} query what you're building (e.g. "analytics dashboard")
8
9
  * @param {{cwd?: string, type?: import('../../search/search.type.mjs').SearchDomain, limit?: number}} [options]
@@ -13,6 +14,3 @@ export function buildKit(query: string, options?: {
13
14
  type?: import("../../search/search.type.mjs").SearchDomain;
14
15
  limit?: number;
15
16
  }): Promise<import("../build.type.mjs").BuildKitResponse>;
16
- export type SearchResultEntry = import("../../search/search.type.mjs").SearchResultEntry;
17
- export type BuildStart = import("../build.type.mjs").BuildStart;
18
- export type PageTemplate = import("../_adapter.mjs").PageTemplate;