@design-ai/cli 4.56.0 → 4.58.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 (271) hide show
  1. package/.claude-plugin/plugin.json +109 -25
  2. package/CHANGELOG.md +68 -0
  3. package/README.ko.md +11 -28
  4. package/README.md +12 -25
  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/route.mjs +13 -1
  16. package/cli/commands/search.mjs +94 -1
  17. package/cli/lib/dispatch.mjs +3 -0
  18. package/cli/lib/embedding-index.mjs +199 -0
  19. package/cli/lib/embedding-provider.mjs +121 -0
  20. package/cli/lib/embedding-rerank.mjs +52 -0
  21. package/cli/lib/learn-args.mjs +490 -0
  22. package/cli/lib/learn-backup.mjs +748 -0
  23. package/cli/lib/learn-curation.mjs +612 -0
  24. package/cli/lib/learn-eval.mjs +459 -0
  25. package/cli/lib/learn-profile.mjs +763 -0
  26. package/cli/lib/learn-select.mjs +223 -0
  27. package/cli/lib/learn-shared.mjs +158 -0
  28. package/cli/lib/learn-test-support.mjs +218 -0
  29. package/cli/lib/learn-usage.mjs +360 -0
  30. package/cli/lib/learn.mjs +83 -3694
  31. package/cli/lib/lexical.mjs +137 -0
  32. package/cli/lib/local-config.mjs +110 -0
  33. package/cli/lib/mcp-server.mjs +13 -3
  34. package/cli/lib/pack.mjs +31 -4
  35. package/cli/lib/prompt.mjs +42 -4
  36. package/cli/lib/recall.mjs +193 -0
  37. package/cli/lib/retrieval-index.mjs +0 -0
  38. package/cli/lib/route.mjs +62 -5
  39. package/cli/lib/search-ranked.mjs +202 -0
  40. package/cli/lib/search.mjs +11 -1
  41. package/cli/lib/signals-backlog-commands.mjs +673 -0
  42. package/cli/lib/signals-backlog.mjs +361 -0
  43. package/cli/lib/signals-eval.mjs +176 -0
  44. package/cli/lib/signals-readiness.mjs +247 -0
  45. package/cli/lib/signals-registry.mjs +377 -0
  46. package/cli/lib/signals-render.mjs +478 -0
  47. package/cli/lib/signals-shared.mjs +75 -0
  48. package/cli/lib/signals.mjs +16 -2337
  49. package/cli/lib/site-bundle-handoff-expected.mjs +173 -0
  50. package/cli/lib/site-bundle-handoff-runbook-action-summary.mjs +332 -0
  51. package/cli/lib/site-bundle-handoff-runbook-human-lines.mjs +167 -0
  52. package/cli/lib/site-bundle-handoff-runbook-maps.mjs +238 -0
  53. package/cli/lib/site-bundle-handoff-runbook-next-step.mjs +278 -0
  54. package/cli/lib/site-bundle-handoff-runbook.mjs +44 -931
  55. package/cli/lib/site-test-support.mjs +68 -0
  56. package/cli/lib/skill-proposals-apply-commands.mjs +135 -0
  57. package/cli/lib/skill-proposals-apply-contract.mjs +750 -0
  58. package/cli/lib/skill-proposals-apply-plan.mjs +118 -0
  59. package/cli/lib/skill-proposals-generate.mjs +298 -0
  60. package/cli/lib/skill-proposals-render.mjs +532 -0
  61. package/cli/lib/skill-proposals-review.mjs +262 -0
  62. package/cli/lib/skill-proposals.mjs +15 -2046
  63. package/cli/lib/workspace-args.mjs +83 -0
  64. package/cli/lib/workspace-git.mjs +169 -0
  65. package/cli/lib/workspace-learning.mjs +483 -0
  66. package/cli/lib/workspace-repo.mjs +139 -0
  67. package/cli/lib/workspace-report.mjs +283 -0
  68. package/cli/lib/workspace-test-support.mjs +99 -0
  69. package/cli/lib/workspace.mjs +30 -1056
  70. package/docs/AI-LEARNING-PHASE2.md +215 -0
  71. package/docs/DISTRIBUTION.ko.md +2 -2
  72. package/docs/DISTRIBUTION.md +2 -2
  73. package/docs/NEXT-SURFACE-DECISION.md +125 -0
  74. package/docs/PRODUCT-READINESS.md +3 -3
  75. package/docs/RELEASE-GATES.ko.md +35 -0
  76. package/docs/RELEASE-GATES.md +234 -0
  77. package/docs/ROADMAP.md +82 -8
  78. package/docs/external-status.md +9 -11
  79. package/docs/inspection-20260630.md +169 -0
  80. package/docs/integrations/design-ai-mcp-server.md +4 -4
  81. package/docs/reference/ant-design.md +413 -0
  82. package/docs/reference/awesome-design-md.md +439 -0
  83. package/docs/reference/mui.md +783 -0
  84. package/docs/reference/shadcn-ui.md +298 -0
  85. package/examples/component-accordion-actions.md +5 -5
  86. package/examples/component-accordion-details.md +5 -5
  87. package/examples/component-accordion-summary.md +5 -5
  88. package/examples/component-accordion.md +3 -3
  89. package/examples/component-affix.md +1 -1
  90. package/examples/component-alert-dialog.md +1 -1
  91. package/examples/component-alert-title.md +1 -1
  92. package/examples/component-alert.md +3 -3
  93. package/examples/component-anchor.md +1 -1
  94. package/examples/component-app-bar.md +1 -1
  95. package/examples/component-aspect-ratio.md +1 -1
  96. package/examples/component-auto-complete.md +2 -2
  97. package/examples/component-avatar-group.md +5 -5
  98. package/examples/component-avatar.md +3 -3
  99. package/examples/component-back-top.md +1 -1
  100. package/examples/component-backdrop.md +1 -1
  101. package/examples/component-badge.md +3 -3
  102. package/examples/component-border-beam.md +3 -3
  103. package/examples/component-bottom-navigation.md +1 -1
  104. package/examples/component-box.md +1 -1
  105. package/examples/component-breadcrumb.md +3 -3
  106. package/examples/component-button-base.md +3 -3
  107. package/examples/component-button-group.md +2 -2
  108. package/examples/component-button.md +3 -3
  109. package/examples/component-calendar.md +2 -2
  110. package/examples/component-card-actions.md +1 -1
  111. package/examples/component-card-content.md +1 -1
  112. package/examples/component-card-header.md +1 -1
  113. package/examples/component-card-media.md +1 -1
  114. package/examples/component-card.md +3 -3
  115. package/examples/component-carousel.md +2 -2
  116. package/examples/component-cascader.md +1 -1
  117. package/examples/component-chart.md +1 -1
  118. package/examples/component-checkbox.md +3 -3
  119. package/examples/component-click-away-listener.md +1 -1
  120. package/examples/component-code.md +1 -1
  121. package/examples/component-collapsible.md +1 -1
  122. package/examples/component-color-picker.md +1 -1
  123. package/examples/component-combobox.md +1 -1
  124. package/examples/component-command.md +1 -1
  125. package/examples/component-config-provider.md +3 -3
  126. package/examples/component-context-menu.md +1 -1
  127. package/examples/component-css-baseline.md +3 -3
  128. package/examples/component-date-picker.md +2 -2
  129. package/examples/component-descriptions.md +1 -1
  130. package/examples/component-dialog-actions.md +1 -1
  131. package/examples/component-dialog-content-text.md +1 -1
  132. package/examples/component-dialog-content.md +1 -1
  133. package/examples/component-dialog-title.md +1 -1
  134. package/examples/component-dialog.md +2 -2
  135. package/examples/component-divider.md +3 -3
  136. package/examples/component-drawer.md +3 -3
  137. package/examples/component-dropdown.md +3 -3
  138. package/examples/component-empty.md +2 -2
  139. package/examples/component-fade.md +1 -1
  140. package/examples/component-field.md +1 -1
  141. package/examples/component-filled-input.md +1 -1
  142. package/examples/component-flex.md +1 -1
  143. package/examples/component-float-button.md +2 -2
  144. package/examples/component-form-control-label.md +1 -1
  145. package/examples/component-form-control.md +1 -1
  146. package/examples/component-form-controls.md +1 -1
  147. package/examples/component-form-group.md +1 -1
  148. package/examples/component-form-helper-text.md +1 -1
  149. package/examples/component-form-label.md +1 -1
  150. package/examples/component-form.md +2 -2
  151. package/examples/component-grid.md +2 -2
  152. package/examples/component-grow.md +1 -1
  153. package/examples/component-hover-card.md +1 -1
  154. package/examples/component-icon-button.md +1 -1
  155. package/examples/component-icon.md +1 -1
  156. package/examples/component-image-list.md +1 -1
  157. package/examples/component-image.md +1 -1
  158. package/examples/component-input-adornment.md +1 -1
  159. package/examples/component-input-base.md +1 -1
  160. package/examples/component-input-number.md +1 -1
  161. package/examples/component-input-otp.md +2 -2
  162. package/examples/component-input.md +3 -3
  163. package/examples/component-item.md +1 -1
  164. package/examples/component-label.md +1 -1
  165. package/examples/component-layout.md +1 -1
  166. package/examples/component-link.md +1 -1
  167. package/examples/component-list-item-avatar.md +1 -1
  168. package/examples/component-list-item-button.md +1 -1
  169. package/examples/component-list-item-icon.md +1 -1
  170. package/examples/component-list-item-text.md +1 -1
  171. package/examples/component-list-item.md +1 -1
  172. package/examples/component-list-subheader.md +1 -1
  173. package/examples/component-list.md +2 -2
  174. package/examples/component-masonry.md +1 -1
  175. package/examples/component-mentions.md +1 -1
  176. package/examples/component-menu-item.md +1 -1
  177. package/examples/component-menu-list.md +1 -1
  178. package/examples/component-menu.md +1 -1
  179. package/examples/component-menubar.md +1 -1
  180. package/examples/component-message.md +1 -1
  181. package/examples/component-mobile-stepper.md +1 -1
  182. package/examples/component-modal.md +5 -5
  183. package/examples/component-navigation-menu.md +1 -1
  184. package/examples/component-notification.md +1 -1
  185. package/examples/component-outlined-input.md +1 -1
  186. package/examples/component-pagination.md +3 -3
  187. package/examples/component-paper.md +1 -1
  188. package/examples/component-popconfirm.md +1 -1
  189. package/examples/component-popover.md +3 -3
  190. package/examples/component-popper.md +1 -1
  191. package/examples/component-progress.md +2 -2
  192. package/examples/component-qr-code.md +1 -1
  193. package/examples/component-radio.md +3 -3
  194. package/examples/component-rate.md +2 -2
  195. package/examples/component-resizable.md +1 -1
  196. package/examples/component-result.md +1 -1
  197. package/examples/component-scroll-area.md +1 -1
  198. package/examples/component-segmented.md +3 -3
  199. package/examples/component-select.md +3 -3
  200. package/examples/component-separator.md +1 -1
  201. package/examples/component-sheet.md +1 -1
  202. package/examples/component-sidebar.md +1 -1
  203. package/examples/component-skeleton.md +3 -3
  204. package/examples/component-slide.md +1 -1
  205. package/examples/component-slider.md +3 -3
  206. package/examples/component-snackbar-content.md +1 -1
  207. package/examples/component-snackbar.md +1 -1
  208. package/examples/component-sonner.md +1 -1
  209. package/examples/component-space.md +1 -1
  210. package/examples/component-speed-dial-action.md +1 -1
  211. package/examples/component-speed-dial.md +1 -1
  212. package/examples/component-spin.md +2 -2
  213. package/examples/component-spinner.md +1 -1
  214. package/examples/component-splitter.md +2 -2
  215. package/examples/component-stack.md +1 -1
  216. package/examples/component-statistic.md +1 -1
  217. package/examples/component-step-button.md +4 -4
  218. package/examples/component-step-connector.md +4 -4
  219. package/examples/component-step-content.md +1 -1
  220. package/examples/component-step-icon.md +1 -1
  221. package/examples/component-step-label.md +1 -1
  222. package/examples/component-step.md +2 -2
  223. package/examples/component-steps.md +2 -2
  224. package/examples/component-swipeable-drawer.md +1 -1
  225. package/examples/component-switch.md +3 -3
  226. package/examples/component-tab-scroll-button.md +5 -5
  227. package/examples/component-tab.md +1 -1
  228. package/examples/component-table-body.md +1 -1
  229. package/examples/component-table-cell.md +1 -1
  230. package/examples/component-table-container.md +1 -1
  231. package/examples/component-table-footer.md +1 -1
  232. package/examples/component-table-head.md +1 -1
  233. package/examples/component-table-pagination.md +1 -1
  234. package/examples/component-table-row.md +1 -1
  235. package/examples/component-table-sort-label.md +1 -1
  236. package/examples/component-table.md +2 -2
  237. package/examples/component-tabs.md +3 -3
  238. package/examples/component-tag-badge.md +2 -2
  239. package/examples/component-tag.md +1 -1
  240. package/examples/component-textarea-autosize.md +1 -1
  241. package/examples/component-textarea.md +1 -1
  242. package/examples/component-time-picker.md +1 -1
  243. package/examples/component-timeline.md +1 -1
  244. package/examples/component-toast.md +4 -4
  245. package/examples/component-toggle-button.md +1 -1
  246. package/examples/component-toggle.md +1 -1
  247. package/examples/component-toolbar.md +1 -1
  248. package/examples/component-tooltip.md +3 -3
  249. package/examples/component-tour.md +1 -1
  250. package/examples/component-transfer.md +1 -1
  251. package/examples/component-tree-select.md +1 -1
  252. package/examples/component-tree.md +1 -1
  253. package/examples/component-typography.md +2 -2
  254. package/examples/component-upload.md +1 -1
  255. package/examples/component-watermark.md +1 -1
  256. package/examples/component-zoom.md +1 -1
  257. package/knowledge/COVERAGE.md +12 -9
  258. package/knowledge/components/INDEX.md +202 -202
  259. package/knowledge/i18n/korean-density-conventions.md +108 -0
  260. package/knowledge/i18n/korean-product-conventions.md +2 -0
  261. package/knowledge/patterns/async-control.md +233 -0
  262. package/knowledge/patterns/brand-references.md +72 -72
  263. package/knowledge/patterns/error-states.md +1 -0
  264. package/knowledge/patterns/form-design.md +2 -0
  265. package/package.json +1 -1
  266. package/tools/audit/local-ci.py +16 -1
  267. package/tools/audit/package-smoke.py +611 -9
  268. package/tools/audit/registry-smoke.py +537 -3
  269. package/tools/audit/release-metadata.py +47 -1
  270. package/tools/audit/smoke_assertions.py +918 -13
  271. package/tools/migrations/refs-links-to-reference-pages.py +179 -0
