@design-ai/cli 4.55.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 (301) hide show
  1. package/.claude-plugin/plugin.json +109 -25
  2. package/CHANGELOG.md +75 -0
  3. package/README.ko.md +15 -32
  4. package/README.md +18 -31
  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 +221 -13
  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-analysis.mjs +297 -0
  48. package/cli/lib/site-args.mjs +433 -0
  49. package/cli/lib/site-bundle-build.mjs +127 -0
  50. package/cli/lib/site-bundle-check.mjs +454 -0
  51. package/cli/lib/site-bundle-commands.mjs +95 -0
  52. package/cli/lib/site-bundle-compare.mjs +157 -0
  53. package/cli/lib/site-bundle-contract.mjs +79 -0
  54. package/cli/lib/site-bundle-files.mjs +87 -0
  55. package/cli/lib/site-bundle-handoff-expected.mjs +173 -0
  56. package/cli/lib/site-bundle-handoff-runbook-action-summary.mjs +332 -0
  57. package/cli/lib/site-bundle-handoff-runbook-actions.mjs +113 -0
  58. package/cli/lib/site-bundle-handoff-runbook-evidence-fields.mjs +164 -0
  59. package/cli/lib/site-bundle-handoff-runbook-evidence.mjs +334 -0
  60. package/cli/lib/site-bundle-handoff-runbook-format.mjs +31 -0
  61. package/cli/lib/site-bundle-handoff-runbook-human-lines.mjs +167 -0
  62. package/cli/lib/site-bundle-handoff-runbook-maps.mjs +238 -0
  63. package/cli/lib/site-bundle-handoff-runbook-next-step.mjs +278 -0
  64. package/cli/lib/site-bundle-handoff-runbook-stage-metadata.mjs +84 -0
  65. package/cli/lib/site-bundle-handoff-runbook.mjs +444 -0
  66. package/cli/lib/site-bundle-handoff-summary.mjs +183 -0
  67. package/cli/lib/site-bundle-handoff.mjs +271 -0
  68. package/cli/lib/site-bundle-readme.mjs +98 -0
  69. package/cli/lib/site-bundle-repair-report.mjs +143 -0
  70. package/cli/lib/site-bundle-repair.mjs +68 -0
  71. package/cli/lib/site-content.mjs +399 -0
  72. package/cli/lib/site-evidence.mjs +35 -0
  73. package/cli/lib/site-mcp-commands.mjs +28 -0
  74. package/cli/lib/site-mcp-probes.mjs +159 -0
  75. package/cli/lib/site-mcp-readiness.mjs +157 -0
  76. package/cli/lib/site-mcp-report.mjs +324 -0
  77. package/cli/lib/site-next-actions.mjs +333 -0
  78. package/cli/lib/site-options.mjs +104 -0
  79. package/cli/lib/site-prompts.mjs +332 -0
  80. package/cli/lib/site-starter.mjs +153 -0
  81. package/cli/lib/site-strings.mjs +23 -0
  82. package/cli/lib/site-tasks.mjs +93 -0
  83. package/cli/lib/site-test-support.mjs +68 -0
  84. package/cli/lib/site-workflow-graph.mjs +309 -0
  85. package/cli/lib/site-workspace.mjs +492 -0
  86. package/cli/lib/site.mjs +108 -6617
  87. package/cli/lib/skill-proposals-apply-commands.mjs +135 -0
  88. package/cli/lib/skill-proposals-apply-contract.mjs +750 -0
  89. package/cli/lib/skill-proposals-apply-plan.mjs +118 -0
  90. package/cli/lib/skill-proposals-generate.mjs +298 -0
  91. package/cli/lib/skill-proposals-render.mjs +532 -0
  92. package/cli/lib/skill-proposals-review.mjs +262 -0
  93. package/cli/lib/skill-proposals.mjs +15 -2046
  94. package/cli/lib/workspace-args.mjs +83 -0
  95. package/cli/lib/workspace-git.mjs +169 -0
  96. package/cli/lib/workspace-learning.mjs +483 -0
  97. package/cli/lib/workspace-repo.mjs +139 -0
  98. package/cli/lib/workspace-report.mjs +283 -0
  99. package/cli/lib/workspace-test-support.mjs +99 -0
  100. package/cli/lib/workspace.mjs +30 -1056
  101. package/docs/AI-LEARNING-PHASE2.md +215 -0
  102. package/docs/DISTRIBUTION.ko.md +35 -6
  103. package/docs/DISTRIBUTION.md +35 -8
  104. package/docs/NEXT-SURFACE-DECISION.md +125 -0
  105. package/docs/PRODUCT-READINESS.md +3 -3
  106. package/docs/RELEASE-CHECKLIST.md +20 -3
  107. package/docs/RELEASE-GATES.ko.md +35 -0
  108. package/docs/RELEASE-GATES.md +234 -0
  109. package/docs/ROADMAP.md +2232 -8
  110. package/docs/external-status.md +20 -7
  111. package/docs/inspection-20260630.md +169 -0
  112. package/docs/integrations/design-ai-mcp-server.md +35 -3
  113. package/docs/integrations/vscode-walkthrough.ko.md +3 -3
  114. package/docs/integrations/vscode-walkthrough.md +3 -3
  115. package/docs/reference/ant-design.md +413 -0
  116. package/docs/reference/awesome-design-md.md +439 -0
  117. package/docs/reference/mui.md +783 -0
  118. package/docs/reference/shadcn-ui.md +298 -0
  119. package/docs/site-overrides/main.html +1 -1
  120. package/examples/component-accordion-actions.md +5 -5
  121. package/examples/component-accordion-details.md +5 -5
  122. package/examples/component-accordion-summary.md +5 -5
  123. package/examples/component-accordion.md +3 -3
  124. package/examples/component-affix.md +1 -1
  125. package/examples/component-alert-dialog.md +1 -1
  126. package/examples/component-alert-title.md +1 -1
  127. package/examples/component-alert.md +3 -3
  128. package/examples/component-anchor.md +1 -1
  129. package/examples/component-app-bar.md +1 -1
  130. package/examples/component-aspect-ratio.md +1 -1
  131. package/examples/component-auto-complete.md +2 -2
  132. package/examples/component-avatar-group.md +5 -5
  133. package/examples/component-avatar.md +3 -3
  134. package/examples/component-back-top.md +1 -1
  135. package/examples/component-backdrop.md +1 -1
  136. package/examples/component-badge.md +3 -3
  137. package/examples/component-border-beam.md +3 -3
  138. package/examples/component-bottom-navigation.md +1 -1
  139. package/examples/component-box.md +1 -1
  140. package/examples/component-breadcrumb.md +3 -3
  141. package/examples/component-button-base.md +3 -3
  142. package/examples/component-button-group.md +2 -2
  143. package/examples/component-button.md +3 -3
  144. package/examples/component-calendar.md +2 -2
  145. package/examples/component-card-actions.md +1 -1
  146. package/examples/component-card-content.md +1 -1
  147. package/examples/component-card-header.md +1 -1
  148. package/examples/component-card-media.md +1 -1
  149. package/examples/component-card.md +3 -3
  150. package/examples/component-carousel.md +2 -2
  151. package/examples/component-cascader.md +1 -1
  152. package/examples/component-chart.md +1 -1
  153. package/examples/component-checkbox.md +3 -3
  154. package/examples/component-click-away-listener.md +1 -1
  155. package/examples/component-code.md +1 -1
  156. package/examples/component-collapsible.md +1 -1
  157. package/examples/component-color-picker.md +1 -1
  158. package/examples/component-combobox.md +1 -1
  159. package/examples/component-command.md +1 -1
  160. package/examples/component-config-provider.md +3 -3
  161. package/examples/component-context-menu.md +1 -1
  162. package/examples/component-css-baseline.md +3 -3
  163. package/examples/component-date-picker.md +2 -2
  164. package/examples/component-descriptions.md +1 -1
  165. package/examples/component-dialog-actions.md +1 -1
  166. package/examples/component-dialog-content-text.md +1 -1
  167. package/examples/component-dialog-content.md +1 -1
  168. package/examples/component-dialog-title.md +1 -1
  169. package/examples/component-dialog.md +2 -2
  170. package/examples/component-divider.md +3 -3
  171. package/examples/component-drawer.md +3 -3
  172. package/examples/component-dropdown.md +3 -3
  173. package/examples/component-empty.md +2 -2
  174. package/examples/component-fade.md +1 -1
  175. package/examples/component-field.md +1 -1
  176. package/examples/component-filled-input.md +1 -1
  177. package/examples/component-flex.md +1 -1
  178. package/examples/component-float-button.md +2 -2
  179. package/examples/component-form-control-label.md +1 -1
  180. package/examples/component-form-control.md +1 -1
  181. package/examples/component-form-controls.md +1 -1
  182. package/examples/component-form-group.md +1 -1
  183. package/examples/component-form-helper-text.md +1 -1
  184. package/examples/component-form-label.md +1 -1
  185. package/examples/component-form.md +2 -2
  186. package/examples/component-grid.md +2 -2
  187. package/examples/component-grow.md +1 -1
  188. package/examples/component-hover-card.md +1 -1
  189. package/examples/component-icon-button.md +1 -1
  190. package/examples/component-icon.md +1 -1
  191. package/examples/component-image-list.md +1 -1
  192. package/examples/component-image.md +1 -1
  193. package/examples/component-input-adornment.md +1 -1
  194. package/examples/component-input-base.md +1 -1
  195. package/examples/component-input-number.md +1 -1
  196. package/examples/component-input-otp.md +2 -2
  197. package/examples/component-input.md +3 -3
  198. package/examples/component-item.md +1 -1
  199. package/examples/component-label.md +1 -1
  200. package/examples/component-layout.md +1 -1
  201. package/examples/component-link.md +1 -1
  202. package/examples/component-list-item-avatar.md +1 -1
  203. package/examples/component-list-item-button.md +1 -1
  204. package/examples/component-list-item-icon.md +1 -1
  205. package/examples/component-list-item-text.md +1 -1
  206. package/examples/component-list-item.md +1 -1
  207. package/examples/component-list-subheader.md +1 -1
  208. package/examples/component-list.md +2 -2
  209. package/examples/component-masonry.md +1 -1
  210. package/examples/component-mentions.md +1 -1
  211. package/examples/component-menu-item.md +1 -1
  212. package/examples/component-menu-list.md +1 -1
  213. package/examples/component-menu.md +1 -1
  214. package/examples/component-menubar.md +1 -1
  215. package/examples/component-message.md +1 -1
  216. package/examples/component-mobile-stepper.md +1 -1
  217. package/examples/component-modal.md +5 -5
  218. package/examples/component-navigation-menu.md +1 -1
  219. package/examples/component-notification.md +1 -1
  220. package/examples/component-outlined-input.md +1 -1
  221. package/examples/component-pagination.md +3 -3
  222. package/examples/component-paper.md +1 -1
  223. package/examples/component-popconfirm.md +1 -1
  224. package/examples/component-popover.md +3 -3
  225. package/examples/component-popper.md +1 -1
  226. package/examples/component-progress.md +2 -2
  227. package/examples/component-qr-code.md +1 -1
  228. package/examples/component-radio.md +3 -3
  229. package/examples/component-rate.md +2 -2
  230. package/examples/component-resizable.md +1 -1
  231. package/examples/component-result.md +1 -1
  232. package/examples/component-scroll-area.md +1 -1
  233. package/examples/component-segmented.md +3 -3
  234. package/examples/component-select.md +3 -3
  235. package/examples/component-separator.md +1 -1
  236. package/examples/component-sheet.md +1 -1
  237. package/examples/component-sidebar.md +1 -1
  238. package/examples/component-skeleton.md +3 -3
  239. package/examples/component-slide.md +1 -1
  240. package/examples/component-slider.md +3 -3
  241. package/examples/component-snackbar-content.md +1 -1
  242. package/examples/component-snackbar.md +1 -1
  243. package/examples/component-sonner.md +1 -1
  244. package/examples/component-space.md +1 -1
  245. package/examples/component-speed-dial-action.md +1 -1
  246. package/examples/component-speed-dial.md +1 -1
  247. package/examples/component-spin.md +2 -2
  248. package/examples/component-spinner.md +1 -1
  249. package/examples/component-splitter.md +2 -2
  250. package/examples/component-stack.md +1 -1
  251. package/examples/component-statistic.md +1 -1
  252. package/examples/component-step-button.md +4 -4
  253. package/examples/component-step-connector.md +4 -4
  254. package/examples/component-step-content.md +1 -1
  255. package/examples/component-step-icon.md +1 -1
  256. package/examples/component-step-label.md +1 -1
  257. package/examples/component-step.md +2 -2
  258. package/examples/component-steps.md +2 -2
  259. package/examples/component-swipeable-drawer.md +1 -1
  260. package/examples/component-switch.md +3 -3
  261. package/examples/component-tab-scroll-button.md +5 -5
  262. package/examples/component-tab.md +1 -1
  263. package/examples/component-table-body.md +1 -1
  264. package/examples/component-table-cell.md +1 -1
  265. package/examples/component-table-container.md +1 -1
  266. package/examples/component-table-footer.md +1 -1
  267. package/examples/component-table-head.md +1 -1
  268. package/examples/component-table-pagination.md +1 -1
  269. package/examples/component-table-row.md +1 -1
  270. package/examples/component-table-sort-label.md +1 -1
  271. package/examples/component-table.md +2 -2
  272. package/examples/component-tabs.md +3 -3
  273. package/examples/component-tag-badge.md +2 -2
  274. package/examples/component-tag.md +1 -1
  275. package/examples/component-textarea-autosize.md +1 -1
  276. package/examples/component-textarea.md +1 -1
  277. package/examples/component-time-picker.md +1 -1
  278. package/examples/component-timeline.md +1 -1
  279. package/examples/component-toast.md +4 -4
  280. package/examples/component-toggle-button.md +1 -1
  281. package/examples/component-toggle.md +1 -1
  282. package/examples/component-toolbar.md +1 -1
  283. package/examples/component-tooltip.md +3 -3
  284. package/examples/component-tour.md +1 -1
  285. package/examples/component-transfer.md +1 -1
  286. package/examples/component-tree-select.md +1 -1
  287. package/examples/component-tree.md +1 -1
  288. package/examples/component-typography.md +2 -2
  289. package/examples/component-upload.md +1 -1
  290. package/examples/component-watermark.md +1 -1
  291. package/examples/component-zoom.md +1 -1
  292. package/knowledge/COVERAGE.md +4 -3
  293. package/knowledge/components/INDEX.md +202 -202
  294. package/knowledge/patterns/brand-references.md +72 -72
  295. package/package.json +1 -1
  296. package/tools/audit/local-ci.py +16 -1
  297. package/tools/audit/package-smoke.py +717 -9
  298. package/tools/audit/registry-smoke.py +912 -10
  299. package/tools/audit/release-metadata.py +47 -1
  300. package/tools/audit/smoke_assertions.py +998 -35
  301. 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,
