@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
@@ -0,0 +1,137 @@
1
+ // Shared deterministic lexical (BM25-style) scoring for corpus and learning retrieval.
2
+ // Phase A of docs/AI-LEARNING-PHASE2.md: zero-dependency, no randomness, no time-dependent scoring.
3
+
4
+ export const BM25_K1 = 1.2;
5
+ export const BM25_B = 0.75;
6
+
7
+ // Hangul syllable block (U+AC00–U+D7A3). Korean surface forms are written without
8
+ // spaces between stem and particle (버튼을, 접근성이), so a whitespace-delimited token
9
+ // is an atomic surface form. To let a stem query match particle-attached documents
10
+ // (and vice versa), we deterministically expand every Hangul run of length >= 2 into
11
+ // its overlapping character bigrams and emit them ALONGSIDE the original token.
12
+ // 버튼을 -> keep 버튼을, add 버튼, 튼을
13
+ // 접근성 -> keep 접근성, add 접근, 근성
14
+ // 버튼 -> the 2-char run's only bigram is itself; kept via dedupe
15
+ // Latin/number tokens and mixed tokens' non-Hangul parts are unchanged. The rule is
16
+ // pure and deterministic (no locale, no time, no randomness); order is normalized by
17
+ // the dedupe+sort in lexicalQueryTokens and by termFrequencies for stats.
18
+ const HANGUL_RUN = /[가-힣]{2,}/gu;
19
+
20
+ function hangulBigrams(token) {
21
+ const bigrams = [];
22
+ for (const run of token.match(HANGUL_RUN) || []) {
23
+ for (let i = 0; i + 2 <= run.length; i += 1) {
24
+ bigrams.push(run.slice(i, i + 2));
25
+ }
26
+ }
27
+ return bigrams;
28
+ }
29
+
30
+ // Same base token model as learn-select.mjs: Unicode letters/numbers, lowercased,
31
+ // length >= 2. Hangul tokens additionally emit character bigrams (see HANGUL_RUN).
32
+ export function lexicalTokens(text) {
33
+ const base = (String(text || "")
34
+ .toLowerCase()
35
+ .match(/[\p{L}\p{N}]+/gu) || [])
36
+ .filter((token) => token.length >= 2);
37
+
38
+ const expanded = [];
39
+ for (const token of base) {
40
+ expanded.push(token);
41
+ for (const bigram of hangulBigrams(token)) {
42
+ // A 2-char Hangul run's only bigram is the token itself; do not re-emit it,
43
+ // which would double-count its term frequency and distort BM25.
44
+ if (bigram !== token) expanded.push(bigram);
45
+ }
46
+ }
47
+ return expanded;
48
+ }
49
+
50
+ export function lexicalQueryTokens(query) {
51
+ return Array.from(new Set(lexicalTokens(query))).sort();
52
+ }
53
+
54
+ function termFrequencies(tokens) {
55
+ const terms = {};
56
+ for (const token of tokens) {
57
+ terms[token] = (terms[token] || 0) + 1;
58
+ }
59
+ const sorted = {};
60
+ for (const term of Object.keys(terms).sort()) {
61
+ sorted[term] = terms[term];
62
+ }
63
+ return sorted;
64
+ }
65
+
66
+ // documents: [{ id, text }] -> deterministic index stats with sorted ids and term keys.
67
+ export function buildLexicalStats(documents) {
68
+ const indexed = [...documents]
69
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
70
+ .map((doc) => {
71
+ const tokens = lexicalTokens(doc.text);
72
+ return { id: doc.id, length: tokens.length, terms: termFrequencies(tokens) };
73
+ });
74
+
75
+ const docFrequency = {};
76
+ for (const doc of indexed) {
77
+ for (const term of Object.keys(doc.terms)) {
78
+ docFrequency[term] = (docFrequency[term] || 0) + 1;
79
+ }
80
+ }
81
+ const sortedDocFrequency = {};
82
+ for (const term of Object.keys(docFrequency).sort()) {
83
+ sortedDocFrequency[term] = docFrequency[term];
84
+ }
85
+
86
+ const totalLength = indexed.reduce((sum, doc) => sum + doc.length, 0);
87
+ return {
88
+ documentCount: indexed.length,
89
+ termCount: Object.keys(sortedDocFrequency).length,
90
+ avgDocLength: indexed.length > 0 ? totalLength / indexed.length : 0,
91
+ docFrequency: sortedDocFrequency,
92
+ documents: indexed,
93
+ };
94
+ }
95
+
96
+ function inverseDocumentFrequency(documentCount, docFrequency) {
97
+ return Math.log(1 + (documentCount - docFrequency + 0.5) / (docFrequency + 0.5));
98
+ }
99
+
100
+ export function bm25Score(queryTokens, doc, stats) {
101
+ if (stats.documentCount === 0 || doc.length === 0) return { score: 0, matchedTokens: [] };
102
+
103
+ let score = 0;
104
+ const matchedTokens = [];
105
+ for (const token of queryTokens) {
106
+ const tf = doc.terms[token] || 0;
107
+ if (tf === 0) continue;
108
+ const df = stats.docFrequency[token] || 0;
109
+ const idf = inverseDocumentFrequency(stats.documentCount, df);
110
+ const lengthNorm = 1 - BM25_B + BM25_B * (doc.length / (stats.avgDocLength || 1));
111
+ score += idf * ((tf * (BM25_K1 + 1)) / (tf + BM25_K1 * lengthNorm));
112
+ matchedTokens.push(token);
113
+ }
114
+ return { score, matchedTokens };
115
+ }
116
+
117
+ // Fully ordered ranking: score desc, then id asc. Zero-score documents are excluded.
118
+ export function rankLexical(query, stats, { limit = 20 } = {}) {
119
+ const queryTokens = lexicalQueryTokens(query);
120
+ if (queryTokens.length === 0) return [];
121
+
122
+ const ranked = [];
123
+ for (const doc of stats.documents) {
124
+ const { score, matchedTokens } = bm25Score(queryTokens, doc, stats);
125
+ if (score > 0) {
126
+ ranked.push({ id: doc.id, score, matchedTokens });
127
+ }
128
+ }
129
+ ranked.sort((a, b) => {
130
+ if (b.score !== a.score) return b.score - a.score;
131
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
132
+ });
133
+ return ranked.slice(0, limit).map((hit) => ({
134
+ ...hit,
135
+ score: Number(hit.score.toFixed(6)),
136
+ }));
137
+ }
@@ -0,0 +1,110 @@
1
+ // Local, user-authored configuration for optional Phase B features
2
+ // (docs/AI-LEARNING-PHASE2.md, "Phase A implementation review", decision 5 / FU-4).
3
+ //
4
+ // design-ai never writes this file. It is durable, per-machine, user-authored
5
+ // configuration — the CLI only reads it. Presence of a configured provider is
6
+ // NOT enough to enable embeddings: every consumer still requires the explicit
7
+ // per-invocation --embeddings flag (config supplies the provider, the flag arms it).
8
+
9
+ import { existsSync, readFileSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import path from "node:path";
12
+
13
+ export function defaultConfigFile() {
14
+ return process.env.DESIGN_AI_CONFIG_FILE || path.join(homedir(), ".design-ai", "config.json");
15
+ }
16
+
17
+ const TOP_LEVEL_KEYS = new Set(["version", "embeddings"]);
18
+ const EMBEDDINGS_KEYS = new Set(["provider", "modelLabel"]);
19
+ const PROVIDER_KEYS = new Set(["command", "args"]);
20
+
21
+ function fail(message) {
22
+ return { present: true, config: null, error: message };
23
+ }
24
+
25
+ function isPlainObject(value) {
26
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
27
+ }
28
+
29
+ function validateProvider(provider) {
30
+ if (!isPlainObject(provider)) return "embeddings.provider must be an object";
31
+
32
+ const unknown = Object.keys(provider).filter((key) => !PROVIDER_KEYS.has(key));
33
+ if (unknown.length > 0) return `embeddings.provider has unknown key(s): ${unknown.join(", ")}`;
34
+
35
+ if (typeof provider.command !== "string" || provider.command.trim() === "") {
36
+ return "embeddings.provider.command is required and must be a non-empty string";
37
+ }
38
+
39
+ if (provider.args !== undefined) {
40
+ if (!Array.isArray(provider.args) || !provider.args.every((arg) => typeof arg === "string")) {
41
+ return "embeddings.provider.args must be an array of strings";
42
+ }
43
+ }
44
+
45
+ return "";
46
+ }
47
+
48
+ function validateEmbeddings(embeddings) {
49
+ if (!isPlainObject(embeddings)) return "embeddings must be an object";
50
+
51
+ const unknown = Object.keys(embeddings).filter((key) => !EMBEDDINGS_KEYS.has(key));
52
+ if (unknown.length > 0) return `embeddings has unknown key(s): ${unknown.join(", ")}`;
53
+
54
+ if (embeddings.provider === undefined) return "embeddings.provider is required";
55
+ const providerError = validateProvider(embeddings.provider);
56
+ if (providerError) return providerError;
57
+
58
+ if (embeddings.modelLabel !== undefined && typeof embeddings.modelLabel !== "string") {
59
+ return "embeddings.modelLabel must be a string";
60
+ }
61
+
62
+ return "";
63
+ }
64
+
65
+ function validateConfig(config) {
66
+ if (!isPlainObject(config)) return "config must be a JSON object";
67
+
68
+ const unknown = Object.keys(config).filter((key) => !TOP_LEVEL_KEYS.has(key));
69
+ if (unknown.length > 0) return `config has unknown top-level key(s): ${unknown.join(", ")}`;
70
+
71
+ if (config.version !== 1) return "config.version must be 1";
72
+
73
+ if (config.embeddings !== undefined) {
74
+ const embeddingsError = validateEmbeddings(config.embeddings);
75
+ if (embeddingsError) return embeddingsError;
76
+ }
77
+
78
+ return "";
79
+ }
80
+
81
+ // Never writes; design-ai only reads this user-authored file.
82
+ export function loadLocalConfig(filePath = defaultConfigFile()) {
83
+ if (!existsSync(filePath)) return { present: false, config: null, error: "" };
84
+
85
+ let raw;
86
+ try {
87
+ raw = readFileSync(filePath, "utf8");
88
+ } catch (error) {
89
+ return fail(`unreadable config file: ${error.message}`);
90
+ }
91
+
92
+ let parsed;
93
+ try {
94
+ parsed = JSON.parse(raw);
95
+ } catch (error) {
96
+ return fail(`config file is not valid JSON: ${error.message}`);
97
+ }
98
+
99
+ const error = validateConfig(parsed);
100
+ if (error) return fail(error);
101
+
102
+ return { present: true, config: parsed, error: "" };
103
+ }
104
+
105
+ // Convenience accessor: the configured embedding provider, or null when absent
106
+ // or the config is malformed/missing. Callers still must gate on --embeddings.
107
+ export function configuredEmbeddingProvider(filePath = defaultConfigFile()) {
108
+ const { config } = loadLocalConfig(filePath);
109
+ return config?.embeddings?.provider || null;
110
+ }
@@ -50,7 +50,7 @@ export const MCP_TOOLS = [
50
50
  {
51
51
  name: "design_ai_prompt",
52
52
  title: "Generate an agent prompt",
53
- description: "Generate a ready-to-use design-ai agent prompt from a brief. Read-only unless withLearning is true, which records local usage metadata.",
53
+ description: "Generate a ready-to-use design-ai agent prompt from a brief. Read-only unless withLearning is true, which records local usage metadata. Supports opt-in recall augmentation via withRecall, which enriches the output with brief-relevant shipped corpus knowledge.",
54
54
  inputSchema: {
55
55
  type: "object",
56
56
  properties: {
@@ -59,6 +59,8 @@ export const MCP_TOOLS = [
59
59
  withLearning: optionalBoolean("Include local learning preferences and record local usage metadata."),
60
60
  learningCategory: optionalString("Optional learning category when withLearning is true."),
61
61
  learningLimit: optionalInteger({ description: "Maximum learning entries, 1-100.", minimum: 1, maximum: 100 }),
62
+ withRecall: optionalBoolean("Opt-in recall augmentation: enriches the output with brief-relevant shipped corpus knowledge ranked by the deterministic lexical scorer. Requires no index and makes no network call."),
63
+ recallLimit: optionalInteger({ description: "Maximum recall passages, 1-20.", minimum: 1, maximum: 20 }),
62
64
  json: optionalBoolean("Return machine-readable prompt plan JSON instead of Markdown."),
63
65
  },
64
66
  required: ["brief"],
@@ -68,7 +70,7 @@ export const MCP_TOOLS = [
68
70
  {
69
71
  name: "design_ai_pack",
70
72
  title: "Generate prompt plus bounded context",
71
- description: "Generate a prompt pack with relevant design-ai context files. Read-only unless withLearning is true, which records local usage metadata.",
73
+ description: "Generate a prompt pack with relevant design-ai context files. Read-only unless withLearning is true, which records local usage metadata. Supports opt-in recall augmentation via withRecall, which enriches the output with brief-relevant shipped corpus knowledge.",
72
74
  inputSchema: {
73
75
  type: "object",
74
76
  properties: {
@@ -78,6 +80,8 @@ export const MCP_TOOLS = [
78
80
  withLearning: optionalBoolean("Include local learning preferences and record local usage metadata."),
79
81
  learningCategory: optionalString("Optional learning category when withLearning is true."),
80
82
  learningLimit: optionalInteger({ description: "Maximum learning entries, 1-100.", minimum: 1, maximum: 100 }),
83
+ withRecall: optionalBoolean("Opt-in recall augmentation: enriches the output with brief-relevant shipped corpus knowledge ranked by the deterministic lexical scorer. Requires no index and makes no network call."),
84
+ recallLimit: optionalInteger({ description: "Maximum recall passages, 1-20.", minimum: 1, maximum: 20 }),
81
85
  json: optionalBoolean("Return machine-readable pack JSON instead of Markdown."),
82
86
  },
83
87
  required: ["brief"],
@@ -87,13 +91,14 @@ export const MCP_TOOLS = [
87
91
  {
88
92
  name: "design_ai_search",
89
93
  title: "Search design-ai corpus",
90
- description: "Search knowledge, examples, skills, docs, agents, or commands. Read-only.",
94
+ description: "Search knowledge, examples, skills, docs, agents, or commands. Read-only. Supports an opt-in ranked mode for deterministic BM25-style results.",
91
95
  inputSchema: {
92
96
  type: "object",
93
97
  properties: {
94
98
  query: { type: "string", minLength: 2, description: "Search query." },
95
99
  dir: optionalString("Optional corpus directory: knowledge, examples, skills, docs, agents, commands."),
96
100
  limit: optionalInteger({ description: "Maximum hits, 1-500.", minimum: 1, maximum: 500 }),
101
+ ranked: optionalBoolean("Opt-in deterministic BM25-style ranked results. Requires no index and never builds one; falls back to a live corpus scan when no index is present."),
97
102
  },
98
103
  required: ["query"],
99
104
  additionalProperties: false,
@@ -281,6 +286,8 @@ export function buildCliInvocation(toolName, input = {}) {
281
286
  maybeBool(args, "--with-learning", input.withLearning);
282
287
  maybePush(args, "--learning-category", input.learningCategory);
283
288
  maybePush(args, "--learning-limit", input.learningLimit);
289
+ maybeBool(args, "--with-recall", input.withRecall);
290
+ maybePush(args, "--recall-limit", input.recallLimit);
284
291
  maybeBool(args, "--json", input.json);
285
292
  return { args, stdin };
286
293
  }
@@ -292,6 +299,8 @@ export function buildCliInvocation(toolName, input = {}) {
292
299
  maybeBool(args, "--with-learning", input.withLearning);
293
300
  maybePush(args, "--learning-category", input.learningCategory);
294
301
  maybePush(args, "--learning-limit", input.learningLimit);
302
+ maybeBool(args, "--with-recall", input.withRecall);
303
+ maybePush(args, "--recall-limit", input.recallLimit);
295
304
  maybeBool(args, "--json", input.json);
296
305
  return { args, stdin };
297
306
  }
@@ -300,6 +309,7 @@ export function buildCliInvocation(toolName, input = {}) {
300
309
  args.push("search", assertString(input.query, "query"), "--json");
301
310
  maybePush(args, "--dir", input.dir);
302
311
  maybePush(args, "--limit", input.limit);
312
+ maybeBool(args, "--ranked", input.ranked);
303
313
  return { args, stdin };
304
314
  }
305
315
 
package/cli/lib/pack.mjs CHANGED
@@ -4,6 +4,7 @@ import { readFileSync } from "node:fs";
4
4
 
5
5
  import { parseBriefSourceFlag } from "./brief.mjs";
6
6
  import { normalizeCategory, parseLearningLimit } from "./learn.mjs";
7
+ import { parseRecallLimit } from "./recall.mjs";
7
8
  import { SYMLINK_PREFIX } from "./paths.mjs";
8
9
  import { parseOutputFlags } from "./output.mjs";
9
10
  import { readRouteManifestVersion } from "./route.mjs";
@@ -29,6 +30,8 @@ const PACK_OPTIONS = [
29
30
  "--with-learning",
30
31
  "--learning-category",
31
32
  "--learning-limit",
33
+ "--with-recall",
34
+ "--recall-limit",
32
35
  ];
33
36
 
34
37
  export function parsePackArgs(args) {
@@ -44,6 +47,8 @@ export function parsePackArgs(args) {
44
47
  withLearning: false,
45
48
  learningCategory: "",
46
49
  learningLimit: 0,
50
+ withRecall: false,
51
+ recallLimit: 0,
47
52
  evalTemplate: false,
48
53
  eval: false,
49
54
  strict: false,
@@ -58,6 +63,17 @@ export function parsePackArgs(args) {
58
63
  out.json = true;
59
64
  } else if (arg === "--with-learning") {
60
65
  out.withLearning = true;
66
+ } else if (arg === "--with-recall") {
67
+ out.withRecall = true;
68
+ } else if (arg === "--recall-limit") {
69
+ const limit = args[i + 1];
70
+ if (!limit || limit.startsWith("--")) throw new Error("--recall-limit expects an integer from 1 to 20");
71
+ try {
72
+ out.recallLimit = parseRecallLimit(limit);
73
+ } catch {
74
+ throw new Error("--recall-limit expects an integer from 1 to 20");
75
+ }
76
+ i += 1;
61
77
  } else if (arg === "--eval-template") {
62
78
  out.evalTemplate = true;
63
79
  } else if (arg === "--eval") {
@@ -108,20 +124,23 @@ export function parsePackArgs(args) {
108
124
  if ((out.learningCategory || out.learningLimit) && !out.withLearning) {
109
125
  throw new Error("--learning-category and --learning-limit require --with-learning");
110
126
  }
127
+ if (out.recallLimit && !out.withRecall) {
128
+ throw new Error("--recall-limit requires --with-recall");
129
+ }
111
130
  if (out.eval && out.evalTemplate) {
112
131
  throw new Error("Choose either --eval-template or --eval, not both");
113
132
  }
114
133
  if (out.strict && !out.eval) {
115
134
  throw new Error("--strict can only be used with --eval");
116
135
  }
117
- if (out.evalTemplate && (out.briefParts.length > 0 || out.fromFile || out.stdin || out.routeId || out.withLearning)) {
118
- throw new Error("--eval-template cannot be combined with a brief, --from-file, --stdin, --route, or --with-learning");
136
+ if (out.evalTemplate && (out.briefParts.length > 0 || out.fromFile || out.stdin || out.routeId || out.withLearning || out.withRecall)) {
137
+ throw new Error("--eval-template cannot be combined with a brief, --from-file, --stdin, --route, --with-learning, or --with-recall");
119
138
  }
120
139
  if (out.eval && (!out.fromFile && !out.stdin)) {
121
140
  throw new Error("--eval requires --from-file or --stdin");
122
141
  }
123
- if (out.eval && (out.briefParts.length > 0 || out.routeId || out.withLearning)) {
124
- throw new Error("--eval cannot be combined with an inline brief, --route, or --with-learning");
142
+ if (out.eval && (out.briefParts.length > 0 || out.routeId || out.withLearning || out.withRecall)) {
143
+ throw new Error("--eval cannot be combined with an inline brief, --route, --with-learning, or --with-recall");
125
144
  }
126
145
 
127
146
  return {
@@ -200,6 +219,8 @@ export function buildPromptPack({
200
219
  learningFilePath = "",
201
220
  learningCategory = "",
202
221
  learningLimit = 0,
222
+ withRecall = false,
223
+ recallLimit = 0,
203
224
  }) {
204
225
  const plan = buildPromptPlan({
205
226
  brief,
@@ -210,6 +231,8 @@ export function buildPromptPack({
210
231
  learningFilePath,
211
232
  learningCategory,
212
233
  learningLimit,
234
+ withRecall,
235
+ recallLimit,
213
236
  });
214
237
  const files = [];
215
238
  let usedBytes = 0;
@@ -572,6 +595,10 @@ export function renderPromptPack({ plan, files, summary, warnings = [] }) {
572
595
  lines.push("");
573
596
  lines.push("## Prompt");
574
597
  lines.push("");
598
+ // plan.prompt already carries the --with-recall "## Recalled design knowledge"
599
+ // section (rendered by renderPrompt). Recall respects the byte budget by being
600
+ // subject to the same whole-pack takeUtf8(maxBytes) truncation as everything else —
601
+ // there is no recall-specific budget carve-out.
575
602
  lines.push(plan.prompt);
576
603
  lines.push("");
577
604
  lines.push("## Context Files");
@@ -6,6 +6,7 @@ import { existsSync } from "node:fs";
6
6
  import { parseBriefSourceFlag } from "./brief.mjs";
7
7
  import { listExamples } from "./examples.mjs";
8
8
  import { buildLearningContext, normalizeCategory, parseLearningLimit } from "./learn.mjs";
9
+ import { buildRecallContext, DEFAULT_RECALL_LIMIT, parseRecallLimit } from "./recall.mjs";
9
10
  import { SYMLINK_PREFIX } from "./paths.mjs";
10
11
  import { parseOutputFlags } from "./output.mjs";
11
12
  import { readRouteManifestVersion, routeBrief, routeById } from "./route.mjs";
@@ -26,6 +27,8 @@ const PROMPT_OPTIONS = [
26
27
  "--with-learning",
27
28
  "--learning-category",
28
29
  "--learning-limit",
30
+ "--with-recall",
31
+ "--recall-limit",
29
32
  ];
30
33
  const PROMPT_EVAL_VERSION = 1;
31
34
  const PROMPT_EVAL_DEFAULT_LIMIT = 12;
@@ -42,6 +45,8 @@ export function parsePromptArgs(args) {
42
45
  withLearning: false,
43
46
  learningCategory: "",
44
47
  learningLimit: 0,
48
+ withRecall: false,
49
+ recallLimit: 0,
45
50
  evalTemplate: false,
46
51
  eval: false,
47
52
  strict: false,
@@ -57,6 +62,17 @@ export function parsePromptArgs(args) {
57
62
  out.json = true;
58
63
  } else if (arg === "--with-learning") {
59
64
  out.withLearning = true;
65
+ } else if (arg === "--with-recall") {
66
+ out.withRecall = true;
67
+ } else if (arg === "--recall-limit") {
68
+ const limit = args[i + 1];
69
+ if (!limit || limit.startsWith("--")) throw new Error("--recall-limit expects an integer from 1 to 20");
70
+ try {
71
+ out.recallLimit = parseRecallLimit(limit);
72
+ } catch {
73
+ throw new Error("--recall-limit expects an integer from 1 to 20");
74
+ }
75
+ i += 1;
60
76
  } else if (arg === "--eval-template") {
61
77
  out.evalTemplate = true;
62
78
  } else if (arg === "--eval") {
@@ -96,20 +112,23 @@ export function parsePromptArgs(args) {
96
112
  if ((out.learningCategory || out.learningLimit) && !out.withLearning) {
97
113
  throw new Error("--learning-category and --learning-limit require --with-learning");
98
114
  }
115
+ if (out.recallLimit && !out.withRecall) {
116
+ throw new Error("--recall-limit requires --with-recall");
117
+ }
99
118
  if (out.eval && out.evalTemplate) {
100
119
  throw new Error("Choose either --eval-template or --eval, not both");
101
120
  }
102
121
  if (out.strict && !out.eval) {
103
122
  throw new Error("--strict can only be used with --eval");
104
123
  }
105
- if (out.evalTemplate && (out.briefParts.length > 0 || out.fromFile || out.stdin || out.routeId || out.withLearning)) {
106
- throw new Error("--eval-template cannot be combined with a brief, --from-file, --stdin, --route, or --with-learning");
124
+ if (out.evalTemplate && (out.briefParts.length > 0 || out.fromFile || out.stdin || out.routeId || out.withLearning || out.withRecall)) {
125
+ throw new Error("--eval-template cannot be combined with a brief, --from-file, --stdin, --route, --with-learning, or --with-recall");
107
126
  }
108
127
  if (out.eval && (!out.fromFile && !out.stdin)) {
109
128
  throw new Error("--eval requires --from-file or --stdin");
110
129
  }
111
- if (out.eval && (out.briefParts.length > 0 || out.routeId || out.withLearning)) {
112
- throw new Error("--eval cannot be combined with an inline brief, --route, or --with-learning");
130
+ if (out.eval && (out.briefParts.length > 0 || out.routeId || out.withLearning || out.withRecall)) {
131
+ throw new Error("--eval cannot be combined with an inline brief, --route, --with-learning, or --with-recall");
113
132
  }
114
133
 
115
134
  return {
@@ -209,6 +228,8 @@ export function buildPromptPlan({
209
228
  learningFilePath = "",
210
229
  learningCategory = "",
211
230
  learningLimit = 0,
231
+ withRecall = false,
232
+ recallLimit = 0,
212
233
  }) {
213
234
  const route = routeId
214
235
  ? routeById({ routeId, sourceRoot })
@@ -243,6 +264,13 @@ export function buildPromptPlan({
243
264
  query: brief,
244
265
  })
245
266
  : null;
267
+ const recallContext = withRecall
268
+ ? buildRecallContext({
269
+ brief,
270
+ recallLimit: recallLimit || DEFAULT_RECALL_LIMIT,
271
+ designAiPath: sourceRoot,
272
+ })
273
+ : null;
246
274
 
247
275
  return {
248
276
  brief,
@@ -254,6 +282,7 @@ export function buildPromptPlan({
254
282
  checklist,
255
283
  qualityCommand,
256
284
  ...(learningContext ? { learningContext } : {}),
285
+ ...(recallContext ? { recall: recallContext } : {}),
257
286
  prompt: renderPrompt({
258
287
  brief,
259
288
  route,
@@ -263,6 +292,7 @@ export function buildPromptPlan({
263
292
  checklist,
264
293
  qualityCommand,
265
294
  learningContext,
295
+ recallContext,
266
296
  }),
267
297
  };
268
298
  }
@@ -526,6 +556,7 @@ export function renderPrompt({
526
556
  checklist = checklistForRoute(route),
527
557
  qualityCommand = qualityCommandForRoute(route.id),
528
558
  learningContext = null,
559
+ recallContext = null,
529
560
  }) {
530
561
  const lines = [];
531
562
  lines.push("# design-ai task prompt");
@@ -571,6 +602,13 @@ export function renderPrompt({
571
602
  lines.push("");
572
603
  }
573
604
 
605
+ if (recallContext) {
606
+ lines.push("Recalled corpus knowledge:");
607
+ lines.push("");
608
+ lines.push(recallContext.markdown);
609
+ lines.push("");
610
+ }
611
+
574
612
  lines.push("Before producing the artifact, read these files in order:");
575
613
  for (const file of filesToRead) {
576
614
  lines.push(`- ${file}`);