@astryxdesign/cli 0.6.5-canary.031021b → 0.6.5-canary.0353bb2

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 (214) hide show
  1. package/README.md +16 -15
  2. package/api/build/_adapter.d.mts +50 -2
  3. package/api/build/_adapter.mjs +98 -10
  4. package/api/build/build.doc.mjs +2 -2
  5. package/api/build/build.test.mjs +111 -2
  6. package/api/build/kit/kit.mjs +98 -22
  7. package/api/build/kit/rank.d.mts +32 -8
  8. package/api/build/kit/rank.mjs +291 -97
  9. package/api/build/kit/rank.test.mjs +231 -48
  10. package/api/build/kit/weights.d.mts +82 -0
  11. package/api/build/kit/weights.json +1 -0
  12. package/api/build/kit/weights.mjs +305 -0
  13. package/api/build/kit/weights.test.mjs +190 -0
  14. package/api/docs/docs.d.mts +6 -0
  15. package/api/docs/docs.depth.test.mjs +132 -0
  16. package/api/docs/docs.doc.mjs +31 -2
  17. package/api/docs/docs.mjs +44 -1
  18. package/api/docs/docs.test.mjs +279 -0
  19. package/api/docs/docs.type.d.mts +38 -1
  20. package/api/docs/docs.type.mjs +18 -1
  21. package/api/docs/integration-tree.test.mjs +572 -0
  22. package/api/docs/integrationDocs.test.mjs +336 -0
  23. package/api/docs/node/node.d.mts +28 -3
  24. package/api/docs/node/node.mjs +146 -23
  25. package/api/error.d.mts +22 -0
  26. package/api/error.mjs +42 -0
  27. package/api/gap-report/gap-report.d.mts +3 -1
  28. package/api/gap-report/gap-report.mjs +12 -2
  29. package/api/gap-report/gap-report.test.mjs +106 -0
  30. package/api/hook/list/list.d.mts +1 -1
  31. package/api/integration/add-contribution.d.mts +2 -1
  32. package/api/integration/add-contribution.mjs +7 -3
  33. package/api/integration/add-contribution.test.mjs +3 -3
  34. package/api/integration/add-theme.mjs +338 -26
  35. package/api/integration/add-theme.test.mjs +314 -1
  36. package/api/integration/authoring-checks.mjs +10 -7
  37. package/api/integration/authoring-checks.type.d.mts +8 -0
  38. package/api/integration/authoring-checks.type.mjs +7 -3
  39. package/api/integration/integration-authoring.type.d.mts +4 -1
  40. package/api/integration/integration-authoring.type.mjs +6 -1
  41. package/api/integration/integrationAdd.doc.mjs +6 -0
  42. package/api/integration/integrationAddTheme.doc.mjs +10 -0
  43. package/api/integration/integrationComponentConflicts.doc.mjs +1 -1
  44. package/api/integration/integrationDocConflicts.doc.mjs +1 -1
  45. package/api/integration/integrationTemplateConflicts.doc.mjs +1 -1
  46. package/api/integration/pack-check.mjs +12 -3
  47. package/api/integration/pack-check.test.mjs +52 -3
  48. package/api/integration/validate-integration.d.mts +4 -2
  49. package/api/integration/validate-integration.mjs +7 -2
  50. package/api/integration/validate-integration.test.mjs +55 -0
  51. package/api/integration/validate-integration.type.d.mts +5 -0
  52. package/api/integration/validate-integration.type.mjs +5 -1
  53. package/api/integration/validateIntegration.doc.mjs +1 -1
  54. package/api/layout/expand/expand.mjs +12 -7
  55. package/api/layout/expand/expand.receipt.test.mjs +74 -0
  56. package/api/layout/layout.type.d.mts +1 -0
  57. package/api/layout/layout.type.mjs +1 -0
  58. package/api/layout/layoutExpand.doc.mjs +1 -1
  59. package/api/search/search.d.mts +36 -5
  60. package/api/search/search.doc.mjs +4 -4
  61. package/api/search/search.mjs +194 -54
  62. package/api/search/search.test.mjs +825 -0
  63. package/api/search/search.type.d.mts +5 -5
  64. package/api/search/search.type.mjs +5 -5
  65. package/api/swizzle/copy/copy.mjs +66 -3
  66. package/api/swizzle/swizzle.doc.mjs +4 -0
  67. package/api/template/copy/copy.mjs +14 -8
  68. package/api/template/copy/copy.receipt.test.mjs +77 -0
  69. package/api/template/show/show.mjs +15 -4
  70. package/api/template/show/show.test.mjs +76 -0
  71. package/api/template/template-integration.test.mjs +14 -0
  72. package/api/template/template.d.mts +1 -1
  73. package/api/template/template.doc.mjs +6 -2
  74. package/api/template/template.mjs +1 -0
  75. package/api/template/template.type.d.mts +2 -0
  76. package/api/template/template.type.mjs +2 -0
  77. package/api/theme/build/build.d.mts +24 -0
  78. package/api/theme/build/build.icon-lineage.test.mjs +117 -0
  79. package/api/theme/build/build.icon-preservation.test.mjs +639 -0
  80. package/api/theme/build/build.mjs +381 -77
  81. package/api/theme/build/build.project-core.test.mjs +165 -0
  82. package/api/theme/build/build.test.mjs +190 -1
  83. package/api/theme/build/icon-imports.d.mts +47 -0
  84. package/api/theme/build/icon-imports.mjs +691 -0
  85. package/api/theme/themeBuild.doc.d.mts +4 -1
  86. package/api/theme/themeBuild.doc.mjs +14 -3
  87. package/api/upgrade/run/run.mjs +20 -3
  88. package/api/upgrade/run/run.test.mjs +45 -1
  89. package/api/upgrade/upgrade.doc.mjs +1 -1
  90. package/api/upgrade/upgrade.type.d.mts +1 -0
  91. package/api/upgrade/upgrade.type.mjs +1 -0
  92. package/assets/docs/icons.doc.mjs +1 -0
  93. package/assets/docs/theme.doc.mjs +2 -1
  94. package/assets/docs/tree/add-a-theme.doc.mjs +4 -0
  95. package/assets/docs/tree/add-a-topic.doc.mjs +1 -1
  96. package/assets/docs/tree/check-your-docs.doc.mjs +2 -2
  97. package/assets/docs/tree/checks.doc.mjs +1 -1
  98. package/assets/docs/tree/debug-and-gap-reports.doc.mjs +1 -1
  99. package/assets/docs/tree/define-the-theme.doc.mjs +1 -1
  100. package/assets/docs/tree/sections-and-placement.doc.mjs +1 -1
  101. package/assets/docs/tree/template-doc-overview.doc.mjs +13 -1
  102. package/assets/docs/tree/troubleshooting.doc.mjs +9 -5
  103. package/assets/docs/tree/versioning.doc.mjs +7 -6
  104. package/assets/templates/blocks/components/DropdownMenuGroup/DropdownMenuGroupShowcase.doc.mjs +15 -0
  105. package/assets/templates/blocks/components/DropdownMenuGroup/DropdownMenuGroupShowcase.tsx +38 -0
  106. package/assets/templates/blocks/components/DropdownMenuGroup/DropdownMenuGroupWithSelectable.doc.mjs +20 -0
  107. package/assets/templates/blocks/components/DropdownMenuGroup/DropdownMenuGroupWithSelectable.tsx +48 -0
  108. package/assets/templates/pages/ai-chat/template.doc.mjs +16 -1
  109. package/assets/templates/pages/ai-chat-landing/template.doc.mjs +9 -1
  110. package/assets/templates/pages/blank/template.doc.mjs +3 -1
  111. package/assets/templates/pages/canvas-editor/template.doc.mjs +9 -1
  112. package/assets/templates/pages/centered-hero/template.doc.mjs +3 -1
  113. package/assets/templates/pages/checkout-wizard/template.doc.mjs +1 -0
  114. package/assets/templates/pages/classic-gallery/template.doc.mjs +3 -1
  115. package/assets/templates/pages/contact-form/template.doc.mjs +16 -1
  116. package/assets/templates/pages/dashboard/template.doc.mjs +15 -1
  117. package/assets/templates/pages/dashboard-alert-rail/template.doc.mjs +23 -1
  118. package/assets/templates/pages/dashboard-cohort-funnel/template.doc.mjs +10 -1
  119. package/assets/templates/pages/dashboard-comparison/template.doc.mjs +9 -1
  120. package/assets/templates/pages/dashboard-composition/template.doc.mjs +10 -1
  121. package/assets/templates/pages/dashboard-progress/template.doc.mjs +17 -1
  122. package/assets/templates/pages/dashboard-scorecard/template.doc.mjs +2 -1
  123. package/assets/templates/pages/detail-page/template.doc.mjs +8 -0
  124. package/assets/templates/pages/documentation/template.doc.mjs +10 -1
  125. package/assets/templates/pages/documentation-design/template.doc.mjs +10 -1
  126. package/assets/templates/pages/documentation-technical/template.doc.mjs +10 -1
  127. package/assets/templates/pages/editor/template.doc.mjs +9 -1
  128. package/assets/templates/pages/file-explorer/template.doc.mjs +3 -1
  129. package/assets/templates/pages/form-two-column/template.doc.mjs +17 -1
  130. package/assets/templates/pages/form-wizard/template.doc.mjs +14 -1
  131. package/assets/templates/pages/form-wizard-dialog/template.doc.mjs +1 -0
  132. package/assets/templates/pages/gallery-hero/template.doc.mjs +10 -1
  133. package/assets/templates/pages/ide/template.doc.mjs +9 -1
  134. package/assets/templates/pages/incident-console/template.doc.mjs +10 -1
  135. package/assets/templates/pages/kanban-board/template.doc.mjs +11 -1
  136. package/assets/templates/pages/library/template.doc.mjs +15 -1
  137. package/assets/templates/pages/login/template.doc.mjs +10 -1
  138. package/assets/templates/pages/login-card/template.doc.mjs +11 -1
  139. package/assets/templates/pages/login-split/template.doc.mjs +10 -1
  140. package/assets/templates/pages/login-sso/template.doc.mjs +10 -1
  141. package/assets/templates/pages/messaging-shell/template.doc.mjs +11 -1
  142. package/assets/templates/pages/mixed-gallery/template.doc.mjs +10 -1
  143. package/assets/templates/pages/payment-form/template.doc.mjs +3 -1
  144. package/assets/templates/pages/product-detail/template.doc.mjs +9 -1
  145. package/assets/templates/pages/product-gallery/template.doc.mjs +10 -1
  146. package/assets/templates/pages/settings/template.doc.mjs +3 -1
  147. package/assets/templates/pages/settings-dialog/template.doc.mjs +9 -1
  148. package/assets/templates/pages/settings-sidebar/template.doc.mjs +9 -1
  149. package/assets/templates/pages/shell-nav/template.doc.mjs +10 -1
  150. package/assets/templates/pages/shell-side-nav/template.doc.mjs +14 -1
  151. package/assets/templates/pages/shell-top-nav/template.doc.mjs +11 -1
  152. package/assets/templates/pages/side-gallery/template.doc.mjs +3 -1
  153. package/assets/templates/pages/table/template.doc.mjs +11 -1
  154. package/assets/templates/pages/table-filter/template.doc.mjs +21 -1
  155. package/assets/templates/pages/table-grouped/template.doc.mjs +15 -1
  156. package/assets/templates/pages/table-inbox/template.doc.mjs +18 -6
  157. package/assets/templates/pages/table-page/template.doc.mjs +20 -1
  158. package/assets/templates/pages/table-tree/template.doc.mjs +14 -1
  159. package/assets/templates/pages/theme-showcase/template.doc.mjs +10 -1
  160. package/assets/templates/pages/work-item-detail/template.doc.mjs +10 -0
  161. package/assets/templates/themes/butter/icons.tsx +2 -0
  162. package/assets/templates/themes/chocolate/icons.tsx +2 -0
  163. package/assets/templates/themes/gothic/icons.tsx +2 -0
  164. package/assets/templates/themes/matcha/icons.tsx +2 -0
  165. package/assets/templates/themes/neutral/icons.tsx +2 -0
  166. package/assets/templates/themes/stone/icons.tsx +2 -0
  167. package/assets/templates/themes/y2k/icons.tsx +2 -0
  168. package/authoring/doctypes/template/parse.d.mts +2 -0
  169. package/authoring/doctypes/template/parse.mjs +1 -0
  170. package/authoring/doctypes/template/parse.test.mjs +21 -0
  171. package/authoring/doctypes/template/template.doc.mjs +6 -0
  172. package/authoring/doctypes/template/type.ts +10 -0
  173. package/clients/cli/commands/build-theme.mjs +8 -2
  174. package/clients/cli/commands/discover.mjs +29 -1
  175. package/clients/cli/commands/discover.no-source.test.mjs +75 -0
  176. package/clients/cli/commands/docs.depth.test.mjs +219 -0
  177. package/clients/cli/commands/docs.doc.mjs +15 -2
  178. package/clients/cli/commands/docs.mjs +222 -5
  179. package/clients/cli/commands/docs.test.mjs +383 -0
  180. package/clients/cli/commands/doctor.mjs +4 -4
  181. package/clients/cli/commands/gap-report.mjs +12 -0
  182. package/clients/cli/commands/gap-report.test.mjs +63 -0
  183. package/clients/cli/commands/integration-add.doc.mjs +10 -0
  184. package/clients/cli/commands/integration-authoring.test.mjs +12 -2
  185. package/clients/cli/commands/integration.mjs +3 -1
  186. package/clients/cli/commands/layout-expand.doc.mjs +3 -1
  187. package/clients/cli/commands/layout.expand-receipt.test.mjs +94 -0
  188. package/clients/cli/commands/layout.mjs +16 -0
  189. package/clients/cli/commands/search.doc.mjs +5 -4
  190. package/clients/cli/commands/search.mjs +30 -6
  191. package/clients/cli/commands/search.test.mjs +60 -16
  192. package/clients/cli/commands/template.copy-receipt.test.mjs +60 -0
  193. package/clients/cli/commands/template.mjs +19 -5
  194. package/clients/cli/commands/template.show-media.test.mjs +62 -0
  195. package/clients/cli/commands/theme-list.behavior.test.mjs +41 -0
  196. package/clients/cli/commands/upgrade.ascii-output.test.mjs +14 -0
  197. package/clients/cli/commands/write-failure.test.mjs +175 -0
  198. package/clients/cli/lib/manifest.mjs +44 -165
  199. package/clients/cli/lib/manifest.test.mjs +98 -7
  200. package/foundation/agent-docs/agent-docs.mjs +2 -1
  201. package/foundation/agent-docs/agent-docs.test.mjs +1168 -0
  202. package/foundation/discovery/template-adapter.d.mts +14 -0
  203. package/foundation/discovery/template-adapter.fixture-refs.test.mjs +37 -1
  204. package/foundation/discovery/template-adapter.mjs +21 -1
  205. package/foundation/discovery/theme-discovery.d.mts +6 -0
  206. package/foundation/discovery/theme-discovery.mjs +9 -0
  207. package/foundation/doc-compiler/doc-loads.test.mjs +3 -3
  208. package/foundation/doc-compiler/tree.test.mjs +649 -0
  209. package/foundation/integrations/cli-requirement.d.mts +57 -13
  210. package/foundation/integrations/cli-requirement.mjs +84 -22
  211. package/foundation/integrations/cli-requirement.test.mjs +135 -8
  212. package/foundation/response/response-types.doc.d.mts +5 -5
  213. package/foundation/response/response-types.doc.mjs +13 -13
  214. package/package.json +9 -9