@@ -0,0 +1,459 @@
1
+ // Learning eval checkpoint generation and reports for `design-ai learn`.
2
+
3
+ import { existsSync } from "node:fs";
4
+ import path from "node:path";
5
+
6
+ import { normalizeCategory } from "./learn-args.mjs";
7
+ import { auditLearningProfile, loadLearningProfile } from "./learn-profile.mjs";
8
+ import { selectLearningEntrySet } from "./learn-select.mjs";
9
+ import { cleanNoteText, defaultLearningFile, shortHash } from "./learn-shared.mjs";
10
+
11
+ function parseLearningEvalPayload(evalText, source = "input") {
12
+ let payload = null;
13
+ try {
14
+ payload = JSON.parse(String(evalText || ""));
15
+ } catch {
16
+ throw new Error("Learning eval checkpoint is not valid JSON");
17
+ }
18
+
19
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
20
+ throw new Error("Learning eval checkpoint must be a JSON object with a cases array");
21
+ }
22
+ if (!Array.isArray(payload.cases)) {
23
+ throw new Error("Learning eval checkpoint must include a cases array");
24
+ }
25
+ if (payload.cases.length === 0) {
26
+ throw new Error("Learning eval checkpoint has no cases");
27
+ }
28
+
29
+ return {
30
+ source,
31
+ version: Number.isInteger(payload.version) ? payload.version : 1,
32
+ ranker: typeof payload.ranker === "string" && payload.ranker.trim() ? payload.ranker.trim() : "lexical",
33
+ generatedAt: safeIsoString(payload.generatedAt),
34
+ sourceProfile: summarizeLearningEvalSourceProfile(payload.sourceProfile),
35
+ cases: payload.cases,
36
+ };
37
+ }
38
+
39
+ function safeIsoString(value) {
40
+ const text = String(value || "").trim();
41
+ if (!text) return "";
42
+ return Number.isNaN(Date.parse(text)) ? "" : text;
43
+ }
44
+
45
+ function nullableBoolean(value) {
46
+ return typeof value === "boolean" ? value : null;
47
+ }
48
+
49
+ function nullableNonNegativeInteger(value) {
50
+ return Number.isInteger(value) && value >= 0 ? value : null;
51
+ }
52
+
53
+ function summarizeLearningEvalSourceProfile(value) {
54
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
55
+
56
+ return {
57
+ file: String(value.file || "").trim(),
58
+ exists: nullableBoolean(value.exists),
59
+ entryCount: nullableNonNegativeInteger(value.entryCount),
60
+ auditStatus: ["pass", "warn", "fail"].includes(String(value.auditStatus || ""))
61
+ ? String(value.auditStatus)
62
+ : "",
63
+ category: value.category ? normalizeCategory(value.category) : "",
64
+ queryPresent: Boolean(cleanNoteText(value.query)),
65
+ limit: nullableNonNegativeInteger(value.limit),
66
+ };
67
+ }
68
+
69
+ function evalStringList(value, { field, caseId }) {
70
+ if (value === undefined || value === null) return [];
71
+ if (!Array.isArray(value)) {
72
+ throw new Error(`Learning eval case ${caseId} field ${field} must be an array of ids`);
73
+ }
74
+ return value.map((item) => String(item || "").trim()).filter(Boolean);
75
+ }
76
+
77
+ function evalPositiveInteger(value, { field, caseId, fallback }) {
78
+ if (value === undefined || value === null || value === "") return fallback;
79
+ const number = Number(value);
80
+ if (!Number.isInteger(number) || number < 1 || number > 100) {
81
+ throw new Error(`Learning eval case ${caseId} field ${field} must be an integer from 1 to 100`);
82
+ }
83
+ return number;
84
+ }
85
+
86
+ function evalNonNegativeInteger(value, { field, caseId, fallback = 0 }) {
87
+ if (value === undefined || value === null || value === "") return fallback;
88
+ const number = Number(value);
89
+ if (!Number.isInteger(number) || number < 0 || number > 100) {
90
+ throw new Error(`Learning eval case ${caseId} field ${field} must be an integer from 0 to 100`);
91
+ }
92
+ return number;
93
+ }
94
+
95
+ function normalizeLearningEvalCase(rawCase, index, {
96
+ defaultLimit = 12,
97
+ defaultCategory = "",
98
+ } = {}) {
99
+ const caseId = String(rawCase?.id || `case-${index + 1}`).trim() || `case-${index + 1}`;
100
+ if (!rawCase || typeof rawCase !== "object" || Array.isArray(rawCase)) {
101
+ throw new Error(`Learning eval case ${caseId} must be an object`);
102
+ }
103
+
104
+ const brief = cleanNoteText(rawCase.brief || rawCase.query);
105
+ if (!brief) {
106
+ throw new Error(`Learning eval case ${caseId} requires a brief`);
107
+ }
108
+
109
+ const category = rawCase.category !== undefined
110
+ ? String(rawCase.category || "").trim()
111
+ : defaultCategory;
112
+
113
+ return {
114
+ id: caseId,
115
+ routeId: String(rawCase.routeId || "").trim(),
116
+ brief,
117
+ briefHash: shortHash(brief),
118
+ category: category ? normalizeCategory(category) : "",
119
+ limit: evalPositiveInteger(rawCase.limit, {
120
+ field: "limit",
121
+ caseId,
122
+ fallback: defaultLimit,
123
+ }),
124
+ expectedSelectedIds: evalStringList(
125
+ rawCase.expectedSelectedIds ?? rawCase.expectSelectedIds ?? rawCase.expectedEntryIds,
126
+ { field: "expectedSelectedIds", caseId },
127
+ ),
128
+ avoidedSelectedIds: evalStringList(
129
+ rawCase.avoidedSelectedIds ?? rawCase.avoidSelectedIds ?? rawCase.avoidEntryIds,
130
+ { field: "avoidedSelectedIds", caseId },
131
+ ),
132
+ minMatchedCount: evalNonNegativeInteger(rawCase.minMatchedCount, {
133
+ field: "minMatchedCount",
134
+ caseId,
135
+ fallback: 0,
136
+ }),
137
+ requireNoFallback: Boolean(rawCase.requireNoFallback),
138
+ };
139
+ }
140
+
141
+ function learningEvalIssue({ level = "warning", code, message }) {
142
+ return { level, code, message };
143
+ }
144
+
145
+ function summarizeLearningEvalIssues(issues) {
146
+ const failures = issues.filter((issue) => issue.level === "failure").length;
147
+ const warnings = issues.filter((issue) => issue.level === "warning").length;
148
+ return {
149
+ status: failures > 0 ? "fail" : warnings > 0 ? "warn" : "pass",
150
+ failures,
151
+ warnings,
152
+ };
153
+ }
154
+
155
+ function selectedEvalEntry(item) {
156
+ return {
157
+ id: item.id,
158
+ category: item.category,
159
+ score: item.score,
160
+ reason: item.reason,
161
+ };
162
+ }
163
+
164
+ function evaluateLearningCase(profile, rawCase, index, {
165
+ defaultLimit = 12,
166
+ defaultCategory = "",
167
+ } = {}) {
168
+ const evalCase = normalizeLearningEvalCase(rawCase, index, {
169
+ defaultLimit,
170
+ defaultCategory,
171
+ });
172
+ const { selection } = selectLearningEntrySet(profile, {
173
+ category: evalCase.category,
174
+ limit: evalCase.limit,
175
+ query: evalCase.brief,
176
+ includeFallback: true,
177
+ });
178
+ const selected = Array.isArray(selection.selected) ? selection.selected : [];
179
+ const selectedEntryIds = selected.map((item) => item.id).filter(Boolean);
180
+ const selectedEntryIdSet = new Set(selectedEntryIds);
181
+ const profileEntryIds = new Set(profile.entries.map((entry) => entry.id).filter(Boolean));
182
+ const issues = [];
183
+
184
+ const missingProfileExpectedIds = evalCase.expectedSelectedIds
185
+ .filter((entryId) => !profileEntryIds.has(entryId));
186
+ const missingExpectedIds = evalCase.expectedSelectedIds
187
+ .filter((entryId) => !selectedEntryIdSet.has(entryId));
188
+ const unexpectedAvoidedIds = evalCase.avoidedSelectedIds
189
+ .filter((entryId) => selectedEntryIdSet.has(entryId));
190
+
191
+ for (const entryId of missingProfileExpectedIds) {
192
+ issues.push(learningEvalIssue({
193
+ level: "failure",
194
+ code: "expected-entry-not-in-profile",
195
+ message: `Expected entry ${entryId} is not present in the active learning profile.`,
196
+ }));
197
+ }
198
+ if (missingExpectedIds.length > 0) {
199
+ issues.push(learningEvalIssue({
200
+ level: "failure",
201
+ code: "expected-entry-not-selected",
202
+ message: `Expected selected entries were missing: ${missingExpectedIds.join(", ")}.`,
203
+ }));
204
+ }
205
+ if (unexpectedAvoidedIds.length > 0) {
206
+ issues.push(learningEvalIssue({
207
+ level: "failure",
208
+ code: "avoided-entry-selected",
209
+ message: `Avoided entries were selected: ${unexpectedAvoidedIds.join(", ")}.`,
210
+ }));
211
+ }
212
+ if (selection.matchedCount < evalCase.minMatchedCount) {
213
+ issues.push(learningEvalIssue({
214
+ level: "failure",
215
+ code: "matched-count-below-minimum",
216
+ message: `Matched ${selection.matchedCount} learning entries, expected at least ${evalCase.minMatchedCount}.`,
217
+ }));
218
+ }
219
+ if (evalCase.requireNoFallback && selection.fallbackCount > 0) {
220
+ issues.push(learningEvalIssue({
221
+ level: "failure",
222
+ code: "fallback-selected",
223
+ message: `Selected ${selection.fallbackCount} recency fallback entr${selection.fallbackCount === 1 ? "y" : "ies"}.`,
224
+ }));
225
+ }
226
+ if (
227
+ evalCase.expectedSelectedIds.length === 0
228
+ && evalCase.avoidedSelectedIds.length === 0
229
+ && evalCase.minMatchedCount === 0
230
+ && !evalCase.requireNoFallback
231
+ ) {
232
+ issues.push(learningEvalIssue({
233
+ code: "no-eval-assertions",
234
+ message: "Case has no expected ids, avoided ids, minMatchedCount, or requireNoFallback assertion.",
235
+ }));
236
+ }
237
+
238
+ const summary = summarizeLearningEvalIssues(issues);
239
+
240
+ return {
241
+ id: evalCase.id,
242
+ routeId: evalCase.routeId,
243
+ briefHash: evalCase.briefHash,
244
+ category: evalCase.category,
245
+ limit: evalCase.limit,
246
+ status: summary.status,
247
+ failures: summary.failures,
248
+ warnings: summary.warnings,
249
+ candidateCount: selection.candidateCount,
250
+ matchedCount: selection.matchedCount,
251
+ selectedCount: selection.selectedCount,
252
+ fallbackCount: selection.fallbackCount,
253
+ expectedSelectedIds: evalCase.expectedSelectedIds,
254
+ missingExpectedIds,
255
+ avoidedSelectedIds: evalCase.avoidedSelectedIds,
256
+ unexpectedAvoidedIds,
257
+ minMatchedCount: evalCase.minMatchedCount,
258
+ requireNoFallback: evalCase.requireNoFallback,
259
+ selectedEntryIds,
260
+ selected: selected.map(selectedEvalEntry),
261
+ issues,
262
+ };
263
+ }
264
+
265
+ export function learningEvalReport({
266
+ filePath = defaultLearningFile(),
267
+ evalText = "",
268
+ source = "input",
269
+ limit = 12,
270
+ category = "",
271
+ } = {}) {
272
+ const resolvedFile = path.resolve(filePath);
273
+ const defaultLimit = Number.isInteger(limit) && limit > 0 ? limit : 12;
274
+ const defaultCategory = category ? normalizeCategory(category) : "";
275
+ const checkpoint = parseLearningEvalPayload(evalText, source);
276
+ const profileExists = existsSync(resolvedFile);
277
+ const profile = loadLearningProfile(resolvedFile);
278
+ const audit = auditLearningProfile({ filePath: resolvedFile });
279
+ const cases = checkpoint.cases.map((rawCase, index) => evaluateLearningCase(profile, rawCase, index, {
280
+ defaultLimit,
281
+ defaultCategory,
282
+ }));
283
+ const failed = cases.filter((item) => item.status === "fail").length;
284
+ const warned = cases.filter((item) => item.status === "warn").length;
285
+ const passed = cases.filter((item) => item.status === "pass").length;
286
+ const recommendations = [];
287
+
288
+ if (!profileExists) {
289
+ recommendations.push({
290
+ level: "warning",
291
+ text: "Learning profile does not exist; initialize or import entries before relying on eval results.",
292
+ });
293
+ }
294
+ if (audit.summary.status !== "pass") {
295
+ recommendations.push({
296
+ level: audit.summary.failures > 0 ? "warning" : "info",
297
+ text: "Run `design-ai learn --audit` before using eval checkpoints as a release gate.",
298
+ });
299
+ }
300
+ if (failed > 0) {
301
+ recommendations.push({
302
+ level: "warning",
303
+ text: "Review failed eval cases before trusting prompt/pack --with-learning selection.",
304
+ });
305
+ }
306
+
307
+ return {
308
+ file: resolvedFile,
309
+ source,
310
+ profileExists,
311
+ profileEntryCount: profile.entries.length,
312
+ checkpointVersion: checkpoint.version,
313
+ ranker: checkpoint.ranker,
314
+ generatedAt: checkpoint.generatedAt,
315
+ sourceProfile: checkpoint.sourceProfile,
316
+ defaultLimit,
317
+ defaultCategory,
318
+ status: failed > 0 ? "fail" : warned > 0 ? "warn" : "pass",
319
+ caseCount: cases.length,
320
+ passed,
321
+ warned,
322
+ failed,
323
+ auditSummary: audit.summary,
324
+ cases,
325
+ recommendations,
326
+ privacy: {
327
+ storesRawBriefText: false,
328
+ storesBriefHash: true,
329
+ exposesMatchedTokens: false,
330
+ },
331
+ };
332
+ }
333
+
334
+ function learningEvalTemplateCaseId(seed, index) {
335
+ return `eval-${index + 1}-${shortHash(seed).slice(0, 10)}`;
336
+ }
337
+
338
+ function learningEvalTemplateCaseFromEntry(entry, index) {
339
+ return {
340
+ id: learningEvalTemplateCaseId(`${entry.id}\n${entry.category}\n${entry.text}`, index),
341
+ brief: entry.text,
342
+ category: entry.category,
343
+ limit: 1,
344
+ expectedSelectedIds: [entry.id],
345
+ minMatchedCount: 1,
346
+ requireNoFallback: true,
347
+ };
348
+ }
349
+
350
+ export function buildLearningEvalTemplate({
351
+ filePath = defaultLearningFile(),
352
+ query = "",
353
+ category = "",
354
+ limit = 6,
355
+ now = new Date(),
356
+ } = {}) {
357
+ const resolvedFile = path.resolve(filePath);
358
+ const normalizedCategory = category ? normalizeCategory(category) : "";
359
+ const maxCases = Number.isInteger(limit) && limit > 0 ? limit : 6;
360
+ const profileExists = existsSync(resolvedFile);
361
+ const profile = loadLearningProfile(resolvedFile);
362
+ const audit = auditLearningProfile({ filePath: resolvedFile });
363
+ const cleanedQuery = cleanNoteText(query);
364
+ const recommendations = [];
365
+ let cases = [];
366
+ let selectionSummary = null;
367
+
368
+ if (cleanedQuery) {
369
+ const { selection } = selectLearningEntrySet(profile, {
370
+ category: normalizedCategory,
371
+ limit: maxCases,
372
+ query: cleanedQuery,
373
+ includeFallback: false,
374
+ });
375
+ const expectedSelectedIds = selection.selected.map((item) => item.id).filter(Boolean);
376
+ selectionSummary = {
377
+ mode: selection.mode,
378
+ candidateCount: selection.candidateCount,
379
+ matchedCount: selection.matchedCount,
380
+ selectedCount: expectedSelectedIds.length,
381
+ queryTokenCount: selection.queryTokenCount,
382
+ fallbackCount: selection.fallbackCount,
383
+ };
384
+ if (expectedSelectedIds.length > 0) {
385
+ const evalLimit = expectedSelectedIds.length;
386
+ cases = [
387
+ {
388
+ id: learningEvalTemplateCaseId(`${cleanedQuery}\n${normalizedCategory}`, 0),
389
+ brief: cleanedQuery,
390
+ ...(normalizedCategory ? { category: normalizedCategory } : {}),
391
+ limit: evalLimit,
392
+ expectedSelectedIds,
393
+ minMatchedCount: expectedSelectedIds.length,
394
+ requireNoFallback: true,
395
+ },
396
+ ];
397
+ }
398
+ } else {
399
+ const { entries, selection } = selectLearningEntrySet(profile, {
400
+ category: normalizedCategory,
401
+ limit: maxCases,
402
+ includeFallback: false,
403
+ });
404
+ selectionSummary = {
405
+ mode: selection.mode,
406
+ candidateCount: selection.candidateCount,
407
+ matchedCount: selection.matchedCount,
408
+ selectedCount: entries.length,
409
+ queryTokenCount: selection.queryTokenCount,
410
+ fallbackCount: selection.fallbackCount,
411
+ };
412
+ cases = entries.map((entry, index) => learningEvalTemplateCaseFromEntry(entry, index));
413
+ }
414
+
415
+ if (!profileExists) {
416
+ recommendations.push({
417
+ level: "warning",
418
+ text: "Learning profile does not exist; create entries before generating durable eval checkpoints.",
419
+ });
420
+ }
421
+ if (audit.summary.status !== "pass") {
422
+ recommendations.push({
423
+ level: audit.summary.failures > 0 ? "warning" : "info",
424
+ text: "Run `design-ai learn --audit` before using generated eval checkpoints as a gate.",
425
+ });
426
+ }
427
+ if (cases.length === 0) {
428
+ recommendations.push({
429
+ level: "info",
430
+ text: cleanedQuery
431
+ ? "No matching learning entries found for the query; add or adjust learning entries before saving this checkpoint."
432
+ : "No learning entries are available for checkpoint generation.",
433
+ });
434
+ }
435
+
436
+ return {
437
+ version: 1,
438
+ ranker: "lexical",
439
+ generatedAt: now.toISOString(),
440
+ sourceProfile: {
441
+ file: resolvedFile,
442
+ exists: profileExists,
443
+ entryCount: profile.entries.length,
444
+ auditStatus: audit.summary.status,
445
+ category: normalizedCategory,
446
+ query: cleanedQuery,
447
+ limit: maxCases,
448
+ },
449
+ selection: selectionSummary,
450
+ caseCount: cases.length,
451
+ cases,
452
+ recommendations,
453
+ privacy: {
454
+ storesRawBriefText: true,
455
+ storesBriefHash: false,
456
+ exposesMatchedTokens: false,
457
+ },
458
+ };
459
+ }