@design-ai/cli 4.56.0 → 4.57.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 (264) hide show
  1. package/.claude-plugin/plugin.json +109 -25
  2. package/CHANGELOG.md +36 -0
  3. package/README.ko.md +9 -26
  4. package/README.md +10 -23
  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/search.mjs +94 -1
  16. package/cli/lib/dispatch.mjs +3 -0
  17. package/cli/lib/embedding-index.mjs +199 -0
  18. package/cli/lib/embedding-provider.mjs +121 -0
  19. package/cli/lib/embedding-rerank.mjs +52 -0
  20. package/cli/lib/learn-args.mjs +490 -0
  21. package/cli/lib/learn-backup.mjs +748 -0
  22. package/cli/lib/learn-curation.mjs +612 -0
  23. package/cli/lib/learn-eval.mjs +459 -0
  24. package/cli/lib/learn-profile.mjs +763 -0
  25. package/cli/lib/learn-select.mjs +223 -0
  26. package/cli/lib/learn-shared.mjs +158 -0
  27. package/cli/lib/learn-test-support.mjs +218 -0
  28. package/cli/lib/learn-usage.mjs +360 -0
  29. package/cli/lib/learn.mjs +83 -3694
  30. package/cli/lib/lexical.mjs +137 -0
  31. package/cli/lib/local-config.mjs +110 -0
  32. package/cli/lib/mcp-server.mjs +13 -3
  33. package/cli/lib/pack.mjs +31 -4
  34. package/cli/lib/prompt.mjs +42 -4
  35. package/cli/lib/recall.mjs +193 -0
  36. package/cli/lib/retrieval-index.mjs +0 -0
  37. package/cli/lib/search-ranked.mjs +170 -0
  38. package/cli/lib/search.mjs +11 -1
  39. package/cli/lib/signals-backlog-commands.mjs +673 -0
  40. package/cli/lib/signals-backlog.mjs +361 -0
  41. package/cli/lib/signals-eval.mjs +176 -0
  42. package/cli/lib/signals-readiness.mjs +247 -0
  43. package/cli/lib/signals-registry.mjs +377 -0
  44. package/cli/lib/signals-render.mjs +478 -0
  45. package/cli/lib/signals-shared.mjs +75 -0
  46. package/cli/lib/signals.mjs +16 -2337
  47. package/cli/lib/site-bundle-handoff-expected.mjs +173 -0
  48. package/cli/lib/site-bundle-handoff-runbook-action-summary.mjs +332 -0
  49. package/cli/lib/site-bundle-handoff-runbook-human-lines.mjs +167 -0
  50. package/cli/lib/site-bundle-handoff-runbook-maps.mjs +238 -0
  51. package/cli/lib/site-bundle-handoff-runbook-next-step.mjs +278 -0
  52. package/cli/lib/site-bundle-handoff-runbook.mjs +44 -931
  53. package/cli/lib/site-test-support.mjs +68 -0
  54. package/cli/lib/skill-proposals-apply-commands.mjs +135 -0
  55. package/cli/lib/skill-proposals-apply-contract.mjs +750 -0
  56. package/cli/lib/skill-proposals-apply-plan.mjs +118 -0
  57. package/cli/lib/skill-proposals-generate.mjs +298 -0
  58. package/cli/lib/skill-proposals-render.mjs +532 -0
  59. package/cli/lib/skill-proposals-review.mjs +262 -0
  60. package/cli/lib/skill-proposals.mjs +15 -2046
  61. package/cli/lib/workspace-args.mjs +83 -0
  62. package/cli/lib/workspace-git.mjs +169 -0
  63. package/cli/lib/workspace-learning.mjs +483 -0
  64. package/cli/lib/workspace-repo.mjs +139 -0
  65. package/cli/lib/workspace-report.mjs +283 -0
  66. package/cli/lib/workspace-test-support.mjs +99 -0
  67. package/cli/lib/workspace.mjs +30 -1056
  68. package/docs/AI-LEARNING-PHASE2.md +215 -0
  69. package/docs/DISTRIBUTION.ko.md +2 -2
  70. package/docs/DISTRIBUTION.md +2 -2
  71. package/docs/NEXT-SURFACE-DECISION.md +125 -0
  72. package/docs/PRODUCT-READINESS.md +3 -3
  73. package/docs/RELEASE-GATES.ko.md +35 -0
  74. package/docs/RELEASE-GATES.md +234 -0
  75. package/docs/ROADMAP.md +53 -8
  76. package/docs/external-status.md +6 -8
  77. package/docs/inspection-20260630.md +169 -0
  78. package/docs/integrations/design-ai-mcp-server.md +4 -4
  79. package/docs/reference/ant-design.md +413 -0
  80. package/docs/reference/awesome-design-md.md +439 -0
  81. package/docs/reference/mui.md +783 -0
  82. package/docs/reference/shadcn-ui.md +298 -0
  83. package/examples/component-accordion-actions.md +5 -5
  84. package/examples/component-accordion-details.md +5 -5
  85. package/examples/component-accordion-summary.md +5 -5
  86. package/examples/component-accordion.md +3 -3
  87. package/examples/component-affix.md +1 -1
  88. package/examples/component-alert-dialog.md +1 -1
  89. package/examples/component-alert-title.md +1 -1
  90. package/examples/component-alert.md +3 -3
  91. package/examples/component-anchor.md +1 -1
  92. package/examples/component-app-bar.md +1 -1
  93. package/examples/component-aspect-ratio.md +1 -1
  94. package/examples/component-auto-complete.md +2 -2
  95. package/examples/component-avatar-group.md +5 -5
  96. package/examples/component-avatar.md +3 -3
  97. package/examples/component-back-top.md +1 -1
  98. package/examples/component-backdrop.md +1 -1
  99. package/examples/component-badge.md +3 -3
  100. package/examples/component-border-beam.md +3 -3
  101. package/examples/component-bottom-navigation.md +1 -1
  102. package/examples/component-box.md +1 -1
  103. package/examples/component-breadcrumb.md +3 -3
  104. package/examples/component-button-base.md +3 -3
  105. package/examples/component-button-group.md +2 -2
  106. package/examples/component-button.md +3 -3
  107. package/examples/component-calendar.md +2 -2
  108. package/examples/component-card-actions.md +1 -1
  109. package/examples/component-card-content.md +1 -1
  110. package/examples/component-card-header.md +1 -1
  111. package/examples/component-card-media.md +1 -1
  112. package/examples/component-card.md +3 -3
  113. package/examples/component-carousel.md +2 -2
  114. package/examples/component-cascader.md +1 -1
  115. package/examples/component-chart.md +1 -1
  116. package/examples/component-checkbox.md +3 -3
  117. package/examples/component-click-away-listener.md +1 -1
  118. package/examples/component-code.md +1 -1
  119. package/examples/component-collapsible.md +1 -1
  120. package/examples/component-color-picker.md +1 -1
  121. package/examples/component-combobox.md +1 -1
  122. package/examples/component-command.md +1 -1
  123. package/examples/component-config-provider.md +3 -3
  124. package/examples/component-context-menu.md +1 -1
  125. package/examples/component-css-baseline.md +3 -3
  126. package/examples/component-date-picker.md +2 -2
  127. package/examples/component-descriptions.md +1 -1
  128. package/examples/component-dialog-actions.md +1 -1
  129. package/examples/component-dialog-content-text.md +1 -1
  130. package/examples/component-dialog-content.md +1 -1
  131. package/examples/component-dialog-title.md +1 -1
  132. package/examples/component-dialog.md +2 -2
  133. package/examples/component-divider.md +3 -3
  134. package/examples/component-drawer.md +3 -3
  135. package/examples/component-dropdown.md +3 -3
  136. package/examples/component-empty.md +2 -2
  137. package/examples/component-fade.md +1 -1
  138. package/examples/component-field.md +1 -1
  139. package/examples/component-filled-input.md +1 -1
  140. package/examples/component-flex.md +1 -1
  141. package/examples/component-float-button.md +2 -2
  142. package/examples/component-form-control-label.md +1 -1
  143. package/examples/component-form-control.md +1 -1
  144. package/examples/component-form-controls.md +1 -1
  145. package/examples/component-form-group.md +1 -1
  146. package/examples/component-form-helper-text.md +1 -1
  147. package/examples/component-form-label.md +1 -1
  148. package/examples/component-form.md +2 -2
  149. package/examples/component-grid.md +2 -2
  150. package/examples/component-grow.md +1 -1
  151. package/examples/component-hover-card.md +1 -1
  152. package/examples/component-icon-button.md +1 -1
  153. package/examples/component-icon.md +1 -1
  154. package/examples/component-image-list.md +1 -1
  155. package/examples/component-image.md +1 -1
  156. package/examples/component-input-adornment.md +1 -1
  157. package/examples/component-input-base.md +1 -1
  158. package/examples/component-input-number.md +1 -1
  159. package/examples/component-input-otp.md +2 -2
  160. package/examples/component-input.md +3 -3
  161. package/examples/component-item.md +1 -1
  162. package/examples/component-label.md +1 -1
  163. package/examples/component-layout.md +1 -1
  164. package/examples/component-link.md +1 -1
  165. package/examples/component-list-item-avatar.md +1 -1
  166. package/examples/component-list-item-button.md +1 -1
  167. package/examples/component-list-item-icon.md +1 -1
  168. package/examples/component-list-item-text.md +1 -1
  169. package/examples/component-list-item.md +1 -1
  170. package/examples/component-list-subheader.md +1 -1
  171. package/examples/component-list.md +2 -2
  172. package/examples/component-masonry.md +1 -1
  173. package/examples/component-mentions.md +1 -1
  174. package/examples/component-menu-item.md +1 -1
  175. package/examples/component-menu-list.md +1 -1
  176. package/examples/component-menu.md +1 -1
  177. package/examples/component-menubar.md +1 -1
  178. package/examples/component-message.md +1 -1
  179. package/examples/component-mobile-stepper.md +1 -1
  180. package/examples/component-modal.md +5 -5
  181. package/examples/component-navigation-menu.md +1 -1
  182. package/examples/component-notification.md +1 -1
  183. package/examples/component-outlined-input.md +1 -1
  184. package/examples/component-pagination.md +3 -3
  185. package/examples/component-paper.md +1 -1
  186. package/examples/component-popconfirm.md +1 -1
  187. package/examples/component-popover.md +3 -3
  188. package/examples/component-popper.md +1 -1
  189. package/examples/component-progress.md +2 -2
  190. package/examples/component-qr-code.md +1 -1
  191. package/examples/component-radio.md +3 -3
  192. package/examples/component-rate.md +2 -2
  193. package/examples/component-resizable.md +1 -1
  194. package/examples/component-result.md +1 -1
  195. package/examples/component-scroll-area.md +1 -1
  196. package/examples/component-segmented.md +3 -3
  197. package/examples/component-select.md +3 -3
  198. package/examples/component-separator.md +1 -1
  199. package/examples/component-sheet.md +1 -1
  200. package/examples/component-sidebar.md +1 -1
  201. package/examples/component-skeleton.md +3 -3
  202. package/examples/component-slide.md +1 -1
  203. package/examples/component-slider.md +3 -3
  204. package/examples/component-snackbar-content.md +1 -1
  205. package/examples/component-snackbar.md +1 -1
  206. package/examples/component-sonner.md +1 -1
  207. package/examples/component-space.md +1 -1
  208. package/examples/component-speed-dial-action.md +1 -1
  209. package/examples/component-speed-dial.md +1 -1
  210. package/examples/component-spin.md +2 -2
  211. package/examples/component-spinner.md +1 -1
  212. package/examples/component-splitter.md +2 -2
  213. package/examples/component-stack.md +1 -1
  214. package/examples/component-statistic.md +1 -1
  215. package/examples/component-step-button.md +4 -4
  216. package/examples/component-step-connector.md +4 -4
  217. package/examples/component-step-content.md +1 -1
  218. package/examples/component-step-icon.md +1 -1
  219. package/examples/component-step-label.md +1 -1
  220. package/examples/component-step.md +2 -2
  221. package/examples/component-steps.md +2 -2
  222. package/examples/component-swipeable-drawer.md +1 -1
  223. package/examples/component-switch.md +3 -3
  224. package/examples/component-tab-scroll-button.md +5 -5
  225. package/examples/component-tab.md +1 -1
  226. package/examples/component-table-body.md +1 -1
  227. package/examples/component-table-cell.md +1 -1
  228. package/examples/component-table-container.md +1 -1
  229. package/examples/component-table-footer.md +1 -1
  230. package/examples/component-table-head.md +1 -1
  231. package/examples/component-table-pagination.md +1 -1
  232. package/examples/component-table-row.md +1 -1
  233. package/examples/component-table-sort-label.md +1 -1
  234. package/examples/component-table.md +2 -2
  235. package/examples/component-tabs.md +3 -3
  236. package/examples/component-tag-badge.md +2 -2
  237. package/examples/component-tag.md +1 -1
  238. package/examples/component-textarea-autosize.md +1 -1
  239. package/examples/component-textarea.md +1 -1
  240. package/examples/component-time-picker.md +1 -1
  241. package/examples/component-timeline.md +1 -1
  242. package/examples/component-toast.md +4 -4
  243. package/examples/component-toggle-button.md +1 -1
  244. package/examples/component-toggle.md +1 -1
  245. package/examples/component-toolbar.md +1 -1
  246. package/examples/component-tooltip.md +3 -3
  247. package/examples/component-tour.md +1 -1
  248. package/examples/component-transfer.md +1 -1
  249. package/examples/component-tree-select.md +1 -1
  250. package/examples/component-tree.md +1 -1
  251. package/examples/component-typography.md +2 -2
  252. package/examples/component-upload.md +1 -1
  253. package/examples/component-watermark.md +1 -1
  254. package/examples/component-zoom.md +1 -1
  255. package/knowledge/COVERAGE.md +4 -3
  256. package/knowledge/components/INDEX.md +202 -202
  257. package/knowledge/patterns/brand-references.md +72 -72
  258. package/package.json +1 -1
  259. package/tools/audit/local-ci.py +16 -1
  260. package/tools/audit/package-smoke.py +611 -9
  261. package/tools/audit/registry-smoke.py +534 -0
  262. package/tools/audit/release-metadata.py +47 -1
  263. package/tools/audit/smoke_assertions.py +915 -13
  264. package/tools/migrations/refs-links-to-reference-pages.py +179 -0