package/README.md CHANGED
@@ -69,7 +69,7 @@ Results for "button" (20 of 239):
69
69
 
70
70
  Options:
71
71
 
72
- - `--type <component|hook|doc|template>`: restrict to a single domain
72
+ - `--type <component|hook|doc|template|theme>`: restrict to a single domain (`doc` and `theme` work outside an app too)
73
73
  - `--limit <n>`: cap the number of results (default 20)
74
74
  - `--verbose`: also print each result's match score and reason
75
75
  - `--json`: typed `{ apiVersion, type: 'search', data: { query, matchCount, results } }` envelope — `matchCount` is how many candidates matched in total, `results` the slice `--limit` allowed
@@ -91,7 +91,7 @@ Options:
91
91
  | `init` | Initialize the design system in your project |
92
92
  | `integration` | Author and verify an Astryx integration package |
93
93
  | `layout` | Generate XDS layouts from compressed expressions (XLE/XLO) |
94
- | `search` | Search components, hooks, docs, and templates in one ranked list |
94
+ | `search` | Search components, hooks, docs, templates, and themes in one ranked list |
95
95
  | `swizzle` | Copy component source for customization |
96
96
  | `template` | List, show, or scaffold page and block templates |
97
97
  | `theme` | Create and build themes: add a shipped one, compile to CSS, or list what a theme can override |
