@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,483 @@
1
+ // Learning profile/usage/eval readiness reports for `design-ai workspace`.
2
+
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import path from "node:path";
5
+
6
+ import { existsSync as retrievalIndexFileExists } from "node:fs";
7
+
8
+ import {
9
+ corpusIndexFile,
10
+ learningIndexFile,
11
+ retrievalIndexStatus,
12
+ } from "./retrieval-index.mjs";
13
+ import {
14
+ defaultLearningFile,
15
+ defaultLearningUsageFile,
16
+ learningEvalReport,
17
+ learningStats,
18
+ learningUsageStats,
19
+ listLearningRestoreBackups,
20
+ } from "./learn.mjs";
21
+
22
+ const DEFAULT_LEARNING_EVAL_FILE = "learning-eval.json";
23
+ const DEFAULT_LEARNING_CURATION_REPORT_FILE = "learning-curation-report.md";
24
+ const DEFAULT_RESTORE_BACKUP_KEEP = 5;
25
+
26
+ export function collectLearningReport({
27
+ filePath = defaultLearningFile(),
28
+ learningStatsProvider = learningStats,
29
+ } = {}) {
30
+ const resolvedFile = path.resolve(filePath);
31
+ try {
32
+ const stats = learningStatsProvider({ filePath: resolvedFile });
33
+ return {
34
+ file: stats.file || resolvedFile,
35
+ exists: Boolean(stats.exists),
36
+ updatedAt: stats.updatedAt || "",
37
+ count: Number.isInteger(stats.count) ? stats.count : 0,
38
+ categoryCounts: stats.categoryCounts || {},
39
+ sourceCounts: stats.sourceCounts || {},
40
+ latestEntry: stats.latestEntry || null,
41
+ auditSummary: stats.auditSummary || { status: "pass", failures: 0, warnings: 0 },
42
+ error: "",
43
+ };
44
+ } catch (error) {
45
+ return {
46
+ file: resolvedFile,
47
+ exists: existsSync(resolvedFile),
48
+ updatedAt: "",
49
+ count: 0,
50
+ categoryCounts: {},
51
+ sourceCounts: {},
52
+ latestEntry: null,
53
+ auditSummary: { status: "fail", failures: 1, warnings: 0 },
54
+ error: error?.message || String(error),
55
+ };
56
+ }
57
+ }
58
+
59
+ export function collectLearningEvalReport({
60
+ learningFilePath = defaultLearningFile(),
61
+ learningEvalPath = "",
62
+ learningEvalReportProvider = learningEvalReport,
63
+ } = {}) {
64
+ if (!learningEvalPath) return null;
65
+
66
+ const resolvedEvalPath = path.resolve(learningEvalPath);
67
+ const resolvedLearningFile = path.resolve(learningFilePath);
68
+ try {
69
+ const evalText = readFileSync(resolvedEvalPath, "utf8");
70
+ const report = learningEvalReportProvider({
71
+ filePath: resolvedLearningFile,
72
+ evalText,
73
+ source: resolvedEvalPath,
74
+ });
75
+ return {
76
+ source: resolvedEvalPath,
77
+ file: report.file,
78
+ status: report.status,
79
+ caseCount: report.caseCount,
80
+ passed: report.passed,
81
+ warned: report.warned,
82
+ failed: report.failed,
83
+ generatedAt: report.generatedAt || "",
84
+ sourceProfile: report.sourceProfile || null,
85
+ profileExists: report.profileExists,
86
+ profileEntryCount: report.profileEntryCount,
87
+ auditSummary: report.auditSummary,
88
+ privacy: report.privacy,
89
+ error: "",
90
+ };
91
+ } catch (error) {
92
+ return {
93
+ source: resolvedEvalPath,
94
+ file: resolvedLearningFile,
95
+ status: "fail",
96
+ caseCount: 0,
97
+ passed: 0,
98
+ warned: 0,
99
+ failed: 0,
100
+ generatedAt: "",
101
+ sourceProfile: null,
102
+ profileExists: existsSync(resolvedLearningFile),
103
+ profileEntryCount: 0,
104
+ auditSummary: { status: "fail", failures: 1, warnings: 0 },
105
+ privacy: {
106
+ storesRawBriefText: false,
107
+ storesBriefHash: true,
108
+ exposesMatchedTokens: false,
109
+ },
110
+ error: error?.message || String(error),
111
+ };
112
+ }
113
+ }
114
+
115
+ export function assessLearningRestoreBackupReadiness({
116
+ totalCount = 0,
117
+ keep = DEFAULT_RESTORE_BACKUP_KEEP,
118
+ error = "",
119
+ } = {}) {
120
+ if (error) {
121
+ return {
122
+ status: "fail",
123
+ reason: "restore-backup-inventory-error",
124
+ keep,
125
+ totalCount,
126
+ pruneCandidateCount: 0,
127
+ };
128
+ }
129
+
130
+ if (totalCount <= 0) {
131
+ return {
132
+ status: "unknown",
133
+ reason: "no restore rollback backups found",
134
+ keep,
135
+ totalCount,
136
+ pruneCandidateCount: 0,
137
+ };
138
+ }
139
+
140
+ const pruneCandidateCount = Math.max(0, totalCount - keep);
141
+ if (pruneCandidateCount > 0) {
142
+ return {
143
+ status: "warn",
144
+ reason: `${pruneCandidateCount} older restore rollback backup(s) can be pruned`,
145
+ keep,
146
+ totalCount,
147
+ pruneCandidateCount,
148
+ };
149
+ }
150
+
151
+ return {
152
+ status: "pass",
153
+ reason: "",
154
+ keep,
155
+ totalCount,
156
+ pruneCandidateCount,
157
+ };
158
+ }
159
+
160
+ export function collectLearningRestoreBackupsReport({
161
+ learningFilePath = defaultLearningFile(),
162
+ limit = DEFAULT_RESTORE_BACKUP_KEEP,
163
+ keep = DEFAULT_RESTORE_BACKUP_KEEP,
164
+ learningRestoreBackupsProvider = listLearningRestoreBackups,
165
+ } = {}) {
166
+ const resolvedLearningFile = path.resolve(learningFilePath);
167
+
168
+ try {
169
+ const inventory = learningRestoreBackupsProvider({
170
+ filePath: resolvedLearningFile,
171
+ limit,
172
+ });
173
+ const totalCount = Number.isInteger(inventory.totalCount) ? inventory.totalCount : 0;
174
+ if (totalCount <= 0) return null;
175
+
176
+ const backups = Array.isArray(inventory.backups) ? inventory.backups : [];
177
+ const readiness = assessLearningRestoreBackupReadiness({ totalCount, keep });
178
+
179
+ return {
180
+ file: inventory.file || resolvedLearningFile,
181
+ directory: inventory.directory || path.dirname(resolvedLearningFile),
182
+ pattern: inventory.pattern || "",
183
+ generatedAt: inventory.generatedAt || "",
184
+ limit: Number.isInteger(inventory.limit) ? inventory.limit : limit,
185
+ totalCount,
186
+ count: Number.isInteger(inventory.count) ? inventory.count : backups.length,
187
+ latestBackup: backups[0] || null,
188
+ backups,
189
+ readiness,
190
+ privacy: inventory.privacy || {
191
+ storesRawBriefText: false,
192
+ exposesEntryTextPreview: false,
193
+ mutatesProfile: false,
194
+ },
195
+ error: "",
196
+ };
197
+ } catch (error) {
198
+ const message = error?.message || String(error);
199
+ return {
200
+ file: resolvedLearningFile,
201
+ directory: path.dirname(resolvedLearningFile),
202
+ pattern: "",
203
+ generatedAt: "",
204
+ limit,
205
+ totalCount: 0,
206
+ count: 0,
207
+ latestBackup: null,
208
+ backups: [],
209
+ readiness: assessLearningRestoreBackupReadiness({ totalCount: 0, keep, error: message }),
210
+ privacy: {
211
+ storesRawBriefText: false,
212
+ exposesEntryTextPreview: false,
213
+ mutatesProfile: false,
214
+ },
215
+ error: message,
216
+ };
217
+ }
218
+ }
219
+
220
+ export function defaultLearningEvalPath(learningFilePath = defaultLearningFile()) {
221
+ return path.join(path.dirname(path.resolve(learningFilePath)), DEFAULT_LEARNING_EVAL_FILE);
222
+ }
223
+
224
+ export function defaultLearningUsagePath(learningFilePath = defaultLearningFile()) {
225
+ return defaultLearningUsageFile(learningFilePath);
226
+ }
227
+
228
+ export function action(level, text, command = "") {
229
+ return command ? { level, text, command } : { level, text };
230
+ }
231
+
232
+ export function quoteShellArg(value) {
233
+ const text = String(value ?? "");
234
+ if (/^[A-Za-z0-9_@%+=:,./-]+$/u.test(text)) return text;
235
+ return `'${text.replace(/'/gu, "'\\''")}'`;
236
+ }
237
+
238
+ export function pushUniqueAction(actions, nextAction) {
239
+ if (nextAction.command && actions.some((item) => item.command === nextAction.command)) return;
240
+ actions.push(nextAction);
241
+ }
242
+
243
+ export function learningCurationCommand(learning, learningUsage = null) {
244
+ const usagePart = learningUsage?.usageFile
245
+ ? ` --usage-file ${quoteShellArg(learningUsage.usageFile)}`
246
+ : "";
247
+ return `design-ai learn --curate --file ${quoteShellArg(learning.file)}${usagePart}`;
248
+ }
249
+
250
+ export function defaultLearningCurationReportPath(learningFilePath = defaultLearningFile()) {
251
+ return path.join(path.dirname(path.resolve(learningFilePath)), DEFAULT_LEARNING_CURATION_REPORT_FILE);
252
+ }
253
+
254
+ export function learningCurationReportCommand(learning, learningUsage = null) {
255
+ return `${learningCurationCommand(learning, learningUsage)} --report --out ${quoteShellArg(defaultLearningCurationReportPath(learning.file))}`;
256
+ }
257
+
258
+ export function learningRestoreBackupsCommand(learning) {
259
+ return `design-ai learn --restore-backups --file ${quoteShellArg(learning.file)}`;
260
+ }
261
+
262
+ export function learningRestoreBackupPruneCommand(learning, keep = DEFAULT_RESTORE_BACKUP_KEEP) {
263
+ return `${learningRestoreBackupsCommand(learning)} --prune --keep ${keep}`;
264
+ }
265
+
266
+ function parsedTimestamp(value) {
267
+ const time = Date.parse(String(value || ""));
268
+ return Number.isNaN(time) ? null : time;
269
+ }
270
+
271
+ export function assessLearningEvalFreshness({ learning, learningEval } = {}) {
272
+ const profileUpdatedAt = learning?.updatedAt || "";
273
+ const checkpointGeneratedAt = learningEval?.generatedAt || "";
274
+ const sourceProfile = learningEval?.sourceProfile || null;
275
+ const issues = [];
276
+
277
+ if (!learningEval) return null;
278
+
279
+ if (sourceProfile?.file && path.resolve(sourceProfile.file) !== path.resolve(learning?.file || "")) {
280
+ issues.push("source-profile-file-mismatch");
281
+ }
282
+
283
+ if (
284
+ Number.isInteger(sourceProfile?.entryCount)
285
+ && Number.isInteger(learning?.count)
286
+ && sourceProfile.entryCount !== learning.count
287
+ ) {
288
+ issues.push("source-profile-entry-count-changed");
289
+ }
290
+
291
+ const profileUpdatedTime = parsedTimestamp(profileUpdatedAt);
292
+ const checkpointGeneratedTime = parsedTimestamp(checkpointGeneratedAt);
293
+ if (
294
+ profileUpdatedTime !== null
295
+ && checkpointGeneratedTime !== null
296
+ && profileUpdatedTime > checkpointGeneratedTime
297
+ ) {
298
+ issues.push("profile-updated-after-checkpoint");
299
+ }
300
+
301
+ if (issues.length > 0) {
302
+ return {
303
+ status: "warn",
304
+ stale: true,
305
+ reason: issues.join(", "),
306
+ profileUpdatedAt,
307
+ checkpointGeneratedAt,
308
+ sourceProfileFile: sourceProfile?.file || "",
309
+ sourceProfileEntryCount: Number.isInteger(sourceProfile?.entryCount) ? sourceProfile.entryCount : null,
310
+ };
311
+ }
312
+
313
+ if (!checkpointGeneratedAt && !sourceProfile) {
314
+ return {
315
+ status: "unknown",
316
+ stale: false,
317
+ reason: "checkpoint metadata unavailable",
318
+ profileUpdatedAt,
319
+ checkpointGeneratedAt,
320
+ sourceProfileFile: "",
321
+ sourceProfileEntryCount: null,
322
+ };
323
+ }
324
+
325
+ return {
326
+ status: "pass",
327
+ stale: false,
328
+ reason: "",
329
+ profileUpdatedAt,
330
+ checkpointGeneratedAt,
331
+ sourceProfileFile: sourceProfile?.file || "",
332
+ sourceProfileEntryCount: Number.isInteger(sourceProfile?.entryCount) ? sourceProfile.entryCount : null,
333
+ };
334
+ }
335
+
336
+ export function collectLearningUsageReport({
337
+ learningFilePath = defaultLearningFile(),
338
+ learningUsagePath = "",
339
+ learningUsageStatsProvider = learningUsageStats,
340
+ } = {}) {
341
+ if (!learningUsagePath) return null;
342
+
343
+ const resolvedLearningFile = path.resolve(learningFilePath);
344
+ const resolvedUsagePath = path.resolve(learningUsagePath);
345
+ try {
346
+ const stats = learningUsageStatsProvider({
347
+ filePath: resolvedLearningFile,
348
+ usageFile: resolvedUsagePath,
349
+ });
350
+ return {
351
+ file: stats.file || resolvedLearningFile,
352
+ usageFile: stats.usageFile || resolvedUsagePath,
353
+ exists: Boolean(stats.exists),
354
+ profileExists: Boolean(stats.profileExists),
355
+ profileFile: stats.profileFile || resolvedLearningFile,
356
+ version: Number.isInteger(stats.version) ? stats.version : 1,
357
+ updatedAt: stats.updatedAt || "",
358
+ eventCount: Number.isInteger(stats.eventCount) ? stats.eventCount : 0,
359
+ profileEntryCount: Number.isInteger(stats.profileEntryCount) ? stats.profileEntryCount : 0,
360
+ usedEntryCount: Number.isInteger(stats.usedEntryCount) ? stats.usedEntryCount : 0,
361
+ unusedEntryCount: Number.isInteger(stats.unusedEntryCount) ? stats.unusedEntryCount : 0,
362
+ staleSelectedEntryCount: Number.isInteger(stats.staleSelectedEntryCount) ? stats.staleSelectedEntryCount : 0,
363
+ commandCounts: stats.commandCounts || {},
364
+ routeCounts: stats.routeCounts || {},
365
+ categoryCounts: stats.categoryCounts || {},
366
+ auditStatusCounts: stats.auditStatusCounts || {},
367
+ latestEvent: stats.latestEvent || null,
368
+ privacy: stats.privacy || {
369
+ storesRawBriefText: false,
370
+ storesBriefHash: true,
371
+ storesSelectedEntryIds: true,
372
+ },
373
+ recommendations: Array.isArray(stats.recommendations) ? stats.recommendations : [],
374
+ readiness: assessLearningUsageReadiness({ learningFilePath: resolvedLearningFile, usage: stats }),
375
+ error: "",
376
+ };
377
+ } catch (error) {
378
+ return {
379
+ file: resolvedLearningFile,
380
+ usageFile: resolvedUsagePath,
381
+ exists: existsSync(resolvedUsagePath),
382
+ profileExists: existsSync(resolvedLearningFile),
383
+ profileFile: resolvedLearningFile,
384
+ version: 1,
385
+ updatedAt: "",
386
+ eventCount: 0,
387
+ profileEntryCount: 0,
388
+ usedEntryCount: 0,
389
+ unusedEntryCount: 0,
390
+ staleSelectedEntryCount: 0,
391
+ commandCounts: {},
392
+ routeCounts: {},
393
+ categoryCounts: {},
394
+ auditStatusCounts: {},
395
+ latestEvent: null,
396
+ privacy: {
397
+ storesRawBriefText: false,
398
+ storesBriefHash: true,
399
+ storesSelectedEntryIds: true,
400
+ },
401
+ recommendations: [],
402
+ readiness: {
403
+ status: "fail",
404
+ reason: "usage-sidecar-error",
405
+ profileFile: resolvedLearningFile,
406
+ profileFileMatches: true,
407
+ staleSelectedEntryCount: 0,
408
+ },
409
+ error: error?.message || String(error),
410
+ };
411
+ }
412
+ }
413
+
414
+ export function assessLearningUsageReadiness({ learningFilePath = defaultLearningFile(), usage } = {}) {
415
+ if (!usage) return null;
416
+
417
+ const resolvedLearningFile = path.resolve(learningFilePath);
418
+ const profileFile = usage.profileFile ? path.resolve(usage.profileFile) : "";
419
+ const profileFileMatches = profileFile ? profileFile === resolvedLearningFile : true;
420
+ const staleSelectedEntryCount = Number.isInteger(usage.staleSelectedEntryCount)
421
+ ? usage.staleSelectedEntryCount
422
+ : 0;
423
+ const issues = [];
424
+
425
+ if (!profileFileMatches) issues.push("usage-profile-file-mismatch");
426
+ if (staleSelectedEntryCount > 0) issues.push("stale-selected-entry-ids");
427
+
428
+ if (issues.length > 0) {
429
+ return {
430
+ status: "warn",
431
+ reason: issues.join(", "),
432
+ profileFile: usage.profileFile || "",
433
+ profileFileMatches,
434
+ staleSelectedEntryCount,
435
+ };
436
+ }
437
+
438
+ if (!usage.exists || usage.eventCount === 0) {
439
+ return {
440
+ status: "unknown",
441
+ reason: usage.exists ? "usage sidecar has no events" : "usage sidecar unavailable",
442
+ profileFile: usage.profileFile || "",
443
+ profileFileMatches,
444
+ staleSelectedEntryCount,
445
+ };
446
+ }
447
+
448
+ return {
449
+ status: "pass",
450
+ reason: "",
451
+ profileFile: usage.profileFile || "",
452
+ profileFileMatches,
453
+ staleSelectedEntryCount,
454
+ };
455
+ }
456
+
457
+ // Retrieval-index readiness (docs/AI-LEARNING-PHASE2.md, Phase A): the index is an
458
+ // opt-in derived cache, so a missing index is "unused" (null section), while a
459
+ // present-but-stale or unreadable index is a readiness warning.
460
+ export function collectRetrievalIndexReport({
461
+ sourceRoot,
462
+ learningFilePath = defaultLearningFile(),
463
+ retrievalIndexStatusProvider = retrievalIndexStatus,
464
+ } = {}) {
465
+ const corpusFile = corpusIndexFile();
466
+ const learningIdxFile = learningIndexFile();
467
+ if (!retrievalIndexFileExists(corpusFile) && !retrievalIndexFileExists(learningIdxFile)) {
468
+ return null;
469
+ }
470
+
471
+ const status = retrievalIndexStatusProvider({
472
+ designAiPath: sourceRoot,
473
+ learningFile: learningFilePath,
474
+ });
475
+ return {
476
+ indexDir: status.indexDir,
477
+ corpus: status.corpus,
478
+ learning: status.learning,
479
+ fresh: status.fresh,
480
+ status: status.fresh ? "pass" : "warn",
481
+ buildCommand: status.buildCommand,
482
+ };
483
+ }
@@ -0,0 +1,139 @@
1
+ // Repository metadata and release-script readiness for `design-ai workspace`.
2
+
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import path from "node:path";
5
+
6
+ import { DESIGN_AI_HOME } from "./paths.mjs";
7
+
8
+ const RELEASE_SCRIPT_NAMES = [
9
+ "test",
10
+ "audit:strict",
11
+ "release:metadata",
12
+ "release:self-test",
13
+ "package:smoke",
14
+ "release:check",
15
+ "ci:local",
16
+ ];
17
+
18
+ const UNIQUE_RELEASE_SCRIPT_NAMES = [...new Set(RELEASE_SCRIPT_NAMES)];
19
+ const CANONICAL_REPOSITORY_SLUG = "sungjin9288/design-ai";
20
+ const CANONICAL_REPOSITORY_URL = `https://github.com/${CANONICAL_REPOSITORY_SLUG}`;
21
+
22
+ function safeReadJsonFile(filePath) {
23
+ try {
24
+ return JSON.parse(readFileSync(filePath, "utf8"));
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ function safeReadPackageJson(sourceRoot) {
31
+ return safeReadJsonFile(path.join(sourceRoot, "package.json"));
32
+ }
33
+
34
+ function safeReadPluginJson(sourceRoot) {
35
+ return safeReadJsonFile(path.join(sourceRoot, ".claude-plugin", "plugin.json"));
36
+ }
37
+
38
+ export function collectReleaseScriptReport({ sourceRoot = DESIGN_AI_HOME } = {}) {
39
+ const packageJson = safeReadPackageJson(sourceRoot);
40
+ const scripts = packageJson?.scripts && typeof packageJson.scripts === "object"
41
+ ? packageJson.scripts
42
+ : {};
43
+ const available = UNIQUE_RELEASE_SCRIPT_NAMES.filter((name) => typeof scripts[name] === "string");
44
+ const missing = UNIQUE_RELEASE_SCRIPT_NAMES.filter((name) => typeof scripts[name] !== "string");
45
+
46
+ return {
47
+ packageName: packageJson?.name || "",
48
+ version: packageJson?.version || "",
49
+ scripts: Object.fromEntries(available.map((name) => [name, scripts[name]])),
50
+ available,
51
+ missing,
52
+ };
53
+ }
54
+
55
+ export function normalizeRepositoryUrl(value) {
56
+ let text = String(value || "").trim();
57
+ if (!text) return "";
58
+ if (text.startsWith("git+")) text = text.slice(4);
59
+ text = text.replace(/\.git$/u, "");
60
+
61
+ const scpMatch = text.match(/^git@github\.com:(?<slug>[^/]+\/[^/]+)$/u);
62
+ if (scpMatch?.groups?.slug) return `https://github.com/${scpMatch.groups.slug}`;
63
+
64
+ const sshMatch = text.match(/^ssh:\/\/git@github\.com\/(?<slug>[^/]+\/[^/]+)$/u);
65
+ if (sshMatch?.groups?.slug) return `https://github.com/${sshMatch.groups.slug}`;
66
+
67
+ const httpsMatch = text.match(/^https?:\/\/github\.com\/(?<slug>[^/]+\/[^/]+)$/u);
68
+ if (httpsMatch?.groups?.slug) return `https://github.com/${httpsMatch.groups.slug}`;
69
+
70
+ return text;
71
+ }
72
+
73
+ export function repositorySlugFromUrl(value) {
74
+ const normalized = normalizeRepositoryUrl(value);
75
+ const match = normalized.match(/^https:\/\/github\.com\/(?<slug>[^/]+\/[^/]+)$/u);
76
+ return match?.groups?.slug || "";
77
+ }
78
+
79
+ export function collectRepositoryReport({
80
+ sourceRoot = DESIGN_AI_HOME,
81
+ git = null,
82
+ } = {}) {
83
+ const packageJson = safeReadPackageJson(sourceRoot) || {};
84
+ const pluginJson = safeReadPluginJson(sourceRoot) || {};
85
+ const expectedPackageRepositoryUrl = `git+${CANONICAL_REPOSITORY_URL}.git`;
86
+ const expectedPackageHomepage = `${CANONICAL_REPOSITORY_URL}#readme`;
87
+ const expectedPackageBugsUrl = `${CANONICAL_REPOSITORY_URL}/issues`;
88
+ const expectedPluginUrl = CANONICAL_REPOSITORY_URL;
89
+
90
+ const packageRepositoryUrl = typeof packageJson.repository === "object" && packageJson.repository
91
+ ? packageJson.repository.url || ""
92
+ : "";
93
+ const packageHomepage = packageJson.homepage || "";
94
+ const packageBugsUrl = typeof packageJson.bugs === "object" && packageJson.bugs
95
+ ? packageJson.bugs.url || ""
96
+ : "";
97
+ const pluginHomepage = pluginJson.homepage || "";
98
+ const pluginRepository = pluginJson.repository || "";
99
+
100
+ const metadataChecks = [
101
+ ["package.repository.url", packageRepositoryUrl, expectedPackageRepositoryUrl],
102
+ ["package.homepage", packageHomepage, expectedPackageHomepage],
103
+ ["package.bugs.url", packageBugsUrl, expectedPackageBugsUrl],
104
+ ["plugin.homepage", pluginHomepage, expectedPluginUrl],
105
+ ["plugin.repository", pluginRepository, expectedPluginUrl],
106
+ ].map(([label, actual, expected]) => ({
107
+ label,
108
+ actual,
109
+ expected,
110
+ aligned: actual === expected,
111
+ }));
112
+
113
+ const issues = metadataChecks
114
+ .filter((check) => !check.aligned)
115
+ .map((check) => `${check.label} mismatch: ${check.actual || "missing"} != ${check.expected}`);
116
+
117
+ const remoteUrl = git?.remote || "";
118
+ const remoteSlug = repositorySlugFromUrl(remoteUrl);
119
+ const remoteAligned = git?.isRepo && remoteUrl ? remoteSlug === CANONICAL_REPOSITORY_SLUG : null;
120
+ if (remoteAligned === false) {
121
+ issues.push(`git remote origin points to ${remoteSlug || remoteUrl}, expected ${CANONICAL_REPOSITORY_SLUG}`);
122
+ }
123
+
124
+ return {
125
+ slug: CANONICAL_REPOSITORY_SLUG,
126
+ url: CANONICAL_REPOSITORY_URL,
127
+ expectedRemoteUrl: `${CANONICAL_REPOSITORY_URL}.git`,
128
+ packageRepositoryUrl,
129
+ packageHomepage,
130
+ packageBugsUrl,
131
+ pluginHomepage,
132
+ pluginRepository,
133
+ metadataAligned: metadataChecks.every((check) => check.aligned),
134
+ remoteUrl,
135
+ remoteSlug,
136
+ remoteAligned,
137
+ issues,
138
+ };
139
+ }