@@ -34,6 +34,8 @@ from smoke_assertions import (
34
34
  EXPECTED_HELP_ALIASES,
35
35
  EXPECTED_NUMERIC_VALUE_SMOKES,
36
36
  EXPECTED_PACK_MAX_BYTES,
37
+ EXPECTED_RANKED_SEARCH_LIMIT,
38
+ EXPECTED_RANKED_SEARCH_NOT_BUILT_NOTICE,
37
39
  EXPECTED_REPOSITORY_URL,
38
40
  EXPECTED_ROUTE_BRIEF,
39
41
  EXPECTED_ROUTE_ID,
@@ -58,6 +60,9 @@ from smoke_assertions import (
58
60
  assert_force_overwrite_replaced,
59
61
  assert_functional_alias_smokes,
60
62
  assert_help_topic_output,
63
+ assert_index_build_json,
64
+ assert_index_status_json,
65
+ assert_index_verify_json,
61
66
  assert_install_doctor_lifecycle_output,
62
67
  assert_install_output,
63
68
  assert_list_catalog_output,
@@ -76,6 +81,8 @@ from smoke_assertions import (
76
81
  assert_prompt_json_component_spec,
77
82
  assert_prompt_markdown_body_component_spec,
78
83
  assert_prompt_markdown_component_spec,
84
+ assert_ranked_search_determinism,
85
+ assert_ranked_search_json,
79
86
  assert_route_catalog_json,
80
87
  assert_route_explain_human_output,
81
88
  assert_route_json_component_spec,
@@ -5155,6 +5162,97 @@ def assert_learning_stats_human(raw: str, *, context: str, cmd: list[str]) -> No
5155
5162
  )
5156
5163
 
5157
5164
 
5165
+ def assert_learning_recall_json(
5166
+ raw: str,
5167
+ *,
5168
+ context: str,
5169
+ cmd: list[str],
5170
+ ) -> None:
5171
+ assert_no_ansi(raw, cmd)
5172
+ try:
5173
+ payload = json.loads(raw)
5174
+ except json.JSONDecodeError as error:
5175
+ raise SystemExit(f"{context}: failed to parse learn recall JSON") from error
5176
+
5177
+ require_package_smoke(
5178
+ isinstance(payload, dict) and payload.get("query") == "korean mobile",
5179
+ context=context,
5180
+ cmd=cmd,
5181
+ message="learn recall JSON must echo the query",
5182
+ )
5183
+ corpus = payload.get("corpus")
5184
+ require_package_smoke(
5185
+ isinstance(corpus, dict)
5186
+ and isinstance(corpus.get("candidateCount"), int)
5187
+ and corpus.get("candidateCount") > 0
5188
+ and isinstance(corpus.get("selectedCount"), int)
5189
+ and isinstance(corpus.get("selected"), list),
5190
+ context=context,
5191
+ cmd=cmd,
5192
+ message="learn recall corpus block shape changed",
5193
+ )
5194
+ learning = payload.get("learning")
5195
+ require_package_smoke(
5196
+ isinstance(learning, dict)
5197
+ and learning.get("mode") == "brief-relevance"
5198
+ and learning.get("candidateCount") == 3
5199
+ and isinstance(learning.get("selected"), list),
5200
+ context=context,
5201
+ cmd=cmd,
5202
+ message="learn recall learning block shape changed",
5203
+ )
5204
+ selected = learning.get("selected")
5205
+ require_package_smoke(
5206
+ len(selected) >= 1
5207
+ and isinstance(selected[0], dict)
5208
+ and selected[0].get("id") == "learn-korean"
5209
+ and selected[0].get("category") == "korean"
5210
+ and isinstance(selected[0].get("matchedTokens"), list),
5211
+ context=context,
5212
+ cmd=cmd,
5213
+ message="learn recall should rank the Korean learning entry for a Korean query",
5214
+ )
5215
+
5216
+
5217
+ def assert_learning_recall_smoke(
5218
+ command_factory,
5219
+ profile_path: Path,
5220
+ *,
5221
+ env: dict[str, str],
5222
+ cwd: Path | None = None,
5223
+ context: str,
5224
+ ) -> None:
5225
+ write_learning_stats_fixture(profile_path)
5226
+
5227
+ json_cmd = command_factory(
5228
+ "learn",
5229
+ "--recall",
5230
+ "korean mobile",
5231
+ "--file",
5232
+ str(profile_path),
5233
+ "--json",
5234
+ )
5235
+ json_result = run_plain(json_cmd, cwd=cwd, env=env)
5236
+ assert_learning_recall_json(json_result.stdout, context=f"{context} JSON", cmd=json_cmd)
5237
+
5238
+ before = profile_path.read_text(encoding="utf-8")
5239
+ human_cmd = command_factory("learn", "--recall", "korean mobile", "--file", str(profile_path))
5240
+ human_result = run_plain(human_cmd, cwd=cwd, env=env)
5241
+ require_package_smoke(
5242
+ "Recall (read-only)" in human_result.stdout
5243
+ and "Privacy: recall is read-only" in human_result.stdout,
5244
+ context=f"{context} human",
5245
+ cmd=human_cmd,
5246
+ message="learn recall human output missing read-only markers",
5247
+ )
5248
+ require_package_smoke(
5249
+ profile_path.read_text(encoding="utf-8") == before,
5250
+ context=f"{context} read-only",
5251
+ cmd=human_cmd,
5252
+ message="learn recall must not mutate the learning profile",
5253
+ )
5254
+
5255
+
5158
5256
  def assert_learning_stats_smoke(
5159
5257
  command_factory,
5160
5258
  profile_path: Path,
@@ -8215,7 +8313,7 @@ def assert_learning_relevance_context(payload: dict[str, object], *, context: st
8215
8313
  message="learning selection explanation should mark the relevant entry as a brief match",
8216
8314
  )
8217
8315
  require_package_smoke(
8218
- isinstance(selected_entry.get("score"), int) and selected_entry.get("score") > 0,
8316
+ type(selected_entry.get("score")) in (int, float) and selected_entry.get("score") > 0,
8219
8317
  context=context,
8220
8318
  cmd=cmd,
8221
8319
  message="learning selection explanation should include a positive relevance score",
@@ -8268,6 +8366,85 @@ def assert_learning_relevance_context(payload: dict[str, object], *, context: st
8268
8366
  )
8269
8367
 
8270
8368
 
8369
+ def assert_recall_context(payload: dict[str, object], *, context: str, cmd: list[str]) -> None:
8370
+ recall = payload.get("recall")
8371
+ require_package_smoke(
8372
+ isinstance(recall, dict),
8373
+ context=context,
8374
+ cmd=cmd,
8375
+ message="recall should be present when --with-recall is used",
8376
+ )
8377
+
8378
+ require_package_smoke(
8379
+ recall.get("mode") == "lexical",
8380
+ context=context,
8381
+ cmd=cmd,
8382
+ message="recall should use the deterministic lexical scorer",
8383
+ )
8384
+ require_package_smoke(
8385
+ isinstance(recall.get("candidateCount"), int) and recall.get("candidateCount") > 0,
8386
+ context=context,
8387
+ cmd=cmd,
8388
+ message="recall should report the corpus candidate count it scanned",
8389
+ )
8390
+ selected_count = recall.get("selectedCount")
8391
+ require_package_smoke(
8392
+ isinstance(selected_count, int) and selected_count >= 1,
8393
+ context=context,
8394
+ cmd=cmd,
8395
+ message="recall should select at least one corpus file for the Button brief",
8396
+ )
8397
+
8398
+ selected = recall.get("selected")
8399
+ require_package_smoke(
8400
+ isinstance(selected, list) and len(selected) == selected_count and len(selected) >= 1,
8401
+ context=context,
8402
+ cmd=cmd,
8403
+ message="recall selected list should match the reported selected count",
8404
+ )
8405
+ top = selected[0]
8406
+ require_package_smoke(
8407
+ isinstance(top, dict) and isinstance(top.get("id"), str) and top.get("id"),
8408
+ context=context,
8409
+ cmd=cmd,
8410
+ message="recall selection should cite the corpus relPath as its id",
8411
+ )
8412
+ require_package_smoke(
8413
+ type(top.get("score")) in (int, float) and top.get("score") > 0,
8414
+ context=context,
8415
+ cmd=cmd,
8416
+ message="recall selection should include a positive relevance score",
8417
+ )
8418
+ require_package_smoke(
8419
+ isinstance(top.get("matchedTokens"), list) and len(top.get("matchedTokens")) >= 1,
8420
+ context=context,
8421
+ cmd=cmd,
8422
+ message="recall selection should list the matched brief tokens",
8423
+ )
8424
+
8425
+ markdown = recall.get("markdown")
8426
+ require_package_smoke(
8427
+ isinstance(markdown, str) and "## Recalled design knowledge" in markdown,
8428
+ context=context,
8429
+ cmd=cmd,
8430
+ message="recall markdown should carry the Recalled design knowledge section",
8431
+ )
8432
+ require_package_smoke(
8433
+ top.get("id") in markdown,
8434
+ context=context,
8435
+ cmd=cmd,
8436
+ message="recall markdown should cite the top recalled corpus file",
8437
+ )
8438
+
8439
+ prompt = payload.get("prompt")
8440
+ require_package_smoke(
8441
+ isinstance(prompt, str) and "## Recalled design knowledge" in prompt,
8442
+ context=context,
8443
+ cmd=cmd,
8444
+ message="prompt markdown should render the recalled knowledge section",
8445
+ )
8446
+
8447
+
8271
8448
  def assert_learning_usage_payload(
8272
8449
  payload: dict[str, object],
8273
8450
  *,
@@ -11067,7 +11244,7 @@ def assert_learning_query_json(
11067
11244
  require_package_smoke(
11068
11245
  selected[0].get("id") == "learn-relevant"
11069
11246
  and selected[0].get("reason") == "brief-match"
11070
- and isinstance(selected[0].get("score"), int)
11247
+ and type(selected[0].get("score")) in (int, float)
11071
11248
  and selected[0].get("score") > 0,
11072
11249
  context=context,
11073
11250
  cmd=cmd,
@@ -11093,7 +11270,7 @@ def assert_learning_query_human(raw: str, *, context: str, cmd: list[str]) -> No
11093
11270
  "Limit: 2",
11094
11271
  "Explain: selection score, matched tokens, and reason",
11095
11272
  "[accessibility] Prioritize keyboard accessibility details for Button component API specs",
11096
- "matched keyboard, accessibility",
11273
+ "matched accessibility, keyboard",
11097
11274
  "reason brief-match",
11098
11275
  ):
11099
11276
  require_package_smoke(
@@ -11303,6 +11480,49 @@ def assert_learning_relevance_smoke(
11303
11480
  cmd=pack_cmd,
11304
11481
  )
11305
11482
 
11483
+ recall_prompt_cmd = command_factory(
11484
+ "prompt",
11485
+ EXPECTED_ROUTE_BRIEF,
11486
+ "--route",
11487
+ EXPECTED_ROUTE_ID,
11488
+ "--with-recall",
11489
+ "--recall-limit",
11490
+ "3",
11491
+ "--json",
11492
+ )
11493
+ recall_prompt_result = run_plain(recall_prompt_cmd, cwd=cwd, env=relevance_env)
11494
+ try:
11495
+ recall_prompt_payload = json.loads(recall_prompt_result.stdout)
11496
+ except json.JSONDecodeError as error:
11497
+ raise SystemExit(f"{context}: failed to parse prompt recall JSON") from error
11498
+ assert_recall_context(recall_prompt_payload, context=f"{context} prompt recall", cmd=recall_prompt_cmd)
11499
+
11500
+ recall_pack_cmd = command_factory(
11501
+ "pack",
11502
+ EXPECTED_ROUTE_BRIEF,
11503
+ "--route",
11504
+ EXPECTED_ROUTE_ID,
11505
+ "--with-recall",
11506
+ "--recall-limit",
11507
+ "3",
11508
+ "--max-bytes",
11509
+ str(EXPECTED_PACK_MAX_BYTES),
11510
+ "--json",
11511
+ )
11512
+ recall_pack_result = run_plain(recall_pack_cmd, cwd=cwd, env=relevance_env)
11513
+ try:
11514
+ recall_pack_payload = json.loads(recall_pack_result.stdout)
11515
+ except json.JSONDecodeError as error:
11516
+ raise SystemExit(f"{context}: failed to parse pack recall JSON") from error
11517
+ recall_plan = recall_pack_payload.get("plan")
11518
+ require_package_smoke(
11519
+ isinstance(recall_plan, dict),
11520
+ context=f"{context} pack recall",
11521
+ cmd=recall_pack_cmd,
11522
+ message="pack recall plan missing",
11523
+ )
11524
+ assert_recall_context(recall_plan, context=f"{context} pack recall plan", cmd=recall_pack_cmd)
11525
+
11306
11526
  usage_human_cmd = command_factory(
11307
11527
  "learn",
11308
11528
  "--usage",
@@ -12454,6 +12674,258 @@ def assert_learning_relevance_smoke(
12454
12674
  )
12455
12675
 
12456
12676
 
12677
+ def assert_index_roundtrip_smoke(
12678
+ command_factory,
12679
+ index_dir: Path,
12680
+ profile_path: Path,
12681
+ *,
12682
+ env: dict[str, str],
12683
+ cwd: Path | None = None,
12684
+ context: str,
12685
+ ) -> None:
12686
+ index_dir.mkdir(parents=True, exist_ok=True)
12687
+ write_learning_relevance_fixture(profile_path)
12688
+ index_env = env.copy()
12689
+ index_env["DESIGN_AI_INDEX_DIR"] = str(index_dir)
12690
+ index_env["DESIGN_AI_LEARNING_FILE"] = str(profile_path)
12691
+
12692
+ build_cmd = command_factory("index", "--build", "--json")
12693
+ build_result = run_plain(build_cmd, cwd=cwd, env=index_env)
12694
+ assert_index_build_json(
12695
+ build_result.stdout,
12696
+ index_dir=str(index_dir),
12697
+ context=f"{context} build",
12698
+ cmd=build_cmd,
12699
+ )
12700
+
12701
+ status_cmd = command_factory("index", "--status", "--json")
12702
+ status_result = run_plain(status_cmd, cwd=cwd, env=index_env)
12703
+ assert_index_status_json(
12704
+ status_result.stdout,
12705
+ index_dir=str(index_dir),
12706
+ context=f"{context} status",
12707
+ cmd=status_cmd,
12708
+ )
12709
+
12710
+ verify_cmd = command_factory("index", "--verify", "--json")
12711
+ verify_result = run_plain(verify_cmd, cwd=cwd, env=index_env)
12712
+ assert_index_verify_json(
12713
+ verify_result.stdout,
12714
+ index_dir=str(index_dir),
12715
+ context=f"{context} verify",
12716
+ cmd=verify_cmd,
12717
+ )
12718
+
12719
+
12720
+ def assert_ranked_search_determinism_smoke(
12721
+ command_factory,
12722
+ index_dir: Path,
12723
+ *,
12724
+ env: dict[str, str],
12725
+ cwd: Path | None = None,
12726
+ context: str,
12727
+ ) -> None:
12728
+ index_dir.mkdir(parents=True, exist_ok=True)
12729
+ ranked_env = env.copy()
12730
+ ranked_env["DESIGN_AI_INDEX_DIR"] = str(index_dir)
12731
+
12732
+ ranked_cmd = command_factory(
12733
+ "search",
12734
+ EXPECTED_CORPUS_SEARCH_QUERY,
12735
+ "--dir",
12736
+ "knowledge",
12737
+ "--limit",
12738
+ str(EXPECTED_RANKED_SEARCH_LIMIT),
12739
+ "--ranked",
12740
+ "--json",
12741
+ )
12742
+ first_result = run_plain(ranked_cmd, cwd=cwd, env=ranked_env)
12743
+ second_result = run_plain(ranked_cmd, cwd=cwd, env=ranked_env)
12744
+ assert_ranked_search_json(
12745
+ first_result.stdout,
12746
+ context=f"{context} payload",
12747
+ cmd=ranked_cmd,
12748
+ expected_notice=EXPECTED_RANKED_SEARCH_NOT_BUILT_NOTICE,
12749
+ )
12750
+ assert_ranked_search_determinism(
12751
+ first_result.stdout,
12752
+ second_result.stdout,
12753
+ context=context,
12754
+ cmd=ranked_cmd,
12755
+ )
12756
+
12757
+
12758
+ def assert_embeddings_off_by_default_smoke(
12759
+ command_factory,
12760
+ index_dir: Path,
12761
+ profile_path: Path,
12762
+ *,
12763
+ env: dict[str, str],
12764
+ cwd: Path | None = None,
12765
+ context: str,
12766
+ ) -> None:
12767
+ """Phase B is opt-in: a plain `index --build` never touches embeddings, and
12768
+ `index --status` reports `embeddings: null` (the feature is simply unused)."""
12769
+ index_dir.mkdir(parents=True, exist_ok=True)
12770
+ write_learning_relevance_fixture(profile_path)
12771
+ index_env = env.copy()
12772
+ index_env["DESIGN_AI_INDEX_DIR"] = str(index_dir)
12773
+ index_env["DESIGN_AI_LEARNING_FILE"] = str(profile_path)
12774
+ # Point at a config file that is guaranteed not to exist, rather than relying on
12775
+ # the ambient environment lacking DESIGN_AI_CONFIG_FILE — a real ~/.design-ai/
12776
+ # config.json on the machine running this smoke must not leak into the test.
12777
+ index_env["DESIGN_AI_CONFIG_FILE"] = str(index_dir.parent / "no-config-here" / "config.json")
12778
+
12779
+ build_cmd = command_factory("index", "--build", "--json")
12780
+ build_result = run_plain(build_cmd, cwd=cwd, env=index_env)
12781
+ build_payload = json.loads(build_result.stdout)
12782
+ if "embeddings" in build_payload:
12783
+ raise SystemExit(f"index build JSON after {context} unexpectedly ran provider execution with plain --build")
12784
+
12785
+ status_cmd = command_factory("index", "--status", "--json")
12786
+ status_result = run_plain(status_cmd, cwd=cwd, env=index_env)
12787
+ assert_index_status_json(
12788
+ status_result.stdout,
12789
+ index_dir=str(index_dir),
12790
+ context=f"{context} status",
12791
+ cmd=status_cmd,
12792
+ expect_embeddings=False,
12793
+ )
12794
+
12795
+
12796
+ def assert_search_embeddings_no_provider_fallback_smoke(
12797
+ command_factory,
12798
+ *,
12799
+ env: dict[str, str],
12800
+ cwd: Path | None = None,
12801
+ context: str,
12802
+ config_file: Path,
12803
+ ) -> None:
12804
+ """`search --ranked --embeddings` with no provider configured (flag or config)
12805
+ degrades to the lexical backend with a notice and exit code 0."""
12806
+ fallback_env = env.copy()
12807
+ # Same isolation rationale as assert_embeddings_off_by_default_smoke: point at a
12808
+ # config path guaranteed not to exist rather than trusting the ambient environment.
12809
+ fallback_env["DESIGN_AI_CONFIG_FILE"] = str(config_file)
12810
+
12811
+ search_cmd = command_factory(
12812
+ "search",
12813
+ EXPECTED_CORPUS_SEARCH_QUERY,
12814
+ "--dir",
12815
+ "knowledge",
12816
+ "--limit",
12817
+ str(EXPECTED_RANKED_SEARCH_LIMIT),
12818
+ "--ranked",
12819
+ "--embeddings",
12820
+ "--json",
12821
+ )
12822
+ result = run_plain(search_cmd, cwd=cwd, env=fallback_env)
12823
+ assert_ranked_search_json(
12824
+ result.stdout,
12825
+ context=f"{context} payload",
12826
+ cmd=search_cmd,
12827
+ expected_backend="lexical",
12828
+ )
12829
+ payload = json.loads(result.stdout)
12830
+ if "embedding provider" not in payload.get("notice", "") and "no embedding provider configured" not in payload.get("notice", ""):
12831
+ raise SystemExit(f"search ranked JSON after {context} fallback notice does not mention the missing provider")
12832
+
12833
+
12834
+ STUB_EMBEDDING_PROVIDER_SCRIPT = """
12835
+ const chunks = [];
12836
+ process.stdin.on("data", (c) => chunks.push(c));
12837
+ process.stdin.on("end", () => {
12838
+ const lines = Buffer.concat(chunks).toString("utf8").trim().split("\\n").filter(Boolean);
12839
+ for (const line of lines) {
12840
+ const { id } = JSON.parse(line);
12841
+ let seed = 0;
12842
+ for (const ch of id) seed = (seed * 31 + ch.charCodeAt(0)) % 997;
12843
+ const vector = [seed / 997, (seed + 1) / 997, (seed + 2) / 997];
12844
+ process.stdout.write(JSON.stringify({ id, vector }) + "\\n");
12845
+ }
12846
+ });
12847
+ """
12848
+
12849
+
12850
+ def write_stub_embedding_provider(root: Path) -> Path:
12851
+ root.mkdir(parents=True, exist_ok=True)
12852
+ script_path = root / "stub-embedding-provider.mjs"
12853
+ script_path.write_text(STUB_EMBEDDING_PROVIDER_SCRIPT, encoding="utf-8")
12854
+ return script_path
12855
+
12856
+
12857
+ def assert_embedding_stub_provider_roundtrip_smoke(
12858
+ command_factory,
12859
+ index_dir: Path,
12860
+ profile_path: Path,
12861
+ script_dir: Path,
12862
+ *,
12863
+ env: dict[str, str],
12864
+ cwd: Path | None = None,
12865
+ context: str,
12866
+ ) -> None:
12867
+ """A tiny deterministic node stub provider round-trips through `index --build
12868
+ --embeddings --provider`, `index --status`, `index --verify`, and
12869
+ `search --ranked --embeddings` (installed-bin path; documents the --provider
12870
+ "command args" convention of splitting a single string on spaces)."""
12871
+ index_dir.mkdir(parents=True, exist_ok=True)
12872
+ write_learning_relevance_fixture(profile_path)
12873
+ script_path = write_stub_embedding_provider(script_dir)
12874
+ provider_flag = f"node {script_path}"
12875
+
12876
+ index_env = env.copy()
12877
+ index_env["DESIGN_AI_INDEX_DIR"] = str(index_dir)
12878
+ index_env["DESIGN_AI_LEARNING_FILE"] = str(profile_path)
12879
+ index_env.pop("DESIGN_AI_CONFIG_FILE", None)
12880
+
12881
+ build_cmd = command_factory("index", "--build", "--embeddings", "--provider", provider_flag, "--json")
12882
+ build_result = run_plain(build_cmd, cwd=cwd, env=index_env)
12883
+ build_payload = json.loads(build_result.stdout)
12884
+ if build_payload.get("embeddings", {}).get("ok") is not True:
12885
+ raise SystemExit(f"index build JSON after {context} embeddings did not build successfully")
12886
+
12887
+ status_cmd = command_factory("index", "--status", "--json")
12888
+ status_result = run_plain(status_cmd, cwd=cwd, env=index_env)
12889
+ assert_index_status_json(
12890
+ status_result.stdout,
12891
+ index_dir=str(index_dir),
12892
+ context=f"{context} status",
12893
+ cmd=status_cmd,
12894
+ expect_embeddings=True,
12895
+ )
12896
+
12897
+ verify_cmd = command_factory("index", "--verify", "--provider", provider_flag, "--json")
12898
+ verify_result = run_plain(verify_cmd, cwd=cwd, env=index_env)
12899
+ assert_index_verify_json(
12900
+ verify_result.stdout,
12901
+ index_dir=str(index_dir),
12902
+ context=f"{context} verify",
12903
+ cmd=verify_cmd,
12904
+ expect_embeddings_check=True,
12905
+ )
12906
+
12907
+ search_cmd = command_factory(
12908
+ "search",
12909
+ EXPECTED_CORPUS_SEARCH_QUERY,
12910
+ "--dir",
12911
+ "knowledge",
12912
+ "--limit",
12913
+ str(EXPECTED_RANKED_SEARCH_LIMIT),
12914
+ "--ranked",
12915
+ "--embeddings",
12916
+ "--provider",
12917
+ provider_flag,
12918
+ "--json",
12919
+ )
12920
+ search_result = run_plain(search_cmd, cwd=cwd, env=index_env)
12921
+ assert_ranked_search_json(
12922
+ search_result.stdout,
12923
+ context=f"{context} search payload",
12924
+ cmd=search_cmd,
12925
+ expected_backend="embeddings",
12926
+ )
12927
+
12928
+
12457
12929
  def run_self_test() -> None:
12458
12930
  context = "package smoke self-test"
12459
12931
  cmd = ["design-ai", "doctor", "--json"]
@@ -14059,8 +14531,8 @@ def run_self_test() -> None:
14059
14531
  {
14060
14532
  "id": "learn-relevant",
14061
14533
  "category": "accessibility",
14062
- "score": 4,
14063
- "matchedTokens": ["keyboard", "accessibility"],
14534
+ "score": 2.114533,
14535
+ "matchedTokens": ["accessibility", "keyboard"],
14064
14536
  "reason": "brief-match",
14065
14537
  },
14066
14538
  ],
@@ -14126,7 +14598,7 @@ def run_self_test() -> None:
14126
14598
  "",
14127
14599
  "1. [accessibility] Prioritize keyboard accessibility details for Button component API specs",
14128
14600
  " learn-relevant · 2026-05-22T00:00:01.000Z",
14129
- " score 4 · matched keyboard, accessibility · reason brief-match",
14601
+ " score 2.114533 · matched accessibility, keyboard · reason brief-match",
14130
14602
  ]),
14131
14603
  context=context,
14132
14604
  cmd=learn_query_human_cmd,
@@ -14140,7 +14612,7 @@ def run_self_test() -> None:
14140
14612
  "Limit: 2",
14141
14613
  "Explain: selection score, matched tokens, and reason",
14142
14614
  "[accessibility] Prioritize keyboard accessibility details for Button component API specs",
14143
- "matched keyboard, accessibility",
14615
+ "matched accessibility, keyboard",
14144
14616
  ]),
14145
14617
  context=context,
14146
14618
  cmd=learn_query_human_cmd,
@@ -14167,8 +14639,8 @@ def run_self_test() -> None:
14167
14639
  {
14168
14640
  "id": "learn-relevant",
14169
14641
  "category": "accessibility",
14170
- "score": 4,
14171
- "matchedTokens": ["keyboard", "accessibility"],
14642
+ "score": 2.114533,
14643
+ "matchedTokens": ["accessibility", "keyboard"],
14172
14644
  "reason": "brief-match",
14173
14645
  },
14174
14646
  ],
@@ -14319,6 +14791,59 @@ def run_self_test() -> None:
14319
14791
  scope="package smoke",
14320
14792
  )
14321
14793
 
14794
+ recall_payload = {
14795
+ "recall": {
14796
+ "query": EXPECTED_ROUTE_BRIEF,
14797
+ "mode": "lexical",
14798
+ "candidateCount": 42,
14799
+ "selectedCount": 2,
14800
+ "selected": [
14801
+ {
14802
+ "id": "knowledge/components/INDEX.md",
14803
+ "score": 9.5,
14804
+ "matchedTokens": ["button", "component"],
14805
+ },
14806
+ {
14807
+ "id": "knowledge/a11y/keyboard-and-focus.md",
14808
+ "score": 4.2,
14809
+ "matchedTokens": ["accessibility"],
14810
+ },
14811
+ ],
14812
+ "markdown": (
14813
+ "## Recalled design knowledge\n\n"
14814
+ "- knowledge/components/INDEX.md\n"
14815
+ " - Component index\n"
14816
+ "- knowledge/a11y/keyboard-and-focus.md\n"
14817
+ " - Keyboard and focus"
14818
+ ),
14819
+ },
14820
+ "prompt": (
14821
+ "Recalled corpus knowledge:\n\n"
14822
+ "## Recalled design knowledge\n\n"
14823
+ "- knowledge/components/INDEX.md"
14824
+ ),
14825
+ }
14826
+ recall_cmd = ["design-ai", "prompt", EXPECTED_ROUTE_BRIEF, "--with-recall", "--json"]
14827
+ assert_recall_context(recall_payload, context=context, cmd=recall_cmd)
14828
+ expect_self_test_failure(
14829
+ lambda: assert_recall_context(
14830
+ {**recall_payload, "recall": {**recall_payload["recall"], "mode": "embeddings"}},
14831
+ context=context,
14832
+ cmd=recall_cmd,
14833
+ ),
14834
+ expected="recall should use the deterministic lexical scorer",
14835
+ scope="package smoke",
14836
+ )
14837
+ expect_self_test_failure(
14838
+ lambda: assert_recall_context(
14839
+ {**recall_payload, "recall": {**recall_payload["recall"], "selectedCount": 0, "selected": []}},
14840
+ context=context,
14841
+ cmd=recall_cmd,
14842
+ ),
14843
+ expected="recall should select at least one corpus file",
14844
+ scope="package smoke",
14845
+ )
14846
+
14322
14847
  learning_usage_path = Path(tmp) / "learning.usage.json"
14323
14848
  learning_usage_path.write_text(
14324
14849
  json.dumps({
@@ -20565,6 +21090,40 @@ def smoke_tarball(tarball: Path) -> None:
20565
21090
  env=smoke_env,
20566
21091
  context="package smoke installed bin search human corpus",
20567
21092
  )
21093
+ assert_index_roundtrip_smoke(
21094
+ lambda *args: [str(bin_path), *args],
21095
+ tmp_root / "installed-retrieval-index",
21096
+ tmp_root / "installed-index-learning.json",
21097
+ env=smoke_env,
21098
+ context="package smoke installed bin index round-trip",
21099
+ )
21100
+ assert_ranked_search_determinism_smoke(
21101
+ lambda *args: [str(bin_path), *args],
21102
+ tmp_root / "installed-ranked-index",
21103
+ env=smoke_env,
21104
+ context="package smoke installed bin ranked search determinism",
21105
+ )
21106
+ assert_embeddings_off_by_default_smoke(
21107
+ lambda *args: [str(bin_path), *args],
21108
+ tmp_root / "installed-embeddings-off-index",
21109
+ tmp_root / "installed-embeddings-off-learning.json",
21110
+ env=smoke_env,
21111
+ context="package smoke installed bin embeddings off by default",
21112
+ )
21113
+ assert_search_embeddings_no_provider_fallback_smoke(
21114
+ lambda *args: [str(bin_path), *args],
21115
+ env=smoke_env,
21116
+ context="package smoke installed bin search embeddings no-provider fallback",
21117
+ config_file=tmp_root / "installed-no-config-here" / "config.json",
21118
+ )
21119
+ assert_embedding_stub_provider_roundtrip_smoke(
21120
+ lambda *args: [str(bin_path), *args],
21121
+ tmp_root / "installed-embedding-index",
21122
+ tmp_root / "installed-embedding-learning.json",
21123
+ tmp_root / "installed-embedding-provider",
21124
+ env=smoke_env,
21125
+ context="package smoke installed bin embedding stub provider round-trip",
21126
+ )
20568
21127
  assert_show_smoke(
20569
21128
  [str(bin_path), "show", EXPECTED_CORPUS_SHOW_TARGET, "--context", "0", "--json"],
20570
21129
  env=smoke_env,
@@ -21012,6 +21571,12 @@ def smoke_tarball(tarball: Path) -> None:
21012
21571
  env=smoke_env,
21013
21572
  context="package smoke installed bin learn stats",
21014
21573
  )
21574
+ assert_learning_recall_smoke(
21575
+ lambda *args: [str(bin_path), *args],
21576
+ tmp_root / "installed-recall-learning.json",
21577
+ env=smoke_env,
21578
+ context="package smoke installed bin learn recall",
21579
+ )
21015
21580
  assert_learning_audit_cleanup_smoke(
21016
21581
  lambda *args: [str(bin_path), *args],
21017
21582
  tmp_root / "installed-learning.json",
@@ -21943,6 +22508,36 @@ def smoke_tarball(tarball: Path) -> None:
21943
22508
  env=npx_env,
21944
22509
  context="package smoke npm exec search human corpus",
21945
22510
  )
22511
+ assert_index_roundtrip_smoke(
22512
+ lambda *args: npm_exec_cmd(tarball, *args),
22513
+ npx_root / "npx-retrieval-index",
22514
+ npx_root / "npx-index-learning.json",
22515
+ cwd=npx_root,
22516
+ env=npx_env,
22517
+ context="package smoke npm exec index round-trip",
22518
+ )
22519
+ assert_ranked_search_determinism_smoke(
22520
+ lambda *args: npm_exec_cmd(tarball, *args),
22521
+ npx_root / "npx-ranked-index",
22522
+ cwd=npx_root,
22523
+ env=npx_env,
22524
+ context="package smoke npm exec ranked search determinism",
22525
+ )
22526
+ assert_embeddings_off_by_default_smoke(
22527
+ lambda *args: npm_exec_cmd(tarball, *args),
22528
+ npx_root / "npx-embeddings-off-index",
22529
+ npx_root / "npx-embeddings-off-learning.json",
22530
+ cwd=npx_root,
22531
+ env=npx_env,
22532
+ context="package smoke npm exec embeddings off by default",
22533
+ )
22534
+ assert_search_embeddings_no_provider_fallback_smoke(
22535
+ lambda *args: npm_exec_cmd(tarball, *args),
22536
+ cwd=npx_root,
22537
+ env=npx_env,
22538
+ context="package smoke npm exec search embeddings no-provider fallback",
22539
+ config_file=npx_root / "npx-no-config-here" / "config.json",
22540
+ )
21946
22541
  assert_show_smoke(
21947
22542
  npm_exec_cmd(tarball, "show", EXPECTED_CORPUS_SHOW_TARGET, "--context", "0", "--json"),
21948
22543
  cwd=npx_root,
@@ -22435,6 +23030,13 @@ def smoke_tarball(tarball: Path) -> None:
22435
23030
  env=npx_env,
22436
23031
  context="package smoke npm exec learn stats",
22437
23032
  )
23033
+ assert_learning_recall_smoke(
23034
+ lambda *args: npm_exec_cmd(tarball, *args),
23035
+ npx_root / "npx-recall-learning.json",
23036
+ cwd=npx_root,
23037
+ env=npx_env,
23038
+ context="package smoke npm exec learn recall",
23039
+ )
22438
23040
  assert_learning_audit_cleanup_smoke(
22439
23041
  lambda *args: npm_exec_cmd(tarball, *args),
22440
23042
  npx_root / "npx-learning.json",