@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
@@ -39,6 +39,8 @@ from smoke_assertions import (
39
39
  EXPECTED_HELP_ALIASES,
40
40
  EXPECTED_NUMERIC_VALUE_SMOKES,
41
41
  EXPECTED_PACK_MAX_BYTES,
42
+ EXPECTED_RANKED_SEARCH_LIMIT,
43
+ EXPECTED_RANKED_SEARCH_NOT_BUILT_NOTICE,
42
44
  EXPECTED_REPOSITORY_URL,
43
45
  EXPECTED_ROUTE_BRIEF,
44
46
  EXPECTED_ROUTE_ID,
@@ -61,6 +63,9 @@ from smoke_assertions import (
61
63
  assert_force_overwrite_replaced,
62
64
  assert_functional_alias_smokes,
63
65
  assert_help_topic_output,
66
+ assert_index_build_json,
67
+ assert_index_status_json,
68
+ assert_index_verify_json,
64
69
  assert_install_doctor_lifecycle_output,
65
70
  assert_list_catalog_output,
66
71
  assert_list_catalog_json,
@@ -72,6 +77,8 @@ from smoke_assertions import (
72
77
  assert_pack_json_component_spec,
73
78
  assert_pack_markdown_body_component_spec,
74
79
  assert_pack_markdown_component_spec,
80
+ assert_ranked_search_determinism,
81
+ assert_ranked_search_json,
75
82
  assert_site_repair_apply_report_payload,
76
83
  assert_site_repair_guidance_report_contract,
77
84
  assert_site_repair_preview_report_payload,
@@ -132,6 +139,10 @@ from smoke_assertions import (
132
139
  parse_help_topics,
133
140
  passing_doctor_report_json,
134
141
  passing_check_artifact_content,
142
+ passing_index_build_json,
143
+ passing_index_status_json,
144
+ passing_index_verify_json,
145
+ passing_ranked_search_json,
135
146
  passing_site_json,
136
147
  passing_site_mcp_check_json,
137
148
  passing_site_mcp_check_probes_human,
@@ -2205,6 +2216,102 @@ def assert_learning_redact_smoke(
2205
2216
  )
2206
2217
 
2207
2218
 
2219
+ def assert_learning_recall_json(
2220
+ raw: str,
2221
+ *,
2222
+ context: str,
2223
+ cmd: list[str],
2224
+ ) -> None:
2225
+ assert_no_ansi(raw, cmd)
2226
+ try:
2227
+ payload = json.loads(raw)
2228
+ except json.JSONDecodeError as error:
2229
+ raise SystemExit(f"{context}: failed to parse learn recall JSON") from error
2230
+
2231
+ require_registry_smoke(
2232
+ isinstance(payload, dict) and payload.get("query") == "korean mobile",
2233
+ context=context,
2234
+ cmd=cmd,
2235
+ message="learn recall JSON must echo the query",
2236
+ )
2237
+ corpus = payload.get("corpus")
2238
+ require_registry_smoke(
2239
+ isinstance(corpus, dict)
2240
+ and isinstance(corpus.get("candidateCount"), int)
2241
+ and corpus.get("candidateCount") > 0
2242
+ and isinstance(corpus.get("selectedCount"), int)
2243
+ and isinstance(corpus.get("selected"), list),
2244
+ context=context,
2245
+ cmd=cmd,
2246
+ message="learn recall corpus block shape changed",
2247
+ )
2248
+ learning = payload.get("learning")
2249
+ require_registry_smoke(
2250
+ isinstance(learning, dict)
2251
+ and learning.get("mode") == "brief-relevance"
2252
+ and learning.get("candidateCount") == 3
2253
+ and isinstance(learning.get("selected"), list),
2254
+ context=context,
2255
+ cmd=cmd,
2256
+ message="learn recall learning block shape changed",
2257
+ )
2258
+ selected = learning.get("selected")
2259
+ require_registry_smoke(
2260
+ len(selected) >= 1
2261
+ and isinstance(selected[0], dict)
2262
+ and selected[0].get("id") == "registry-korean"
2263
+ and selected[0].get("category") == "korean"
2264
+ and isinstance(selected[0].get("matchedTokens"), list),
2265
+ context=context,
2266
+ cmd=cmd,
2267
+ message="learn recall should rank the Korean learning entry for a Korean query",
2268
+ )
2269
+
2270
+
2271
+ def assert_learning_recall_smoke(
2272
+ command_factory,
2273
+ profile_path: Path,
2274
+ *,
2275
+ env: dict[str, str],
2276
+ cwd: Path | None = None,
2277
+ context: str,
2278
+ ) -> None:
2279
+ write_learning_stats_fixture(profile_path)
2280
+
2281
+ json_cmd = command_factory(
2282
+ "learn",
2283
+ "--recall",
2284
+ "korean mobile",
2285
+ "--file",
2286
+ str(profile_path),
2287
+ "--json",
2288
+ )
2289
+ json_result = run_plain(json_cmd, cwd=cwd, env=env)
2290
+ assert_learning_recall_json(json_result.stdout, context=f"{context} JSON", cmd=json_cmd)
2291
+
2292
+ # Read-only: the profile file is unchanged after recall.
2293
+ before = write_learning_stats_fixture_snapshot(profile_path)
2294
+ human_cmd = command_factory("learn", "--recall", "korean mobile", "--file", str(profile_path))
2295
+ human_result = run_plain(human_cmd, cwd=cwd, env=env)
2296
+ require_registry_smoke(
2297
+ "Recall (read-only)" in human_result.stdout
2298
+ and "Privacy: recall is read-only" in human_result.stdout,
2299
+ context=f"{context} human",
2300
+ cmd=human_cmd,
2301
+ message="learn recall human output missing read-only markers",
2302
+ )
2303
+ require_registry_smoke(
2304
+ profile_path.read_text(encoding="utf-8") == before,
2305
+ context=f"{context} read-only",
2306
+ cmd=human_cmd,
2307
+ message="learn recall must not mutate the learning profile",
2308
+ )
2309
+
2310
+
2311
+ def write_learning_stats_fixture_snapshot(profile_path: Path) -> str:
2312
+ return profile_path.read_text(encoding="utf-8")
2313
+
2314
+
2208
2315
  def assert_learning_stats_json(
2209
2316
  raw: str,
2210
2317
  *,
@@ -4412,6 +4519,56 @@ def assert_pack_stdin_smoke(
4412
4519
  )
4413
4520
 
4414
4521
 
4522
+ def assert_recall_context(payload: dict[str, object], *, context: str, cmd: list[str]) -> None:
4523
+ recall = payload.get("recall")
4524
+ require_registry_smoke(
4525
+ isinstance(recall, dict),
4526
+ context=context,
4527
+ cmd=cmd,
4528
+ message="recall should be present when --with-recall is used",
4529
+ )
4530
+ require_registry_smoke(
4531
+ recall.get("mode") == "lexical",
4532
+ context=context,
4533
+ cmd=cmd,
4534
+ message="recall should use the deterministic lexical scorer",
4535
+ )
4536
+ selected_count = recall.get("selectedCount")
4537
+ require_registry_smoke(
4538
+ isinstance(selected_count, int) and selected_count >= 1,
4539
+ context=context,
4540
+ cmd=cmd,
4541
+ message="recall should select at least one corpus file for the Button brief",
4542
+ )
4543
+ selected = recall.get("selected")
4544
+ require_registry_smoke(
4545
+ isinstance(selected, list) and len(selected) == selected_count and len(selected) >= 1,
4546
+ context=context,
4547
+ cmd=cmd,
4548
+ message="recall selected list should match the reported selected count",
4549
+ )
4550
+ top = selected[0]
4551
+ require_registry_smoke(
4552
+ isinstance(top, dict) and isinstance(top.get("id"), str) and top.get("id"),
4553
+ context=context,
4554
+ cmd=cmd,
4555
+ message="recall selection should cite the corpus relPath as its id",
4556
+ )
4557
+ require_registry_smoke(
4558
+ type(top.get("score")) in (int, float) and top.get("score") > 0,
4559
+ context=context,
4560
+ cmd=cmd,
4561
+ message="recall selection should include a positive relevance score",
4562
+ )
4563
+ markdown = recall.get("markdown")
4564
+ require_registry_smoke(
4565
+ isinstance(markdown, str) and "## Recalled design knowledge" in markdown and top.get("id") in markdown,
4566
+ context=context,
4567
+ cmd=cmd,
4568
+ message="recall markdown should carry the Recalled design knowledge section and cite the top file",
4569
+ )
4570
+
4571
+
4415
4572
  def assert_learning_relevance_context(payload: dict[str, object], *, context: str, cmd: list[str]) -> None:
4416
4573
  learning_context = payload.get("learningContext")
4417
4574
  require_registry_smoke(
@@ -4989,6 +5146,49 @@ def assert_learning_relevance_smoke(
4989
5146
  require_registry_smoke(isinstance(plan, dict), context=f"{context} pack", cmd=pack_cmd, message="pack plan missing")
4990
5147
  assert_learning_relevance_context(plan, context=f"{context} pack plan", cmd=pack_cmd)
4991
5148
 
5149
+ recall_prompt_cmd = command_factory(
5150
+ "prompt",
5151
+ EXPECTED_ROUTE_BRIEF,
5152
+ "--route",
5153
+ EXPECTED_ROUTE_ID,
5154
+ "--with-recall",
5155
+ "--recall-limit",
5156
+ "3",
5157
+ "--json",
5158
+ )
5159
+ recall_prompt_result = run_plain(recall_prompt_cmd, cwd=cwd, env=relevance_env)
5160
+ try:
5161
+ recall_prompt_payload = json.loads(recall_prompt_result.stdout)
5162
+ except json.JSONDecodeError as error:
5163
+ raise SystemExit(f"{context}: failed to parse prompt recall JSON") from error
5164
+ assert_recall_context(recall_prompt_payload, context=f"{context} prompt recall", cmd=recall_prompt_cmd)
5165
+
5166
+ recall_pack_cmd = command_factory(
5167
+ "pack",
5168
+ EXPECTED_ROUTE_BRIEF,
5169
+ "--route",
5170
+ EXPECTED_ROUTE_ID,
5171
+ "--with-recall",
5172
+ "--recall-limit",
5173
+ "3",
5174
+ "--max-bytes",
5175
+ str(EXPECTED_PACK_MAX_BYTES),
5176
+ "--json",
5177
+ )
5178
+ recall_pack_result = run_plain(recall_pack_cmd, cwd=cwd, env=relevance_env)
5179
+ try:
5180
+ recall_pack_payload = json.loads(recall_pack_result.stdout)
5181
+ except json.JSONDecodeError as error:
5182
+ raise SystemExit(f"{context}: failed to parse pack recall JSON") from error
5183
+ recall_plan = recall_pack_payload.get("plan")
5184
+ require_registry_smoke(
5185
+ isinstance(recall_plan, dict),
5186
+ context=f"{context} pack recall",
5187
+ cmd=recall_pack_cmd,
5188
+ message="pack recall plan missing",
5189
+ )
5190
+ assert_recall_context(recall_plan, context=f"{context} pack recall plan", cmd=recall_pack_cmd)
5191
+
4992
5192
  eval_template_path = profile_path.with_name(f"{profile_path.stem}-eval-template.json")
4993
5193
  eval_template_path.write_text("stale eval template output\n", encoding="utf-8")
4994
5194
  eval_template_cmd = command_factory(
@@ -5055,6 +5255,166 @@ def assert_learning_relevance_smoke(
5055
5255
  )
5056
5256
 
5057
5257
 
5258
+ def assert_index_roundtrip_smoke(
5259
+ command_factory,
5260
+ index_dir: Path,
5261
+ profile_path: Path,
5262
+ *,
5263
+ env: dict[str, str],
5264
+ cwd: Path | None = None,
5265
+ context: str,
5266
+ ) -> None:
5267
+ """`index --build` -> `index --status` -> `index --verify` round-trip against a
5268
+ temp index dir and learning profile, isolated from the ambient environment."""
5269
+ index_dir.mkdir(parents=True, exist_ok=True)
5270
+ write_learning_relevance_fixture(profile_path)
5271
+ index_env = env.copy()
5272
+ index_env["DESIGN_AI_INDEX_DIR"] = str(index_dir)
5273
+ index_env["DESIGN_AI_LEARNING_FILE"] = str(profile_path)
5274
+
5275
+ build_cmd = command_factory("index", "--build", "--json")
5276
+ build_result = run_plain(build_cmd, cwd=cwd, env=index_env)
5277
+ assert_index_build_json(
5278
+ build_result.stdout,
5279
+ index_dir=str(index_dir),
5280
+ context=f"{context} build",
5281
+ cmd=build_cmd,
5282
+ )
5283
+
5284
+ status_cmd = command_factory("index", "--status", "--json")
5285
+ status_result = run_plain(status_cmd, cwd=cwd, env=index_env)
5286
+ assert_index_status_json(
5287
+ status_result.stdout,
5288
+ index_dir=str(index_dir),
5289
+ context=f"{context} status",
5290
+ cmd=status_cmd,
5291
+ )
5292
+
5293
+ verify_cmd = command_factory("index", "--verify", "--json")
5294
+ verify_result = run_plain(verify_cmd, cwd=cwd, env=index_env)
5295
+ assert_index_verify_json(
5296
+ verify_result.stdout,
5297
+ index_dir=str(index_dir),
5298
+ context=f"{context} verify",
5299
+ cmd=verify_cmd,
5300
+ )
5301
+
5302
+
5303
+ def assert_ranked_search_determinism_smoke(
5304
+ command_factory,
5305
+ index_dir: Path,
5306
+ *,
5307
+ env: dict[str, str],
5308
+ cwd: Path | None = None,
5309
+ context: str,
5310
+ ) -> None:
5311
+ """Two identical `search --ranked --json` runs must produce byte-identical output."""
5312
+ index_dir.mkdir(parents=True, exist_ok=True)
5313
+ ranked_env = env.copy()
5314
+ ranked_env["DESIGN_AI_INDEX_DIR"] = str(index_dir)
5315
+
5316
+ ranked_cmd = command_factory(
5317
+ "search",
5318
+ EXPECTED_CORPUS_SEARCH_QUERY,
5319
+ "--dir",
5320
+ "knowledge",
5321
+ "--limit",
5322
+ str(EXPECTED_RANKED_SEARCH_LIMIT),
5323
+ "--ranked",
5324
+ "--json",
5325
+ )
5326
+ first_result = run_plain(ranked_cmd, cwd=cwd, env=ranked_env)
5327
+ second_result = run_plain(ranked_cmd, cwd=cwd, env=ranked_env)
5328
+ assert_ranked_search_json(
5329
+ first_result.stdout,
5330
+ context=f"{context} payload",
5331
+ cmd=ranked_cmd,
5332
+ expected_notice=EXPECTED_RANKED_SEARCH_NOT_BUILT_NOTICE,
5333
+ )
5334
+ assert_ranked_search_determinism(
5335
+ first_result.stdout,
5336
+ second_result.stdout,
5337
+ context=context,
5338
+ cmd=ranked_cmd,
5339
+ )
5340
+
5341
+
5342
+ def assert_embeddings_off_by_default_smoke(
5343
+ command_factory,
5344
+ index_dir: Path,
5345
+ profile_path: Path,
5346
+ *,
5347
+ env: dict[str, str],
5348
+ cwd: Path | None = None,
5349
+ context: str,
5350
+ ) -> None:
5351
+ """Phase B is opt-in: a plain `index --build` never touches embeddings, and
5352
+ `index --status` reports `embeddings: null` (the feature is simply unused)."""
5353
+ index_dir.mkdir(parents=True, exist_ok=True)
5354
+ write_learning_relevance_fixture(profile_path)
5355
+ index_env = env.copy()
5356
+ index_env["DESIGN_AI_INDEX_DIR"] = str(index_dir)
5357
+ index_env["DESIGN_AI_LEARNING_FILE"] = str(profile_path)
5358
+ # Point at a config file that is guaranteed not to exist, rather than relying on
5359
+ # the ambient environment lacking DESIGN_AI_CONFIG_FILE — a real ~/.design-ai/
5360
+ # config.json on the machine running this smoke must not leak into the test.
5361
+ index_env["DESIGN_AI_CONFIG_FILE"] = str(index_dir.parent / "no-config-here" / "config.json")
5362
+
5363
+ build_cmd = command_factory("index", "--build", "--json")
5364
+ build_result = run_plain(build_cmd, cwd=cwd, env=index_env)
5365
+ build_payload = json.loads(build_result.stdout)
5366
+ if "embeddings" in build_payload:
5367
+ raise SystemExit(f"index build JSON after {context} unexpectedly ran provider execution with plain --build")
5368
+
5369
+ status_cmd = command_factory("index", "--status", "--json")
5370
+ status_result = run_plain(status_cmd, cwd=cwd, env=index_env)
5371
+ assert_index_status_json(
5372
+ status_result.stdout,
5373
+ index_dir=str(index_dir),
5374
+ context=f"{context} status",
5375
+ cmd=status_cmd,
5376
+ expect_embeddings=False,
5377
+ )
5378
+
5379
+
5380
+ def assert_search_embeddings_no_provider_fallback_smoke(
5381
+ command_factory,
5382
+ *,
5383
+ env: dict[str, str],
5384
+ cwd: Path | None = None,
5385
+ context: str,
5386
+ config_file: Path,
5387
+ ) -> None:
5388
+ """`search --ranked --embeddings` with no provider configured (flag or config)
5389
+ degrades to the lexical backend with a notice and exit code 0."""
5390
+ fallback_env = env.copy()
5391
+ # Same isolation rationale as assert_embeddings_off_by_default_smoke: point at a
5392
+ # config path guaranteed not to exist rather than trusting the ambient environment.
5393
+ fallback_env["DESIGN_AI_CONFIG_FILE"] = str(config_file)
5394
+
5395
+ search_cmd = command_factory(
5396
+ "search",
5397
+ EXPECTED_CORPUS_SEARCH_QUERY,
5398
+ "--dir",
5399
+ "knowledge",
5400
+ "--limit",
5401
+ str(EXPECTED_RANKED_SEARCH_LIMIT),
5402
+ "--ranked",
5403
+ "--embeddings",
5404
+ "--json",
5405
+ )
5406
+ result = run_plain(search_cmd, cwd=cwd, env=fallback_env)
5407
+ assert_ranked_search_json(
5408
+ result.stdout,
5409
+ context=f"{context} payload",
5410
+ cmd=search_cmd,
5411
+ expected_backend="lexical",
5412
+ )
5413
+ payload = json.loads(result.stdout)
5414
+ if "embedding provider" not in payload.get("notice", "") and "no embedding provider configured" not in payload.get("notice", ""):
5415
+ raise SystemExit(f"search ranked JSON after {context} fallback notice does not mention the missing provider")
5416
+
5417
+
5058
5418
  def wait_for_registry_package(
5059
5419
  package_spec: str,
5060
5420
  *,
@@ -6086,6 +6446,13 @@ def smoke_registry_package(package_spec: str, *, retries: int, delay: float) ->
6086
6446
  env=env,
6087
6447
  context="registry smoke npm exec learn stats",
6088
6448
  )
6449
+ assert_learning_recall_smoke(
6450
+ lambda *args: npm_exec_cmd(package_spec, *args),
6451
+ npx_root / "registry-recall-learning.json",
6452
+ cwd=npx_root,
6453
+ env=env,
6454
+ context="registry smoke npm exec learn recall",
6455
+ )
6089
6456
  assert_learning_audit_cleanup_smoke(
6090
6457
  lambda *args: npm_exec_cmd(package_spec, *args),
6091
6458
  npx_root / "registry-audit-learning.json",
@@ -6100,6 +6467,36 @@ def smoke_registry_package(package_spec: str, *, retries: int, delay: float) ->
6100
6467
  env=env,
6101
6468
  context="registry smoke npm exec learning relevance",
6102
6469
  )
6470
+ assert_index_roundtrip_smoke(
6471
+ lambda *args: npm_exec_cmd(package_spec, *args),
6472
+ npx_root / "registry-index",
6473
+ npx_root / "registry-index-learning.json",
6474
+ cwd=npx_root,
6475
+ env=env,
6476
+ context="registry smoke npm exec index roundtrip",
6477
+ )
6478
+ assert_ranked_search_determinism_smoke(
6479
+ lambda *args: npm_exec_cmd(package_spec, *args),
6480
+ npx_root / "registry-ranked-search-index",
6481
+ cwd=npx_root,
6482
+ env=env,
6483
+ context="registry smoke npm exec ranked search determinism",
6484
+ )
6485
+ assert_embeddings_off_by_default_smoke(
6486
+ lambda *args: npm_exec_cmd(package_spec, *args),
6487
+ npx_root / "registry-embeddings-off-index",
6488
+ npx_root / "registry-embeddings-off-learning.json",
6489
+ cwd=npx_root,
6490
+ env=env,
6491
+ context="registry smoke npm exec embeddings off by default",
6492
+ )
6493
+ assert_search_embeddings_no_provider_fallback_smoke(
6494
+ lambda *args: npm_exec_cmd(package_spec, *args),
6495
+ cwd=npx_root,
6496
+ env=env,
6497
+ context="registry smoke npm exec search embeddings no provider fallback",
6498
+ config_file=npx_root / "registry-no-config-here" / "config.json",
6499
+ )
6103
6500
  assert_update_dry_run_smoke(
6104
6501
  npm_exec_cmd(package_spec, "update", "--dry-run"),
6105
6502
  cwd=npx_root,
@@ -8297,6 +8694,38 @@ def run_self_test() -> None:
8297
8694
  scope="registry smoke",
8298
8695
  )
8299
8696
 
8697
+ recall_payload = {
8698
+ "recall": {
8699
+ "query": EXPECTED_ROUTE_BRIEF,
8700
+ "mode": "lexical",
8701
+ "candidateCount": 42,
8702
+ "selectedCount": 1,
8703
+ "selected": [
8704
+ {
8705
+ "id": "knowledge/components/INDEX.md",
8706
+ "score": 9.5,
8707
+ "matchedTokens": ["button", "component"],
8708
+ },
8709
+ ],
8710
+ "markdown": (
8711
+ "## Recalled design knowledge\n\n"
8712
+ "- knowledge/components/INDEX.md\n"
8713
+ " - Component index"
8714
+ ),
8715
+ },
8716
+ }
8717
+ recall_cmd = ["design-ai", "prompt", EXPECTED_ROUTE_BRIEF, "--with-recall", "--json"]
8718
+ assert_recall_context(recall_payload, context="registry smoke self-test", cmd=recall_cmd)
8719
+ expect_self_test_failure(
8720
+ lambda: assert_recall_context(
8721
+ {**recall_payload, "recall": {**recall_payload["recall"], "selectedCount": 0, "selected": []}},
8722
+ context="registry smoke self-test",
8723
+ cmd=recall_cmd,
8724
+ ),
8725
+ expected="recall should select at least one corpus file",
8726
+ scope="registry smoke",
8727
+ )
8728
+
8300
8729
  learning_audit_path = tmp_root / "learning-audit.json"
8301
8730
  duplicate_command_args = [
8302
8731
  "design-ai",
@@ -8503,6 +8932,111 @@ def run_self_test() -> None:
8503
8932
  scope="registry smoke",
8504
8933
  )
8505
8934
 
8935
+ index_dir = str(tmp_root / "registry-index-self-test")
8936
+ index_build_cmd = ["design-ai", "index", "--build", "--json"]
8937
+ assert_index_build_json(
8938
+ passing_index_build_json(index_dir),
8939
+ index_dir=index_dir,
8940
+ context="registry smoke self-test",
8941
+ cmd=index_build_cmd,
8942
+ )
8943
+ expect_self_test_failure(
8944
+ lambda: assert_index_build_json(
8945
+ "{",
8946
+ index_dir=index_dir,
8947
+ context="registry smoke self-test",
8948
+ cmd=index_build_cmd,
8949
+ ),
8950
+ expected="failed to parse index build JSON",
8951
+ scope="registry smoke",
8952
+ )
8953
+
8954
+ index_status_cmd = ["design-ai", "index", "--status", "--json"]
8955
+ assert_index_status_json(
8956
+ passing_index_status_json(index_dir),
8957
+ index_dir=index_dir,
8958
+ context="registry smoke self-test",
8959
+ cmd=index_status_cmd,
8960
+ )
8961
+ assert_index_status_json(
8962
+ passing_index_status_json(index_dir, with_embeddings=True),
8963
+ index_dir=index_dir,
8964
+ context="registry smoke self-test embeddings",
8965
+ cmd=index_status_cmd,
8966
+ expect_embeddings=True,
8967
+ )
8968
+ expect_self_test_failure(
8969
+ lambda: assert_index_status_json(
8970
+ passing_index_status_json(index_dir, with_embeddings=True),
8971
+ index_dir=index_dir,
8972
+ context="registry smoke self-test embeddings unexpected",
8973
+ cmd=index_status_cmd,
8974
+ expect_embeddings=False,
8975
+ ),
8976
+ expected="embeddings is not null when embeddings were not built",
8977
+ scope="registry smoke",
8978
+ )
8979
+
8980
+ index_verify_cmd = ["design-ai", "index", "--verify", "--json"]
8981
+ assert_index_verify_json(
8982
+ passing_index_verify_json(index_dir),
8983
+ index_dir=index_dir,
8984
+ context="registry smoke self-test",
8985
+ cmd=index_verify_cmd,
8986
+ )
8987
+ expect_self_test_failure(
8988
+ lambda: assert_index_verify_json(
8989
+ json.dumps({**json.loads(passing_index_verify_json(index_dir)), "ok": False}),
8990
+ index_dir=index_dir,
8991
+ context="registry smoke self-test",
8992
+ cmd=index_verify_cmd,
8993
+ ),
8994
+ expected="ok is not true",
8995
+ scope="registry smoke",
8996
+ )
8997
+
8998
+ ranked_search_cmd = ["design-ai", "search", EXPECTED_CORPUS_SEARCH_QUERY, "--ranked", "--json"]
8999
+ ranked_search_json = passing_ranked_search_json()
9000
+ assert_ranked_search_json(
9001
+ ranked_search_json,
9002
+ context="registry smoke self-test",
9003
+ cmd=ranked_search_cmd,
9004
+ )
9005
+ assert_ranked_search_determinism(
9006
+ ranked_search_json,
9007
+ ranked_search_json,
9008
+ context="registry smoke self-test",
9009
+ cmd=ranked_search_cmd,
9010
+ )
9011
+ expect_self_test_failure(
9012
+ lambda: assert_ranked_search_determinism(
9013
+ ranked_search_json,
9014
+ passing_ranked_search_json(backend="lexical"),
9015
+ context="registry smoke self-test",
9016
+ cmd=ranked_search_cmd,
9017
+ ),
9018
+ expected="differs between identical runs",
9019
+ scope="registry smoke",
9020
+ )
9021
+
9022
+ ranked_search_fallback_json = passing_ranked_search_json(backend="lexical")
9023
+ assert_ranked_search_json(
9024
+ ranked_search_fallback_json,
9025
+ context="registry smoke self-test fallback",
9026
+ cmd=ranked_search_cmd,
9027
+ expected_backend="lexical",
9028
+ )
9029
+ expect_self_test_failure(
9030
+ lambda: assert_ranked_search_json(
9031
+ passing_ranked_search_json(backend="embeddings"),
9032
+ context="registry smoke self-test fallback mismatch",
9033
+ cmd=ranked_search_cmd,
9034
+ expected_backend="lexical",
9035
+ ),
9036
+ expected="backend differs from expected backend",
9037
+ scope="registry smoke",
9038
+ )
9039
+
8506
9040
  expect_self_test_failure(
8507
9041
  lambda: smoke_registry_package("self-test-package", retries=0, delay=0),
8508
9042
  expected="--retries must be at least 1",
@@ -38,6 +38,10 @@ REQUIRED_RELEASE_POLICY_DOC_LABELS = (
38
38
  RELEASE_POLICY_DOC_PATHS = tuple(
39
39
  (label, ROOT / label) for label in REQUIRED_RELEASE_POLICY_DOC_LABELS
40
40
  )
41
+ RELEASE_POLICY_COMPANION_DOC_PATHS: dict[str, Path] = {
42
+ "README.md": ROOT / "docs" / "RELEASE-GATES.md",
43
+ "README.ko.md": ROOT / "docs" / "RELEASE-GATES.ko.md",
44
+ }
41
45
  PRODUCT_READINESS_WARNING_STRICT_COMPARE_TERM_GROUPS = (
42
46
  (
43
47
  "warning-state Website Console bundle-compare strict smoke coverage",
@@ -3837,16 +3841,32 @@ def load_text_input(label: str, path: Path) -> tuple[str, list[str]]:
3837
3841
 
3838
3842
  def load_release_policy_docs(
3839
3843
  policy_doc_paths: tuple[tuple[str, Path], ...] = RELEASE_POLICY_DOC_PATHS,
3844
+ companion_doc_paths: dict[str, Path] | None = None,
3840
3845
  ) -> tuple[dict[str, str], list[str]]:
3846
+ if companion_doc_paths is None:
3847
+ companion_doc_paths = RELEASE_POLICY_COMPANION_DOC_PATHS
3841
3848
  docs: dict[str, str] = {}
3842
3849
  errors: list[str] = []
3843
3850
  for label, path in policy_doc_paths:
3844
3851
  try:
3845
- docs[label] = path.read_text(encoding="utf-8")
3852
+ text = path.read_text(encoding="utf-8")
3846
3853
  except FileNotFoundError:
3847
3854
  errors.append(f"release policy docs required file is missing on disk: {label} ({path})")
3855
+ continue
3848
3856
  except OSError as exc:
3849
3857
  errors.append(f"release policy docs required file cannot be read: {label} ({path}): {exc}")
3858
+ continue
3859
+ companion_path = companion_doc_paths.get(label)
3860
+ if companion_path is not None and companion_path.exists():
3861
+ try:
3862
+ companion_text = companion_path.read_text(encoding="utf-8")
3863
+ except OSError as exc:
3864
+ errors.append(
3865
+ f"release policy companion doc cannot be read: {label} ({companion_path}): {exc}"
3866
+ )
3867
+ else:
3868
+ text = f"{text}\n\n{companion_text}"
3869
+ docs[label] = text
3850
3870
  return docs, errors
3851
3871
 
3852
3872
 
@@ -9736,6 +9756,32 @@ class AuditResult:
9736
9756
  "release policy doc loader should report missing files without a traceback",
9737
9757
  )
9738
9758
 
9759
+ with tempfile.TemporaryDirectory() as companion_temp_dir:
9760
+ companion_root = Path(companion_temp_dir)
9761
+ base_doc = companion_root / "README.md"
9762
+ base_doc.write_text("summary only\n", encoding="utf-8")
9763
+ companion_doc = companion_root / "RELEASE-GATES.md"
9764
+ companion_doc.write_text("companion evidence phrase\n", encoding="utf-8")
9765
+ companion_docs, companion_errors = load_release_policy_docs(
9766
+ (("README.md", base_doc),),
9767
+ {"README.md": companion_doc},
9768
+ )
9769
+ assert_condition(
9770
+ companion_errors == []
9771
+ and "summary only" in companion_docs["README.md"]
9772
+ and "companion evidence phrase" in companion_docs["README.md"],
9773
+ "release policy doc loader should append companion evidence docs to their base doc",
9774
+ )
9775
+ missing_companion_docs, missing_companion_errors = load_release_policy_docs(
9776
+ (("README.md", base_doc),),
9777
+ {"README.md": companion_root / "MISSING-GATES.md"},
9778
+ )
9779
+ assert_condition(
9780
+ missing_companion_errors == []
9781
+ and missing_companion_docs["README.md"] == "summary only\n",
9782
+ "release policy doc loader should treat a missing companion doc as optional",
9783
+ )
9784
+
9739
9785
  with tempfile.TemporaryDirectory() as temp_dir:
9740
9786
  temp_path = Path(temp_dir)
9741
9787
  valid_json = temp_path / "valid.json"