@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,360 @@
1
+ // Learning usage sidecar log and stats for `design-ai learn`.
2
+
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import path from "node:path";
5
+
6
+ import { auditLearningProfile, loadLearningProfile } from "./learn-profile.mjs";
7
+ import {
8
+ countBy,
9
+ defaultLearningFile,
10
+ defaultLearningUsageFile,
11
+ previewText,
12
+ shortHash,
13
+ statsEntry,
14
+ } from "./learn-shared.mjs";
15
+
16
+ const DEFAULT_LEARNING_USAGE_EVENT_LIMIT = 500;
17
+ export function emptyLearningUsageLog({ profileFile = "" } = {}) {
18
+ return {
19
+ version: 1,
20
+ updatedAt: "",
21
+ profileFile,
22
+ events: [],
23
+ };
24
+ }
25
+
26
+ function normalizeLearningUsageEvent(event) {
27
+ if (!event || typeof event !== "object" || Array.isArray(event)) return null;
28
+ const createdAt = String(event.createdAt || "").trim();
29
+ const command = String(event.command || "").trim();
30
+ const selectedEntryIds = Array.isArray(event.selectedEntryIds)
31
+ ? event.selectedEntryIds.map((id) => String(id || "").trim()).filter(Boolean)
32
+ : [];
33
+
34
+ if (!createdAt || !command) return null;
35
+
36
+ return {
37
+ id: String(event.id || `learn-use-${shortHash(`${createdAt}\n${command}`)}`).trim(),
38
+ command,
39
+ routeId: String(event.routeId || "").trim(),
40
+ profileFile: String(event.profileFile || "").trim(),
41
+ briefHash: String(event.briefHash || "").trim(),
42
+ category: String(event.category || "").trim(),
43
+ limit: Number.isInteger(event.limit) ? event.limit : null,
44
+ selectedEntryIds,
45
+ selectedCount: Number.isInteger(event.selectedCount) ? event.selectedCount : selectedEntryIds.length,
46
+ candidateCount: Number.isInteger(event.candidateCount) ? event.candidateCount : 0,
47
+ matchedCount: Number.isInteger(event.matchedCount) ? event.matchedCount : 0,
48
+ fallbackCount: Number.isInteger(event.fallbackCount) ? event.fallbackCount : 0,
49
+ queryTokenCount: Number.isInteger(event.queryTokenCount) ? event.queryTokenCount : 0,
50
+ auditStatus: String(event.auditStatus || "").trim(),
51
+ createdAt,
52
+ };
53
+ }
54
+
55
+ export function normalizeLearningUsageLog(rawLog, { profileFile = "" } = {}) {
56
+ const log = rawLog && typeof rawLog === "object" ? rawLog : {};
57
+ const events = Array.isArray(log.events)
58
+ ? log.events.map(normalizeLearningUsageEvent).filter(Boolean)
59
+ : [];
60
+
61
+ return {
62
+ version: Number.isInteger(log.version) ? log.version : 1,
63
+ updatedAt: String(log.updatedAt || "").trim(),
64
+ profileFile: String(log.profileFile || profileFile || "").trim(),
65
+ events,
66
+ };
67
+ }
68
+
69
+ export function loadLearningUsageLog(filePath = defaultLearningUsageFile(), { profileFile = "" } = {}) {
70
+ if (!existsSync(filePath)) {
71
+ return emptyLearningUsageLog({ profileFile });
72
+ }
73
+
74
+ const raw = readFileSync(filePath, "utf8");
75
+ try {
76
+ return normalizeLearningUsageLog(JSON.parse(raw), { profileFile });
77
+ } catch {
78
+ throw new Error(`Learning usage log is not valid JSON: ${filePath}`);
79
+ }
80
+ }
81
+
82
+ function writeLearningUsageLog(filePath, log) {
83
+ mkdirSync(path.dirname(filePath), { recursive: true });
84
+ writeFileSync(filePath, `${JSON.stringify(log, null, 2)}\n`, "utf8");
85
+ }
86
+
87
+ export function buildLearningUsageEvent({
88
+ command,
89
+ routeId = "",
90
+ learningContext,
91
+ now = new Date(),
92
+ } = {}) {
93
+ if (!learningContext) return null;
94
+
95
+ const createdAt = now.toISOString();
96
+ const selection = learningContext.selection || {};
97
+ const selectedEntryIds = Array.isArray(selection.selected) && selection.selected.length > 0
98
+ ? selection.selected.map((item) => item?.id).filter(Boolean)
99
+ : (learningContext.entries || []).map((entry) => entry.id).filter(Boolean);
100
+ const normalizedCommand = String(command || "").trim();
101
+
102
+ return {
103
+ id: `learn-use-${shortHash([
104
+ createdAt,
105
+ normalizedCommand,
106
+ routeId,
107
+ selectedEntryIds.join(","),
108
+ learningContext.query || "",
109
+ ].join("\n"))}`,
110
+ command: normalizedCommand,
111
+ routeId: String(routeId || "").trim(),
112
+ profileFile: String(learningContext.file || "").trim(),
113
+ briefHash: shortHash(learningContext.query || ""),
114
+ category: String(learningContext.category || "").trim(),
115
+ limit: Number.isInteger(learningContext.limit) ? learningContext.limit : null,
116
+ selectedEntryIds,
117
+ selectedCount: Number.isInteger(selection.selectedCount) ? selection.selectedCount : selectedEntryIds.length,
118
+ candidateCount: Number.isInteger(selection.candidateCount) ? selection.candidateCount : 0,
119
+ matchedCount: Number.isInteger(selection.matchedCount) ? selection.matchedCount : 0,
120
+ fallbackCount: Number.isInteger(selection.fallbackCount) ? selection.fallbackCount : 0,
121
+ queryTokenCount: Number.isInteger(selection.queryTokenCount) ? selection.queryTokenCount : 0,
122
+ auditStatus: String(learningContext.auditSummary?.status || "").trim(),
123
+ createdAt,
124
+ };
125
+ }
126
+
127
+ export function recordLearningUsage({
128
+ command,
129
+ routeId = "",
130
+ learningContext,
131
+ usageFile = defaultLearningUsageFile(learningContext?.file || defaultLearningFile()),
132
+ now = new Date(),
133
+ eventLimit = DEFAULT_LEARNING_USAGE_EVENT_LIMIT,
134
+ } = {}) {
135
+ const event = buildLearningUsageEvent({
136
+ command,
137
+ routeId,
138
+ learningContext,
139
+ now,
140
+ });
141
+ const resolvedUsageFile = path.resolve(usageFile);
142
+
143
+ if (!event) {
144
+ return {
145
+ file: resolvedUsageFile,
146
+ recorded: false,
147
+ reason: "missing-learning-context",
148
+ count: 0,
149
+ event: null,
150
+ };
151
+ }
152
+
153
+ const log = loadLearningUsageLog(resolvedUsageFile, { profileFile: event.profileFile });
154
+ const updatedAt = event.createdAt;
155
+ const maxEvents = Number.isInteger(eventLimit) && eventLimit > 0
156
+ ? eventLimit
157
+ : DEFAULT_LEARNING_USAGE_EVENT_LIMIT;
158
+ const events = [...log.events, event].slice(-maxEvents);
159
+ const nextLog = {
160
+ version: 1,
161
+ updatedAt,
162
+ profileFile: event.profileFile || log.profileFile,
163
+ events,
164
+ };
165
+
166
+ writeLearningUsageLog(resolvedUsageFile, nextLog);
167
+
168
+ return {
169
+ file: resolvedUsageFile,
170
+ recorded: true,
171
+ event,
172
+ count: nextLog.events.length,
173
+ eventLimit: maxEvents,
174
+ };
175
+ }
176
+
177
+ function usageEventTime(event) {
178
+ const time = Date.parse(event.createdAt);
179
+ return Number.isNaN(time) ? 0 : time;
180
+ }
181
+
182
+ function usageEventSummary(event) {
183
+ return {
184
+ id: event.id,
185
+ command: event.command,
186
+ routeId: event.routeId,
187
+ category: event.category,
188
+ limit: event.limit,
189
+ selectedEntryIds: event.selectedEntryIds,
190
+ selectedCount: event.selectedCount,
191
+ candidateCount: event.candidateCount,
192
+ matchedCount: event.matchedCount,
193
+ fallbackCount: event.fallbackCount,
194
+ queryTokenCount: event.queryTokenCount,
195
+ auditStatus: event.auditStatus,
196
+ briefHash: event.briefHash,
197
+ createdAt: event.createdAt,
198
+ };
199
+ }
200
+
201
+ function incrementUsageEntry(entryUsage, entryId, event) {
202
+ if (!entryId) return;
203
+ const existing = entryUsage.get(entryId) || {
204
+ id: entryId,
205
+ count: 0,
206
+ commands: {},
207
+ routes: {},
208
+ latestUsedAt: "",
209
+ };
210
+ existing.count += 1;
211
+ if (event.command) existing.commands[event.command] = (existing.commands[event.command] || 0) + 1;
212
+ if (event.routeId) existing.routes[event.routeId] = (existing.routes[event.routeId] || 0) + 1;
213
+ if (!existing.latestUsedAt || usageEventTime(event) >= Date.parse(existing.latestUsedAt || "1970-01-01T00:00:00.000Z")) {
214
+ existing.latestUsedAt = event.createdAt;
215
+ }
216
+ entryUsage.set(entryId, existing);
217
+ }
218
+
219
+ function usageEntrySummary(entry, usage) {
220
+ return {
221
+ id: entry.id,
222
+ category: entry.category,
223
+ source: entry.source,
224
+ textPreview: previewText(entry.text),
225
+ usageCount: usage?.count || 0,
226
+ latestUsedAt: usage?.latestUsedAt || "",
227
+ commands: usage?.commands || {},
228
+ routes: usage?.routes || {},
229
+ };
230
+ }
231
+
232
+ export function learningStats({ filePath = defaultLearningFile() } = {}) {
233
+ const audit = auditLearningProfile({ filePath });
234
+ const payload = {
235
+ file: filePath,
236
+ exists: audit.exists,
237
+ version: audit.version,
238
+ updatedAt: audit.updatedAt,
239
+ count: audit.count,
240
+ categoryCounts: audit.categoryCounts,
241
+ sourceCounts: {},
242
+ oldestEntry: null,
243
+ latestEntry: null,
244
+ auditSummary: audit.summary,
245
+ };
246
+
247
+ if (!audit.exists || audit.summary.failures > 0) return payload;
248
+
249
+ const profile = loadLearningProfile(filePath);
250
+ const entries = profile.entries.filter((entry) => entry.text);
251
+ payload.sourceCounts = countBy(entries, (entry) => entry.source || "cli");
252
+
253
+ const datedEntries = entries
254
+ .map((entry) => ({ entry, time: Date.parse(entry.createdAt) }))
255
+ .filter(({ time }) => !Number.isNaN(time))
256
+ .sort((a, b) => a.time - b.time);
257
+
258
+ if (datedEntries.length > 0) {
259
+ payload.oldestEntry = statsEntry(datedEntries[0].entry);
260
+ payload.latestEntry = statsEntry(datedEntries[datedEntries.length - 1].entry);
261
+ }
262
+
263
+ return payload;
264
+ }
265
+
266
+ export function learningUsageStats({
267
+ filePath = defaultLearningFile(),
268
+ usageFile = defaultLearningUsageFile(filePath),
269
+ limit = 10,
270
+ } = {}) {
271
+ const resolvedFile = path.resolve(filePath);
272
+ const resolvedUsageFile = path.resolve(usageFile);
273
+ const profileExists = existsSync(resolvedFile);
274
+ const usageExists = existsSync(resolvedUsageFile);
275
+ const profile = loadLearningProfile(resolvedFile);
276
+ const usageLog = loadLearningUsageLog(resolvedUsageFile, { profileFile: resolvedFile });
277
+ const events = usageLog.events;
278
+ const maxRecentEvents = Number.isInteger(limit) && limit > 0 ? limit : 10;
279
+ const sortedEvents = [...events].sort((a, b) => usageEventTime(a) - usageEventTime(b));
280
+ const recentEvents = [...sortedEvents].reverse().slice(0, maxRecentEvents).map(usageEventSummary);
281
+ const entryUsage = new Map();
282
+
283
+ for (const event of events) {
284
+ for (const entryId of event.selectedEntryIds) {
285
+ incrementUsageEntry(entryUsage, entryId, event);
286
+ }
287
+ }
288
+
289
+ const profileEntryIds = new Set(profile.entries.map((entry) => entry.id));
290
+ const selectedEntryIds = [...entryUsage.keys()].sort();
291
+ const usedEntryIds = selectedEntryIds.filter((entryId) => profileEntryIds.has(entryId));
292
+ const staleSelectedEntryIds = selectedEntryIds.filter((entryId) => !profileEntryIds.has(entryId));
293
+ const unusedEntryIds = profile.entries
294
+ .filter((entry) => !entryUsage.has(entry.id))
295
+ .map((entry) => entry.id);
296
+ const topSelectedEntries = profile.entries
297
+ .map((entry) => usageEntrySummary(entry, entryUsage.get(entry.id)))
298
+ .filter((entry) => entry.usageCount > 0)
299
+ .sort((a, b) => b.usageCount - a.usageCount || String(b.latestUsedAt).localeCompare(String(a.latestUsedAt)))
300
+ .slice(0, maxRecentEvents);
301
+ const recommendations = [];
302
+
303
+ if (!usageExists) {
304
+ recommendations.push({
305
+ level: "info",
306
+ text: "No learning usage sidecar exists yet. Run prompt or pack with --with-learning to record local usage metadata.",
307
+ });
308
+ } else if (events.length === 0) {
309
+ recommendations.push({
310
+ level: "info",
311
+ text: "Learning usage sidecar exists but has no events yet.",
312
+ });
313
+ }
314
+ if (profile.entries.length > 0 && unusedEntryIds.length > 0) {
315
+ recommendations.push({
316
+ level: "info",
317
+ text: "Review unused learning entries before curating; unused does not mean obsolete until enough prompt/pack usage has accumulated.",
318
+ });
319
+ }
320
+ if (staleSelectedEntryIds.length > 0) {
321
+ recommendations.push({
322
+ level: "warning",
323
+ text: "Usage sidecar references entry ids that are no longer present in the active learning profile.",
324
+ });
325
+ }
326
+
327
+ return {
328
+ file: resolvedFile,
329
+ usageFile: resolvedUsageFile,
330
+ exists: usageExists,
331
+ profileExists,
332
+ profileFile: usageLog.profileFile || resolvedFile,
333
+ version: usageLog.version,
334
+ updatedAt: usageLog.updatedAt,
335
+ eventCount: events.length,
336
+ profileEntryCount: profile.entries.length,
337
+ usedEntryCount: usedEntryIds.length,
338
+ unusedEntryCount: unusedEntryIds.length,
339
+ staleSelectedEntryCount: staleSelectedEntryIds.length,
340
+ commandCounts: countBy(events, (event) => event.command),
341
+ routeCounts: countBy(events, (event) => event.routeId || "unrouted"),
342
+ categoryCounts: countBy(events, (event) => event.category || "all"),
343
+ auditStatusCounts: countBy(events, (event) => event.auditStatus || "unknown"),
344
+ selectedEntryCounts: Object.fromEntries(
345
+ selectedEntryIds.map((entryId) => [entryId, entryUsage.get(entryId).count]),
346
+ ),
347
+ topSelectedEntries,
348
+ unusedEntryIds,
349
+ staleSelectedEntryIds,
350
+ oldestEvent: sortedEvents.length > 0 ? usageEventSummary(sortedEvents[0]) : null,
351
+ latestEvent: sortedEvents.length > 0 ? usageEventSummary(sortedEvents[sortedEvents.length - 1]) : null,
352
+ recentEvents,
353
+ recommendations,
354
+ privacy: {
355
+ storesRawBriefText: false,
356
+ storesBriefHash: true,
357
+ storesSelectedEntryIds: true,
358
+ },
359
+ };
360
+ }