@design-ai/cli 4.56.0 → 4.58.0

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 (271) hide show
  1. package/.claude-plugin/plugin.json +109 -25
  2. package/CHANGELOG.md +68 -0
  3. package/README.ko.md +11 -28
  4. package/README.md +12 -25
  5. package/cli/bin/design-ai.mjs +1 -0
  6. package/cli/commands/help.mjs +7 -4
  7. package/cli/commands/index.mjs +292 -0
  8. package/cli/commands/learn-help.mjs +149 -0
  9. package/cli/commands/learn-print-profile.mjs +373 -0
  10. package/cli/commands/learn-print-restore.mjs +349 -0
  11. package/cli/commands/learn-print-signals.mjs +453 -0
  12. package/cli/commands/learn.mjs +60 -1281
  13. package/cli/commands/pack.mjs +7 -3
  14. package/cli/commands/prompt.mjs +7 -3
  15. package/cli/commands/route.mjs +13 -1
  16. package/cli/commands/search.mjs +94 -1
  17. package/cli/lib/dispatch.mjs +3 -0
  18. package/cli/lib/embedding-index.mjs +199 -0
  19. package/cli/lib/embedding-provider.mjs +121 -0
  20. package/cli/lib/embedding-rerank.mjs +52 -0
  21. package/cli/lib/learn-args.mjs +490 -0
  22. package/cli/lib/learn-backup.mjs +748 -0
  23. package/cli/lib/learn-curation.mjs +612 -0
  24. package/cli/lib/learn-eval.mjs +459 -0
  25. package/cli/lib/learn-profile.mjs +763 -0
  26. package/cli/lib/learn-select.mjs +223 -0
  27. package/cli/lib/learn-shared.mjs +158 -0
  28. package/cli/lib/learn-test-support.mjs +218 -0
  29. package/cli/lib/learn-usage.mjs +360 -0
  30. package/cli/lib/learn.mjs +83 -3694
  31. package/cli/lib/lexical.mjs +137 -0
  32. package/cli/lib/local-config.mjs +110 -0
  33. package/cli/lib/mcp-server.mjs +13 -3
  34. package/cli/lib/pack.mjs +31 -4
  35. package/cli/lib/prompt.mjs +42 -4
  36. package/cli/lib/recall.mjs +193 -0
  37. package/cli/lib/retrieval-index.mjs +0 -0
  38. package/cli/lib/route.mjs +62 -5
  39. package/cli/lib/search-ranked.mjs +202 -0
  40. package/cli/lib/search.mjs +11 -1
  41. package/cli/lib/signals-backlog-commands.mjs +673 -0
  42. package/cli/lib/signals-backlog.mjs +361 -0
  43. package/cli/lib/signals-eval.mjs +176 -0
  44. package/cli/lib/signals-readiness.mjs +247 -0
  45. package/cli/lib/signals-registry.mjs +377 -0
  46. package/cli/lib/signals-render.mjs +478 -0
  47. package/cli/lib/signals-shared.mjs +75 -0
  48. package/cli/lib/signals.mjs +16 -2337
  49. package/cli/lib/site-bundle-handoff-expected.mjs +173 -0
  50. package/cli/lib/site-bundle-handoff-runbook-action-summary.mjs +332 -0
  51. package/cli/lib/site-bundle-handoff-runbook-human-lines.mjs +167 -0
  52. package/cli/lib/site-bundle-handoff-runbook-maps.mjs +238 -0
  53. package/cli/lib/site-bundle-handoff-runbook-next-step.mjs +278 -0
  54. package/cli/lib/site-bundle-handoff-runbook.mjs +44 -931
  55. package/cli/lib/site-test-support.mjs +68 -0
  56. package/cli/lib/skill-proposals-apply-commands.mjs +135 -0
  57. package/cli/lib/skill-proposals-apply-contract.mjs +750 -0
  58. package/cli/lib/skill-proposals-apply-plan.mjs +118 -0
  59. package/cli/lib/skill-proposals-generate.mjs +298 -0
  60. package/cli/lib/skill-proposals-render.mjs +532 -0
  61. package/cli/lib/skill-proposals-review.mjs +262 -0
  62. package/cli/lib/skill-proposals.mjs +15 -2046
  63. package/cli/lib/workspace-args.mjs +83 -0
  64. package/cli/lib/workspace-git.mjs +169 -0
  65. package/cli/lib/workspace-learning.mjs +483 -0
  66. package/cli/lib/workspace-repo.mjs +139 -0
  67. package/cli/lib/workspace-report.mjs +283 -0
  68. package/cli/lib/workspace-test-support.mjs +99 -0
  69. package/cli/lib/workspace.mjs +30 -1056
  70. package/docs/AI-LEARNING-PHASE2.md +215 -0
  71. package/docs/DISTRIBUTION.ko.md +2 -2
  72. package/docs/DISTRIBUTION.md +2 -2
  73. package/docs/NEXT-SURFACE-DECISION.md +125 -0
  74. package/docs/PRODUCT-READINESS.md +3 -3
  75. package/docs/RELEASE-GATES.ko.md +35 -0
  76. package/docs/RELEASE-GATES.md +234 -0
  77. package/docs/ROADMAP.md +82 -8
  78. package/docs/external-status.md +9 -11
  79. package/docs/inspection-20260630.md +169 -0
  80. package/docs/integrations/design-ai-mcp-server.md +4 -4
  81. package/docs/reference/ant-design.md +413 -0
  82. package/docs/reference/awesome-design-md.md +439 -0
  83. package/docs/reference/mui.md +783 -0
  84. package/docs/reference/shadcn-ui.md +298 -0
  85. package/examples/component-accordion-actions.md +5 -5
  86. package/examples/component-accordion-details.md +5 -5
  87. package/examples/component-accordion-summary.md +5 -5
  88. package/examples/component-accordion.md +3 -3
  89. package/examples/component-affix.md +1 -1
  90. package/examples/component-alert-dialog.md +1 -1
  91. package/examples/component-alert-title.md +1 -1
  92. package/examples/component-alert.md +3 -3
  93. package/examples/component-anchor.md +1 -1
  94. package/examples/component-app-bar.md +1 -1
  95. package/examples/component-aspect-ratio.md +1 -1
  96. package/examples/component-auto-complete.md +2 -2
  97. package/examples/component-avatar-group.md +5 -5
  98. package/examples/component-avatar.md +3 -3
  99. package/examples/component-back-top.md +1 -1
  100. package/examples/component-backdrop.md +1 -1
  101. package/examples/component-badge.md +3 -3
  102. package/examples/component-border-beam.md +3 -3
  103. package/examples/component-bottom-navigation.md +1 -1
  104. package/examples/component-box.md +1 -1
  105. package/examples/component-breadcrumb.md +3 -3
  106. package/examples/component-button-base.md +3 -3
  107. package/examples/component-button-group.md +2 -2
  108. package/examples/component-button.md +3 -3
  109. package/examples/component-calendar.md +2 -2
  110. package/examples/component-card-actions.md +1 -1
  111. package/examples/component-card-content.md +1 -1
  112. package/examples/component-card-header.md +1 -1
  113. package/examples/component-card-media.md +1 -1
  114. package/examples/component-card.md +3 -3
  115. package/examples/component-carousel.md +2 -2
  116. package/examples/component-cascader.md +1 -1
  117. package/examples/component-chart.md +1 -1
  118. package/examples/component-checkbox.md +3 -3
  119. package/examples/component-click-away-listener.md +1 -1
  120. package/examples/component-code.md +1 -1
  121. package/examples/component-collapsible.md +1 -1
  122. package/examples/component-color-picker.md +1 -1
  123. package/examples/component-combobox.md +1 -1
  124. package/examples/component-command.md +1 -1
  125. package/examples/component-config-provider.md +3 -3
  126. package/examples/component-context-menu.md +1 -1
  127. package/examples/component-css-baseline.md +3 -3
  128. package/examples/component-date-picker.md +2 -2
  129. package/examples/component-descriptions.md +1 -1
  130. package/examples/component-dialog-actions.md +1 -1
  131. package/examples/component-dialog-content-text.md +1 -1
  132. package/examples/component-dialog-content.md +1 -1
  133. package/examples/component-dialog-title.md +1 -1
  134. package/examples/component-dialog.md +2 -2
  135. package/examples/component-divider.md +3 -3
  136. package/examples/component-drawer.md +3 -3
  137. package/examples/component-dropdown.md +3 -3
  138. package/examples/component-empty.md +2 -2
  139. package/examples/component-fade.md +1 -1
  140. package/examples/component-field.md +1 -1
  141. package/examples/component-filled-input.md +1 -1
  142. package/examples/component-flex.md +1 -1
  143. package/examples/component-float-button.md +2 -2
  144. package/examples/component-form-control-label.md +1 -1
  145. package/examples/component-form-control.md +1 -1
  146. package/examples/component-form-controls.md +1 -1
  147. package/examples/component-form-group.md +1 -1
  148. package/examples/component-form-helper-text.md +1 -1
  149. package/examples/component-form-label.md +1 -1
  150. package/examples/component-form.md +2 -2
  151. package/examples/component-grid.md +2 -2
  152. package/examples/component-grow.md +1 -1
  153. package/examples/component-hover-card.md +1 -1
  154. package/examples/component-icon-button.md +1 -1
  155. package/examples/component-icon.md +1 -1
  156. package/examples/component-image-list.md +1 -1
  157. package/examples/component-image.md +1 -1
  158. package/examples/component-input-adornment.md +1 -1
  159. package/examples/component-input-base.md +1 -1
  160. package/examples/component-input-number.md +1 -1
  161. package/examples/component-input-otp.md +2 -2
  162. package/examples/component-input.md +3 -3
  163. package/examples/component-item.md +1 -1
  164. package/examples/component-label.md +1 -1
  165. package/examples/component-layout.md +1 -1
  166. package/examples/component-link.md +1 -1
  167. package/examples/component-list-item-avatar.md +1 -1
  168. package/examples/component-list-item-button.md +1 -1
  169. package/examples/component-list-item-icon.md +1 -1
  170. package/examples/component-list-item-text.md +1 -1
  171. package/examples/component-list-item.md +1 -1
  172. package/examples/component-list-subheader.md +1 -1
  173. package/examples/component-list.md +2 -2
  174. package/examples/component-masonry.md +1 -1
  175. package/examples/component-mentions.md +1 -1
  176. package/examples/component-menu-item.md +1 -1
  177. package/examples/component-menu-list.md +1 -1
  178. package/examples/component-menu.md +1 -1
  179. package/examples/component-menubar.md +1 -1
  180. package/examples/component-message.md +1 -1
  181. package/examples/component-mobile-stepper.md +1 -1
  182. package/examples/component-modal.md +5 -5
  183. package/examples/component-navigation-menu.md +1 -1
  184. package/examples/component-notification.md +1 -1
  185. package/examples/component-outlined-input.md +1 -1
  186. package/examples/component-pagination.md +3 -3
  187. package/examples/component-paper.md +1 -1
  188. package/examples/component-popconfirm.md +1 -1
  189. package/examples/component-popover.md +3 -3
  190. package/examples/component-popper.md +1 -1
  191. package/examples/component-progress.md +2 -2
  192. package/examples/component-qr-code.md +1 -1
  193. package/examples/component-radio.md +3 -3
  194. package/examples/component-rate.md +2 -2
  195. package/examples/component-resizable.md +1 -1
  196. package/examples/component-result.md +1 -1
  197. package/examples/component-scroll-area.md +1 -1
  198. package/examples/component-segmented.md +3 -3
  199. package/examples/component-select.md +3 -3
  200. package/examples/component-separator.md +1 -1
  201. package/examples/component-sheet.md +1 -1
  202. package/examples/component-sidebar.md +1 -1
  203. package/examples/component-skeleton.md +3 -3
  204. package/examples/component-slide.md +1 -1
  205. package/examples/component-slider.md +3 -3
  206. package/examples/component-snackbar-content.md +1 -1
  207. package/examples/component-snackbar.md +1 -1
  208. package/examples/component-sonner.md +1 -1
  209. package/examples/component-space.md +1 -1
  210. package/examples/component-speed-dial-action.md +1 -1
  211. package/examples/component-speed-dial.md +1 -1
  212. package/examples/component-spin.md +2 -2
  213. package/examples/component-spinner.md +1 -1
  214. package/examples/component-splitter.md +2 -2
  215. package/examples/component-stack.md +1 -1
  216. package/examples/component-statistic.md +1 -1
  217. package/examples/component-step-button.md +4 -4
  218. package/examples/component-step-connector.md +4 -4
  219. package/examples/component-step-content.md +1 -1
  220. package/examples/component-step-icon.md +1 -1
  221. package/examples/component-step-label.md +1 -1
  222. package/examples/component-step.md +2 -2
  223. package/examples/component-steps.md +2 -2
  224. package/examples/component-swipeable-drawer.md +1 -1
  225. package/examples/component-switch.md +3 -3
  226. package/examples/component-tab-scroll-button.md +5 -5
  227. package/examples/component-tab.md +1 -1
  228. package/examples/component-table-body.md +1 -1
  229. package/examples/component-table-cell.md +1 -1
  230. package/examples/component-table-container.md +1 -1
  231. package/examples/component-table-footer.md +1 -1
  232. package/examples/component-table-head.md +1 -1
  233. package/examples/component-table-pagination.md +1 -1
  234. package/examples/component-table-row.md +1 -1
  235. package/examples/component-table-sort-label.md +1 -1
  236. package/examples/component-table.md +2 -2
  237. package/examples/component-tabs.md +3 -3
  238. package/examples/component-tag-badge.md +2 -2
  239. package/examples/component-tag.md +1 -1
  240. package/examples/component-textarea-autosize.md +1 -1
  241. package/examples/component-textarea.md +1 -1
  242. package/examples/component-time-picker.md +1 -1
  243. package/examples/component-timeline.md +1 -1
  244. package/examples/component-toast.md +4 -4
  245. package/examples/component-toggle-button.md +1 -1
  246. package/examples/component-toggle.md +1 -1
  247. package/examples/component-toolbar.md +1 -1
  248. package/examples/component-tooltip.md +3 -3
  249. package/examples/component-tour.md +1 -1
  250. package/examples/component-transfer.md +1 -1
  251. package/examples/component-tree-select.md +1 -1
  252. package/examples/component-tree.md +1 -1
  253. package/examples/component-typography.md +2 -2
  254. package/examples/component-upload.md +1 -1
  255. package/examples/component-watermark.md +1 -1
  256. package/examples/component-zoom.md +1 -1
  257. package/knowledge/COVERAGE.md +12 -9
  258. package/knowledge/components/INDEX.md +202 -202
  259. package/knowledge/i18n/korean-density-conventions.md +108 -0
  260. package/knowledge/i18n/korean-product-conventions.md +2 -0
  261. package/knowledge/patterns/async-control.md +233 -0
  262. package/knowledge/patterns/brand-references.md +72 -72
  263. package/knowledge/patterns/error-states.md +1 -0
  264. package/knowledge/patterns/form-design.md +2 -0
  265. package/package.json +1 -1
  266. package/tools/audit/local-ci.py +16 -1
  267. package/tools/audit/package-smoke.py +611 -9
  268. package/tools/audit/registry-smoke.py +537 -3
  269. package/tools/audit/release-metadata.py +47 -1
  270. package/tools/audit/smoke_assertions.py +918 -13
  271. package/tools/migrations/refs-links-to-reference-pages.py +179 -0