@@ -50,6 +52,7 @@ from smoke_assertions import (
50
52
  assert_check_examples_json_component_spec,
51
53
  assert_check_stdin_json_component_spec,
52
54
  assert_command_alias_output,
55
+ assert_design_ai_mcp_protocol_responses,
53
56
  assert_doctor_json_clean,
54
57
  assert_doctor_strict_output,
55
58
  assert_examples_human_output,
@@ -57,6 +60,9 @@ from smoke_assertions import (
57
60
  assert_force_overwrite_replaced,
58
61
  assert_functional_alias_smokes,
59
62
  assert_help_topic_output,
63
+ assert_index_build_json,
64
+ assert_index_status_json,
65
+ assert_index_verify_json,
60
66
  assert_install_doctor_lifecycle_output,
61
67
  assert_install_output,
62
68
  assert_list_catalog_output,
@@ -75,6 +81,8 @@ from smoke_assertions import (
75
81
  assert_prompt_json_component_spec,
76
82
  assert_prompt_markdown_body_component_spec,
77
83
  assert_prompt_markdown_component_spec,
84
+ assert_ranked_search_determinism,
85
+ assert_ranked_search_json,
78
86
  assert_route_catalog_json,
79
87
  assert_route_explain_human_output,
80
88
  assert_route_json_component_spec,
@@ -135,6 +143,7 @@ from smoke_assertions import (
135
143
  format_cmd,
136
144
  help_alias_script,
137
145
  help_topic_script,
146
+ mcp_smoke_input,
138
147
  passing_doctor_report_json,
139
148
  passing_check_artifact_content,
140
149
  passing_site_mcp_check_probes_human,
@@ -379,6 +388,18 @@ def npm_exec_cmd(tarball: Path, *args: str) -> list[str]:
379
388
  ]
380
389
 
381
390
 
391
+ def npm_exec_mcp_cmd(tarball: Path) -> list[str]:
392
+ return [
393
+ "npm",
394
+ "exec",
395
+ "--yes",
396
+ "--package",
397
+ str(tarball),
398
+ "--",
399
+ "design-ai-mcp",
400
+ ]
401
+
402
+
382
403
  def npm_exec_shell_cmd(tarball: Path, script: str) -> list[str]:
383
404
  return [
384
405
  "npm",
@@ -533,6 +554,52 @@ def require_package_smoke(condition: bool, *, context: str, cmd: list[str], mess
533
554
  fail_package_smoke(context, cmd, message)
534
555
 
535
556
 
557
+ def assert_design_ai_mcp_protocol_smoke(
558
+ cmd: list[str],
559
+ *,
560
+ env: dict[str, str],
561
+ cwd: Path | None,
562
+ context: str,
563
+ ) -> None:
564
+ print(f"$ {format_cmd(cmd)} < MCP smoke", flush=True)
565
+ result = subprocess.run(
566
+ cmd,
567
+ cwd=cwd,
568
+ env=env,
569
+ input=mcp_smoke_input(),
570
+ text=True,
571
+ capture_output=True,
572
+ timeout=15,
573
+ )
574
+ if result.stdout:
575
+ print(result.stdout, end="")
576
+ if result.stderr:
577
+ print(result.stderr, end="", file=sys.stderr)
578
+
579
+ if is_npm_exec_cache_enoent(cmd, result):
580
+ result = retry_npm_exec_once(cmd, cwd=cwd, env=env, input_text=mcp_smoke_input())
581
+ if result.stdout:
582
+ print(result.stdout, end="")
583
+ if result.stderr:
584
+ print(result.stderr, end="", file=sys.stderr)
585
+
586
+ if result.returncode != 0:
587
+ raise SystemExit(f"command failed with exit code {result.returncode}: {format_cmd(cmd)}")
588
+
589
+ output = f"{result.stdout}\n{result.stderr}"
590
+ assert_no_ansi(output, cmd)
591
+
592
+ responses = []
593
+ for line in result.stdout.splitlines():
594
+ if not line.strip():
595
+ continue
596
+ try:
597
+ responses.append(json.loads(line))
598
+ except json.JSONDecodeError as error:
599
+ fail_package_smoke(context, cmd, f"MCP stdout line is not JSON: {error}")
600
+ assert_design_ai_mcp_protocol_responses(responses, context=context, cmd=cmd)
601
+
602
+
536
603
  def run_expected_failure(
537
604
  cmd: list[str],
538
605
  *,
@@ -5095,6 +5162,97 @@ def assert_learning_stats_human(raw: str, *, context: str, cmd: list[str]) -> No
5095
5162
  )
5096
5163
 
5097
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
+
5098
5256
  def assert_learning_stats_smoke(
5099
5257
  command_factory,
5100
5258
  profile_path: Path,
@@ -8155,7 +8313,7 @@ def assert_learning_relevance_context(payload: dict[str, object], *, context: st
8155
8313
  message="learning selection explanation should mark the relevant entry as a brief match",
8156
8314
  )
8157
8315
  require_package_smoke(
8158
- 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,
8159
8317
  context=context,
8160
8318
  cmd=cmd,
8161
8319
  message="learning selection explanation should include a positive relevance score",
@@ -8208,6 +8366,85 @@ def assert_learning_relevance_context(payload: dict[str, object], *, context: st
8208
8366
  )
8209
8367
 
8210
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
+
8211
8448
  def assert_learning_usage_payload(
8212
8449
  payload: dict[str, object],
8213
8450
  *,
@@ -11007,7 +11244,7 @@ def assert_learning_query_json(
11007
11244
  require_package_smoke(
11008
11245
  selected[0].get("id") == "learn-relevant"
11009
11246
  and selected[0].get("reason") == "brief-match"
11010
- and isinstance(selected[0].get("score"), int)
11247
+ and type(selected[0].get("score")) in (int, float)
11011
11248
  and selected[0].get("score") > 0,
11012
11249
  context=context,
11013
11250
  cmd=cmd,
@@ -11033,7 +11270,7 @@ def assert_learning_query_human(raw: str, *, context: str, cmd: list[str]) -> No
11033
11270
  "Limit: 2",
11034
11271
  "Explain: selection score, matched tokens, and reason",
11035
11272
  "[accessibility] Prioritize keyboard accessibility details for Button component API specs",
11036
- "matched keyboard, accessibility",
11273
+ "matched accessibility, keyboard",
11037
11274
  "reason brief-match",
11038
11275
  ):
11039
11276
  require_package_smoke(
@@ -11243,6 +11480,49 @@ def assert_learning_relevance_smoke(
11243
11480
  cmd=pack_cmd,
11244
11481
  )
11245
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
+
11246
11526
  usage_human_cmd = command_factory(
11247
11527
  "learn",
11248
11528
  "--usage",
@@ -12394,6 +12674,258 @@ def assert_learning_relevance_smoke(
12394
12674
  )
12395
12675
 
12396
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
+
12397
12929
  def run_self_test() -> None:
12398
12930
  context = "package smoke self-test"
12399
12931
  cmd = ["design-ai", "doctor", "--json"]
@@ -12482,6 +13014,37 @@ def run_self_test() -> None:
12482
13014
  site_bundle_mcp_probes_payload,
12483
13015
  context=f"{context} site bundle mcp-probes.json",
12484
13016
  )
13017
+ mcp_protocol_cmd = ["design-ai-mcp"]
13018
+ assert_design_ai_mcp_protocol_responses(
13019
+ [
13020
+ {"jsonrpc": "2.0", "id": 1, "result": {"serverInfo": {"name": "design-ai"}}},
13021
+ {"jsonrpc": "2.0", "id": 2, "result": {"tools": [{"name": "design_ai_route"}, {"name": "design_ai_search"}]}},
13022
+ {
13023
+ "jsonrpc": "2.0",
13024
+ "id": 3,
13025
+ "error": {"code": -32602, "message": "design_ai_search.limit must be an integer"},
13026
+ },
13027
+ ],
13028
+ context=f"{context} design-ai MCP protocol",
13029
+ cmd=mcp_protocol_cmd,
13030
+ )
13031
+ expect_self_test_failure(
13032
+ lambda: assert_design_ai_mcp_protocol_responses(
13033
+ [
13034
+ {"jsonrpc": "2.0", "id": 1, "result": {"serverInfo": {"name": "design-ai"}}},
13035
+ {"jsonrpc": "2.0", "id": 2, "result": {"tools": [{"name": "design_ai_route"}, {"name": "design_ai_search"}]}},
13036
+ {
13037
+ "jsonrpc": "2.0",
13038
+ "id": 3,
13039
+ "error": {"code": -32602, "message": "wrong validation message"},
13040
+ },
13041
+ ],
13042
+ context=f"{context} design-ai MCP protocol",
13043
+ cmd=mcp_protocol_cmd,
13044
+ ),
13045
+ expected="MCP invalid argument response did not preserve invalid params validation",
13046
+ scope="package smoke",
13047
+ )
12485
13048
  for label in (
12486
13049
  "site bundle check",
12487
13050
  "site bundle check summary",
@@ -13968,8 +14531,8 @@ def run_self_test() -> None:
13968
14531
  {
13969
14532
  "id": "learn-relevant",
13970
14533
  "category": "accessibility",
13971
- "score": 4,
13972
- "matchedTokens": ["keyboard", "accessibility"],
14534
+ "score": 2.114533,
14535
+ "matchedTokens": ["accessibility", "keyboard"],
13973
14536
  "reason": "brief-match",
13974
14537
  },
13975
14538
  ],
@@ -14035,7 +14598,7 @@ def run_self_test() -> None:
14035
14598
  "",
14036
14599
  "1. [accessibility] Prioritize keyboard accessibility details for Button component API specs",
14037
14600
  " learn-relevant · 2026-05-22T00:00:01.000Z",
14038
- " score 4 · matched keyboard, accessibility · reason brief-match",
14601
+ " score 2.114533 · matched accessibility, keyboard · reason brief-match",
14039
14602
  ]),
14040
14603
  context=context,
14041
14604
  cmd=learn_query_human_cmd,
@@ -14049,7 +14612,7 @@ def run_self_test() -> None:
14049
14612
  "Limit: 2",
14050
14613
  "Explain: selection score, matched tokens, and reason",
14051
14614
  "[accessibility] Prioritize keyboard accessibility details for Button component API specs",
14052
- "matched keyboard, accessibility",
14615
+ "matched accessibility, keyboard",
14053
14616
  ]),
14054
14617
  context=context,
14055
14618
  cmd=learn_query_human_cmd,
@@ -14076,8 +14639,8 @@ def run_self_test() -> None:
14076
14639
  {
14077
14640
  "id": "learn-relevant",
14078
14641
  "category": "accessibility",
14079
- "score": 4,
14080
- "matchedTokens": ["keyboard", "accessibility"],
14642
+ "score": 2.114533,
14643
+ "matchedTokens": ["accessibility", "keyboard"],
14081
14644
  "reason": "brief-match",
14082
14645
  },
14083
14646
  ],
@@ -14228,6 +14791,59 @@ def run_self_test() -> None:
14228
14791
  scope="package smoke",
14229
14792
  )
14230
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
+
14231
14847
  learning_usage_path = Path(tmp) / "learning.usage.json"
14232
14848
  learning_usage_path.write_text(
14233
14849
  json.dumps({
@@ -19602,6 +20218,9 @@ def smoke_tarball(tarball: Path) -> None:
19602
20218
  bin_path = install_root / "node_modules" / ".bin" / "design-ai"
19603
20219
  if not bin_path.exists():
19604
20220
  raise SystemExit(f"design-ai bin shim not found: {bin_path}")
20221
+ mcp_bin_path = install_root / "node_modules" / ".bin" / "design-ai-mcp"
20222
+ if not mcp_bin_path.exists():
20223
+ raise SystemExit(f"design-ai-mcp bin shim not found: {mcp_bin_path}")
19605
20224
 
19606
20225
  smoke_env = base_env.copy()
19607
20226
  smoke_env.update({
@@ -19620,6 +20239,12 @@ def smoke_tarball(tarball: Path) -> None:
19620
20239
  env=smoke_env,
19621
20240
  context="package smoke installed bin version JSON",
19622
20241
  )
20242
+ assert_design_ai_mcp_protocol_smoke(
20243
+ [str(mcp_bin_path)],
20244
+ cwd=install_root,
20245
+ env=smoke_env,
20246
+ context="package smoke installed bin design-ai-mcp protocol",
20247
+ )
19623
20248
  assert_workspace_json_smoke(
19624
20249
  [str(bin_path), "workspace", "--json"],
19625
20250
  cwd=install_root,
@@ -20465,6 +21090,40 @@ def smoke_tarball(tarball: Path) -> None:
20465
21090
  env=smoke_env,
20466
21091
  context="package smoke installed bin search human corpus",
20467
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
+ )
20468
21127
  assert_show_smoke(
20469
21128
  [str(bin_path), "show", EXPECTED_CORPUS_SHOW_TARGET, "--context", "0", "--json"],
20470
21129
  env=smoke_env,
@@ -20912,6 +21571,12 @@ def smoke_tarball(tarball: Path) -> None:
20912
21571
  env=smoke_env,
20913
21572
  context="package smoke installed bin learn stats",
20914
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
+ )
20915
21580
  assert_learning_audit_cleanup_smoke(
20916
21581
  lambda *args: [str(bin_path), *args],
20917
21582
  tmp_root / "installed-learning.json",
@@ -20999,6 +21664,12 @@ def smoke_tarball(tarball: Path) -> None:
20999
21664
  env=npx_env,
21000
21665
  context="package smoke npm exec version JSON",
21001
21666
  )
21667
+ assert_design_ai_mcp_protocol_smoke(
21668
+ npm_exec_mcp_cmd(tarball),
21669
+ cwd=npx_root,
21670
+ env=npx_env,
21671
+ context="package smoke npm exec design-ai-mcp protocol",
21672
+ )
21002
21673
  assert_workspace_json_smoke(
21003
21674
  npm_exec_cmd(tarball, "workspace", "--json"),
21004
21675
  cwd=npx_root,
@@ -21837,6 +22508,36 @@ def smoke_tarball(tarball: Path) -> None:
21837
22508
  env=npx_env,
21838
22509
  context="package smoke npm exec search human corpus",
21839
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
+ )
21840
22541
  assert_show_smoke(
21841
22542
  npm_exec_cmd(tarball, "show", EXPECTED_CORPUS_SHOW_TARGET, "--context", "0", "--json"),
21842
22543
  cwd=npx_root,
@@ -22329,6 +23030,13 @@ def smoke_tarball(tarball: Path) -> None:
22329
23030
  env=npx_env,
22330
23031
  context="package smoke npm exec learn stats",
22331
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
+ )
22332
23040
  assert_learning_audit_cleanup_smoke(
22333
23041
  lambda *args: npm_exec_cmd(tarball, *args),
22334
23042
  npx_root / "npx-learning.json",