@@ -303,11 +303,12 @@ Shape:
303
303
  ```
304
304
 
305
305
  The manifest is **derived from Commander metadata** (commands, arguments, options)
306
- so it can't drift from the real command definitions. The two facts Commander
307
- doesn't track (`--json` support and emitted response types) are layered on from
308
- the `JSON_SUPPORTED` allowlist and a small declarative `RESPONSE_TYPES` map in
309
- `src/lib/manifest.mjs`, guarded by a drift test (`manifest.test.mjs`) so adding a
310
- command without describing it fails CI.
306
+ so it can't drift from the real command definitions. The facts Commander doesn't
307
+ track come from each command's docs: examples from its CommandDoc, and emitted
308
+ response types from the returns of the FunctionDoc it wraps (plus the few
309
+ envelopes the CLI layer builds itself). `--json` support comes from the
310
+ `JSON_SUPPORTED` allowlist. Drift tests (`manifest.test.mjs`) fail CI when a
311
+ command is added without describing it.
311
312
 
312
313
  **Backwards-compat:** the bare `astryx --json` envelope keeps `type: "help"` and its
313
314
  original shallow fields (`name`, `version`, `commands` as a `string[]` of names,
@@ -450,9 +451,9 @@ Every response has a `type` discriminant. The full set is below (generated from
450
451
  | `gap-report.categories` | The fixed gap category values and human-readable labels. |
451
452
  | `gap-report.file` | An aggregate receipt: overall status, the selected package, issuesUrl (or null), deliveries in handler order, each {handlerType: project \| integration \| fallback, handler, audience, status, url, message}, and filedCount/routedOnlyCount totals. |
452
453
  | `template.list` | The effective discovered TemplateListEntry[] for pages and blocks. A winning replacement entry includes optional `replaces`, naming the Core id omitted from the default list. |
453
- | `template.show` | The resolved template's raw source plus its description, kind, and the component names it composes. |
454
+ | `template.show` | The resolved template's source, exactly as a copy writes it, plus its description, kind, the component names it composes, and demoMediaReplaced (how many Astryx demo media references were replaced with placeholders for you to swap for your own media). |
454
455
  | `template.skeleton` | A layout skeleton (structural tags with spatial annotations) plus the template's description and the components it composes. |
455
- | `template.copy` | A scaffold receipt: template id, output directory, written file name, and file count. |
456
+ | `template.copy` | A scaffold receipt: template id, output directory, written file name, file count, and demoMediaReplaced (how many Astryx demo media references were replaced with placeholders for you to swap for your own media). |
456
457
  | `template.cdn` | A write receipt for the no-build-step CDN starter page: the path (relative to cwd), the Astryx version every CDN URL was pinned to, whether it was written, and the reason it was not. `exists` when a file was already there, which is a success. |
457
458
  | `hook.list` | The hook catalog grouped by category: `detail` (the level: names \| compact \| full) and `components`, the grouped map of hook names, brief entries, or a full HookDoc per entry. |
458
459
  | `hook.detail` | One hook's full authored HookDoc. |
@@ -468,18 +469,18 @@ Every response has a `type` discriminant. The full set is below (generated from
468
469
  | `upgrade.list` | Every available codemod, oldest→newest, as {name, title, version, optional}; returned for --list without running anything. |
469
470
  | `upgrade.registry` | The copied-composition receipt for --registry: applied, ok, the counts (found, current, wouldUpdate, updated, wouldMerge, merged, wouldRefreshReceipt, receiptsRefreshed, conflicts, missing, invalid, failed), and items. |
470
471
  | `upgrade.status` | A short-circuit outcome with no codemods run (up_to_date, no_codemods, or config_fixable), each carrying the agent-docs summary. |
471
- | `upgrade.run` | The run receipt: from/to versions, codemod count, integrations processed, the agent-docs summary, and (apply mode) filesChanged, transformsApplied, and per-codemod errors. |
472
+ | `upgrade.run` | The run receipt: from/to versions, codemod count, integrations processed, the agent-docs summary, sourcePathFound (false when the resolved source directory does not exist, so no source file was read), and (apply mode) filesChanged, transformsApplied, and per-codemod errors. |
472
473
  | `manifest` | The CLI capability manifest: name, version, apiVersion, description, globalOptions, commands (each name, description, arguments, options, json, aliases?, responseTypes?, examples?, exitCodes? as [{code, when}], subcommands?), jsonSupported, and the flat responseTypes index. |
473
474
  | `help` | Help, in one of two shapes. A bare `astryx --json` returns the root manifest: name, version, commands (the command names), jsonSupported, and manifest (the full payload `astryx manifest --json` returns). `--help --json` on any command, or `astryx help [command] --json`, returns that command's help: command, description, usage, options (each flags, description, and defaultValue and choices when set), and subcommands (each name and description). data.manifest marks the first shape; data.usage marks the second. |
474
475
  | `version` | The CLI version, for `astryx --version --json`: {version}. |
475
476
  | `doctor` | The health-check report: `checks` (each with id, label, status: pass \| warn \| fail \| info, a message, and an optional fix, always present on warn and fail) plus a `summary` of counts per status. |
476
477
  | `integration.add` | A contribution-writer receipt: kind, name, optional root {path, created}, integration-manifest path, every affected project-relative path, written, and dryRun. |
477
478
  | `integration.pack-check` | The packed-package check: name, version, packable, tarball {filename, fileCount, size, unpackedSize} or null, inventory {manifest, roots [{kind, path, expectedFiles, missingFiles, complete}], expectedFiles, packedFiles}, contributions {local, packed}, each null or {themes [{slug, exportName}], components, templates [{id, type, name}], codemods [{version, id}], docs, agentDocsAppend}, and issues [{code, severity, message}]. |
478
- | `integration.validate` | The validation result: the package name and version (both null when no local manifest is found) plus issues, an AstryxIntegrationIssue[] of {code, severity: warning \| error, message}. |
479
- | `integration.template-conflicts` | The integration identity, structural issues, and non-blocking Core template-id conflicts as {id, severity: warning, integrationPackage, integrationType, integrationName, coreMatches, message, command}. |
480
- | `integration.component-conflicts` | The integration identity, structural issues, and non-blocking conflicts where an integration component name is also owned by Core; each conflict includes the exact package-qualified command. |
481
- | `integration.doc-conflicts` | The integration identity, structural issues, and Core doc overlaps. Each finding includes `severity` (`info` \| `error`) and `relationship` (`replaces` \| `extends` \| `accidental`). |
482
- | `layout.expand` | The expansion: parsed form, generated TSX code, componentsUsed, states (count of useState hooks scaffolded), todos, blocksReferenced (each {name, mode}), warnings, and written (the output path, or null when nothing was written). |
479
+ | `integration.validate` | The validation result: validated (false when no integration manifest was found, so nothing was checked and the empty issues list proves nothing), the package name and version (both null when validated is false) plus issues, an AstryxIntegrationIssue[] of {code, severity: warning \| error, message}. |
480
+ | `integration.template-conflicts` | validated (false when no integration manifest was found, so nothing was inspected), the integration identity, structural issues, and non-blocking Core template-id conflicts as {id, severity: warning, integrationPackage, integrationType, integrationName, coreMatches, message, command}. |
481
+ | `integration.component-conflicts` | validated (false when no integration manifest was found, so nothing was inspected), the integration identity, structural issues, and non-blocking conflicts where an integration component name is also owned by Core; each conflict includes the exact package-qualified command. |
482
+ | `integration.doc-conflicts` | validated (false when no integration manifest was found, so nothing was inspected), the integration identity, structural issues, and Core doc overlaps. Each finding includes `severity` (`info` \| `error`) and `relationship` (`replaces` \| `extends` \| `accidental`). |
483
+ | `layout.expand` | The expansion: parsed form, generated TSX code, componentsUsed, states (count of useState hooks scaffolded), todos, blocksReferenced (each {name, mode}), warnings, written (the output path, or null when nothing was written), and demoMediaReplaced (count of demo media placeholders). |
483
484
  | `layout.check` | The validation result: a valid flag, the detected form, errors (each with line/col, message, formatted text, and suggestions), warnings, and the expression re-printed in both canonical surfaces (compact and outline). |
484
485
  | `layout.grammar` | The XLE/XLO grammar cheatsheet: a text field with the full reference plus an aliases map (short name → canonical component) generated from this install's registry. |
485
486
 
@@ -7,8 +7,15 @@
7
7
  * @property {string} name The template's own id, as search reports it.
8
8
  * @property {string} command `astryx template <id> --type page`, the command that selects exactly this template: an integration replacement is selected by the Core id it replaces, and `--type page` keeps a block with the same id from making it ambiguous. Search prints template commands the same way.
9
9
  * @property {string} displayName Human-facing name.
10
- * @property {string} description What the page is: its layout and the ideas it serves.
10
+ * @property {string} description What the page is and how it is laid out.
11
11
  * @property {string} category The template's own `Family - Variant` label; empty when it declares none.
12
+ * @property {string[]} keywords The ideas the page serves, as its own descriptor names them; empty when it declares none.
13
+ */
14
+ /**
15
+ * A component the project can use, as the ranker reads it.
16
+ * @typedef {object} ComponentWords
17
+ * @property {string} name The component's name, e.g. `DateRangeInput`.
18
+ * @property {string[]} keywords The keywords its own doc declares.
12
19
  */
13
20
  /**
14
21
  * Every ready page template the project can scaffold, in discovery order. This
@@ -23,6 +30,30 @@
23
30
  * @returns {Promise<PageTemplate[]>}
24
31
  */
25
32
  export function loadPageTemplates(cwd: string): Promise<PageTemplate[]>;
33
+ /**
34
+ * The components the project can use, Core's and its integrations', each with
35
+ * the keywords its own doc declares: what the ranker reads to tell a part of a
36
+ * page from a page. Search's own discovery, so both agree on what exists; empty
37
+ * when Core cannot be found.
38
+ *
39
+ * @param {string} cwd
40
+ * @returns {Promise<ComponentWords[]>}
41
+ */
42
+ export function loadComponents(cwd: string): Promise<ComponentWords[]>;
43
+ /**
44
+ * The matcher weights checked in beside the kit (`kit/weights.json`), read
45
+ * once; null when the file is absent or unreadable.
46
+ * @returns {import('./kit/weights.mjs').WeightsFile | null}
47
+ */
48
+ export function loadWeights(): import("./kit/weights.mjs").WeightsFile | null;
49
+ /**
50
+ * Whether a parsed weights file has the shape the kit reads: every row has a
51
+ * weight per candidate, every bias a number per candidate, and three blend
52
+ * numbers per member (the tables plus the ranker) and one for the shell.
53
+ * @param {any} file
54
+ * @returns {file is import('./kit/weights.mjs').WeightsFile}
55
+ */
56
+ export function isWeightsFile(file: any): file is import("./kit/weights.mjs").WeightsFile;
26
57
  /**
27
58
  * A page template the kit can recommend starting from.
28
59
  */
@@ -40,11 +71,28 @@ export type PageTemplate = {
40
71
  */
41
72
  displayName: string;
42
73
  /**
43
- * What the page is: its layout and the ideas it serves.
74
+ * What the page is and how it is laid out.
44
75
  */
45
76
  description: string;
46
77
  /**
47
78
  * The template's own `Family - Variant` label; empty when it declares none.
48
79
  */
49
80
  category: string;
81
+ /**
82
+ * The ideas the page serves, as its own descriptor names them; empty when it declares none.
83
+ */
84
+ keywords: string[];
85
+ };
86
+ /**
87
+ * A component the project can use, as the ranker reads it.
88
+ */
89
+ export type ComponentWords = {
90
+ /**
91
+ * The component's name, e.g. `DateRangeInput`.
92
+ */
93
+ name: string;
94
+ /**
95
+ * The keywords its own doc declares.
96
+ */
97
+ keywords: string[];
50
98
  };
@@ -2,20 +2,25 @@
2
2
 
3
3
  /**
4
4
  * @file The build subject's environment access: the page templates a project
5
- * can scaffold.
5
+ * can scaffold, the components it can use, and the checked-in matcher weights.
6
6
  *
7
- * @input Template discovery for `cwd` — the CLI's own templates plus any that
8
- * the project's configured integrations contribute.
7
+ * @input Template and component discovery for `cwd` — the CLI's own templates
8
+ * and Core's components, plus any that the project's configured integrations
9
+ * contribute.
9
10
  * @output Ready page templates as `{name, displayName, description, category,
10
- * command}`, where `command` is the `astryx template` command that selects
11
- * exactly that template.
12
- * @position Beside build.mjs (api/build/). The kit leaf reads templates only
13
- * through here, because a subject's `_adapter.mjs` is its only environment
14
- * access. Search keeps its own discovery; this adds none of its own, it
15
- * reuses the template subject's.
11
+ * keywords, command}`, where `command` is the `astryx template` command that
12
+ * selects exactly that template; components as `{name, keywords}`.
13
+ * @position Beside build.mjs (api/build/). The kit leaf reads templates and
14
+ * components only through here, because a subject's `_adapter.mjs` is its
15
+ * only environment access. This adds no discovery of its own: templates come
16
+ * from the template subject's, components from search's.
16
17
  */
