@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
@@ -115,6 +115,7 @@ EXPECTED_HELP_TOPICS = (
115
115
  "status",
116
116
  "list",
117
117
  "search",
118
+ "index",
118
119
  "show",
119
120
  "route",
120
121
  "routes",
@@ -159,17 +160,18 @@ EXPECTED_HELP_TOPIC_USAGES = {
159
160
  "uninstall": "design-ai uninstall [--json]",
160
161
  "status": "design-ai status [--json]",
161
162
  "list": "design-ai list [skills|commands|agents] [--json]",
162
- "search": "design-ai search <query> [--dir kind] [--limit N] [--json]",
163
+ "search": "design-ai search <query> [--dir kind] [--limit N] [--ranked] [--embeddings [--provider \"cmd args\"]] [--json]",
164
+ "index": "design-ai index [--build|--status|--verify] [--json] [--embeddings [--provider \"cmd args\"]]",
163
165
  "show": "design-ai show <file[:line]> [--lines N:M] [--context N] [--json]",
164
166
  "route": "design-ai route <brief|--from-file file|--stdin|--list|--eval-template|--eval> [--limit N]",
165
167
  "routes": "design-ai routes [--json]",
166
- "prompt": "design-ai prompt <brief|--from-file file|--stdin|--eval-template|--eval> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--out file]",
167
- "pack": "design-ai pack <brief|--from-file file|--stdin|--eval-template|--eval> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--max-bytes N]",
168
+ "prompt": "design-ai prompt <brief|--from-file file|--stdin|--eval-template|--eval> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--with-recall] [--recall-limit N] [--out file]",
169
+ "pack": "design-ai pack <brief|--from-file file|--stdin|--eval-template|--eval> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--with-recall] [--recall-limit N] [--max-bytes N]",
168
170
  "check": "design-ai check <artifact.md|--stdin|--examples> [--route id|--all-routes] [--learn]",
169
171
  "audit": "design-ai audit [--strict] [--quiet] [--json]",
170
172
  "doctor": "design-ai doctor [--strict] [--json] [--fix]",
171
173
  "examples": "design-ai examples [query] [--route id] [--limit N] [--json]",
172
- "learn": "design-ai learn [--init|--remember text|--feedback text|--list|--export|--query text|--explain|--backup|--redact|--verify|--diff|--restore|--restore-backups [--prune]|--import|--audit [--fix]|--curate|--stats|--usage|--signals [--strict]|--agent-backlog [--strict]|--propose-skills [--min-evidence N] [--review-file path] [--review-check|--apply-plan] [--strict]|--eval-template|--eval [--strict]|--forget id|--clear] [--json|--report|--patch|--review-template] [--out file]",
174
+ "learn": "design-ai learn [--init|--remember text|--feedback text|--list|--export|--query text|--explain|--recall query|--backup|--redact|--verify|--diff|--restore|--restore-backups [--prune]|--import|--audit [--fix]|--curate|--stats|--usage|--signals [--strict]|--agent-backlog [--strict]|--propose-skills [--min-evidence N] [--review-file path] [--review-check|--apply-plan] [--strict]|--eval-template|--eval [--strict]|--forget id|--clear] [--json|--report|--patch|--review-template] [--out file]",
173
175
  "workspace": "design-ai workspace [--root path] [--learning-file path] [--learning-usage path] [--learning-eval path] [--strict] [--json]",
174
176
  "site": "design-ai site <workspace.json|--stdin> [--strict] [--json|--mcp-check [--probes]|--mcp-plan [--probes] [--json]|--next-actions [--json]|--graph|--tasks|--bundle|--report|--prompts|--prompt id [--task id]] [--out file] | site <bundle-dir> --bundle-check [--json] | site <bundle-dir> --bundle-compare other-bundle-dir [--json] | site <bundle-dir> --bundle-handoff [--task id] [--json] | site <bundle-dir> --bundle-repair [--yes] [--json] [--out file] | site --init --name name --live-url url [--next-actions] [--out file] | site --init --name name --live-url url --bundle --out dir | site --from-intake file.md|--stdin [--json|--next-actions [--json]|--tasks|--bundle [--tasks] --out dir] [--out file] | site --intake-template [--language en|ko] [--json] [--out file] | site --sample [--out file] | site --prompt-list [--json]",
175
177
  "mcp": "design-ai mcp",
@@ -182,19 +184,20 @@ EXPECTED_HELP_TOPIC_FRAGMENTS = {
182
184
  "uninstall": ("Usage:", "design-ai uninstall [--json]"),
183
185
  "status": ("Usage:", "design-ai status [--json]"),
184
186
  "list": ("Usage:", "design-ai list [skills|commands|agents]"),
185
- "search": ("Usage:", "design-ai search <query> [--limit N] [--dir kind] [--json]"),
187
+ "search": ("Usage:", "design-ai search <query> [--limit N] [--dir kind] [--ranked] [--embeddings [--provider \"cmd args\"]] [--json]"),
188
+ "index": ("Usage:", "design-ai index [--build|--status|--verify] [--json] [--embeddings [--provider \"cmd args\"]]"),
186
189
  "show": ("Usage:", "design-ai show <file[:line|start-end]> [--lines N:M] [--context N] [--json]"),
187
190
  "route": ("Usage:", "design-ai route <brief>", "design-ai route --list [--json]", "design-ai route --eval-template [--json]"),
188
191
  "routes": ("Usage:", "design-ai routes [--json]", "Equivalent to: design-ai route --list"),
189
192
  "prompt": (
190
193
  "Usage:",
191
- "design-ai prompt <brief> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--json] [--out file] [--force]",
194
+ "design-ai prompt <brief> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--with-recall] [--recall-limit N] [--json] [--out file] [--force]",
192
195
  "design-ai prompt --eval-template [--json] [--out file] [--force]",
193
196
  "design-ai prompt --eval --from-file prompt-eval.json [--strict] [--json] [--out file] [--force]",
194
197
  ),
195
198
  "pack": (
196
199
  "Usage:",
197
- "design-ai pack <brief> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--max-bytes N] [--json] [--out file] [--force]",
200
+ "design-ai pack <brief> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--with-recall] [--recall-limit N] [--max-bytes N] [--json] [--out file] [--force]",
198
201
  "design-ai pack --eval-template [--json] [--out file] [--force]",
199
202
  "design-ai pack --eval --from-file pack-eval.json [--strict] [--json] [--out file] [--force]",
200
203
  ),
@@ -205,6 +208,8 @@ EXPECTED_HELP_TOPIC_FRAGMENTS = {
205
208
  "learn": (
206
209
  "Usage:",
207
210
  "design-ai learn [--list] [--category kind] [--query text] [--explain] [--limit N] [--json] [--out file] [--force]",
211
+ "design-ai learn --recall query [--limit N] [--category kind] [--json]",
212
+ "--recall",
208
213
  "design-ai learn --init [--yes|--dry-run] [--json] [--out file] [--force]",
209
214
  "design-ai learn --feedback text [--outcome keep|improve|avoid] [--category kind] [--json] [--out file] [--force]",
210
215
  "design-ai learn --feedback --from-file notes.md [--outcome keep|improve|avoid] [--category kind] [--json] [--out file] [--force]",
@@ -368,7 +373,7 @@ EXPECTED_MAIN_HELP_FRAGMENTS = (
368
373
  "pack <brief|--from-file file|--stdin|--eval-template|--eval>",
369
374
  "check <artifact.md|--stdin|--examples>",
370
375
  "examples [query]",
371
- "learn [--init|--remember text|--feedback text|--list|--export|--query text|--explain|--backup|--redact|--verify|--diff|--restore|--restore-backups [--prune]|--import|--audit [--fix]|--curate|--stats|--usage|--signals [--strict]|--agent-backlog [--strict]|--propose-skills [--min-evidence N] [--review-file path] [--review-check|--apply-plan] [--strict]|--eval-template|--eval [--strict]|--forget id|--clear] [--json|--report|--patch|--review-template] [--out file]",
376
+ "learn [--init|--remember text|--feedback text|--list|--export|--query text|--explain|--recall query|--backup|--redact|--verify|--diff|--restore|--restore-backups [--prune]|--import|--audit [--fix]|--curate|--stats|--usage|--signals [--strict]|--agent-backlog [--strict]|--propose-skills [--min-evidence N] [--review-file path] [--review-check|--apply-plan] [--strict]|--eval-template|--eval [--strict]|--forget id|--clear] [--json|--report|--patch|--review-template] [--out file]",
372
377
  "workspace [--root path] [--learning-file path] [--learning-usage path] [--learning-eval path] [--strict] [--json]",
373
378
  "site <workspace.json|--stdin>",
374
379
  "site --init",
@@ -576,6 +581,39 @@ EXPECTED_ERROR_PREFIX = "\u2717"
576
581
  EXPECTED_CORPUS_SEARCH_QUERY = "Pretendard"
577
582
  EXPECTED_CORPUS_SEARCH_HIT = "knowledge/PRINCIPLES.md"
578
583
  EXPECTED_CORPUS_SEARCH_PREVIEW = "Pretendard for Korean primary"
584
+ EXPECTED_RANKED_SEARCH_LIMIT = 3
585
+ EXPECTED_RANKED_SEARCH_NOT_BUILT_NOTICE = (
586
+ "no corpus index built yet; ranked results come from a live corpus scan (design-ai index --build)"
587
+ )
588
+ EXPECTED_INDEX_BUILD_COMMAND = "design-ai index --build"
589
+ EXPECTED_INDEX_FILE_BASENAMES = {
590
+ "corpus": "corpus-index.json",
591
+ "learning": "learning-index.json",
592
+ }
593
+ EXPECTED_EMBEDDING_INDEX_FILE_BASENAMES = {"embedding-index": "embedding-index.json"}
594
+ EXPECTED_INDEX_STATUS_SECTION_KEYS = [
595
+ "file",
596
+ "present",
597
+ "fresh",
598
+ "sourceMatch",
599
+ "generatedAt",
600
+ "documentCount",
601
+ "storedDigest",
602
+ "currentDigest",
603
+ "error",
604
+ ]
605
+ EXPECTED_INDEX_STATUS_EMBEDDINGS_KEYS = [
606
+ "file",
607
+ "present",
608
+ "fresh",
609
+ "sourceMatch",
610
+ "generatedAt",
611
+ "documentCount",
612
+ "provider",
613
+ "storedDigest",
614
+ "currentDigest",
615
+ "error",
616
+ ]
579
617
  EXPECTED_CORPUS_SHOW_TARGET = "knowledge/PRINCIPLES.md:1"
580
618
  EXPECTED_CORPUS_SHOW_REL_PATH = "knowledge/PRINCIPLES.md"
581
619
  EXPECTED_CORPUS_SHOW_TEXT = "<!-- hand-written -->"
@@ -804,7 +842,8 @@ EXPECTED_CHECK_EXAMPLES_PAYLOAD_KEYS = [
804
842
  "examples",
805
843
  ]
806
844
  EXPECTED_CHECK_EXAMPLE_ENTRY_KEYS = ["example", "report"]
807
- EXPECTED_WORKSPACE_PAYLOAD_KEYS = ["context", "git", "repository", "learning", "learningUsage", "learningEval", "learningRestoreBackups", "release", "nextActions"]
845
+ EXPECTED_WORKSPACE_PAYLOAD_KEYS = ["context", "git", "repository", "learning", "learningUsage", "learningEval", "learningRestoreBackups", "retrievalIndex", "release", "nextActions"]
846
+ EXPECTED_WORKSPACE_RETRIEVAL_INDEX_KEYS = ["indexDir", "corpus", "learning", "fresh", "status", "buildCommand"]
808
847
  EXPECTED_WORKSPACE_CONTEXT_KEYS = ["cwd", "root", "sourceRoot", "packageName", "version"]
809
848
  EXPECTED_WORKSPACE_GIT_KEYS = [
810
849
  "isRepo",
@@ -1932,6 +1971,105 @@ def passing_search_human_output() -> str:
1932
1971
  ])
1933
1972
 
1934
1973
 
1974
+ def passing_ranked_search_json(*, backend: str | None = None) -> str:
1975
+ payload = {
1976
+ "query": EXPECTED_CORPUS_SEARCH_QUERY,
1977
+ "ranked": True,
1978
+ "notice": EXPECTED_RANKED_SEARCH_NOT_BUILT_NOTICE,
1979
+ "hits": [
1980
+ {
1981
+ "relPath": EXPECTED_CORPUS_SEARCH_HIT,
1982
+ "file": f"/tmp/design-ai/{EXPECTED_CORPUS_SEARCH_HIT}",
1983
+ "score": 2.886971,
1984
+ "matchedTokens": [EXPECTED_CORPUS_SEARCH_QUERY.lower()],
1985
+ "preview": EXPECTED_CORPUS_SEARCH_PREVIEW,
1986
+ }
1987
+ ],
1988
+ }
1989
+ if backend is not None:
1990
+ payload["backend"] = backend
1991
+ return json.dumps(payload)
1992
+
1993
+
1994
+ def passing_index_build_json(index_dir: str = "/tmp/design-ai-index") -> str:
1995
+ return json.dumps({
1996
+ "action": "build",
1997
+ "indexDir": index_dir,
1998
+ "corpus": {
1999
+ "file": f"{index_dir}/{EXPECTED_INDEX_FILE_BASENAMES['corpus']}",
2000
+ "documentCount": 460,
2001
+ "digest": f"sha256:{'0' * 64}",
2002
+ },
2003
+ "learning": {
2004
+ "file": f"{index_dir}/{EXPECTED_INDEX_FILE_BASENAMES['learning']}",
2005
+ "documentCount": 3,
2006
+ "digest": f"sha256:{'1' * 64}",
2007
+ },
2008
+ })
2009
+
2010
+
2011
+ def passing_index_status_section(index_dir: str, label: str, digest: str) -> dict:
2012
+ return {
2013
+ "file": f"{index_dir}/{EXPECTED_INDEX_FILE_BASENAMES[label]}",
2014
+ "present": True,
2015
+ "fresh": True,
2016
+ "sourceMatch": True,
2017
+ "generatedAt": "2026-05-22T00:00:03.000Z",
2018
+ "documentCount": 460 if label == "corpus" else 3,
2019
+ "storedDigest": digest,
2020
+ "currentDigest": digest,
2021
+ "error": "",
2022
+ }
2023
+
2024
+
2025
+ def passing_index_status_embeddings_section(index_dir: str) -> dict:
2026
+ return {
2027
+ "file": f"{index_dir}/{EXPECTED_EMBEDDING_INDEX_FILE_BASENAMES['embedding-index']}",
2028
+ "present": True,
2029
+ "fresh": True,
2030
+ "sourceMatch": True,
2031
+ "generatedAt": "2026-05-22T00:00:03.000Z",
2032
+ "documentCount": 463,
2033
+ "provider": {"command": "./bin/local-embed", "modelLabel": "stub", "dimensions": 3},
2034
+ "storedDigest": f"sha256:{'0' * 64}",
2035
+ "currentDigest": f"sha256:{'0' * 64}",
2036
+ "error": "",
2037
+ }
2038
+
2039
+
2040
+ def passing_index_status_json(index_dir: str = "/tmp/design-ai-index", *, with_embeddings: bool = False) -> str:
2041
+ return json.dumps({
2042
+ "action": "status",
2043
+ "indexDir": index_dir,
2044
+ "corpus": passing_index_status_section(index_dir, "corpus", f"sha256:{'0' * 64}"),
2045
+ "learning": passing_index_status_section(index_dir, "learning", f"sha256:{'1' * 64}"),
2046
+ "embeddings": passing_index_status_embeddings_section(index_dir) if with_embeddings else None,
2047
+ "fresh": True,
2048
+ "buildCommand": EXPECTED_INDEX_BUILD_COMMAND,
2049
+ })
2050
+
2051
+
2052
+ def passing_index_verify_json(index_dir: str = "/tmp/design-ai-index") -> str:
2053
+ return json.dumps({
2054
+ "action": "verify",
2055
+ "ok": True,
2056
+ "checks": [
2057
+ {
2058
+ "name": "corpus",
2059
+ "file": f"{index_dir}/{EXPECTED_INDEX_FILE_BASENAMES['corpus']}",
2060
+ "matches": True,
2061
+ "reason": "",
2062
+ },
2063
+ {
2064
+ "name": "learning",
2065
+ "file": f"{index_dir}/{EXPECTED_INDEX_FILE_BASENAMES['learning']}",
2066
+ "matches": True,
2067
+ "reason": "",
2068
+ },
2069
+ ],
2070
+ })
2071
+
2072
+
1935
2073
  def passing_show_json() -> str:
1936
2074
  return json.dumps({
1937
2075
  "file": "/tmp/design-ai/knowledge/PRINCIPLES.md",
@@ -2667,6 +2805,321 @@ def assert_search_human_output(raw: str, *, context: str, cmd: list[str]) -> Non
2667
2805
  )
2668
2806
 
2669
2807
 
2808
+ def is_ranked_search_positive_score(value: object) -> bool:
2809
+ return type(value) in (int, float) and value > 0
2810
+
2811
+
2812
+ def is_index_sha256_digest(value: object) -> bool:
2813
+ if not isinstance(value, str) or not value.startswith("sha256:"):
2814
+ return False
2815
+ digest = value[len("sha256:"):]
2816
+ return len(digest) == 64 and all(char in "0123456789abcdef" for char in digest)
2817
+
2818
+
2819
+ def assert_index_file_path(
2820
+ value: object,
2821
+ *,
2822
+ index_dir: str,
2823
+ label: str,
2824
+ context: str,
2825
+ command_label: str,
2826
+ ) -> None:
2827
+ expected = str(Path(index_dir) / EXPECTED_INDEX_FILE_BASENAMES[label])
2828
+ if value != expected:
2829
+ raise SystemExit(f"{command_label} after {context} {label} file differs from expected index path")
2830
+
2831
+
2832
+ def assert_ranked_search_json(
2833
+ raw: str,
2834
+ *,
2835
+ context: str,
2836
+ cmd: list[str],
2837
+ expected_notice: str | None = None,
2838
+ expected_backend: str | None = None,
2839
+ ) -> None:
2840
+ assert_no_ansi(raw, cmd)
2841
+ try:
2842
+ payload = json.loads(raw)
2843
+ except json.JSONDecodeError as error:
2844
+ raise SystemExit(f"failed to parse ranked search JSON after {context}") from error
2845
+
2846
+ expected_keys = ["query", "ranked", "notice", "hits"]
2847
+ if expected_backend is not None:
2848
+ expected_keys.append("backend")
2849
+ payload = assert_corpus_json_keys(
2850
+ payload,
2851
+ expected_keys,
2852
+ label="top-level",
2853
+ context=context,
2854
+ command_label="ranked search JSON",
2855
+ )
2856
+
2857
+ if payload.get("query") != EXPECTED_CORPUS_SEARCH_QUERY:
2858
+ raise SystemExit(f"ranked search JSON after {context} query differs from expected query")
2859
+ if payload.get("ranked") is not True:
2860
+ raise SystemExit(f"ranked search JSON after {context} ranked flag is not true")
2861
+ notice = payload.get("notice")
2862
+ if not isinstance(notice, str):
2863
+ raise SystemExit(f"ranked search JSON after {context} notice is not a string")
2864
+ if expected_notice is not None and notice != expected_notice:
2865
+ raise SystemExit(f"ranked search JSON after {context} notice differs from expected notice")
2866
+ if expected_backend is not None and payload.get("backend") != expected_backend:
2867
+ raise SystemExit(f"ranked search JSON after {context} backend differs from expected backend")
2868
+
2869
+ hits = payload.get("hits")
2870
+ if not isinstance(hits, list):
2871
+ raise SystemExit(f"ranked search JSON after {context} hits is not a list")
2872
+ if not hits:
2873
+ raise SystemExit(f"ranked search JSON after {context} does not contain any hits")
2874
+ if len(hits) > EXPECTED_RANKED_SEARCH_LIMIT:
2875
+ raise SystemExit(f"ranked search JSON after {context} hit count exceeds the expected limit")
2876
+
2877
+ query_token = EXPECTED_CORPUS_SEARCH_QUERY.lower()
2878
+ query_token_matched = False
2879
+ for hit in hits:
2880
+ hit = assert_corpus_json_keys(
2881
+ hit,
2882
+ ["relPath", "file", "score", "matchedTokens", "preview"],
2883
+ label="hit",
2884
+ context=context,
2885
+ command_label="ranked search JSON",
2886
+ )
2887
+ rel_path = hit.get("relPath")
2888
+ if not isinstance(rel_path, str) or not rel_path.startswith("knowledge/"):
2889
+ raise SystemExit(f"ranked search JSON after {context} hit relPath is outside the knowledge dir")
2890
+ assert_corpus_file_path(
2891
+ hit.get("file"),
2892
+ rel_path,
2893
+ label="hit",
2894
+ context=context,
2895
+ command_label="ranked search JSON",
2896
+ )
2897
+ if not is_ranked_search_positive_score(hit.get("score")):
2898
+ raise SystemExit(f"ranked search JSON after {context} hit score is not a positive number")
2899
+ matched_tokens = hit.get("matchedTokens")
2900
+ if not isinstance(matched_tokens, list) or not matched_tokens:
2901
+ raise SystemExit(f"ranked search JSON after {context} hit matchedTokens is empty")
2902
+ if not all(isinstance(token, str) and token for token in matched_tokens):
2903
+ raise SystemExit(f"ranked search JSON after {context} hit matchedTokens contains an invalid token")
2904
+ if not isinstance(hit.get("preview"), str) or not hit.get("preview"):
2905
+ raise SystemExit(f"ranked search JSON after {context} hit preview is empty")
2906
+ if query_token in matched_tokens:
2907
+ query_token_matched = True
2908
+
2909
+ if not query_token_matched:
2910
+ raise SystemExit(f"ranked search JSON after {context} is missing a hit matching the query token")
2911
+
2912
+
2913
+ def assert_ranked_search_determinism(
2914
+ first_raw: str,
2915
+ second_raw: str,
2916
+ *,
2917
+ context: str,
2918
+ cmd: list[str],
2919
+ ) -> None:
2920
+ assert_no_ansi(first_raw, cmd)
2921
+ assert_no_ansi(second_raw, cmd)
2922
+ if first_raw != second_raw:
2923
+ raise SystemExit(f"ranked search JSON after {context} differs between identical runs")
2924
+
2925
+
2926
+ def assert_index_build_json(raw: str, *, index_dir: str, context: str, cmd: list[str]) -> None:
2927
+ assert_no_ansi(raw, cmd)
2928
+ try:
2929
+ payload = json.loads(raw)
2930
+ except json.JSONDecodeError as error:
2931
+ raise SystemExit(f"failed to parse index build JSON after {context}") from error
2932
+
2933
+ payload = assert_corpus_json_keys(
2934
+ payload,
2935
+ ["action", "indexDir", "corpus", "learning"],
2936
+ label="top-level",
2937
+ context=context,
2938
+ command_label="index build JSON",
2939
+ )
2940
+
2941
+ if payload.get("action") != "build":
2942
+ raise SystemExit(f"index build JSON after {context} action differs from build")
2943
+ if payload.get("indexDir") != str(index_dir):
2944
+ raise SystemExit(f"index build JSON after {context} indexDir differs from expected directory")
2945
+
2946
+ for label in EXPECTED_INDEX_FILE_BASENAMES:
2947
+ section = assert_corpus_json_keys(
2948
+ payload.get(label),
2949
+ ["file", "documentCount", "digest"],
2950
+ label=label,
2951
+ context=context,
2952
+ command_label="index build JSON",
2953
+ )
2954
+ assert_index_file_path(
2955
+ section.get("file"),
2956
+ index_dir=index_dir,
2957
+ label=label,
2958
+ context=context,
2959
+ command_label="index build JSON",
2960
+ )
2961
+ if not is_corpus_json_positive_int(section.get("documentCount")):
2962
+ raise SystemExit(f"index build JSON after {context} {label} documentCount is not a positive integer")
2963
+ if not is_index_sha256_digest(section.get("digest")):
2964
+ raise SystemExit(f"index build JSON after {context} {label} digest is not a sha256 digest")
2965
+
2966
+
2967
+ def assert_index_status_json(
2968
+ raw: str,
2969
+ *,
2970
+ index_dir: str,
2971
+ context: str,
2972
+ cmd: list[str],
2973
+ expect_embeddings: bool = False,
2974
+ ) -> None:
2975
+ assert_no_ansi(raw, cmd)
2976
+ try:
2977
+ payload = json.loads(raw)
2978
+ except json.JSONDecodeError as error:
2979
+ raise SystemExit(f"failed to parse index status JSON after {context}") from error
2980
+
2981
+ payload = assert_corpus_json_keys(
2982
+ payload,
2983
+ ["action", "indexDir", "corpus", "learning", "embeddings", "fresh", "buildCommand"],
2984
+ label="top-level",
2985
+ context=context,
2986
+ command_label="index status JSON",
2987
+ )
2988
+
2989
+ if payload.get("action") != "status":
2990
+ raise SystemExit(f"index status JSON after {context} action differs from status")
2991
+ if payload.get("indexDir") != str(index_dir):
2992
+ raise SystemExit(f"index status JSON after {context} indexDir differs from expected directory")
2993
+ if payload.get("fresh") is not True:
2994
+ raise SystemExit(f"index status JSON after {context} is not fresh")
2995
+ if payload.get("buildCommand") != EXPECTED_INDEX_BUILD_COMMAND:
2996
+ raise SystemExit(f"index status JSON after {context} buildCommand differs from expected command")
2997
+
2998
+ for label in EXPECTED_INDEX_FILE_BASENAMES:
2999
+ section = assert_corpus_json_keys(
3000
+ payload.get(label),
3001
+ EXPECTED_INDEX_STATUS_SECTION_KEYS,
3002
+ label=label,
3003
+ context=context,
3004
+ command_label="index status JSON",
3005
+ )
3006
+ assert_index_file_path(
3007
+ section.get("file"),
3008
+ index_dir=index_dir,
3009
+ label=label,
3010
+ context=context,
3011
+ command_label="index status JSON",
3012
+ )
3013
+ if section.get("present") is not True:
3014
+ raise SystemExit(f"index status JSON after {context} {label} index is not present")
3015
+ if section.get("fresh") is not True:
3016
+ raise SystemExit(f"index status JSON after {context} {label} index is not fresh")
3017
+ generated_at = section.get("generatedAt")
3018
+ if not isinstance(generated_at, str) or not generated_at:
3019
+ raise SystemExit(f"index status JSON after {context} {label} generatedAt is missing")
3020
+ if not is_corpus_json_positive_int(section.get("documentCount")):
3021
+ raise SystemExit(f"index status JSON after {context} {label} documentCount is not a positive integer")
3022
+ if not is_index_sha256_digest(section.get("storedDigest")):
3023
+ raise SystemExit(f"index status JSON after {context} {label} storedDigest is not a sha256 digest")
3024
+ if section.get("storedDigest") != section.get("currentDigest"):
3025
+ raise SystemExit(f"index status JSON after {context} {label} stored digest differs from current digest")
3026
+ if section.get("error") != "":
3027
+ raise SystemExit(f"index status JSON after {context} {label} error is not empty")
3028
+
3029
+ embeddings = payload.get("embeddings")
3030
+ if not expect_embeddings:
3031
+ if embeddings is not None:
3032
+ raise SystemExit(f"index status JSON after {context} embeddings is not null when embeddings were not built")
3033
+ return
3034
+
3035
+ embeddings = assert_corpus_json_keys(
3036
+ embeddings,
3037
+ EXPECTED_INDEX_STATUS_EMBEDDINGS_KEYS,
3038
+ label="embeddings",
3039
+ context=context,
3040
+ command_label="index status JSON",
3041
+ )
3042
+ expected_embeddings_file = str(Path(index_dir) / EXPECTED_EMBEDDING_INDEX_FILE_BASENAMES["embedding-index"])
3043
+ if embeddings.get("file") != expected_embeddings_file:
3044
+ raise SystemExit(f"index status JSON after {context} embeddings file differs from expected index path")
3045
+ if embeddings.get("present") is not True:
3046
+ raise SystemExit(f"index status JSON after {context} embeddings index is not present")
3047
+ if embeddings.get("fresh") is not True:
3048
+ raise SystemExit(f"index status JSON after {context} embeddings index is not fresh")
3049
+ if not is_corpus_json_positive_int(embeddings.get("documentCount")):
3050
+ raise SystemExit(f"index status JSON after {context} embeddings documentCount is not a positive integer")
3051
+ provider = embeddings.get("provider")
3052
+ if not isinstance(provider, dict) or not provider.get("command"):
3053
+ raise SystemExit(f"index status JSON after {context} embeddings provider is missing a command")
3054
+ if embeddings.get("error") != "":
3055
+ raise SystemExit(f"index status JSON after {context} embeddings error is not empty")
3056
+
3057
+
3058
+ def assert_index_verify_json(
3059
+ raw: str,
3060
+ *,
3061
+ index_dir: str,
3062
+ context: str,
3063
+ cmd: list[str],
3064
+ expect_embeddings_check: bool = False,
3065
+ ) -> None:
3066
+ assert_no_ansi(raw, cmd)
3067
+ try:
3068
+ payload = json.loads(raw)
3069
+ except json.JSONDecodeError as error:
3070
+ raise SystemExit(f"failed to parse index verify JSON after {context}") from error
3071
+
3072
+ payload = assert_corpus_json_keys(
3073
+ payload,
3074
+ ["action", "ok", "checks"],
3075
+ label="top-level",
3076
+ context=context,
3077
+ command_label="index verify JSON",
3078
+ )
3079
+
3080
+ if payload.get("action") != "verify":
3081
+ raise SystemExit(f"index verify JSON after {context} action differs from verify")
3082
+ if payload.get("ok") is not True:
3083
+ raise SystemExit(f"index verify JSON after {context} ok is not true")
3084
+
3085
+ checks = payload.get("checks")
3086
+ if not isinstance(checks, list):
3087
+ raise SystemExit(f"index verify JSON after {context} checks is not a list")
3088
+ expected_names = list(EXPECTED_INDEX_FILE_BASENAMES) + (["embeddings"] if expect_embeddings_check else [])
3089
+ if [check.get("name") for check in checks if isinstance(check, dict)] != expected_names:
3090
+ raise SystemExit(f"index verify JSON after {context} check names changed")
3091
+
3092
+ for check in checks:
3093
+ name = check.get("name") if isinstance(check, dict) else None
3094
+ if name == "embeddings":
3095
+ if check.get("matches") is not True:
3096
+ raise SystemExit(f"index verify JSON after {context} embeddings check does not match")
3097
+ if not isinstance(check.get("file"), str) or not check.get("file"):
3098
+ raise SystemExit(f"index verify JSON after {context} embeddings check file is missing")
3099
+ if not isinstance(check.get("reason"), str):
3100
+ raise SystemExit(f"index verify JSON after {context} embeddings check reason is not a string")
3101
+ continue
3102
+
3103
+ check = assert_corpus_json_keys(
3104
+ check,
3105
+ ["name", "file", "matches", "reason"],
3106
+ label="check",
3107
+ context=context,
3108
+ command_label="index verify JSON",
3109
+ )
3110
+ assert_index_file_path(
3111
+ check.get("file"),
3112
+ index_dir=index_dir,
3113
+ label=name,
3114
+ context=context,
3115
+ command_label="index verify JSON",
3116
+ )
3117
+ if check.get("matches") is not True:
3118
+ raise SystemExit(f"index verify JSON after {context} {name} check does not match")
3119
+ if check.get("reason") != "":
3120
+ raise SystemExit(f"index verify JSON after {context} {name} check reason is not empty")
3121
+
3122
+
2670
3123
  def assert_show_json_line(raw: str, *, context: str, cmd: list[str]) -> None:
2671
3124
  assert_no_ansi(raw, cmd)
2672
3125
  try:
@@ -4213,18 +4666,21 @@ def passing_main_help_output() -> str:
4213
4666
  " design-ai help [command|--json]",
4214
4667
  "",
4215
4668
  " install Symlink design-ai into Claude Code (~/.claude)",
4216
- " search <query> [--dir kind] [--limit N] [--json] Search the local markdown corpus",
4669
+ " search <query> [--dir kind] [--limit N] [--ranked] [--embeddings [--provider \"cmd args\"]] [--json]",
4670
+ " Search the local markdown corpus",
4671
+ " index [--build|--status|--verify] [--json] [--embeddings [--provider \"cmd args\"]]",
4672
+ " Build, inspect, and verify the local retrieval index",
4217
4673
  " show <file[:line]> [--lines N:M] [--context N] [--json] Print a corpus file or line range",
4218
4674
  " route <brief|--from-file file|--stdin|--list|--eval-template|--eval> [--limit N]",
4219
4675
  " Recommend commands, skills, knowledge, and route eval checkpoints",
4220
- " prompt <brief|--from-file file|--stdin|--eval-template|--eval> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--out file]",
4676
+ " prompt <brief|--from-file file|--stdin|--eval-template|--eval> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--with-recall] [--recall-limit N] [--out file]",
4221
4677
  " Generate a ready-to-use agent prompt and prompt-plan eval checkpoints",
4222
- " pack <brief|--from-file file|--stdin|--eval-template|--eval> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--max-bytes N]",
4678
+ " pack <brief|--from-file file|--stdin|--eval-template|--eval> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--with-recall] [--recall-limit N] [--max-bytes N]",
4223
4679
  " Generate prompt plus bounded context and prompt-pack eval checkpoints",
4224
4680
  " check <artifact.md|--stdin|--examples> [--route id|--all-routes] [--learn]",
4225
4681
  " Check generated Markdown artifact quality; add --issues-only or --learn",
4226
4682
  " examples [query] [--route id] [--limit N] [--json] Find worked examples for a route or query",
4227
- " learn [--init|--remember text|--feedback text|--list|--export|--query text|--explain|--backup|--redact|--verify|--diff|--restore|--restore-backups [--prune]|--import|--audit [--fix]|--curate|--stats|--usage|--signals [--strict]|--agent-backlog [--strict]|--propose-skills [--min-evidence N] [--review-file path] [--review-check|--apply-plan] [--strict]|--eval-template|--eval [--strict]|--forget id|--clear] [--json|--report|--patch|--review-template] [--out file]",
4683
+ " learn [--init|--remember text|--feedback text|--list|--export|--query text|--explain|--recall query|--backup|--redact|--verify|--diff|--restore|--restore-backups [--prune]|--import|--audit [--fix]|--curate|--stats|--usage|--signals [--strict]|--agent-backlog [--strict]|--propose-skills [--min-evidence N] [--review-file path] [--review-check|--apply-plan] [--strict]|--eval-template|--eval [--strict]|--forget id|--clear] [--json|--report|--patch|--review-template] [--out file]",
4228
4684
  " Manage local learning preferences, usage reports, signal registry, agent backlog, skill proposals, and eval checkpoints for prompt personalization",
4229
4685
  " workspace [--root path] [--learning-file path] [--learning-usage path] [--learning-eval path] [--strict] [--json]",
4230
4686
  " Show read-only local dogfood readiness: git, repository, learning usage, eval checkpoints, and release scripts",
@@ -4326,6 +4782,7 @@ def passing_workspace_json() -> str:
4326
4782
  "learningUsage": None,
4327
4783
  "learningEval": None,
4328
4784
  "learningRestoreBackups": None,
4785
+ "retrievalIndex": None,
4329
4786
  "release": {
4330
4787
  "packageName": "@design-ai/cli",
4331
4788
  "version": EXPECTED_RELEASE_VERSION,
@@ -6409,6 +6866,44 @@ def assert_workspace_json(raw: str, *, context: str, cmd: list[str]) -> None:
6409
6866
  if privacy.get("mutatesProfile") is not False:
6410
6867
  raise SystemExit(f"workspace JSON after {context} learningRestoreBackups mutation privacy flag changed")
6411
6868
 
6869
+ retrieval_index = payload.get("retrievalIndex")
6870
+ if retrieval_index is not None:
6871
+ retrieval_index = assert_smoke_json_keys(
6872
+ retrieval_index,
6873
+ EXPECTED_WORKSPACE_RETRIEVAL_INDEX_KEYS,
6874
+ label="retrievalIndex",
6875
+ context=context,
6876
+ command_label="workspace JSON",
6877
+ )
6878
+ if not isinstance(retrieval_index.get("indexDir"), str) or not retrieval_index["indexDir"]:
6879
+ raise SystemExit(f"workspace JSON after {context} retrievalIndex indexDir is missing")
6880
+ if type(retrieval_index.get("fresh")) is not bool:
6881
+ raise SystemExit(f"workspace JSON after {context} retrievalIndex fresh is not boolean")
6882
+ if retrieval_index.get("status") not in ("pass", "warn"):
6883
+ raise SystemExit(f"workspace JSON after {context} retrievalIndex status is invalid")
6884
+ if (retrieval_index.get("status") == "pass") is not retrieval_index.get("fresh"):
6885
+ raise SystemExit(f"workspace JSON after {context} retrievalIndex status does not match freshness")
6886
+ if retrieval_index.get("buildCommand") != EXPECTED_INDEX_BUILD_COMMAND:
6887
+ raise SystemExit(f"workspace JSON after {context} retrievalIndex buildCommand differs from expected command")
6888
+ for section_label in EXPECTED_INDEX_FILE_BASENAMES:
6889
+ section = assert_smoke_json_keys(
6890
+ retrieval_index.get(section_label),
6891
+ EXPECTED_INDEX_STATUS_SECTION_KEYS,
6892
+ label=f"retrievalIndex {section_label}",
6893
+ context=context,
6894
+ command_label="workspace JSON",
6895
+ )
6896
+ if not isinstance(section.get("file"), str) or not section["file"]:
6897
+ raise SystemExit(f"workspace JSON after {context} retrievalIndex {section_label} file is missing")
6898
+ for key in ("present", "fresh"):
6899
+ if type(section.get(key)) is not bool:
6900
+ raise SystemExit(f"workspace JSON after {context} retrievalIndex {section_label} {key} is not boolean")
6901
+ if not is_lifecycle_json_non_negative_int(section.get("documentCount")):
6902
+ raise SystemExit(f"workspace JSON after {context} retrievalIndex {section_label} documentCount is invalid")
6903
+ for key in ("generatedAt", "storedDigest", "currentDigest", "error"):
6904
+ if not isinstance(section.get(key), str):
6905
+ raise SystemExit(f"workspace JSON after {context} retrievalIndex {section_label} {key} is not a string")
6906
+
6412
6907
  release = assert_smoke_json_keys(
6413
6908
  payload.get("release"),
6414
6909
  EXPECTED_WORKSPACE_RELEASE_KEYS,
@@ -9149,6 +9644,364 @@ def run_self_test() -> None:
9149
9644
  scope="smoke assertions",
9150
9645
  )
9151
9646
 
9647
+ ranked_search_cmd = [
9648
+ "design-ai",
9649
+ "search",
9650
+ EXPECTED_CORPUS_SEARCH_QUERY,
9651
+ "--dir",
9652
+ "knowledge",
9653
+ "--limit",
9654
+ str(EXPECTED_RANKED_SEARCH_LIMIT),
9655
+ "--ranked",
9656
+ "--json",
9657
+ ]
9658
+ assert_ranked_search_json(passing_ranked_search_json(), context=context, cmd=ranked_search_cmd)
9659
+ assert_ranked_search_json(
9660
+ passing_ranked_search_json(),
9661
+ context=context,
9662
+ cmd=ranked_search_cmd,
9663
+ expected_notice=EXPECTED_RANKED_SEARCH_NOT_BUILT_NOTICE,
9664
+ )
9665
+ expect_self_test_failure(
9666
+ lambda: assert_ranked_search_json("{", context=context, cmd=ranked_search_cmd),
9667
+ expected="failed to parse ranked search JSON",
9668
+ scope="smoke assertions",
9669
+ )
9670
+ assert_ranked_search_json(
9671
+ passing_ranked_search_json(backend="embeddings"),
9672
+ context=context,
9673
+ cmd=ranked_search_cmd,
9674
+ expected_backend="embeddings",
9675
+ )
9676
+ ranked_wrong_backend = json.loads(passing_ranked_search_json(backend="embeddings"))
9677
+ ranked_wrong_backend["backend"] = "lexical"
9678
+ expect_self_test_failure(
9679
+ lambda: assert_ranked_search_json(
9680
+ json.dumps(ranked_wrong_backend), context=context, cmd=ranked_search_cmd, expected_backend="embeddings"
9681
+ ),
9682
+ expected="backend differs from expected backend",
9683
+ scope="smoke assertions",
9684
+ )
9685
+ ranked_wrong_query = json.loads(passing_ranked_search_json())
9686
+ ranked_wrong_query["query"] = "Inter"
9687
+ expect_self_test_failure(
9688
+ lambda: assert_ranked_search_json(json.dumps(ranked_wrong_query), context=context, cmd=ranked_search_cmd),
9689
+ expected="query differs",
9690
+ scope="smoke assertions",
9691
+ )
9692
+ ranked_flag_false = json.loads(passing_ranked_search_json())
9693
+ ranked_flag_false["ranked"] = False
9694
+ expect_self_test_failure(
9695
+ lambda: assert_ranked_search_json(json.dumps(ranked_flag_false), context=context, cmd=ranked_search_cmd),
9696
+ expected="ranked flag is not true",
9697
+ scope="smoke assertions",
9698
+ )
9699
+ ranked_wrong_notice = json.loads(passing_ranked_search_json())
9700
+ ranked_wrong_notice["notice"] = "corpus index is stale"
9701
+ expect_self_test_failure(
9702
+ lambda: assert_ranked_search_json(
9703
+ json.dumps(ranked_wrong_notice),
9704
+ context=context,
9705
+ cmd=ranked_search_cmd,
9706
+ expected_notice=EXPECTED_RANKED_SEARCH_NOT_BUILT_NOTICE,
9707
+ ),
9708
+ expected="notice differs",
9709
+ scope="smoke assertions",
9710
+ )
9711
+ ranked_no_hits = json.loads(passing_ranked_search_json())
9712
+ ranked_no_hits["hits"] = []
9713
+ expect_self_test_failure(
9714
+ lambda: assert_ranked_search_json(json.dumps(ranked_no_hits), context=context, cmd=ranked_search_cmd),
9715
+ expected="does not contain any hits",
9716
+ scope="smoke assertions",
9717
+ )
9718
+ ranked_extra_hits = json.loads(passing_ranked_search_json())
9719
+ while len(ranked_extra_hits["hits"]) <= EXPECTED_RANKED_SEARCH_LIMIT:
9720
+ ranked_extra_hits["hits"].append(dict(ranked_extra_hits["hits"][0]))
9721
+ expect_self_test_failure(
9722
+ lambda: assert_ranked_search_json(json.dumps(ranked_extra_hits), context=context, cmd=ranked_search_cmd),
9723
+ expected="hit count exceeds",
9724
+ scope="smoke assertions",
9725
+ )
9726
+ ranked_zero_score = json.loads(passing_ranked_search_json())
9727
+ ranked_zero_score["hits"][0]["score"] = 0
9728
+ expect_self_test_failure(
9729
+ lambda: assert_ranked_search_json(json.dumps(ranked_zero_score), context=context, cmd=ranked_search_cmd),
9730
+ expected="score is not a positive number",
9731
+ scope="smoke assertions",
9732
+ )
9733
+ ranked_bool_score = json.loads(passing_ranked_search_json())
9734
+ ranked_bool_score["hits"][0]["score"] = True
9735
+ expect_self_test_failure(
9736
+ lambda: assert_ranked_search_json(json.dumps(ranked_bool_score), context=context, cmd=ranked_search_cmd),
9737
+ expected="score is not a positive number",
9738
+ scope="smoke assertions",
9739
+ )
9740
+ ranked_empty_tokens = json.loads(passing_ranked_search_json())
9741
+ ranked_empty_tokens["hits"][0]["matchedTokens"] = []
9742
+ expect_self_test_failure(
9743
+ lambda: assert_ranked_search_json(json.dumps(ranked_empty_tokens), context=context, cmd=ranked_search_cmd),
9744
+ expected="matchedTokens is empty",
9745
+ scope="smoke assertions",
9746
+ )
9747
+ ranked_unmatched_tokens = json.loads(passing_ranked_search_json())
9748
+ ranked_unmatched_tokens["hits"][0]["matchedTokens"] = ["inter"]
9749
+ expect_self_test_failure(
9750
+ lambda: assert_ranked_search_json(json.dumps(ranked_unmatched_tokens), context=context, cmd=ranked_search_cmd),
9751
+ expected="missing a hit matching the query token",
9752
+ scope="smoke assertions",
9753
+ )
9754
+ ranked_missing_hit_key = json.loads(passing_ranked_search_json())
9755
+ del ranked_missing_hit_key["hits"][0]["preview"]
9756
+ expect_self_test_failure(
9757
+ lambda: assert_ranked_search_json(json.dumps(ranked_missing_hit_key), context=context, cmd=ranked_search_cmd),
9758
+ expected="hit keys changed",
9759
+ scope="smoke assertions",
9760
+ )
9761
+ ranked_relative_file = json.loads(passing_ranked_search_json())
9762
+ ranked_relative_file["hits"][0]["file"] = EXPECTED_CORPUS_SEARCH_HIT
9763
+ expect_self_test_failure(
9764
+ lambda: assert_ranked_search_json(json.dumps(ranked_relative_file), context=context, cmd=ranked_search_cmd),
9765
+ expected="file is not absolute",
9766
+ scope="smoke assertions",
9767
+ )
9768
+ expect_self_test_failure(
9769
+ lambda: assert_ranked_search_json("\x1b[31m{}", context=context, cmd=ranked_search_cmd),
9770
+ expected="ANSI escape",
9771
+ scope="smoke assertions",
9772
+ )
9773
+ assert_ranked_search_determinism(
9774
+ passing_ranked_search_json(),
9775
+ passing_ranked_search_json(),
9776
+ context=context,
9777
+ cmd=ranked_search_cmd,
9778
+ )
9779
+ expect_self_test_failure(
9780
+ lambda: assert_ranked_search_determinism(
9781
+ passing_ranked_search_json(),
9782
+ passing_ranked_search_json() + "\n",
9783
+ context=context,
9784
+ cmd=ranked_search_cmd,
9785
+ ),
9786
+ expected="differs between identical runs",
9787
+ scope="smoke assertions",
9788
+ )
9789
+ expect_self_test_failure(
9790
+ lambda: assert_ranked_search_determinism(
9791
+ "\x1b[31m{}",
9792
+ "\x1b[31m{}",
9793
+ context=context,
9794
+ cmd=ranked_search_cmd,
9795
+ ),
9796
+ expected="ANSI escape",
9797
+ scope="smoke assertions",
9798
+ )
9799
+
9800
+ index_dir = "/tmp/design-ai-index"
9801
+ index_build_cmd = ["design-ai", "index", "--build", "--json"]
9802
+ assert_index_build_json(passing_index_build_json(index_dir), index_dir=index_dir, context=context, cmd=index_build_cmd)
9803
+ expect_self_test_failure(
9804
+ lambda: assert_index_build_json("{", index_dir=index_dir, context=context, cmd=index_build_cmd),
9805
+ expected="failed to parse index build JSON",
9806
+ scope="smoke assertions",
9807
+ )
9808
+ index_build_wrong_action = json.loads(passing_index_build_json(index_dir))
9809
+ index_build_wrong_action["action"] = "status"
9810
+ expect_self_test_failure(
9811
+ lambda: assert_index_build_json(
9812
+ json.dumps(index_build_wrong_action), index_dir=index_dir, context=context, cmd=index_build_cmd
9813
+ ),
9814
+ expected="action differs from build",
9815
+ scope="smoke assertions",
9816
+ )
9817
+ expect_self_test_failure(
9818
+ lambda: assert_index_build_json(
9819
+ passing_index_build_json("/tmp/other-index"), index_dir=index_dir, context=context, cmd=index_build_cmd
9820
+ ),
9821
+ expected="indexDir differs",
9822
+ scope="smoke assertions",
9823
+ )
9824
+ index_build_zero_documents = json.loads(passing_index_build_json(index_dir))
9825
+ index_build_zero_documents["corpus"]["documentCount"] = 0
9826
+ expect_self_test_failure(
9827
+ lambda: assert_index_build_json(
9828
+ json.dumps(index_build_zero_documents), index_dir=index_dir, context=context, cmd=index_build_cmd
9829
+ ),
9830
+ expected="documentCount is not a positive integer",
9831
+ scope="smoke assertions",
9832
+ )
9833
+ index_build_bad_digest = json.loads(passing_index_build_json(index_dir))
9834
+ index_build_bad_digest["learning"]["digest"] = "md5:abc"
9835
+ expect_self_test_failure(
9836
+ lambda: assert_index_build_json(
9837
+ json.dumps(index_build_bad_digest), index_dir=index_dir, context=context, cmd=index_build_cmd
9838
+ ),
9839
+ expected="digest is not a sha256 digest",
9840
+ scope="smoke assertions",
9841
+ )
9842
+ index_build_missing_key = json.loads(passing_index_build_json(index_dir))
9843
+ del index_build_missing_key["corpus"]["file"]
9844
+ expect_self_test_failure(
9845
+ lambda: assert_index_build_json(
9846
+ json.dumps(index_build_missing_key), index_dir=index_dir, context=context, cmd=index_build_cmd
9847
+ ),
9848
+ expected="corpus keys changed",
9849
+ scope="smoke assertions",
9850
+ )
9851
+ expect_self_test_failure(
9852
+ lambda: assert_index_build_json("\x1b[31m{}", index_dir=index_dir, context=context, cmd=index_build_cmd),
9853
+ expected="ANSI escape",
9854
+ scope="smoke assertions",
9855
+ )
9856
+
9857
+ index_status_cmd = ["design-ai", "index", "--status", "--json"]
9858
+ assert_index_status_json(passing_index_status_json(index_dir), index_dir=index_dir, context=context, cmd=index_status_cmd)
9859
+ expect_self_test_failure(
9860
+ lambda: assert_index_status_json("{", index_dir=index_dir, context=context, cmd=index_status_cmd),
9861
+ expected="failed to parse index status JSON",
9862
+ scope="smoke assertions",
9863
+ )
9864
+ index_status_stale = json.loads(passing_index_status_json(index_dir))
9865
+ index_status_stale["fresh"] = False
9866
+ expect_self_test_failure(
9867
+ lambda: assert_index_status_json(
9868
+ json.dumps(index_status_stale), index_dir=index_dir, context=context, cmd=index_status_cmd
9869
+ ),
9870
+ expected="is not fresh",
9871
+ scope="smoke assertions",
9872
+ )
9873
+ index_status_wrong_command = json.loads(passing_index_status_json(index_dir))
9874
+ index_status_wrong_command["buildCommand"] = "design-ai rebuild"
9875
+ expect_self_test_failure(
9876
+ lambda: assert_index_status_json(
9877
+ json.dumps(index_status_wrong_command), index_dir=index_dir, context=context, cmd=index_status_cmd
9878
+ ),
9879
+ expected="buildCommand differs",
9880
+ scope="smoke assertions",
9881
+ )
9882
+ index_status_missing_section = json.loads(passing_index_status_json(index_dir))
9883
+ index_status_missing_section["corpus"]["present"] = False
9884
+ expect_self_test_failure(
9885
+ lambda: assert_index_status_json(
9886
+ json.dumps(index_status_missing_section), index_dir=index_dir, context=context, cmd=index_status_cmd
9887
+ ),
9888
+ expected="corpus index is not present",
9889
+ scope="smoke assertions",
9890
+ )
9891
+ index_status_stale_section = json.loads(passing_index_status_json(index_dir))
9892
+ index_status_stale_section["learning"]["fresh"] = False
9893
+ expect_self_test_failure(
9894
+ lambda: assert_index_status_json(
9895
+ json.dumps(index_status_stale_section), index_dir=index_dir, context=context, cmd=index_status_cmd
9896
+ ),
9897
+ expected="learning index is not fresh",
9898
+ scope="smoke assertions",
9899
+ )
9900
+ index_status_digest_drift = json.loads(passing_index_status_json(index_dir))
9901
+ index_status_digest_drift["corpus"]["currentDigest"] = f"sha256:{'2' * 64}"
9902
+ expect_self_test_failure(
9903
+ lambda: assert_index_status_json(
9904
+ json.dumps(index_status_digest_drift), index_dir=index_dir, context=context, cmd=index_status_cmd
9905
+ ),
9906
+ expected="stored digest differs from current digest",
9907
+ scope="smoke assertions",
9908
+ )
9909
+ index_status_error = json.loads(passing_index_status_json(index_dir))
9910
+ index_status_error["learning"]["error"] = "unreadable"
9911
+ expect_self_test_failure(
9912
+ lambda: assert_index_status_json(
9913
+ json.dumps(index_status_error), index_dir=index_dir, context=context, cmd=index_status_cmd
9914
+ ),
9915
+ expected="error is not empty",
9916
+ scope="smoke assertions",
9917
+ )
9918
+ index_status_embeddings_present_unexpectedly = json.loads(passing_index_status_json(index_dir, with_embeddings=True))
9919
+ expect_self_test_failure(
9920
+ lambda: assert_index_status_json(
9921
+ json.dumps(index_status_embeddings_present_unexpectedly), index_dir=index_dir, context=context, cmd=index_status_cmd
9922
+ ),
9923
+ expected="embeddings is not null when embeddings were not built",
9924
+ scope="smoke assertions",
9925
+ )
9926
+
9927
+ assert_index_status_json(
9928
+ passing_index_status_json(index_dir, with_embeddings=True),
9929
+ index_dir=index_dir,
9930
+ context=context,
9931
+ cmd=index_status_cmd,
9932
+ expect_embeddings=True,
9933
+ )
9934
+ index_status_embeddings_stale = json.loads(passing_index_status_json(index_dir, with_embeddings=True))
9935
+ index_status_embeddings_stale["embeddings"]["fresh"] = False
9936
+ expect_self_test_failure(
9937
+ lambda: assert_index_status_json(
9938
+ json.dumps(index_status_embeddings_stale),
9939
+ index_dir=index_dir,
9940
+ context=context,
9941
+ cmd=index_status_cmd,
9942
+ expect_embeddings=True,
9943
+ ),
9944
+ expected="embeddings index is not fresh",
9945
+ scope="smoke assertions",
9946
+ )
9947
+ index_status_embeddings_no_provider_command = json.loads(passing_index_status_json(index_dir, with_embeddings=True))
9948
+ index_status_embeddings_no_provider_command["embeddings"]["provider"] = {"command": "", "modelLabel": "", "dimensions": 3}
9949
+ expect_self_test_failure(
9950
+ lambda: assert_index_status_json(
9951
+ json.dumps(index_status_embeddings_no_provider_command),
9952
+ index_dir=index_dir,
9953
+ context=context,
9954
+ cmd=index_status_cmd,
9955
+ expect_embeddings=True,
9956
+ ),
9957
+ expected="embeddings provider is missing a command",
9958
+ scope="smoke assertions",
9959
+ )
9960
+
9961
+ index_verify_cmd = ["design-ai", "index", "--verify", "--json"]
9962
+ assert_index_verify_json(passing_index_verify_json(index_dir), index_dir=index_dir, context=context, cmd=index_verify_cmd)
9963
+ expect_self_test_failure(
9964
+ lambda: assert_index_verify_json("{", index_dir=index_dir, context=context, cmd=index_verify_cmd),
9965
+ expected="failed to parse index verify JSON",
9966
+ scope="smoke assertions",
9967
+ )
9968
+ index_verify_not_ok = json.loads(passing_index_verify_json(index_dir))
9969
+ index_verify_not_ok["ok"] = False
9970
+ expect_self_test_failure(
9971
+ lambda: assert_index_verify_json(
9972
+ json.dumps(index_verify_not_ok), index_dir=index_dir, context=context, cmd=index_verify_cmd
9973
+ ),
9974
+ expected="ok is not true",
9975
+ scope="smoke assertions",
9976
+ )
9977
+ index_verify_wrong_names = json.loads(passing_index_verify_json(index_dir))
9978
+ index_verify_wrong_names["checks"] = index_verify_wrong_names["checks"][:1]
9979
+ expect_self_test_failure(
9980
+ lambda: assert_index_verify_json(
9981
+ json.dumps(index_verify_wrong_names), index_dir=index_dir, context=context, cmd=index_verify_cmd
9982
+ ),
9983
+ expected="check names changed",
9984
+ scope="smoke assertions",
9985
+ )
9986
+ index_verify_mismatch = json.loads(passing_index_verify_json(index_dir))
9987
+ index_verify_mismatch["checks"][1]["matches"] = False
9988
+ expect_self_test_failure(
9989
+ lambda: assert_index_verify_json(
9990
+ json.dumps(index_verify_mismatch), index_dir=index_dir, context=context, cmd=index_verify_cmd
9991
+ ),
9992
+ expected="learning check does not match",
9993
+ scope="smoke assertions",
9994
+ )
9995
+ index_verify_reason = json.loads(passing_index_verify_json(index_dir))
9996
+ index_verify_reason["checks"][0]["reason"] = "digest drift"
9997
+ expect_self_test_failure(
9998
+ lambda: assert_index_verify_json(
9999
+ json.dumps(index_verify_reason), index_dir=index_dir, context=context, cmd=index_verify_cmd
10000
+ ),
10001
+ expected="corpus check reason is not empty",
10002
+ scope="smoke assertions",
10003
+ )
10004
+
9152
10005
  show_cmd = ["design-ai", "show", EXPECTED_CORPUS_SHOW_TARGET, "--context", "0", "--json"]
9153
10006
  assert_show_json_line(passing_show_json(), context=context, cmd=show_cmd)
9154
10007
  expect_self_test_failure(
@@ -11012,6 +11865,54 @@ def run_self_test() -> None:
11012
11865
  expected="privacy flags changed",
11013
11866
  scope="smoke assertions",
11014
11867
  )
11868
+ workspace_retrieval_index_payload = json.loads(passing_workspace_json())
11869
+ workspace_retrieval_index_payload["retrievalIndex"] = {
11870
+ "indexDir": "/tmp/design-ai-index",
11871
+ "corpus": json.loads(passing_index_status_json())["corpus"],
11872
+ "learning": json.loads(passing_index_status_json())["learning"],
11873
+ "fresh": True,
11874
+ "status": "pass",
11875
+ "buildCommand": EXPECTED_INDEX_BUILD_COMMAND,
11876
+ }
11877
+ assert_workspace_json(json.dumps(workspace_retrieval_index_payload), context=context, cmd=workspace_cmd)
11878
+ stale_retrieval_index_payload = json.loads(json.dumps(workspace_retrieval_index_payload))
11879
+ stale_retrieval_index_payload["retrievalIndex"]["fresh"] = False
11880
+ stale_retrieval_index_payload["retrievalIndex"]["status"] = "warn"
11881
+ stale_retrieval_index_payload["retrievalIndex"]["corpus"]["fresh"] = False
11882
+ assert_workspace_json(json.dumps(stale_retrieval_index_payload), context=context, cmd=workspace_cmd)
11883
+ mismatched_retrieval_index_payload = json.loads(json.dumps(workspace_retrieval_index_payload))
11884
+ mismatched_retrieval_index_payload["retrievalIndex"]["status"] = "warn"
11885
+ expect_self_test_failure(
11886
+ lambda: assert_workspace_json(
11887
+ json.dumps(mismatched_retrieval_index_payload),
11888
+ context=context,
11889
+ cmd=workspace_cmd,
11890
+ ),
11891
+ expected="retrievalIndex status does not match freshness",
11892
+ scope="smoke assertions",
11893
+ )
11894
+ wrong_build_command_retrieval_index_payload = json.loads(json.dumps(workspace_retrieval_index_payload))
11895
+ wrong_build_command_retrieval_index_payload["retrievalIndex"]["buildCommand"] = "design-ai rebuild"
11896
+ expect_self_test_failure(
11897
+ lambda: assert_workspace_json(
11898
+ json.dumps(wrong_build_command_retrieval_index_payload),
11899
+ context=context,
11900
+ cmd=workspace_cmd,
11901
+ ),
11902
+ expected="retrievalIndex buildCommand differs",
11903
+ scope="smoke assertions",
11904
+ )
11905
+ missing_retrieval_index_key_payload = json.loads(json.dumps(workspace_retrieval_index_payload))
11906
+ del missing_retrieval_index_key_payload["retrievalIndex"]["learning"]["documentCount"]
11907
+ expect_self_test_failure(
11908
+ lambda: assert_workspace_json(
11909
+ json.dumps(missing_retrieval_index_key_payload),
11910
+ context=context,
11911
+ cmd=workspace_cmd,
11912
+ ),
11913
+ expected="retrievalIndex learning keys changed",
11914
+ scope="smoke assertions",
11915
+ )
11015
11916
  assert_workspace_strict_failure_json(
11016
11917
  passing_workspace_json(),
11017
11918
  returncode=1,
@@ -11068,6 +11969,7 @@ def run_self_test() -> None:
11068
11969
  "learningUsage": reordered_workspace_payload["learningUsage"],
11069
11970
  "learningEval": reordered_workspace_payload["learningEval"],
11070
11971
  "learningRestoreBackups": reordered_workspace_payload["learningRestoreBackups"],
11972
+ "retrievalIndex": reordered_workspace_payload["retrievalIndex"],
11071
11973
  "release": reordered_workspace_payload["release"],
11072
11974
  "nextActions": reordered_workspace_payload["nextActions"],
11073
11975
  }