@@ -39,6 +39,8 @@ from smoke_assertions import (
39
39
  EXPECTED_HELP_ALIASES,
40
40
  EXPECTED_NUMERIC_VALUE_SMOKES,
41
41
  EXPECTED_PACK_MAX_BYTES,
42
+ EXPECTED_RANKED_SEARCH_LIMIT,
43
+ EXPECTED_RANKED_SEARCH_NOT_BUILT_NOTICE,
42
44
  EXPECTED_REPOSITORY_URL,
43
45
  EXPECTED_ROUTE_BRIEF,
44
46
  EXPECTED_ROUTE_ID,
@@ -61,6 +63,9 @@ from smoke_assertions import (
61
63
  assert_force_overwrite_replaced,
62
64
  assert_functional_alias_smokes,
63
65
  assert_help_topic_output,
66
+ assert_index_build_json,
67
+ assert_index_status_json,
68
+ assert_index_verify_json,
64
69
  assert_install_doctor_lifecycle_output,
65
70
  assert_list_catalog_output,
66
71
  assert_list_catalog_json,
@@ -72,6 +77,8 @@ from smoke_assertions import (
72
77
  assert_pack_json_component_spec,
73
78
  assert_pack_markdown_body_component_spec,
74
79
  assert_pack_markdown_component_spec,
80
+ assert_ranked_search_determinism,
81
+ assert_ranked_search_json,
75
82
  assert_site_repair_apply_report_payload,
76
83
  assert_site_repair_guidance_report_contract,
77
84
  assert_site_repair_preview_report_payload,
@@ -132,6 +139,10 @@ from smoke_assertions import (
132
139
  parse_help_topics,
133
140
  passing_doctor_report_json,
134
141
  passing_check_artifact_content,
142
+ passing_index_build_json,
143
+ passing_index_status_json,
144
+ passing_index_verify_json,
145
+ passing_ranked_search_json,
135
146
  passing_site_json,
136
147
  passing_site_mcp_check_json,
137
148
  passing_site_mcp_check_probes_human,
@@ -2205,6 +2216,102 @@ def assert_learning_redact_smoke(
2205
2216
  )
2206
2217
 
2207
2218
 
2219
+ def assert_learning_recall_json(
2220
+ raw: str,
2221
+ *,
2222
+ context: str,
2223
+ cmd: list[str],
2224
+ ) -> None:
2225
+ assert_no_ansi(raw, cmd)
2226
+ try:
2227
+ payload = json.loads(raw)
2228
+ except json.JSONDecodeError as error:
2229
+ raise SystemExit(f"{context}: failed to parse learn recall JSON") from error
2230
+
2231
+ require_registry_smoke(
2232
+ isinstance(payload, dict) and payload.get("query") == "korean mobile",
2233
+ context=context,
2234
+ cmd=cmd,
2235
+ message="learn recall JSON must echo the query",
2236
+ )
2237
+ corpus = payload.get("corpus")
2238
+ require_registry_smoke(
2239
+ isinstance(corpus, dict)
2240
+ and isinstance(corpus.get("candidateCount"), int)
2241
+ and corpus.get("candidateCount") > 0
2242
+ and isinstance(corpus.get("selectedCount"), int)
2243
+ and isinstance(corpus.get("selected"), list),
2244
+ context=context,
2245
+ cmd=cmd,
2246
+ message="learn recall corpus block shape changed",
2247
+ )
2248
+ learning = payload.get("learning")
2249
+ require_registry_smoke(
2250
+ isinstance(learning, dict)
2251
+ and learning.get("mode") == "brief-relevance"
2252
+ and learning.get("candidateCount") == 3
2253
+ and isinstance(learning.get("selected"), list),
2254
+ context=context,
2255
+ cmd=cmd,
2256
+ message="learn recall learning block shape changed",
2257
+ )
2258
+ selected = learning.get("selected")
2259
+ require_registry_smoke(
2260
+ len(selected) >= 1
2261
+ and isinstance(selected[0], dict)
2262
+ and selected[0].get("id") == "registry-korean"
2263
+ and selected[0].get("category") == "korean"
2264
+ and isinstance(selected[0].get("matchedTokens"), list),
2265
+ context=context,
2266
+ cmd=cmd,
2267
+ message="learn recall should rank the Korean learning entry for a Korean query",
2268
+ )
2269
+
2270
+
2271
+ def assert_learning_recall_smoke(
2272
+ command_factory,
2273
+ profile_path: Path,
2274
+ *,
2275
+ env: dict[str, str],
2276
+ cwd: Path | None = None,
2277
+ context: str,
2278
+ ) -> None:
2279
+ write_learning_stats_fixture(profile_path)
2280
+
2281
+ json_cmd = command_factory(
2282
+ "learn",
2283
+ "--recall",
2284
+ "korean mobile",
2285
+ "--file",
2286
+ str(profile_path),
2287
+ "--json",
2288
+ )
2289
+ json_result = run_plain(json_cmd, cwd=cwd, env=env)
2290
+ assert_learning_recall_json(json_result.stdout, context=f"{context} JSON", cmd=json_cmd)
2291
+
2292
+ # Read-only: the profile file is unchanged after recall.
2293
+ before = write_learning_stats_fixture_snapshot(profile_path)
2294
+ human_cmd = command_factory("learn", "--recall", "korean mobile", "--file", str(profile_path))
2295
+ human_result = run_plain(human_cmd, cwd=cwd, env=env)
2296
+ require_registry_smoke(
2297
+ "Recall (read-only)" in human_result.stdout
2298
+ and "Privacy: recall is read-only" in human_result.stdout,
2299
+ context=f"{context} human",
2300
+ cmd=human_cmd,
2301
+ message="learn recall human output missing read-only markers",
2302
+ )
2303
+ require_registry_smoke(
2304
+ profile_path.read_text(encoding="utf-8") == before,
2305
+ context=f"{context} read-only",
2306
+ cmd=human_cmd,
2307
+ message="learn recall must not mutate the learning profile",
2308
+ )
2309
+
2310
+
2311
+ def write_learning_stats_fixture_snapshot(profile_path: Path) -> str:
2312
+ return profile_path.read_text(encoding="utf-8")
2313
+
2314
+
2208
2315
  def assert_learning_stats_json(
2209
2316
  raw: str,
2210
2317
  *,
@@ -4412,6 +4519,56 @@ def assert_pack_stdin_smoke(
4412
4519
  )
4413
4520
 
4414
4521
 
4522
+ def assert_recall_context(payload: dict[str, object], *, context: str, cmd: list[str]) -> None:
4523
+ recall = payload.get("recall")
4524
+ require_registry_smoke(
4525
+ isinstance(recall, dict),
4526
+ context=context,
4527
+ cmd=cmd,
4528
+ message="recall should be present when --with-recall is used",
4529
+ )
4530
+ require_registry_smoke(
4531
+ recall.get("mode") == "lexical",
4532
+ context=context,
4533
+ cmd=cmd,
4534
+ message="recall should use the deterministic lexical scorer",
4535
+ )
4536
+ selected_count = recall.get("selectedCount")
4537
+ require_registry_smoke(
4538
+ isinstance(selected_count, int) and selected_count >= 1,
4539
+ context=context,
4540
+ cmd=cmd,
4541
+ message="recall should select at least one corpus file for the Button brief",
4542
+ )
4543
+ selected = recall.get("selected")
4544
+ require_registry_smoke(
4545
+ isinstance(selected, list) and len(selected) == selected_count and len(selected) >= 1,
4546
+ context=context,
4547
+ cmd=cmd,
4548
+ message="recall selected list should match the reported selected count",
4549
+ )
4550
+ top = selected[0]
4551
+ require_registry_smoke(
4552
+ isinstance(top, dict) and isinstance(top.get("id"), str) and top.get("id"),
4553
+ context=context,
4554
+ cmd=cmd,
4555
+ message="recall selection should cite the corpus relPath as its id",
4556
+ )
4557
+ require_registry_smoke(
4558
+ type(top.get("score")) in (int, float) and top.get("score") > 0,
4559
+ context=context,
4560
+ cmd=cmd,
4561
+ message="recall selection should include a positive relevance score",
4562
+ )
4563
+ markdown = recall.get("markdown")
4564
+ require_registry_smoke(
4565
+ isinstance(markdown, str) and "## Recalled design knowledge" in markdown and top.get("id") in markdown,
4566
+ context=context,
4567
+ cmd=cmd,
4568
+ message="recall markdown should carry the Recalled design knowledge section and cite the top file",
4569
+ )
4570
+
4571
+
4415
4572
  def assert_learning_relevance_context(payload: dict[str, object], *, context: str, cmd: list[str]) -> None:
4416
4573
  learning_context = payload.get("learningContext")
4417
4574
  require_registry_smoke(
@@ -4628,7 +4785,7 @@ def assert_learning_query_human(raw: str, *, context: str, cmd: list[str]) -> No
4628
4785
  "Limit: 2",
4629
4786
  "Explain: selection score, matched tokens, and reason",
4630
4787
  "[accessibility] Prioritize keyboard accessibility details for Button component API specs",
4631
- "matched keyboard, accessibility",
4788
+ "matched accessibility, keyboard",
4632
4789
  "reason brief-match",
4633
4790
  ):
4634
4791
  require_registry_smoke(
@@ -4989,6 +5146,49 @@ def assert_learning_relevance_smoke(
4989
5146
  require_registry_smoke(isinstance(plan, dict), context=f"{context} pack", cmd=pack_cmd, message="pack plan missing")
4990
5147
  assert_learning_relevance_context(plan, context=f"{context} pack plan", cmd=pack_cmd)
4991
5148
 
5149
+ recall_prompt_cmd = command_factory(
5150
+ "prompt",
5151
+ EXPECTED_ROUTE_BRIEF,
5152
+ "--route",
5153
+ EXPECTED_ROUTE_ID,
5154
+ "--with-recall",
5155
+ "--recall-limit",
5156
+ "3",
5157
+ "--json",
5158
+ )
5159
+ recall_prompt_result = run_plain(recall_prompt_cmd, cwd=cwd, env=relevance_env)
5160
+ try:
5161
+ recall_prompt_payload = json.loads(recall_prompt_result.stdout)
5162
+ except json.JSONDecodeError as error:
5163
+ raise SystemExit(f"{context}: failed to parse prompt recall JSON") from error
5164
+ assert_recall_context(recall_prompt_payload, context=f"{context} prompt recall", cmd=recall_prompt_cmd)
5165
+
5166
+ recall_pack_cmd = command_factory(
5167
+ "pack",
5168
+ EXPECTED_ROUTE_BRIEF,
5169
+ "--route",
5170
+ EXPECTED_ROUTE_ID,
5171
+ "--with-recall",
5172
+ "--recall-limit",
5173
+ "3",
5174
+ "--max-bytes",
5175
+ str(EXPECTED_PACK_MAX_BYTES),
5176
+ "--json",
5177
+ )
5178
+ recall_pack_result = run_plain(recall_pack_cmd, cwd=cwd, env=relevance_env)
5179
+ try:
5180
+ recall_pack_payload = json.loads(recall_pack_result.stdout)
5181
+ except json.JSONDecodeError as error:
5182
+ raise SystemExit(f"{context}: failed to parse pack recall JSON") from error
5183
+ recall_plan = recall_pack_payload.get("plan")
5184
+ require_registry_smoke(
5185
+ isinstance(recall_plan, dict),
5186
+ context=f"{context} pack recall",
5187
+ cmd=recall_pack_cmd,
5188
+ message="pack recall plan missing",
5189
+ )
5190
+ assert_recall_context(recall_plan, context=f"{context} pack recall plan", cmd=recall_pack_cmd)
5191
+
4992
5192
  eval_template_path = profile_path.with_name(f"{profile_path.stem}-eval-template.json")
4993
5193
  eval_template_path.write_text("stale eval template output\n", encoding="utf-8")
4994
5194
  eval_template_cmd = command_factory(
@@ -5055,6 +5255,166 @@ def assert_learning_relevance_smoke(
5055
5255
  )
5056
5256
 
5057
5257
 
5258
+ def assert_index_roundtrip_smoke(
5259
+ command_factory,
5260
+ index_dir: Path,
5261
+ profile_path: Path,
5262
+ *,
5263
+ env: dict[str, str],
5264
+ cwd: Path | None = None,
5265
+ context: str,
5266
+ ) -> None:
5267
+ """`index --build` -> `index --status` -> `index --verify` round-trip against a
5268
+ temp index dir and learning profile, isolated from the ambient environment."""
5269
+ index_dir.mkdir(parents=True, exist_ok=True)
5270
+ write_learning_relevance_fixture(profile_path)
5271
+ index_env = env.copy()
5272
+ index_env["DESIGN_AI_INDEX_DIR"] = str(index_dir)
5273
+ index_env["DESIGN_AI_LEARNING_FILE"] = str(profile_path)
5274
+
5275
+ build_cmd = command_factory("index", "--build", "--json")
5276
+ build_result = run_plain(build_cmd, cwd=cwd, env=index_env)
5277
+ assert_index_build_json(
5278
+ build_result.stdout,
5279
+ index_dir=str(index_dir),
5280
+ context=f"{context} build",
5281
+ cmd=build_cmd,
5282
+ )
5283
+
5284
+ status_cmd = command_factory("index", "--status", "--json")
5285
+ status_result = run_plain(status_cmd, cwd=cwd, env=index_env)
5286
+ assert_index_status_json(
5287
+ status_result.stdout,
5288
+ index_dir=str(index_dir),
5289
+ context=f"{context} status",
5290
+ cmd=status_cmd,
5291
+ )
5292
+
5293
+ verify_cmd = command_factory("index", "--verify", "--json")
5294
+ verify_result = run_plain(verify_cmd, cwd=cwd, env=index_env)
5295
+ assert_index_verify_json(
5296
+ verify_result.stdout,
5297
+ index_dir=str(index_dir),
5298
+ context=f"{context} verify",
5299
+ cmd=verify_cmd,
5300
+ )
5301
+
5302
+
5303
+ def assert_ranked_search_determinism_smoke(
5304
+ command_factory,
5305
+ index_dir: Path,
5306
+ *,
5307
+ env: dict[str, str],
5308
+ cwd: Path | None = None,
5309
+ context: str,
5310
+ ) -> None:
5311
+ """Two identical `search --ranked --json` runs must produce byte-identical output."""
5312
+ index_dir.mkdir(parents=True, exist_ok=True)
5313
+ ranked_env = env.copy()
5314
+ ranked_env["DESIGN_AI_INDEX_DIR"] = str(index_dir)
5315
+
5316
+ ranked_cmd = command_factory(
5317
+ "search",
5318
+ EXPECTED_CORPUS_SEARCH_QUERY,
5319
+ "--dir",
5320
+ "knowledge",
5321
+ "--limit",
5322
+ str(EXPECTED_RANKED_SEARCH_LIMIT),
5323
+ "--ranked",
5324
+ "--json",
5325
+ )
5326
+ first_result = run_plain(ranked_cmd, cwd=cwd, env=ranked_env)
5327
+ second_result = run_plain(ranked_cmd, cwd=cwd, env=ranked_env)
5328
+ assert_ranked_search_json(
5329
+ first_result.stdout,
5330
+ context=f"{context} payload",
5331
+ cmd=ranked_cmd,
5332
+ expected_notice=EXPECTED_RANKED_SEARCH_NOT_BUILT_NOTICE,
5333
+ )
5334
+ assert_ranked_search_determinism(
5335
+ first_result.stdout,
5336
+ second_result.stdout,
5337
+ context=context,
5338
+ cmd=ranked_cmd,
5339
+ )
5340
+
5341
+
5342
+ def assert_embeddings_off_by_default_smoke(
5343
+ command_factory,
5344
+ index_dir: Path,
5345
+ profile_path: Path,
5346
+ *,
5347
+ env: dict[str, str],
5348
+ cwd: Path | None = None,
5349
+ context: str,
5350
+ ) -> None:
5351
+ """Phase B is opt-in: a plain `index --build` never touches embeddings, and
5352
+ `index --status` reports `embeddings: null` (the feature is simply unused)."""
5353
+ index_dir.mkdir(parents=True, exist_ok=True)
5354
+ write_learning_relevance_fixture(profile_path)
5355
+ index_env = env.copy()
5356
+ index_env["DESIGN_AI_INDEX_DIR"] = str(index_dir)
5357
+ index_env["DESIGN_AI_LEARNING_FILE"] = str(profile_path)
5358
+ # Point at a config file that is guaranteed not to exist, rather than relying on
5359
+ # the ambient environment lacking DESIGN_AI_CONFIG_FILE — a real ~/.design-ai/
5360
+ # config.json on the machine running this smoke must not leak into the test.
5361
+ index_env["DESIGN_AI_CONFIG_FILE"] = str(index_dir.parent / "no-config-here" / "config.json")
5362
+
5363
+ build_cmd = command_factory("index", "--build", "--json")
5364
+ build_result = run_plain(build_cmd, cwd=cwd, env=index_env)
5365
+ build_payload = json.loads(build_result.stdout)
5366
+ if "embeddings" in build_payload:
5367
+ raise SystemExit(f"index build JSON after {context} unexpectedly ran provider execution with plain --build")
5368
+
5369
+ status_cmd = command_factory("index", "--status", "--json")
5370
+ status_result = run_plain(status_cmd, cwd=cwd, env=index_env)
5371
+ assert_index_status_json(
5372
+ status_result.stdout,
5373
+ index_dir=str(index_dir),
5374
+ context=f"{context} status",
5375
+ cmd=status_cmd,
5376
+ expect_embeddings=False,
5377
+ )
5378
+
5379
+
5380
+ def assert_search_embeddings_no_provider_fallback_smoke(
5381
+ command_factory,
5382
+ *,
5383
+ env: dict[str, str],
5384
+ cwd: Path | None = None,
5385
+ context: str,
5386
+ config_file: Path,
5387
+ ) -> None:
5388
+ """`search --ranked --embeddings` with no provider configured (flag or config)
5389
+ degrades to the lexical backend with a notice and exit code 0."""
5390
+ fallback_env = env.copy()
5391
+ # Same isolation rationale as assert_embeddings_off_by_default_smoke: point at a
5392
+ # config path guaranteed not to exist rather than trusting the ambient environment.
5393
+ fallback_env["DESIGN_AI_CONFIG_FILE"] = str(config_file)
5394
+
5395
+ search_cmd = command_factory(
5396
+ "search",
5397
+ EXPECTED_CORPUS_SEARCH_QUERY,
5398
+ "--dir",
5399
+ "knowledge",
5400
+ "--limit",
5401
+ str(EXPECTED_RANKED_SEARCH_LIMIT),
5402
+ "--ranked",
5403
+ "--embeddings",
5404
+ "--json",
5405
+ )
5406
+ result = run_plain(search_cmd, cwd=cwd, env=fallback_env)
5407
+ assert_ranked_search_json(
5408
+ result.stdout,
5409
+ context=f"{context} payload",
5410
+ cmd=search_cmd,
5411
+ expected_backend="lexical",
5412
+ )
5413
+ payload = json.loads(result.stdout)
5414
+ if "embedding provider" not in payload.get("notice", "") and "no embedding provider configured" not in payload.get("notice", ""):
5415
+ raise SystemExit(f"search ranked JSON after {context} fallback notice does not mention the missing provider")
5416
+
5417
+
5058
5418
  def wait_for_registry_package(
5059
5419
  package_spec: str,
5060
5420
  *,
@@ -6086,6 +6446,13 @@ def smoke_registry_package(package_spec: str, *, retries: int, delay: float) ->
6086
6446
  env=env,
6087
6447
  context="registry smoke npm exec learn stats",
6088
6448
  )
6449
+ assert_learning_recall_smoke(
6450
+ lambda *args: npm_exec_cmd(package_spec, *args),
6451
+ npx_root / "registry-recall-learning.json",
6452
+ cwd=npx_root,
6453
+ env=env,
6454
+ context="registry smoke npm exec learn recall",
6455
+ )
6089
6456
  assert_learning_audit_cleanup_smoke(
6090
6457
  lambda *args: npm_exec_cmd(package_spec, *args),
6091
6458
  npx_root / "registry-audit-learning.json",
@@ -6100,6 +6467,36 @@ def smoke_registry_package(package_spec: str, *, retries: int, delay: float) ->
6100
6467
  env=env,
6101
6468
  context="registry smoke npm exec learning relevance",
6102
6469
  )
6470
+ assert_index_roundtrip_smoke(
6471
+ lambda *args: npm_exec_cmd(package_spec, *args),
6472
+ npx_root / "registry-index",
6473
+ npx_root / "registry-index-learning.json",
6474
+ cwd=npx_root,
6475
+ env=env,
6476
+ context="registry smoke npm exec index roundtrip",
6477
+ )
6478
+ assert_ranked_search_determinism_smoke(
6479
+ lambda *args: npm_exec_cmd(package_spec, *args),
6480
+ npx_root / "registry-ranked-search-index",
6481
+ cwd=npx_root,
6482
+ env=env,
6483
+ context="registry smoke npm exec ranked search determinism",
6484
+ )
6485
+ assert_embeddings_off_by_default_smoke(
6486
+ lambda *args: npm_exec_cmd(package_spec, *args),
6487
+ npx_root / "registry-embeddings-off-index",
6488
+ npx_root / "registry-embeddings-off-learning.json",
6489
+ cwd=npx_root,
6490
+ env=env,
6491
+ context="registry smoke npm exec embeddings off by default",
6492
+ )
6493
+ assert_search_embeddings_no_provider_fallback_smoke(
6494
+ lambda *args: npm_exec_cmd(package_spec, *args),
6495
+ cwd=npx_root,
6496
+ env=env,
6497
+ context="registry smoke npm exec search embeddings no provider fallback",
6498
+ config_file=npx_root / "registry-no-config-here" / "config.json",
6499
+ )
6103
6500
  assert_update_dry_run_smoke(
6104
6501
  npm_exec_cmd(package_spec, "update", "--dry-run"),
6105
6502
  cwd=npx_root,
@@ -7981,7 +8378,7 @@ def run_self_test() -> None:
7981
8378
  "",
7982
8379
  "1. [accessibility] Prioritize keyboard accessibility details for Button component API specs",
7983
8380
  " learn-relevant · 2026-05-22T00:00:01.000Z",
7984
- " score 4 · matched keyboard, accessibility · reason brief-match",
8381
+ " score 2.114533 · matched accessibility, keyboard · reason brief-match",
7985
8382
  ]),
7986
8383
  context="registry smoke self-test",
7987
8384
  cmd=learn_query_human_cmd,
@@ -7995,7 +8392,7 @@ def run_self_test() -> None:
7995
8392
  "Limit: 2",
7996
8393
  "Explain: selection score, matched tokens, and reason",
7997
8394
  "[accessibility] Prioritize keyboard accessibility details for Button component API specs",
7998
- "matched keyboard, accessibility",
8395
+ "matched accessibility, keyboard",
7999
8396
  ]),
8000
8397
  context="registry smoke self-test",
8001
8398
  cmd=learn_query_human_cmd,
@@ -8297,6 +8694,38 @@ def run_self_test() -> None:
8297
8694
  scope="registry smoke",
8298
8695
  )
8299
8696
 
8697
+ recall_payload = {
8698
+ "recall": {
8699
+ "query": EXPECTED_ROUTE_BRIEF,
8700
+ "mode": "lexical",
8701
+ "candidateCount": 42,
8702
+ "selectedCount": 1,
8703
+ "selected": [
8704
+ {
8705
+ "id": "knowledge/components/INDEX.md",
8706
+ "score": 9.5,
8707
+ "matchedTokens": ["button", "component"],
8708
+ },
8709
+ ],
8710
+ "markdown": (
8711
+ "## Recalled design knowledge\n\n"
8712
+ "- knowledge/components/INDEX.md\n"
8713
+ " - Component index"
8714
+ ),
8715
+ },
8716
+ }
8717
+ recall_cmd = ["design-ai", "prompt", EXPECTED_ROUTE_BRIEF, "--with-recall", "--json"]
8718
+ assert_recall_context(recall_payload, context="registry smoke self-test", cmd=recall_cmd)
8719
+ expect_self_test_failure(
8720
+ lambda: assert_recall_context(
8721
+ {**recall_payload, "recall": {**recall_payload["recall"], "selectedCount": 0, "selected": []}},
8722
+ context="registry smoke self-test",
8723
+ cmd=recall_cmd,
8724
+ ),
8725
+ expected="recall should select at least one corpus file",
8726
+ scope="registry smoke",
8727
+ )
8728
+
8300
8729
  learning_audit_path = tmp_root / "learning-audit.json"
8301
8730
  duplicate_command_args = [
8302
8731
  "design-ai",
@@ -8503,6 +8932,111 @@ def run_self_test() -> None:
8503
8932
  scope="registry smoke",
8504
8933
  )
8505
8934
 
8935
+ index_dir = str(tmp_root / "registry-index-self-test")
8936
+ index_build_cmd = ["design-ai", "index", "--build", "--json"]
8937
+ assert_index_build_json(
8938
+ passing_index_build_json(index_dir),
8939
+ index_dir=index_dir,
8940
+ context="registry smoke self-test",
8941
+ cmd=index_build_cmd,
8942
+ )
8943
+ expect_self_test_failure(
8944
+ lambda: assert_index_build_json(
8945
+ "{",
8946
+ index_dir=index_dir,
8947
+ context="registry smoke self-test",
8948
+ cmd=index_build_cmd,
8949
+ ),
8950
+ expected="failed to parse index build JSON",
8951
+ scope="registry smoke",
8952
+ )
8953
+
8954
+ index_status_cmd = ["design-ai", "index", "--status", "--json"]
8955
+ assert_index_status_json(
8956
+ passing_index_status_json(index_dir),
8957
+ index_dir=index_dir,
8958
+ context="registry smoke self-test",
8959
+ cmd=index_status_cmd,
8960
+ )
8961
+ assert_index_status_json(
8962
+ passing_index_status_json(index_dir, with_embeddings=True),
8963
+ index_dir=index_dir,
8964
+ context="registry smoke self-test embeddings",
8965
+ cmd=index_status_cmd,
8966
+ expect_embeddings=True,
8967
+ )
8968
+ expect_self_test_failure(
8969
+ lambda: assert_index_status_json(
8970
+ passing_index_status_json(index_dir, with_embeddings=True),
8971
+ index_dir=index_dir,
8972
+ context="registry smoke self-test embeddings unexpected",
8973
+ cmd=index_status_cmd,
8974
+ expect_embeddings=False,
8975
+ ),
8976
+ expected="embeddings is not null when embeddings were not built",
8977
+ scope="registry smoke",
8978
+ )
8979
+
8980
+ index_verify_cmd = ["design-ai", "index", "--verify", "--json"]
8981
+ assert_index_verify_json(
8982
+ passing_index_verify_json(index_dir),
8983
+ index_dir=index_dir,
8984
+ context="registry smoke self-test",
8985
+ cmd=index_verify_cmd,
8986
+ )
8987
+ expect_self_test_failure(
8988
+ lambda: assert_index_verify_json(
8989
+ json.dumps({**json.loads(passing_index_verify_json(index_dir)), "ok": False}),
8990
+ index_dir=index_dir,
8991
+ context="registry smoke self-test",
8992
+ cmd=index_verify_cmd,
8993
+ ),
8994
+ expected="ok is not true",
8995
+ scope="registry smoke",
8996
+ )
8997
+
8998
+ ranked_search_cmd = ["design-ai", "search", EXPECTED_CORPUS_SEARCH_QUERY, "--ranked", "--json"]
8999
+ ranked_search_json = passing_ranked_search_json()
9000
+ assert_ranked_search_json(
9001
+ ranked_search_json,
9002
+ context="registry smoke self-test",
9003
+ cmd=ranked_search_cmd,
9004
+ )
9005
+ assert_ranked_search_determinism(
9006
+ ranked_search_json,
9007
+ ranked_search_json,
9008
+ context="registry smoke self-test",
9009
+ cmd=ranked_search_cmd,
9010
+ )
9011
+ expect_self_test_failure(
9012
+ lambda: assert_ranked_search_determinism(
9013
+ ranked_search_json,
9014
+ passing_ranked_search_json(backend="lexical"),
9015
+ context="registry smoke self-test",
9016
+ cmd=ranked_search_cmd,
9017
+ ),
9018
+ expected="differs between identical runs",
9019
+ scope="registry smoke",
9020
+ )
9021
+
9022
+ ranked_search_fallback_json = passing_ranked_search_json(backend="lexical")
9023
+ assert_ranked_search_json(
9024
+ ranked_search_fallback_json,
9025
+ context="registry smoke self-test fallback",
9026
+ cmd=ranked_search_cmd,
9027
+ expected_backend="lexical",
9028
+ )
9029
+ expect_self_test_failure(
9030
+ lambda: assert_ranked_search_json(
9031
+ passing_ranked_search_json(backend="embeddings"),
9032
+ context="registry smoke self-test fallback mismatch",
9033
+ cmd=ranked_search_cmd,
9034
+ expected_backend="lexical",
9035
+ ),
9036
+ expected="backend differs from expected backend",
9037
+ scope="registry smoke",
9038
+ )
9039
+
8506
9040
  expect_self_test_failure(
8507
9041
  lambda: smoke_registry_package("self-test-package", retries=0, delay=0),
8508
9042
  expected="--retries must be at least 1",