17
18
 
19
+ import fs from 'node:fs';
20
+
18
21
  import {discoverTemplates} from '../template/template.mjs';
22
+ import {componentKeywords} from '../search/search.mjs';
23
+ import {findCoreDir} from '../../foundation/fs/paths.mjs';
19
24
 
20
25
  /**
21
26
  * A page template the kit can recommend starting from.
@@ -23,8 +28,16 @@ import {discoverTemplates} from '../template/template.mjs';
23
28
  * @property {string} name The template's own id, as search reports it.
24
29
  * @property {string} command `astryx template <id> --type page`, the command that selects exactly this template: an integration replacement is selected by the Core id it replaces, and `--type page` keeps a block with the same id from making it ambiguous. Search prints template commands the same way.
25
30
  * @property {string} displayName Human-facing name.
26
- * @property {string} description What the page is: its layout and the ideas it serves.
31
+ * @property {string} description What the page is and how it is laid out.
27
32
  * @property {string} category The template's own `Family - Variant` label; empty when it declares none.
33
+ * @property {string[]} keywords The ideas the page serves, as its own descriptor names them; empty when it declares none.
34
+ */
35
+
36
+ /**
37
+ * A component the project can use, as the ranker reads it.
38
+ * @typedef {object} ComponentWords
39
+ * @property {string} name The component's name, e.g. `DateRangeInput`.
40
+ * @property {string[]} keywords The keywords its own doc declares.
28
41
  */
