@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,612 @@
1
+ // Learning archive and curation flow for `design-ai learn`.
2
+
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } 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 {
9
+ defaultLearningFile,
10
+ defaultLearningUsageFile,
11
+ previewText,
12
+ writeLearningProfile,
13
+ } from "./learn-shared.mjs";
14
+ import { learningUsageStats } from "./learn-usage.mjs";
15
+
16
+ export function defaultLearningArchiveFile(filePath = defaultLearningFile()) {
17
+ const parsed = path.parse(filePath);
18
+ const ext = parsed.ext || ".json";
19
+ const base = parsed.ext ? parsed.name : parsed.base;
20
+ return path.join(parsed.dir || ".", `${base}.archive${ext}`);
21
+ }
22
+
23
+ export function emptyLearningArchive(sourceFile = defaultLearningFile()) {
24
+ return {
25
+ version: 1,
26
+ updatedAt: "",
27
+ sourceFile,
28
+ entries: [],
29
+ };
30
+ }
31
+
32
+ function normalizeLearningArchive(rawArchive, { sourceFile = defaultLearningFile() } = {}) {
33
+ const archive = emptyLearningArchive(sourceFile);
34
+ if (!rawArchive || typeof rawArchive !== "object" || Array.isArray(rawArchive)) {
35
+ return archive;
36
+ }
37
+
38
+ archive.version = Number.isInteger(rawArchive.version) ? rawArchive.version : 1;
39
+ archive.updatedAt = String(rawArchive.updatedAt || "").trim();
40
+ archive.sourceFile = String(rawArchive.sourceFile || sourceFile).trim() || sourceFile;
41
+ archive.entries = Array.isArray(rawArchive.entries)
42
+ ? rawArchive.entries
43
+ .filter((entry) => entry && typeof entry === "object" && !Array.isArray(entry))
44
+ .map((entry) => {
45
+ let category = "other";
46
+ try {
47
+ category = normalizeCategory(entry.category || "other");
48
+ } catch {
49
+ category = "other";
50
+ }
51
+ return {
52
+ id: String(entry.id || "").trim(),
53
+ category,
54
+ text: String(entry.text || "").trim(),
55
+ source: String(entry.source || "archive").trim() || "archive",
56
+ createdAt: String(entry.createdAt || "").trim(),
57
+ archivedAt: String(entry.archivedAt || "").trim(),
58
+ archiveReason: String(entry.archiveReason || "curation").trim() || "curation",
59
+ issueCodes: Array.isArray(entry.issueCodes)
60
+ ? entry.issueCodes.map((code) => String(code || "").trim()).filter(Boolean)
61
+ : [],
62
+ originalFile: String(entry.originalFile || sourceFile).trim() || sourceFile,
63
+ };
64
+ })
65
+ .filter((entry) => entry.id && entry.text)
66
+ : [];
67
+ return archive;
68
+ }
69
+
70
+ export function loadLearningArchive(archiveFile = defaultLearningArchiveFile(), {
71
+ sourceFile = defaultLearningFile(),
72
+ } = {}) {
73
+ if (!existsSync(archiveFile)) return emptyLearningArchive(sourceFile);
74
+ const raw = readFileSync(archiveFile, "utf8");
75
+ try {
76
+ return normalizeLearningArchive(JSON.parse(raw), { sourceFile });
77
+ } catch {
78
+ throw new Error(`Learning archive is not valid JSON: ${archiveFile}`);
79
+ }
80
+ }
81
+
82
+ function writeLearningArchive(archiveFile, archive) {
83
+ mkdirSync(path.dirname(archiveFile), { recursive: true });
84
+ writeFileSync(archiveFile, `${JSON.stringify(archive, null, 2)}\n`, "utf8");
85
+ }
86
+
87
+ function learningCurationIssueAction(issue, { ambiguousEntryIds = new Set() } = {}) {
88
+ if (!issue.entryId) {
89
+ return {
90
+ action: "manual-review",
91
+ reason: "profile-level-issue",
92
+ };
93
+ }
94
+ if (ambiguousEntryIds.has(issue.entryId)) {
95
+ return {
96
+ action: "manual-review",
97
+ reason: "ambiguous-entry-id",
98
+ };
99
+ }
100
+ if (issue.code === "duplicate-entry-text") {
101
+ return {
102
+ action: "archive",
103
+ reason: "duplicate-entry",
104
+ };
105
+ }
106
+ if (issue.code.startsWith("sensitive-")) {
107
+ return {
108
+ action: "archive",
109
+ reason: "sensitive-content",
110
+ };
111
+ }
112
+ return {
113
+ action: "manual-review",
114
+ reason: issue.level === "failure" ? "manual-profile-repair" : "manual-quality-review",
115
+ };
116
+ }
117
+
118
+ function issueIsHigherPriority(nextAction, currentAction) {
119
+ if (!currentAction) return true;
120
+ if (nextAction.action === "archive" && currentAction.action !== "archive") return true;
121
+ return false;
122
+ }
123
+
124
+ function buildCurationProposal({ entry, issue, issueAction, existing }) {
125
+ const currentAction = existing
126
+ ? { action: existing.action, reason: existing.reason }
127
+ : null;
128
+ const nextAction = issueIsHigherPriority(issueAction, currentAction)
129
+ ? issueAction
130
+ : currentAction;
131
+ const issueCodes = existing?.issueCodes || [];
132
+ if (!issueCodes.includes(issue.code)) issueCodes.push(issue.code);
133
+
134
+ const messages = existing?.messages || [];
135
+ if (issue.message && !messages.includes(issue.message)) messages.push(issue.message);
136
+
137
+ return {
138
+ entryId: issue.entryId || "",
139
+ action: nextAction.action,
140
+ reason: nextAction.reason,
141
+ issueCodes,
142
+ messages,
143
+ ...(entry ? {
144
+ category: entry.category,
145
+ source: entry.source,
146
+ createdAt: entry.createdAt,
147
+ textPreview: previewText(entry.text),
148
+ } : {}),
149
+ };
150
+ }
151
+
152
+ function learningUsageReviewItem({ level, action, reason, entryId, usageCount = 0, entry = null, message }) {
153
+ return {
154
+ level,
155
+ action,
156
+ reason,
157
+ entryId,
158
+ usageCount,
159
+ message,
160
+ ...(entry ? {
161
+ category: entry.category,
162
+ source: entry.source,
163
+ createdAt: entry.createdAt,
164
+ textPreview: previewText(entry.text),
165
+ } : {}),
166
+ };
167
+ }
168
+
169
+ function emptyLearningUsageCurationReview({
170
+ filePath,
171
+ usageFile,
172
+ exists = false,
173
+ profileFile = "",
174
+ profileFileMatches = true,
175
+ error = "",
176
+ }) {
177
+ return {
178
+ file: path.resolve(filePath),
179
+ usageFile: path.resolve(usageFile),
180
+ profileFile: profileFile ? path.resolve(profileFile) : path.resolve(filePath),
181
+ profileFileMatches,
182
+ exists,
183
+ eventCount: 0,
184
+ usedEntryCount: 0,
185
+ unusedEntryCount: 0,
186
+ staleSelectedEntryCount: 0,
187
+ reviewCount: 0,
188
+ unusedReviewCount: 0,
189
+ staleReviewCount: 0,
190
+ reviews: [],
191
+ recommendations: [],
192
+ error,
193
+ privacy: {
194
+ storesRawBriefText: false,
195
+ storesBriefHash: true,
196
+ storesSelectedEntryIds: true,
197
+ },
198
+ autoArchive: false,
199
+ };
200
+ }
201
+
202
+ function learningUsageCurationReview({
203
+ filePath = defaultLearningFile(),
204
+ usageFile = defaultLearningUsageFile(filePath),
205
+ } = {}) {
206
+ const resolvedFile = path.resolve(filePath);
207
+ const resolvedUsageFile = path.resolve(usageFile);
208
+
209
+ try {
210
+ const stats = learningUsageStats({
211
+ filePath: resolvedFile,
212
+ usageFile: resolvedUsageFile,
213
+ limit: 10,
214
+ });
215
+ const statsProfileFile = stats.profileFile ? path.resolve(stats.profileFile) : resolvedFile;
216
+ const profileFileMatches = statsProfileFile === resolvedFile;
217
+
218
+ if (!stats.exists) {
219
+ return {
220
+ ...emptyLearningUsageCurationReview({
221
+ filePath: resolvedFile,
222
+ usageFile: resolvedUsageFile,
223
+ profileFile: statsProfileFile,
224
+ profileFileMatches,
225
+ exists: false,
226
+ }),
227
+ recommendations: stats.recommendations || [],
228
+ };
229
+ }
230
+
231
+ const profile = loadLearningProfile(resolvedFile);
232
+ const entriesById = new Map(profile.entries.map((entry) => [entry.id, entry]));
233
+ const reviews = [];
234
+ const recommendations = [...(stats.recommendations || [])];
235
+
236
+ if (!profileFileMatches) {
237
+ reviews.push(learningUsageReviewItem({
238
+ level: "warning",
239
+ action: "review-usage-sidecar",
240
+ reason: "usage-profile-file-mismatch",
241
+ entryId: "",
242
+ usageCount: stats.eventCount,
243
+ message: "Usage sidecar was recorded for a different learning profile path.",
244
+ }));
245
+ recommendations.push({
246
+ level: "warning",
247
+ text: "Usage sidecar profile path differs from the active learning profile.",
248
+ });
249
+ }
250
+
251
+ for (const entryId of stats.staleSelectedEntryIds || []) {
252
+ reviews.push(learningUsageReviewItem({
253
+ level: "warning",
254
+ action: "review-usage-sidecar",
255
+ reason: "stale-selected-entry-id",
256
+ entryId,
257
+ usageCount: stats.selectedEntryCounts?.[entryId] || 0,
258
+ message: "Usage sidecar selected an entry id that is no longer present in the active learning profile.",
259
+ }));
260
+ }
261
+
262
+ if (stats.eventCount > 0) {
263
+ for (const entryId of stats.unusedEntryIds || []) {
264
+ const entry = entriesById.get(entryId);
265
+ if (!entry) continue;
266
+ reviews.push(learningUsageReviewItem({
267
+ level: "info",
268
+ action: "manual-review",
269
+ reason: stats.eventCount >= 5
270
+ ? "unused-in-observed-usage"
271
+ : "unused-with-limited-history",
272
+ entryId,
273
+ entry,
274
+ message: "Active entry has not been selected in recorded prompt/pack usage; review manually before archiving.",
275
+ }));
276
+ }
277
+ }
278
+
279
+ const unusedReviewCount = reviews.filter((review) => review.reason.startsWith("unused-")).length;
280
+ const staleReviewCount = reviews.filter((review) => review.reason === "stale-selected-entry-id").length;
281
+
282
+ return {
283
+ file: stats.file,
284
+ usageFile: stats.usageFile,
285
+ profileFile: statsProfileFile,
286
+ profileFileMatches,
287
+ exists: true,
288
+ eventCount: stats.eventCount,
289
+ usedEntryCount: stats.usedEntryCount,
290
+ unusedEntryCount: stats.unusedEntryCount,
291
+ staleSelectedEntryCount: stats.staleSelectedEntryCount,
292
+ reviewCount: reviews.length,
293
+ unusedReviewCount,
294
+ staleReviewCount,
295
+ reviews,
296
+ recommendations,
297
+ error: "",
298
+ privacy: stats.privacy || {
299
+ storesRawBriefText: false,
300
+ storesBriefHash: true,
301
+ storesSelectedEntryIds: true,
302
+ },
303
+ autoArchive: false,
304
+ };
305
+ } catch (error) {
306
+ return emptyLearningUsageCurationReview({
307
+ filePath: resolvedFile,
308
+ usageFile: resolvedUsageFile,
309
+ exists: existsSync(resolvedUsageFile),
310
+ error: error?.message || String(error),
311
+ });
312
+ }
313
+ }
314
+
315
+ export function buildLearningCurationPlan({
316
+ filePath = defaultLearningFile(),
317
+ archiveFile = defaultLearningArchiveFile(filePath),
318
+ usageFile = defaultLearningUsageFile(filePath),
319
+ } = {}) {
320
+ const audit = auditLearningProfile({ filePath });
321
+ const payload = {
322
+ file: filePath,
323
+ archiveFile,
324
+ usage: learningUsageCurationReview({ filePath, usageFile }),
325
+ before: audit.summary,
326
+ proposalCount: 0,
327
+ archiveCount: 0,
328
+ manualReviewCount: 0,
329
+ proposals: [],
330
+ skipped: [],
331
+ count: audit.count,
332
+ };
333
+
334
+ if (!audit.exists) {
335
+ payload.skipped.push({
336
+ reason: "profile-missing",
337
+ message: "No local learning profile exists yet.",
338
+ });
339
+ return payload;
340
+ }
341
+
342
+ if (audit.summary.failures > 0) {
343
+ payload.skipped.push({
344
+ reason: "profile-has-failures",
345
+ message: "Repair failing learning profile issues before automated curation.",
346
+ });
347
+ return payload;
348
+ }
349
+
350
+ const profile = loadLearningProfile(filePath);
351
+ const entriesById = new Map(profile.entries.map((entry) => [entry.id, entry]));
352
+ const ambiguousEntryIds = new Set(
353
+ audit.issues
354
+ .filter((issue) => issue.code === "duplicate-entry-id" && issue.entryId)
355
+ .map((issue) => issue.entryId),
356
+ );
357
+ const proposalsByEntryId = new Map();
358
+
359
+ for (const issue of audit.issues) {
360
+ const issueAction = learningCurationIssueAction(issue, { ambiguousEntryIds });
361
+ const entry = issue.entryId ? entriesById.get(issue.entryId) : null;
362
+ if (issue.entryId && !entry) {
363
+ payload.skipped.push({
364
+ entryId: issue.entryId,
365
+ issueCode: issue.code,
366
+ reason: "entry-not-found",
367
+ message: "The audited entry was not present in the normalized learning profile.",
368
+ });
369
+ continue;
370
+ }
371
+
372
+ const key = issue.entryId || `profile:${issue.code}`;
373
+ const proposal = buildCurationProposal({
374
+ entry,
375
+ issue,
376
+ issueAction,
377
+ existing: proposalsByEntryId.get(key),
378
+ });
379
+ proposalsByEntryId.set(key, proposal);
380
+ }
381
+
382
+ payload.proposals = [...proposalsByEntryId.values()];
383
+ payload.proposalCount = payload.proposals.length;
384
+ payload.archiveCount = payload.proposals.filter((proposal) => proposal.action === "archive").length;
385
+ payload.manualReviewCount = payload.proposals.filter((proposal) => proposal.action === "manual-review").length;
386
+ return payload;
387
+ }
388
+
389
+ export function applyLearningCurationPlan({
390
+ filePath = defaultLearningFile(),
391
+ archiveFile = defaultLearningArchiveFile(filePath),
392
+ usageFile = defaultLearningUsageFile(filePath),
393
+ dryRun = true,
394
+ now = new Date(),
395
+ } = {}) {
396
+ const plan = buildLearningCurationPlan({ filePath, archiveFile, usageFile });
397
+ const payload = {
398
+ ...plan,
399
+ dryRun,
400
+ applied: !dryRun,
401
+ archived: [],
402
+ after: null,
403
+ };
404
+
405
+ if (dryRun || plan.archiveCount === 0) {
406
+ return payload;
407
+ }
408
+
409
+ const archiveIds = new Set(
410
+ plan.proposals
411
+ .filter((proposal) => proposal.action === "archive")
412
+ .map((proposal) => proposal.entryId),
413
+ );
414
+ const proposalByEntryId = new Map(plan.proposals.map((proposal) => [proposal.entryId, proposal]));
415
+ const profile = loadLearningProfile(filePath);
416
+ const archived = [];
417
+ const remaining = [];
418
+ const archivedAt = now.toISOString();
419
+
420
+ for (const entry of profile.entries) {
421
+ if (!archiveIds.has(entry.id)) {
422
+ remaining.push(entry);
423
+ continue;
424
+ }
425
+
426
+ const proposal = proposalByEntryId.get(entry.id);
427
+ archived.push({
428
+ id: entry.id,
429
+ category: entry.category,
430
+ text: entry.text,
431
+ source: entry.source,
432
+ createdAt: entry.createdAt,
433
+ archivedAt,
434
+ archiveReason: proposal?.reason || "curation",
435
+ issueCodes: proposal?.issueCodes || [],
436
+ originalFile: filePath,
437
+ });
438
+ }
439
+
440
+ const archivedIds = new Set(archived.map((entry) => entry.id));
441
+ for (const entryId of archiveIds) {
442
+ if (!archivedIds.has(entryId)) {
443
+ payload.skipped.push({
444
+ entryId,
445
+ reason: "entry-not-found",
446
+ message: "The entry was not present when applying learning curation.",
447
+ });
448
+ }
449
+ }
450
+
451
+ const updatedAt = now.toISOString();
452
+ writeLearningProfile(filePath, {
453
+ version: 1,
454
+ updatedAt,
455
+ entries: remaining,
456
+ });
457
+
458
+ if (archived.length > 0) {
459
+ const archive = loadLearningArchive(archiveFile, { sourceFile: filePath });
460
+ writeLearningArchive(archiveFile, {
461
+ version: 1,
462
+ updatedAt,
463
+ sourceFile: filePath,
464
+ entries: [...archive.entries, ...archived],
465
+ });
466
+ }
467
+
468
+ const afterAudit = auditLearningProfile({ filePath });
469
+ return {
470
+ ...payload,
471
+ archiveCount: archived.length,
472
+ archived,
473
+ after: afterAudit.summary,
474
+ count: remaining.length,
475
+ };
476
+ }
477
+
478
+ export function clearLearning({
479
+ filePath = defaultLearningFile(),
480
+ now = new Date(),
481
+ } = {}) {
482
+ const profile = loadLearningProfile(filePath);
483
+ const updatedAt = now.toISOString();
484
+ const nextProfile = {
485
+ version: 1,
486
+ updatedAt,
487
+ entries: [],
488
+ };
489
+
490
+ writeLearningProfile(filePath, nextProfile);
491
+
492
+ return {
493
+ file: filePath,
494
+ removedCount: profile.entries.length,
495
+ profile: nextProfile,
496
+ };
497
+ }
498
+
499
+ function formatLearningSummary(summary) {
500
+ if (!summary) return "unknown";
501
+ return `${summary.status} (${summary.failures} failure(s), ${summary.warnings} warning(s))`;
502
+ }
503
+
504
+ function yesNo(value) {
505
+ return value ? "yes" : "no";
506
+ }
507
+
508
+ function learningReportListItem(label, value) {
509
+ return `- ${label}: ${value}`;
510
+ }
511
+
512
+ export function renderLearningCurationReport(payload, {
513
+ generatedAt = new Date(),
514
+ } = {}) {
515
+ const generatedAtText = generatedAt instanceof Date ? generatedAt.toISOString() : String(generatedAt || "");
516
+ const mode = payload.applied ? "applied" : "preview";
517
+ const lines = [
518
+ "# Learning Curation Report",
519
+ "",
520
+ learningReportListItem("Generated", generatedAtText),
521
+ learningReportListItem("Mode", mode),
522
+ learningReportListItem("File", payload.file),
523
+ learningReportListItem("Archive", payload.archiveFile),
524
+ learningReportListItem("Before", formatLearningSummary(payload.before)),
525
+ learningReportListItem("After", payload.after ? formatLearningSummary(payload.after) : "not applied"),
526
+ learningReportListItem("Proposals", payload.proposalCount),
527
+ learningReportListItem("Archive candidates", payload.archiveCount),
528
+ learningReportListItem("Manual review", payload.manualReviewCount),
529
+ "",
530
+ "## Archive Candidates",
531
+ "",
532
+ ];
533
+
534
+ const archiveCandidates = (payload.proposals || []).filter((proposal) => proposal.action === "archive");
535
+ if (archiveCandidates.length === 0) {
536
+ lines.push("No archive candidates found.");
537
+ } else {
538
+ for (const proposal of archiveCandidates) {
539
+ lines.push(`- \`${proposal.entryId}\`: ${proposal.reason}`);
540
+ lines.push(` - Issues: ${(proposal.issueCodes || []).join(", ") || "none"}`);
541
+ if (proposal.category) lines.push(` - Category: ${proposal.category}`);
542
+ if (proposal.textPreview) lines.push(` - Preview: ${proposal.textPreview}`);
543
+ }
544
+ }
545
+
546
+ lines.push("", "## Manual Review", "");
547
+ const manualCandidates = (payload.proposals || []).filter((proposal) => proposal.action === "manual-review");
548
+ if (manualCandidates.length === 0) {
549
+ lines.push("No profile curation items need manual review.");
550
+ } else {
551
+ for (const proposal of manualCandidates) {
552
+ const label = proposal.entryId || "profile";
553
+ lines.push(`- \`${label}\`: ${proposal.reason}`);
554
+ lines.push(` - Issues: ${(proposal.issueCodes || []).join(", ") || "none"}`);
555
+ if (proposal.textPreview) lines.push(` - Preview: ${proposal.textPreview}`);
556
+ }
557
+ }
558
+
559
+ lines.push("", "## Usage Review", "");
560
+ const usage = payload.usage || {};
561
+ lines.push(learningReportListItem("Sidecar", usage.usageFile || "not available"));
562
+ lines.push(learningReportListItem("Exists", yesNo(Boolean(usage.exists))));
563
+ lines.push(learningReportListItem("Profile file", usage.profileFile || payload.file));
564
+ lines.push(learningReportListItem("Profile file matches", yesNo(usage.profileFileMatches !== false)));
565
+ lines.push(learningReportListItem("Events", usage.eventCount || 0));
566
+ lines.push(learningReportListItem("Review items", usage.reviewCount || 0));
567
+ lines.push(learningReportListItem("Auto archive from usage", yesNo(Boolean(usage.autoArchive))));
568
+ if (usage.error) lines.push(learningReportListItem("Error", usage.error));
569
+
570
+ if (Array.isArray(usage.reviews) && usage.reviews.length > 0) {
571
+ lines.push("");
572
+ for (const review of usage.reviews) {
573
+ const label = review.entryId || "usage";
574
+ lines.push(`- \`${label}\`: ${review.reason} (${review.action})`);
575
+ if (review.level) lines.push(` - Level: ${review.level}`);
576
+ if (Number.isInteger(review.usageCount)) lines.push(` - Usage count: ${review.usageCount}`);
577
+ if (review.textPreview) lines.push(` - Preview: ${review.textPreview}`);
578
+ }
579
+ } else {
580
+ lines.push("");
581
+ lines.push("No usage-based curation review items found.");
582
+ }
583
+
584
+ lines.push("", "## Skipped", "");
585
+ if (!Array.isArray(payload.skipped) || payload.skipped.length === 0) {
586
+ lines.push("No curation steps were skipped.");
587
+ } else {
588
+ for (const skipped of payload.skipped) {
589
+ const label = skipped.entryId ? `\`${skipped.entryId}\`` : "profile";
590
+ lines.push(`- ${label}: ${skipped.reason}`);
591
+ if (skipped.message) lines.push(` - ${skipped.message}`);
592
+ }
593
+ }
594
+
595
+ lines.push("", "## Privacy", "");
596
+ lines.push("- Report text may include learning entry previews, but usage review does not include raw prompt or pack brief text.");
597
+ lines.push("- Usage sidecars store selected entry ids and short brief hashes; usage review remains advisory and never archives entries by itself.");
598
+
599
+ lines.push("", "## Next Steps", "");
600
+ if (payload.applied) {
601
+ lines.push("- Review the archive file before sharing or deleting local learning history.");
602
+ lines.push("- Run `design-ai learn --audit` and `design-ai workspace --strict` after curation to confirm readiness.");
603
+ } else if ((payload.archiveCount || 0) > 0) {
604
+ lines.push("- Review archive candidates, then rerun `design-ai learn --curate --yes` only if the proposed archive actions are correct.");
605
+ lines.push("- Keep usage-only review items as manual signals until enough prompt/pack usage has accumulated.");
606
+ } else {
607
+ lines.push("- No archive action is required from this curation report.");
608
+ lines.push("- Continue recording prompt/pack usage with `--with-learning` before making usage-based decisions.");
609
+ }
610
+
611
+ return `${lines.join("\n")}\n`;
612
+ }