29
42
 
30
43
  /**
@@ -53,8 +66,83 @@ export async function loadPageTemplates(cwd) {
53
66
  displayName: t.displayName || t.name,
54
67
  description: t.description || '',
55
68
  category: t.category || '',
69
+ keywords: t.keywords ?? [],
56
70
  // The id `template()` resolves back to this entry: an active replacement
57
71
  // owns the Core id it names, so that id selects it, not its own.
58
72
  command: `astryx template ${t.replaces ?? t.dirName} --type page`,
59
73
  }));
60
74
  }
75
+
76
+ /**
77
+ * The components the project can use, Core's and its integrations', each with
78
+ * the keywords its own doc declares: what the ranker reads to tell a part of a
79
+ * page from a page. Search's own discovery, so both agree on what exists; empty
80
+ * when Core cannot be found.
81
+ *
82
+ * @param {string} cwd
83
+ * @returns {Promise<ComponentWords[]>}
84
+ */
85
+ export async function loadComponents(cwd) {
86
+ const coreDir = findCoreDir(cwd);
87
+ if (!coreDir) return [];
88
+ try {
89
+ return await componentKeywords(coreDir, cwd);
90
+ } catch {
91
+ return [];
92
+ }
93
+ }
94
+
95
+ /** @type {import('./kit/weights.mjs').WeightsFile | null | undefined} */
96
+ let weights;
97
+
98
+ /**
99
+ * The matcher weights checked in beside the kit (`kit/weights.json`), read
100
+ * once; null when the file is absent or unreadable.
101
+ * @returns {import('./kit/weights.mjs').WeightsFile | null}
102
+ */
103
+ export function loadWeights() {
104
+ if (weights === undefined) {
105
+ try {
106
+ const file = JSON.parse(
107
+ fs.readFileSync(new URL('./kit/weights.json', import.meta.url), 'utf8'),
108
+ );
109
+ weights = isWeightsFile(file) ? file : null;
110
+ } catch {
111
+ weights = null;
112
+ }
113
+ }
114
+ return weights ?? null;
115
+ }
116
+
117
+ /**
118
+ * Whether a parsed weights file has the shape the kit reads: every row has a
119
+ * weight per candidate, every bias a number per candidate, and three blend
120
+ * numbers per member (the tables plus the ranker) and one for the shell.
121
+ * @param {any} file
122
+ * @returns {file is import('./kit/weights.mjs').WeightsFile}
123
+ */
124
+ export function isWeightsFile(file) {
125
+ const n = Array.isArray(file?.candidates) ? file.candidates.length : 0;
126
+ return (
127
+ n > 0 &&
128
+ Array.isArray(file.tables) &&
129
+ file.tables.length > 0 &&
130
+ file.tables.every(
131
+ (/** @type {any} */ t) =>
132
+ Array.isArray(t?.words) &&
133
+ Array.isArray(t.rows) &&
134
+ t.rows.length === t.words.length &&
135
+ t.rows.every(
136
+ (/** @type {any} */ r) => typeof r === 'string' && r.length === n,
137
+ ) &&
138
+ Array.isArray(t.bias) &&
139
+ t.bias.length === n &&
140
+ t.bias.every((/** @type {any} */ b) => Number.isFinite(b)) &&
141
+ Number.isFinite(t.clip) &&
142
+ Number.isFinite(t.step),
143
+ ) &&
144
+ Array.isArray(file.blend) &&
145
+ file.blend.length === 3 * (file.tables.length + 1) + 1 &&
146
+ file.blend.every((/** @type {any} */ x) => Number.isFinite(x))
147
+ );
148
+ }
@@ -19,8 +19,8 @@ export const doc = {
19
19
  'The "build a page" entry point. Called with no query it returns the ' +
20
20
  'how-to-build-a-page playbook as data: the workflow steps with their ' +
21
21
  'commands, the on-system rules, and related lookups. Called with a query it names the page template to ' +
22
- 'START from (always one: the page template a ranker built for long descriptions puts first, else the app ' +
23
- 'shell) and the next two templates, ' +
22
+ 'START from (always one: the page template a ranker built for long descriptions puts first; for a part ' +
23
+ 'of a page, the page it names; else the app shell) and the next two templates, ' +
24
24
  'and the unified search grouped around it: the other close page templates, drop-in blocks, and ' +
25
25
  'idea-specific components/hooks, plus the always-on frame + foundation. A template carries the page ' +
26
26
  'frame and spacing, so the kit never recommends composing a page from components.',
@@ -8,7 +8,7 @@ import {describe, it, expect, vi} from 'vitest';
8
8
  import * as path from 'node:path';
9
9
  import {fileURLToPath} from 'node:url';
10
10
  import {build} from './build.mjs';
11
- import {search} from '../search/search.mjs';
11
+ import {search, searchedComponents} from '../search/search.mjs';
12
12
 
13
13
  // api/build/ -> up 3 = packages/cli, up 4 = repo root (has packages/core).
14
14
  const REPO = path.resolve(
@@ -152,6 +152,16 @@ describe('build API', () => {
152
152
  });
153
153
  });
154
154
 
155
+ describe('build kit — reuses the components its search gathered', () => {
156
+ it('keeps them beside the search response, out of its JSON', async () => {
157
+ const all = await search('date picker', {cwd: REPO});
158
+ expect(searchedComponents(all)?.map(c => c.name)).toContain('DateRangeInput');
159
+ expect(JSON.stringify(all)).not.toContain('"keywords"');
160
+ const pagesOnly = await search('date picker', {cwd: REPO, type: 'template'});
161
+ expect(searchedComponents(pagesOnly)).toBeNull();
162
+ }, 60_000);
163
+ });
164
+
155
165
  describe('build kit — coverage gates the pages group', () => {
156
166
  it('does not call a one-word coincidence a direct match', async () => {
157
167
  // A page's keywords include every component its source renders, so any
@@ -259,7 +269,7 @@ describe('build kit — a thin kit says what to try next', () => {
259
269
  // A skeleton is a 35-line excerpt: a reader who studies it and composes
260
270
  // the rest loses the spacing the template exists to carry. A loose match
261
271
  // is still the best start there is, so `start` scaffolds it.
262
- const r = await build('executive summary', {cwd: REPO});
272
+ const r = await build('weekly business review with targets', {cwd: REPO});
263
273
  expect(r.type).toBe('build.kit');
264
274
  if (r.type !== 'build.kit') return;
265
275
  expect(r.data.directMatch).toBe(false);
@@ -357,9 +367,35 @@ describe('build kit — every page starts from a template', () => {
357
367
  const placed = await build('an empty state for a settings page', {cwd: REPO});
358
368
  if (placed.type !== 'build.kit') throw new Error(placed.type);
359
369
  expect(placed.data.start).toMatchObject({name: 'settings', basis: 'closest'});
370
+ expect(placed.data.start?.reason).toMatch(/part of a page, so it starts from the page it names/);
360
371
  const loose = await build('a date range picker', {cwd: REPO});
361
372
  if (loose.type !== 'build.kit') throw new Error(loose.type);
362
373
  expect(loose.data.start).toMatchObject({name: 'shell-top-nav', basis: 'fallback'});
374
+ expect(loose.data.start?.reason).toMatch(/part of a page and names no page, so it starts from the app shell/);
375
+ });
376
+
377
+ it('starts a change to an existing page from the app shell', async () => {
378
+ // The page is the builder's to keep; no template scaffolds it.
379
+ const r = await build('add a sort toggle to the existing reports dashboard', {cwd: REPO});
380
+ if (r.type !== 'build.kit') throw new Error(r.type);
381
+ expect(r.data.start).toMatchObject({name: 'shell-top-nav', basis: 'fallback'});
382
+ expect(r.data.start?.reason).toMatch(/changes a page you already have, so keep it/);
383
+ expect(r.data.start?.reason).not.toMatch(/too little of the idea fits/);
384
+ // A direct match the response reports is still named.
385
+ if (r.data.directMatch) expect(r.data.start?.reason).toContain(`\`${r.data.pages[0].name}\``);
386
+ });
387
+
388
+ it('does not call a new page that mentions something existing a change', async () => {
389
+ for (const idea of [
390
+ 'a new dashboard inspired by the existing one',
391
+ 'a new dashboard based on the existing dashboard',
392
+ 'clone the existing dashboard as a new page',
393
+ ]) {
394
+ const r = await build(idea, {cwd: REPO});
395
+ if (r.type !== 'build.kit') throw new Error(r.type);
396
+ expect(r.data.start?.name).toBe('dashboard');
397
+ expect(r.data.start?.reason).not.toMatch(/already have/);
398
+ }
363
399
  });
364
400
 
365
401
  it('names a direct match the ranker outweighed in the reason', async () => {
@@ -441,6 +477,79 @@ describe('build kit — every page starts from a template', () => {
441
477
  expect(r.data.pages.map(p => p.name)).not.toContain('side-gallery');
442
478
  });
443
479
 
480
+ it('names a direct match the start does not use, and calls the start direct only when it is', async () => {
481
+ for (const idea of ['a login form', 'a docs site for our API', 'contact form']) {
482
+ const r = await build(idea, {cwd: REPO});
483
+ if (r.type !== 'build.kit') throw new Error(r.type);
484
+ expect(r.data.directMatch).toBe(true);
485
+ const match = r.data.pages[0].name;
486
+ if (r.data.start?.name === match) {
487
+ expect(r.data.start?.basis).toBe('direct');
488
+ } else {
489
+ expect(['closest', 'fallback']).toContain(r.data.start?.basis);
490
+ expect(r.data.start?.reason).toContain(`\`${match}\``);
491
+ }
492
+ }
493
+ });
494
+
495
+ it('keeps a template search matched directly rather than the app shell', async () => {
496
+ for (const [idea, name] of [
497
+ ['a login screen', 'login'],
498
+ ['a checkout wizard', 'checkout-wizard'],
499
+ ]) {
500
+ const r = await build(idea, {cwd: REPO});
501
+ if (r.type !== 'build.kit') throw new Error(r.type);
502
+ expect(r.data.directMatch).toBe(true);
503
+ expect(r.data.start?.name).toBe(name);
504
+ }
505
+ });
506
+
507
+ it('lets the weights choose another template over a direct match', async () => {
508
+ const r = await build('a login form', {cwd: REPO});
509
+ if (r.type !== 'build.kit') throw new Error(r.type);
510
+ expect(r.data.directMatch).toBe(true);
511
+ const match = r.data.pages[0].name;
512
+ expect(r.data.start?.name).not.toBe('shell-top-nav');
513
+ expect(r.data.start?.name).not.toBe(match);
514
+ expect(r.data.start?.reason).toContain(`\`${match}\``);
515
+ });
516
+
517
+ it('starts a part that names no page from the app shell', async () => {
518
+ for (const idea of ['a kanban card', 'a date range picker']) {
519
+ const r = await build(idea, {cwd: REPO});
520
+ if (r.type !== 'build.kit') throw new Error(r.type);
521
+ expect(r.data.start).toMatchObject({name: 'shell-top-nav', basis: 'fallback'});
522
+ expect(r.data.start?.reason).toMatch(/part of a page/);
523
+ }
524
+ });
525
+
526
+ it('does not keep a loose match over the app shell', async () => {
527
+ // Search matches no template directly, so the ranker's closest page does
528
+ // not override the shell the weights choose.
529
+ const r = await build('quarterly business review', {cwd: REPO});
530
+ if (r.type !== 'build.kit') throw new Error(r.type);
531
+ expect(r.data.directMatch).toBe(false);
532
+ expect(r.data.start).toMatchObject({name: 'shell-top-nav', basis: 'fallback'});
533
+ expect(r.data.start?.reason).toMatch(/closest/);
534
+ });
535
+
536
+ it('starts a new page with no matching template from the app shell', async () => {
537
+ const r = await build('a new page', {cwd: REPO});
538
+ if (r.type !== 'build.kit') throw new Error(r.type);
539
+ expect(r.data.start?.name).toBe('shell-top-nav');
540
+ });
541
+
542
+ it('starts a page the words describe from its template', async () => {
543
+ for (const [idea, name] of [
544
+ ['a weekly report of sales by region', 'dashboard-scorecard'],
545
+ ['a pricing page with three plans and a comparison table', 'table-page'],
546
+ ]) {
547
+ const r = await build(idea, {cwd: REPO});
548
+ if (r.type !== 'build.kit') throw new Error(r.type);
549
+ expect(r.data.start?.name).toBe(name);
550
+ }
551
+ });
552
+
444
553
  it('starts a component in a container from a template with that frame', async () => {
445
554
  // "in a modal": the modal is the frame, so the dialog template leads
446
555
  // instead of the app shell.