@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.
- package/.claude-plugin/plugin.json +109 -25
- package/CHANGELOG.md +36 -0
- package/README.ko.md +9 -26
- package/README.md +10 -23
- package/cli/bin/design-ai.mjs +1 -0
- package/cli/commands/help.mjs +7 -4
- package/cli/commands/index.mjs +292 -0
- package/cli/commands/learn-help.mjs +149 -0
- package/cli/commands/learn-print-profile.mjs +373 -0
- package/cli/commands/learn-print-restore.mjs +349 -0
- package/cli/commands/learn-print-signals.mjs +453 -0
- package/cli/commands/learn.mjs +60 -1281
- package/cli/commands/pack.mjs +7 -3
- package/cli/commands/prompt.mjs +7 -3
- package/cli/commands/search.mjs +94 -1
- package/cli/lib/dispatch.mjs +3 -0
- package/cli/lib/embedding-index.mjs +199 -0
- package/cli/lib/embedding-provider.mjs +121 -0
- package/cli/lib/embedding-rerank.mjs +52 -0
- package/cli/lib/learn-args.mjs +490 -0
- package/cli/lib/learn-backup.mjs +748 -0
- package/cli/lib/learn-curation.mjs +612 -0
- package/cli/lib/learn-eval.mjs +459 -0
- package/cli/lib/learn-profile.mjs +763 -0
- package/cli/lib/learn-select.mjs +223 -0
- package/cli/lib/learn-shared.mjs +158 -0
- package/cli/lib/learn-test-support.mjs +218 -0
- package/cli/lib/learn-usage.mjs +360 -0
- package/cli/lib/learn.mjs +83 -3694
- package/cli/lib/lexical.mjs +137 -0
- package/cli/lib/local-config.mjs +110 -0
- package/cli/lib/mcp-server.mjs +13 -3
- package/cli/lib/pack.mjs +31 -4
- package/cli/lib/prompt.mjs +42 -4
- package/cli/lib/recall.mjs +193 -0
- package/cli/lib/retrieval-index.mjs +0 -0
- package/cli/lib/search-ranked.mjs +170 -0
- package/cli/lib/search.mjs +11 -1
- package/cli/lib/signals-backlog-commands.mjs +673 -0
- package/cli/lib/signals-backlog.mjs +361 -0
- package/cli/lib/signals-eval.mjs +176 -0
- package/cli/lib/signals-readiness.mjs +247 -0
- package/cli/lib/signals-registry.mjs +377 -0
- package/cli/lib/signals-render.mjs +478 -0
- package/cli/lib/signals-shared.mjs +75 -0
- package/cli/lib/signals.mjs +16 -2337
- package/cli/lib/site-bundle-handoff-expected.mjs +173 -0
- package/cli/lib/site-bundle-handoff-runbook-action-summary.mjs +332 -0
- package/cli/lib/site-bundle-handoff-runbook-human-lines.mjs +167 -0
- package/cli/lib/site-bundle-handoff-runbook-maps.mjs +238 -0
- package/cli/lib/site-bundle-handoff-runbook-next-step.mjs +278 -0
- package/cli/lib/site-bundle-handoff-runbook.mjs +44 -931
- package/cli/lib/site-test-support.mjs +68 -0
- package/cli/lib/skill-proposals-apply-commands.mjs +135 -0
- package/cli/lib/skill-proposals-apply-contract.mjs +750 -0
- package/cli/lib/skill-proposals-apply-plan.mjs +118 -0
- package/cli/lib/skill-proposals-generate.mjs +298 -0
- package/cli/lib/skill-proposals-render.mjs +532 -0
- package/cli/lib/skill-proposals-review.mjs +262 -0
- package/cli/lib/skill-proposals.mjs +15 -2046
- package/cli/lib/workspace-args.mjs +83 -0
- package/cli/lib/workspace-git.mjs +169 -0
- package/cli/lib/workspace-learning.mjs +483 -0
- package/cli/lib/workspace-repo.mjs +139 -0
- package/cli/lib/workspace-report.mjs +283 -0
- package/cli/lib/workspace-test-support.mjs +99 -0
- package/cli/lib/workspace.mjs +30 -1056
- package/docs/AI-LEARNING-PHASE2.md +215 -0
- package/docs/DISTRIBUTION.ko.md +2 -2
- package/docs/DISTRIBUTION.md +2 -2
- package/docs/NEXT-SURFACE-DECISION.md +125 -0
- package/docs/PRODUCT-READINESS.md +3 -3
- package/docs/RELEASE-GATES.ko.md +35 -0
- package/docs/RELEASE-GATES.md +234 -0
- package/docs/ROADMAP.md +53 -8
- package/docs/external-status.md +6 -8
- package/docs/inspection-20260630.md +169 -0
- package/docs/integrations/design-ai-mcp-server.md +4 -4
- package/docs/reference/ant-design.md +413 -0
- package/docs/reference/awesome-design-md.md +439 -0
- package/docs/reference/mui.md +783 -0
- package/docs/reference/shadcn-ui.md +298 -0
- package/examples/component-accordion-actions.md +5 -5
- package/examples/component-accordion-details.md +5 -5
- package/examples/component-accordion-summary.md +5 -5
- package/examples/component-accordion.md +3 -3
- package/examples/component-affix.md +1 -1
- package/examples/component-alert-dialog.md +1 -1
- package/examples/component-alert-title.md +1 -1
- package/examples/component-alert.md +3 -3
- package/examples/component-anchor.md +1 -1
- package/examples/component-app-bar.md +1 -1
- package/examples/component-aspect-ratio.md +1 -1
- package/examples/component-auto-complete.md +2 -2
- package/examples/component-avatar-group.md +5 -5
- package/examples/component-avatar.md +3 -3
- package/examples/component-back-top.md +1 -1
- package/examples/component-backdrop.md +1 -1
- package/examples/component-badge.md +3 -3
- package/examples/component-border-beam.md +3 -3
- package/examples/component-bottom-navigation.md +1 -1
- package/examples/component-box.md +1 -1
- package/examples/component-breadcrumb.md +3 -3
- package/examples/component-button-base.md +3 -3
- package/examples/component-button-group.md +2 -2
- package/examples/component-button.md +3 -3
- package/examples/component-calendar.md +2 -2
- package/examples/component-card-actions.md +1 -1
- package/examples/component-card-content.md +1 -1
- package/examples/component-card-header.md +1 -1
- package/examples/component-card-media.md +1 -1
- package/examples/component-card.md +3 -3
- package/examples/component-carousel.md +2 -2
- package/examples/component-cascader.md +1 -1
- package/examples/component-chart.md +1 -1
- package/examples/component-checkbox.md +3 -3
- package/examples/component-click-away-listener.md +1 -1
- package/examples/component-code.md +1 -1
- package/examples/component-collapsible.md +1 -1
- package/examples/component-color-picker.md +1 -1
- package/examples/component-combobox.md +1 -1
- package/examples/component-command.md +1 -1
- package/examples/component-config-provider.md +3 -3
- package/examples/component-context-menu.md +1 -1
- package/examples/component-css-baseline.md +3 -3
- package/examples/component-date-picker.md +2 -2
- package/examples/component-descriptions.md +1 -1
- package/examples/component-dialog-actions.md +1 -1
- package/examples/component-dialog-content-text.md +1 -1
- package/examples/component-dialog-content.md +1 -1
- package/examples/component-dialog-title.md +1 -1
- package/examples/component-dialog.md +2 -2
- package/examples/component-divider.md +3 -3
- package/examples/component-drawer.md +3 -3
- package/examples/component-dropdown.md +3 -3
- package/examples/component-empty.md +2 -2
- package/examples/component-fade.md +1 -1
- package/examples/component-field.md +1 -1
- package/examples/component-filled-input.md +1 -1
- package/examples/component-flex.md +1 -1
- package/examples/component-float-button.md +2 -2
- package/examples/component-form-control-label.md +1 -1
- package/examples/component-form-control.md +1 -1
- package/examples/component-form-controls.md +1 -1
- package/examples/component-form-group.md +1 -1
- package/examples/component-form-helper-text.md +1 -1
- package/examples/component-form-label.md +1 -1
- package/examples/component-form.md +2 -2
- package/examples/component-grid.md +2 -2
- package/examples/component-grow.md +1 -1
- package/examples/component-hover-card.md +1 -1
- package/examples/component-icon-button.md +1 -1
- package/examples/component-icon.md +1 -1
- package/examples/component-image-list.md +1 -1
- package/examples/component-image.md +1 -1
- package/examples/component-input-adornment.md +1 -1
- package/examples/component-input-base.md +1 -1
- package/examples/component-input-number.md +1 -1
- package/examples/component-input-otp.md +2 -2
- package/examples/component-input.md +3 -3
- package/examples/component-item.md +1 -1
- package/examples/component-label.md +1 -1
- package/examples/component-layout.md +1 -1
- package/examples/component-link.md +1 -1
- package/examples/component-list-item-avatar.md +1 -1
- package/examples/component-list-item-button.md +1 -1
- package/examples/component-list-item-icon.md +1 -1
- package/examples/component-list-item-text.md +1 -1
- package/examples/component-list-item.md +1 -1
- package/examples/component-list-subheader.md +1 -1
- package/examples/component-list.md +2 -2
- package/examples/component-masonry.md +1 -1
- package/examples/component-mentions.md +1 -1
- package/examples/component-menu-item.md +1 -1
- package/examples/component-menu-list.md +1 -1
- package/examples/component-menu.md +1 -1
- package/examples/component-menubar.md +1 -1
- package/examples/component-message.md +1 -1
- package/examples/component-mobile-stepper.md +1 -1
- package/examples/component-modal.md +5 -5
- package/examples/component-navigation-menu.md +1 -1
- package/examples/component-notification.md +1 -1
- package/examples/component-outlined-input.md +1 -1
- package/examples/component-pagination.md +3 -3
- package/examples/component-paper.md +1 -1
- package/examples/component-popconfirm.md +1 -1
- package/examples/component-popover.md +3 -3
- package/examples/component-popper.md +1 -1
- package/examples/component-progress.md +2 -2
- package/examples/component-qr-code.md +1 -1
- package/examples/component-radio.md +3 -3
- package/examples/component-rate.md +2 -2
- package/examples/component-resizable.md +1 -1
- package/examples/component-result.md +1 -1
- package/examples/component-scroll-area.md +1 -1
- package/examples/component-segmented.md +3 -3
- package/examples/component-select.md +3 -3
- package/examples/component-separator.md +1 -1
- package/examples/component-sheet.md +1 -1
- package/examples/component-sidebar.md +1 -1
- package/examples/component-skeleton.md +3 -3
- package/examples/component-slide.md +1 -1
- package/examples/component-slider.md +3 -3
- package/examples/component-snackbar-content.md +1 -1
- package/examples/component-snackbar.md +1 -1
- package/examples/component-sonner.md +1 -1
- package/examples/component-space.md +1 -1
- package/examples/component-speed-dial-action.md +1 -1
- package/examples/component-speed-dial.md +1 -1
- package/examples/component-spin.md +2 -2
- package/examples/component-spinner.md +1 -1
- package/examples/component-splitter.md +2 -2
- package/examples/component-stack.md +1 -1
- package/examples/component-statistic.md +1 -1
- package/examples/component-step-button.md +4 -4
- package/examples/component-step-connector.md +4 -4
- package/examples/component-step-content.md +1 -1
- package/examples/component-step-icon.md +1 -1
- package/examples/component-step-label.md +1 -1
- package/examples/component-step.md +2 -2
- package/examples/component-steps.md +2 -2
- package/examples/component-swipeable-drawer.md +1 -1
- package/examples/component-switch.md +3 -3
- package/examples/component-tab-scroll-button.md +5 -5
- package/examples/component-tab.md +1 -1
- package/examples/component-table-body.md +1 -1
- package/examples/component-table-cell.md +1 -1
- package/examples/component-table-container.md +1 -1
- package/examples/component-table-footer.md +1 -1
- package/examples/component-table-head.md +1 -1
- package/examples/component-table-pagination.md +1 -1
- package/examples/component-table-row.md +1 -1
- package/examples/component-table-sort-label.md +1 -1
- package/examples/component-table.md +2 -2
- package/examples/component-tabs.md +3 -3
- package/examples/component-tag-badge.md +2 -2
- package/examples/component-tag.md +1 -1
- package/examples/component-textarea-autosize.md +1 -1
- package/examples/component-textarea.md +1 -1
- package/examples/component-time-picker.md +1 -1
- package/examples/component-timeline.md +1 -1
- package/examples/component-toast.md +4 -4
- package/examples/component-toggle-button.md +1 -1
- package/examples/component-toggle.md +1 -1
- package/examples/component-toolbar.md +1 -1
- package/examples/component-tooltip.md +3 -3
- package/examples/component-tour.md +1 -1
- package/examples/component-transfer.md +1 -1
- package/examples/component-tree-select.md +1 -1
- package/examples/component-tree.md +1 -1
- package/examples/component-typography.md +2 -2
- package/examples/component-upload.md +1 -1
- package/examples/component-watermark.md +1 -1
- package/examples/component-zoom.md +1 -1
- package/knowledge/COVERAGE.md +4 -3
- package/knowledge/components/INDEX.md +202 -202
- package/knowledge/patterns/brand-references.md +72 -72
- package/package.json +1 -1
- package/tools/audit/local-ci.py +16 -1
- package/tools/audit/package-smoke.py +611 -9
- package/tools/audit/registry-smoke.py +534 -0
- package/tools/audit/release-metadata.py +47 -1
- package/tools/audit/smoke_assertions.py +915 -13
- package/tools/migrations/refs-links-to-reference-pages.py +179 -0
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
// Learning profile backup, restore, import, redact, and diff for `design-ai learn`.
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
statSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
|
|
12
|
+
import { normalizeCategory } from "./learn-args.mjs";
|
|
13
|
+
import {
|
|
14
|
+
auditLearningProfile,
|
|
15
|
+
auditLearningProfileObject,
|
|
16
|
+
emptyLearningProfile,
|
|
17
|
+
learningAuditIssue,
|
|
18
|
+
loadLearningProfile,
|
|
19
|
+
normalizeLearningProfile,
|
|
20
|
+
} from "./learn-profile.mjs";
|
|
21
|
+
import {
|
|
22
|
+
LEARNING_SENSITIVE_PATTERNS,
|
|
23
|
+
cleanNoteText,
|
|
24
|
+
commandFromArgs,
|
|
25
|
+
defaultLearningFile,
|
|
26
|
+
defaultLearningRestoreBackupFile,
|
|
27
|
+
learningEntryMergeKey,
|
|
28
|
+
parseLearningProfilePayload,
|
|
29
|
+
previewText,
|
|
30
|
+
shortEntryId,
|
|
31
|
+
statsEntry,
|
|
32
|
+
uniqueImportedEntryId,
|
|
33
|
+
writeLearningProfile,
|
|
34
|
+
} from "./learn-shared.mjs";
|
|
35
|
+
|
|
36
|
+
function learningRestoreBackupPattern(filePath = defaultLearningFile()) {
|
|
37
|
+
const resolvedFile = path.resolve(filePath);
|
|
38
|
+
const parsed = path.parse(resolvedFile);
|
|
39
|
+
const ext = parsed.ext || ".json";
|
|
40
|
+
return {
|
|
41
|
+
directory: parsed.dir,
|
|
42
|
+
prefix: `${parsed.name}.restore-backup-`,
|
|
43
|
+
ext,
|
|
44
|
+
glob: `${parsed.name}.restore-backup-*${ext}`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseRestoreBackupCreatedAt(fileName, { prefix, ext }) {
|
|
49
|
+
if (!fileName.startsWith(prefix) || !fileName.endsWith(ext)) return "";
|
|
50
|
+
const timestamp = fileName.slice(prefix.length, fileName.length - ext.length);
|
|
51
|
+
const match = timestamp.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(\d{3})Z$/);
|
|
52
|
+
if (!match) return "";
|
|
53
|
+
const [, year, month, day, hour, minute, second, millisecond] = match;
|
|
54
|
+
return `${year}-${month}-${day}T${hour}:${minute}:${second}.${millisecond}Z`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeImportedLearningEntry(entry, index, now) {
|
|
58
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
59
|
+
throw new Error(`Learning import entry ${index + 1} must be an object`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const text = cleanNoteText(entry.text);
|
|
63
|
+
if (!text) throw new Error(`Learning import entry ${index + 1} has empty text`);
|
|
64
|
+
|
|
65
|
+
const category = normalizeCategory(entry.category || "preference");
|
|
66
|
+
const rawCreatedAt = String(entry.createdAt || "").trim();
|
|
67
|
+
const createdAt = rawCreatedAt && !Number.isNaN(Date.parse(rawCreatedAt))
|
|
68
|
+
? rawCreatedAt
|
|
69
|
+
: now.toISOString();
|
|
70
|
+
const rawSource = String(entry.source || "cli").trim() || "cli";
|
|
71
|
+
const source = rawSource.startsWith("import") ? rawSource : `import:${rawSource}`;
|
|
72
|
+
const rawId = String(entry.id || "").trim();
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
id: rawId || `learn-${shortEntryId({ text, category, createdAt })}`,
|
|
76
|
+
category,
|
|
77
|
+
text,
|
|
78
|
+
source,
|
|
79
|
+
createdAt,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseLearningImportEntries(importText, now) {
|
|
84
|
+
const payload = parseLearningProfilePayload(importText, "Learning import");
|
|
85
|
+
if (payload.entries.length === 0) {
|
|
86
|
+
throw new Error("Learning import has no entries");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return payload.entries.map((entry, index) => normalizeImportedLearningEntry(entry, index, now));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function importLearningProfile({
|
|
93
|
+
importText,
|
|
94
|
+
filePath = defaultLearningFile(),
|
|
95
|
+
dryRun = true,
|
|
96
|
+
now = new Date(),
|
|
97
|
+
} = {}) {
|
|
98
|
+
const importedEntries = parseLearningImportEntries(String(importText || ""), now);
|
|
99
|
+
const profile = loadLearningProfile(filePath);
|
|
100
|
+
const existingKeys = new Set(profile.entries.map(learningEntryMergeKey));
|
|
101
|
+
const usedIds = new Set(profile.entries.map((entry) => entry.id).filter(Boolean));
|
|
102
|
+
const added = [];
|
|
103
|
+
const skipped = [];
|
|
104
|
+
|
|
105
|
+
for (const importedEntry of importedEntries) {
|
|
106
|
+
const mergeKey = learningEntryMergeKey(importedEntry);
|
|
107
|
+
if (existingKeys.has(mergeKey)) {
|
|
108
|
+
skipped.push({
|
|
109
|
+
...statsEntry(importedEntry),
|
|
110
|
+
reason: "duplicate-entry-text",
|
|
111
|
+
});
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const entry = {
|
|
116
|
+
...importedEntry,
|
|
117
|
+
id: uniqueImportedEntryId(importedEntry, usedIds),
|
|
118
|
+
};
|
|
119
|
+
usedIds.add(entry.id);
|
|
120
|
+
existingKeys.add(mergeKey);
|
|
121
|
+
added.push(entry);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const updatedAt = added.length > 0 ? now.toISOString() : profile.updatedAt;
|
|
125
|
+
const nextProfile = {
|
|
126
|
+
version: 1,
|
|
127
|
+
updatedAt,
|
|
128
|
+
entries: [...profile.entries, ...added],
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
if (!dryRun && added.length > 0) {
|
|
132
|
+
writeLearningProfile(filePath, nextProfile);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
file: filePath,
|
|
137
|
+
dryRun,
|
|
138
|
+
applied: !dryRun,
|
|
139
|
+
importedCount: importedEntries.length,
|
|
140
|
+
addedCount: added.length,
|
|
141
|
+
skippedCount: skipped.length,
|
|
142
|
+
added: added.map(statsEntry),
|
|
143
|
+
skipped,
|
|
144
|
+
count: nextProfile.entries.length,
|
|
145
|
+
profile: nextProfile,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function verifyLearningImportPayload({
|
|
150
|
+
importText,
|
|
151
|
+
source = "input",
|
|
152
|
+
now = new Date(),
|
|
153
|
+
} = {}) {
|
|
154
|
+
const importedEntries = parseLearningImportEntries(String(importText || ""), now);
|
|
155
|
+
const profile = {
|
|
156
|
+
version: 1,
|
|
157
|
+
updatedAt: "",
|
|
158
|
+
entries: importedEntries,
|
|
159
|
+
};
|
|
160
|
+
const audit = auditLearningProfileObject(profile, {
|
|
161
|
+
filePath: source,
|
|
162
|
+
exists: true,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
source,
|
|
167
|
+
importable: audit.summary.failures === 0,
|
|
168
|
+
count: importedEntries.length,
|
|
169
|
+
auditSummary: audit.summary,
|
|
170
|
+
issues: audit.issues,
|
|
171
|
+
entries: importedEntries.map(statsEntry),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function invalidLearningBackupAudit(filePath, message) {
|
|
176
|
+
return {
|
|
177
|
+
file: filePath,
|
|
178
|
+
exists: true,
|
|
179
|
+
version: null,
|
|
180
|
+
updatedAt: "",
|
|
181
|
+
count: 0,
|
|
182
|
+
categoryCounts: {},
|
|
183
|
+
issues: [
|
|
184
|
+
learningAuditIssue({
|
|
185
|
+
level: "failure",
|
|
186
|
+
code: "invalid-json",
|
|
187
|
+
message,
|
|
188
|
+
}),
|
|
189
|
+
],
|
|
190
|
+
summary: {
|
|
191
|
+
status: "fail",
|
|
192
|
+
failures: 1,
|
|
193
|
+
warnings: 0,
|
|
194
|
+
},
|
|
195
|
+
suggestions: [],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function inspectRestoreBackupFile(filePath, fileName, pattern, activeFile) {
|
|
200
|
+
const stat = statSync(filePath);
|
|
201
|
+
const createdAt = parseRestoreBackupCreatedAt(fileName, pattern);
|
|
202
|
+
let rawProfile = null;
|
|
203
|
+
let audit = null;
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
rawProfile = JSON.parse(readFileSync(filePath, "utf8"));
|
|
207
|
+
audit = auditLearningProfileObject(rawProfile, {
|
|
208
|
+
filePath,
|
|
209
|
+
exists: true,
|
|
210
|
+
});
|
|
211
|
+
} catch (error) {
|
|
212
|
+
audit = invalidLearningBackupAudit(filePath, "Rollback backup is not valid JSON.");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
file: filePath,
|
|
217
|
+
name: fileName,
|
|
218
|
+
createdAt,
|
|
219
|
+
modifiedAt: stat.mtime.toISOString(),
|
|
220
|
+
sizeBytes: stat.size,
|
|
221
|
+
updatedAt: audit.updatedAt || "",
|
|
222
|
+
entryCount: audit.count || 0,
|
|
223
|
+
auditSummary: audit.summary,
|
|
224
|
+
issueCount: audit.issues.length,
|
|
225
|
+
restorePreviewCommand: commandFromArgs([
|
|
226
|
+
"design-ai",
|
|
227
|
+
"learn",
|
|
228
|
+
"--restore",
|
|
229
|
+
"--from-file",
|
|
230
|
+
filePath,
|
|
231
|
+
"--file",
|
|
232
|
+
activeFile,
|
|
233
|
+
"--dry-run",
|
|
234
|
+
]),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function listLearningRestoreBackups({
|
|
239
|
+
filePath = defaultLearningFile(),
|
|
240
|
+
limit = 10,
|
|
241
|
+
now = new Date(),
|
|
242
|
+
} = {}) {
|
|
243
|
+
const resolvedFile = path.resolve(filePath);
|
|
244
|
+
const pattern = learningRestoreBackupPattern(resolvedFile);
|
|
245
|
+
const directoryExists = existsSync(pattern.directory);
|
|
246
|
+
const files = directoryExists
|
|
247
|
+
? readdirSync(pattern.directory)
|
|
248
|
+
.filter((fileName) => fileName.startsWith(pattern.prefix) && fileName.endsWith(pattern.ext))
|
|
249
|
+
.map((fileName) => ({
|
|
250
|
+
fileName,
|
|
251
|
+
filePath: path.join(pattern.directory, fileName),
|
|
252
|
+
}))
|
|
253
|
+
.filter(({ filePath: backupPath }) => statSync(backupPath).isFile())
|
|
254
|
+
: [];
|
|
255
|
+
|
|
256
|
+
const backups = files
|
|
257
|
+
.map(({ fileName, filePath: backupPath }) => inspectRestoreBackupFile(backupPath, fileName, pattern, resolvedFile))
|
|
258
|
+
.sort((a, b) => {
|
|
259
|
+
const aKey = a.createdAt || a.modifiedAt || a.name;
|
|
260
|
+
const bKey = b.createdAt || b.modifiedAt || b.name;
|
|
261
|
+
return bKey.localeCompare(aKey);
|
|
262
|
+
});
|
|
263
|
+
const limitedBackups = backups.slice(0, limit || 10);
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
file: resolvedFile,
|
|
267
|
+
directory: pattern.directory,
|
|
268
|
+
pattern: pattern.glob,
|
|
269
|
+
generatedAt: now.toISOString(),
|
|
270
|
+
limit: limit || 10,
|
|
271
|
+
totalCount: backups.length,
|
|
272
|
+
count: limitedBackups.length,
|
|
273
|
+
backups: limitedBackups,
|
|
274
|
+
privacy: {
|
|
275
|
+
storesRawBriefText: false,
|
|
276
|
+
exposesEntryTextPreview: false,
|
|
277
|
+
mutatesProfile: false,
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function pruneLearningRestoreBackups({
|
|
283
|
+
filePath = defaultLearningFile(),
|
|
284
|
+
keep = 5,
|
|
285
|
+
limit = 10,
|
|
286
|
+
dryRun = true,
|
|
287
|
+
now = new Date(),
|
|
288
|
+
} = {}) {
|
|
289
|
+
const maxKeep = Number.isInteger(keep) && keep > 0 ? keep : 5;
|
|
290
|
+
const visibleLimit = Number.isInteger(limit) && limit > 0 ? limit : 10;
|
|
291
|
+
const inventory = listLearningRestoreBackups({
|
|
292
|
+
filePath,
|
|
293
|
+
limit: Number.MAX_SAFE_INTEGER,
|
|
294
|
+
now,
|
|
295
|
+
});
|
|
296
|
+
const retained = inventory.backups.slice(0, maxKeep);
|
|
297
|
+
const candidates = inventory.backups.slice(maxKeep);
|
|
298
|
+
const deleted = [];
|
|
299
|
+
const failures = [];
|
|
300
|
+
|
|
301
|
+
if (!dryRun) {
|
|
302
|
+
for (const backup of candidates) {
|
|
303
|
+
try {
|
|
304
|
+
unlinkSync(backup.file);
|
|
305
|
+
deleted.push(backup);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
failures.push({
|
|
308
|
+
file: backup.file,
|
|
309
|
+
name: backup.name,
|
|
310
|
+
message: error instanceof Error ? error.message : String(error),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
...inventory,
|
|
318
|
+
limit: visibleLimit,
|
|
319
|
+
count: inventory.backups.slice(0, visibleLimit).length,
|
|
320
|
+
backups: inventory.backups.slice(0, visibleLimit),
|
|
321
|
+
prune: {
|
|
322
|
+
dryRun,
|
|
323
|
+
applied: !dryRun,
|
|
324
|
+
keep: maxKeep,
|
|
325
|
+
retainedCount: retained.length,
|
|
326
|
+
candidateCount: candidates.length,
|
|
327
|
+
deletedCount: deleted.length,
|
|
328
|
+
failureCount: failures.length,
|
|
329
|
+
retained,
|
|
330
|
+
candidates,
|
|
331
|
+
deleted,
|
|
332
|
+
failures,
|
|
333
|
+
},
|
|
334
|
+
privacy: {
|
|
335
|
+
storesRawBriefText: false,
|
|
336
|
+
exposesEntryTextPreview: false,
|
|
337
|
+
mutatesProfile: false,
|
|
338
|
+
deletesBackupFiles: !dryRun && deleted.length > 0,
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function restoreLearningProfile({
|
|
344
|
+
restoreText,
|
|
345
|
+
filePath = defaultLearningFile(),
|
|
346
|
+
backupFilePath = "",
|
|
347
|
+
forceBackup = false,
|
|
348
|
+
source = "input",
|
|
349
|
+
dryRun = true,
|
|
350
|
+
now = new Date(),
|
|
351
|
+
} = {}) {
|
|
352
|
+
const resolvedFile = path.resolve(filePath);
|
|
353
|
+
const resolvedSource = source === "stdin" ? "stdin" : String(source || "input");
|
|
354
|
+
const generatedAt = now.toISOString();
|
|
355
|
+
const resolvedBackupFile = path.resolve(backupFilePath || defaultLearningRestoreBackupFile(resolvedFile, now));
|
|
356
|
+
const rawRestore = parseLearningProfilePayload(String(restoreText || ""), "Learning restore");
|
|
357
|
+
const restoreAudit = auditLearningProfileObject(rawRestore, {
|
|
358
|
+
filePath: resolvedSource,
|
|
359
|
+
exists: true,
|
|
360
|
+
});
|
|
361
|
+
const profileExists = existsSync(resolvedFile);
|
|
362
|
+
const currentProfile = loadLearningProfile(resolvedFile);
|
|
363
|
+
const restorable = restoreAudit.summary.failures === 0;
|
|
364
|
+
|
|
365
|
+
if (!dryRun && !restorable) {
|
|
366
|
+
throw new Error("Refusing to restore learning profile with audit failures");
|
|
367
|
+
}
|
|
368
|
+
if (!dryRun && resolvedBackupFile === resolvedFile) {
|
|
369
|
+
throw new Error("Learning restore backup file must be different from the active learning profile");
|
|
370
|
+
}
|
|
371
|
+
if (!dryRun && resolvedSource !== "stdin" && path.resolve(resolvedSource) === resolvedBackupFile) {
|
|
372
|
+
throw new Error("Learning restore backup file must be different from the restore source");
|
|
373
|
+
}
|
|
374
|
+
if (!dryRun && existsSync(resolvedBackupFile) && !forceBackup) {
|
|
375
|
+
throw new Error("Learning restore backup file already exists; pass --force to overwrite or choose another --backup-file path");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const restoredProfile = restorable ? normalizeLearningProfile(rawRestore) : emptyLearningProfile();
|
|
379
|
+
const targetProfile = {
|
|
380
|
+
version: 1,
|
|
381
|
+
updatedAt: restoredProfile.updatedAt || now.toISOString(),
|
|
382
|
+
entries: restoredProfile.entries,
|
|
383
|
+
};
|
|
384
|
+
const diff = restorable
|
|
385
|
+
? diffLearningProfiles({
|
|
386
|
+
filePath: resolvedFile,
|
|
387
|
+
compareText: restoreText,
|
|
388
|
+
source: resolvedSource,
|
|
389
|
+
now,
|
|
390
|
+
})
|
|
391
|
+
: {
|
|
392
|
+
sameTextCount: 0,
|
|
393
|
+
profileOnlyCount: 0,
|
|
394
|
+
comparisonOnlyCount: 0,
|
|
395
|
+
metadataChangedCount: 0,
|
|
396
|
+
idConflictCount: 0,
|
|
397
|
+
profileOnly: [],
|
|
398
|
+
comparisonOnly: [],
|
|
399
|
+
metadataChanged: [],
|
|
400
|
+
idConflicts: [],
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
if (!dryRun) {
|
|
404
|
+
writeLearningProfile(resolvedBackupFile, currentProfile);
|
|
405
|
+
writeLearningProfile(resolvedFile, targetProfile);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
file: resolvedFile,
|
|
410
|
+
source: resolvedSource,
|
|
411
|
+
generatedAt,
|
|
412
|
+
dryRun,
|
|
413
|
+
applied: !dryRun,
|
|
414
|
+
restorable,
|
|
415
|
+
profileExists,
|
|
416
|
+
backupFile: resolvedBackupFile,
|
|
417
|
+
backupCreated: !dryRun,
|
|
418
|
+
backupEntryCount: currentProfile.entries.length,
|
|
419
|
+
backupUpdatedAt: currentProfile.updatedAt,
|
|
420
|
+
rollbackCommand: commandFromArgs([
|
|
421
|
+
"design-ai",
|
|
422
|
+
"learn",
|
|
423
|
+
"--restore",
|
|
424
|
+
"--from-file",
|
|
425
|
+
resolvedBackupFile,
|
|
426
|
+
"--file",
|
|
427
|
+
resolvedFile,
|
|
428
|
+
"--dry-run",
|
|
429
|
+
]),
|
|
430
|
+
previousUpdatedAt: currentProfile.updatedAt,
|
|
431
|
+
restoredUpdatedAt: targetProfile.updatedAt,
|
|
432
|
+
previousCount: currentProfile.entries.length,
|
|
433
|
+
restoredCount: targetProfile.entries.length,
|
|
434
|
+
removedCount: diff.profileOnlyCount,
|
|
435
|
+
addedCount: diff.comparisonOnlyCount,
|
|
436
|
+
sameTextCount: diff.sameTextCount,
|
|
437
|
+
metadataChangedCount: diff.metadataChangedCount,
|
|
438
|
+
idConflictCount: diff.idConflictCount,
|
|
439
|
+
auditSummary: restoreAudit.summary,
|
|
440
|
+
issues: restoreAudit.issues,
|
|
441
|
+
diff: {
|
|
442
|
+
profileOnlyCount: diff.profileOnlyCount,
|
|
443
|
+
comparisonOnlyCount: diff.comparisonOnlyCount,
|
|
444
|
+
metadataChangedCount: diff.metadataChangedCount,
|
|
445
|
+
idConflictCount: diff.idConflictCount,
|
|
446
|
+
profileOnly: diff.profileOnly,
|
|
447
|
+
comparisonOnly: diff.comparisonOnly,
|
|
448
|
+
metadataChanged: diff.metadataChanged,
|
|
449
|
+
idConflicts: diff.idConflicts,
|
|
450
|
+
},
|
|
451
|
+
privacy: {
|
|
452
|
+
storesRawBriefText: false,
|
|
453
|
+
exposesEntryTextPreview: true,
|
|
454
|
+
mutatesProfile: !dryRun,
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function globalSensitivePattern(pattern) {
|
|
460
|
+
const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
|
|
461
|
+
return new RegExp(pattern.source, flags);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function redactLearningText(text) {
|
|
465
|
+
let redactedText = String(text || "");
|
|
466
|
+
const codes = [];
|
|
467
|
+
|
|
468
|
+
for (const sensitivePattern of LEARNING_SENSITIVE_PATTERNS) {
|
|
469
|
+
const pattern = globalSensitivePattern(sensitivePattern.pattern);
|
|
470
|
+
if (!pattern.test(redactedText)) continue;
|
|
471
|
+
|
|
472
|
+
codes.push(`sensitive-${sensitivePattern.code}`);
|
|
473
|
+
redactedText = redactedText.replace(pattern, `[REDACTED:${sensitivePattern.code}]`);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return {
|
|
477
|
+
text: redactedText,
|
|
478
|
+
codes,
|
|
479
|
+
redacted: codes.length > 0,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function buildRedactedLearningPayload({
|
|
484
|
+
profile,
|
|
485
|
+
source,
|
|
486
|
+
sourceAudit,
|
|
487
|
+
now = new Date(),
|
|
488
|
+
}) {
|
|
489
|
+
const redactions = [];
|
|
490
|
+
const entries = profile.entries.map((entry) => {
|
|
491
|
+
const redacted = redactLearningText(entry.text);
|
|
492
|
+
if (!redacted.redacted) return entry;
|
|
493
|
+
|
|
494
|
+
const nextEntry = {
|
|
495
|
+
...entry,
|
|
496
|
+
text: redacted.text,
|
|
497
|
+
};
|
|
498
|
+
redactions.push({
|
|
499
|
+
entryId: entry.id,
|
|
500
|
+
category: entry.category,
|
|
501
|
+
codes: redacted.codes,
|
|
502
|
+
textPreview: previewText(nextEntry.text),
|
|
503
|
+
});
|
|
504
|
+
return nextEntry;
|
|
505
|
+
});
|
|
506
|
+
const redactedProfile = {
|
|
507
|
+
version: profile.version,
|
|
508
|
+
updatedAt: profile.updatedAt,
|
|
509
|
+
entries,
|
|
510
|
+
};
|
|
511
|
+
const redactedAudit = auditLearningProfileObject(redactedProfile, {
|
|
512
|
+
filePath: source,
|
|
513
|
+
exists: sourceAudit.exists,
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
file: source,
|
|
518
|
+
version: profile.version,
|
|
519
|
+
updatedAt: profile.updatedAt,
|
|
520
|
+
exportedAt: now.toISOString(),
|
|
521
|
+
redacted: true,
|
|
522
|
+
count: entries.length,
|
|
523
|
+
redactedCount: redactions.length,
|
|
524
|
+
sourceAuditSummary: sourceAudit.summary,
|
|
525
|
+
auditSummary: redactedAudit.summary,
|
|
526
|
+
redactions,
|
|
527
|
+
entries,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export function buildRedactedLearningBackup({
|
|
532
|
+
filePath = defaultLearningFile(),
|
|
533
|
+
importText,
|
|
534
|
+
source = "",
|
|
535
|
+
now = new Date(),
|
|
536
|
+
} = {}) {
|
|
537
|
+
if (importText !== undefined) {
|
|
538
|
+
const sourceLabel = source || "input";
|
|
539
|
+
const rawProfile = parseLearningProfilePayload(String(importText || ""), "Learning redaction input");
|
|
540
|
+
const profile = normalizeLearningProfile(rawProfile);
|
|
541
|
+
const sourceAudit = auditLearningProfileObject(rawProfile, {
|
|
542
|
+
filePath: sourceLabel,
|
|
543
|
+
exists: true,
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
return buildRedactedLearningPayload({
|
|
547
|
+
profile,
|
|
548
|
+
source: sourceLabel,
|
|
549
|
+
sourceAudit,
|
|
550
|
+
now,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const sourceAudit = auditLearningProfile({ filePath });
|
|
555
|
+
const profile = loadLearningProfile(filePath);
|
|
556
|
+
|
|
557
|
+
return buildRedactedLearningPayload({
|
|
558
|
+
profile,
|
|
559
|
+
source: filePath,
|
|
560
|
+
sourceAudit,
|
|
561
|
+
now,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export function buildLearningBackup({ filePath = defaultLearningFile(), now = new Date() } = {}) {
|
|
566
|
+
const audit = auditLearningProfile({ filePath });
|
|
567
|
+
const profile = loadLearningProfile(filePath);
|
|
568
|
+
return {
|
|
569
|
+
file: filePath,
|
|
570
|
+
version: profile.version,
|
|
571
|
+
updatedAt: profile.updatedAt,
|
|
572
|
+
exportedAt: now.toISOString(),
|
|
573
|
+
count: profile.entries.length,
|
|
574
|
+
auditSummary: audit.summary,
|
|
575
|
+
entries: profile.entries,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function firstEntryByMergeKey(entries) {
|
|
580
|
+
const map = new Map();
|
|
581
|
+
for (const entry of entries) {
|
|
582
|
+
const key = learningEntryMergeKey(entry);
|
|
583
|
+
if (!map.has(key)) map.set(key, entry);
|
|
584
|
+
}
|
|
585
|
+
return map;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function firstEntryById(entries) {
|
|
589
|
+
const map = new Map();
|
|
590
|
+
for (const entry of entries) {
|
|
591
|
+
if (entry.id && !map.has(entry.id)) map.set(entry.id, entry);
|
|
592
|
+
}
|
|
593
|
+
return map;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function metadataDiffFields(profileEntry, comparisonEntry) {
|
|
597
|
+
return ["id", "source", "createdAt"].filter((field) => (
|
|
598
|
+
String(profileEntry?.[field] || "") !== String(comparisonEntry?.[field] || "")
|
|
599
|
+
));
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function learningDiffItem({ key, profileEntry, comparisonEntry, changedFields }) {
|
|
603
|
+
return {
|
|
604
|
+
key,
|
|
605
|
+
changedFields,
|
|
606
|
+
profile: statsEntry(profileEntry),
|
|
607
|
+
comparison: statsEntry(comparisonEntry),
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function learningIdConflict({ id, profileEntry, comparisonEntry }) {
|
|
612
|
+
return {
|
|
613
|
+
id,
|
|
614
|
+
profile: statsEntry(profileEntry),
|
|
615
|
+
comparison: statsEntry(comparisonEntry),
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
export function diffLearningProfiles({
|
|
620
|
+
filePath = defaultLearningFile(),
|
|
621
|
+
compareText = "",
|
|
622
|
+
source = "input",
|
|
623
|
+
now = new Date(),
|
|
624
|
+
} = {}) {
|
|
625
|
+
const resolvedFile = path.resolve(filePath);
|
|
626
|
+
const resolvedSource = source === "stdin" ? "stdin" : String(source || "input");
|
|
627
|
+
const profileExists = existsSync(resolvedFile);
|
|
628
|
+
const profile = loadLearningProfile(resolvedFile);
|
|
629
|
+
const profileAudit = auditLearningProfile({ filePath: resolvedFile });
|
|
630
|
+
const rawComparison = parseLearningProfilePayload(String(compareText || ""), "Learning diff comparison");
|
|
631
|
+
const comparison = normalizeLearningProfile(rawComparison);
|
|
632
|
+
const comparisonAudit = auditLearningProfileObject(rawComparison, {
|
|
633
|
+
filePath: resolvedSource,
|
|
634
|
+
exists: true,
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
const profileByKey = firstEntryByMergeKey(profile.entries);
|
|
638
|
+
const comparisonByKey = firstEntryByMergeKey(comparison.entries);
|
|
639
|
+
const profileKeys = new Set(profileByKey.keys());
|
|
640
|
+
const comparisonKeys = new Set(comparisonByKey.keys());
|
|
641
|
+
const commonKeys = [...profileKeys].filter((key) => comparisonKeys.has(key)).sort();
|
|
642
|
+
const profileOnly = [...profile.entries]
|
|
643
|
+
.filter((entry) => !comparisonKeys.has(learningEntryMergeKey(entry)))
|
|
644
|
+
.map(statsEntry);
|
|
645
|
+
const comparisonOnly = [...comparison.entries]
|
|
646
|
+
.filter((entry) => !profileKeys.has(learningEntryMergeKey(entry)))
|
|
647
|
+
.map(statsEntry);
|
|
648
|
+
const metadataChanged = commonKeys
|
|
649
|
+
.map((key) => {
|
|
650
|
+
const profileEntry = profileByKey.get(key);
|
|
651
|
+
const comparisonEntry = comparisonByKey.get(key);
|
|
652
|
+
const changedFields = metadataDiffFields(profileEntry, comparisonEntry);
|
|
653
|
+
return changedFields.length > 0
|
|
654
|
+
? learningDiffItem({ key, profileEntry, comparisonEntry, changedFields })
|
|
655
|
+
: null;
|
|
656
|
+
})
|
|
657
|
+
.filter(Boolean);
|
|
658
|
+
|
|
659
|
+
const profileById = firstEntryById(profile.entries);
|
|
660
|
+
const comparisonById = firstEntryById(comparison.entries);
|
|
661
|
+
const idConflicts = [...profileById.keys()]
|
|
662
|
+
.filter((id) => comparisonById.has(id))
|
|
663
|
+
.map((id) => {
|
|
664
|
+
const profileEntry = profileById.get(id);
|
|
665
|
+
const comparisonEntry = comparisonById.get(id);
|
|
666
|
+
return learningEntryMergeKey(profileEntry) !== learningEntryMergeKey(comparisonEntry)
|
|
667
|
+
? learningIdConflict({ id, profileEntry, comparisonEntry })
|
|
668
|
+
: null;
|
|
669
|
+
})
|
|
670
|
+
.filter(Boolean);
|
|
671
|
+
|
|
672
|
+
const recommendations = [];
|
|
673
|
+
if (!profileExists) {
|
|
674
|
+
recommendations.push({
|
|
675
|
+
level: "warning",
|
|
676
|
+
text: "Active learning profile does not exist; comparison is against an empty local profile.",
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
if (profileAudit.summary.status !== "pass") {
|
|
680
|
+
recommendations.push({
|
|
681
|
+
level: profileAudit.summary.failures > 0 ? "warning" : "info",
|
|
682
|
+
text: "Run `design-ai learn --audit` on the active profile before applying import or restore decisions.",
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
if (comparisonAudit.summary.status !== "pass") {
|
|
686
|
+
recommendations.push({
|
|
687
|
+
level: comparisonAudit.summary.failures > 0 ? "warning" : "info",
|
|
688
|
+
text: "Review comparison profile audit issues before importing or restoring entries from it.",
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
if (idConflicts.length > 0) {
|
|
692
|
+
recommendations.push({
|
|
693
|
+
level: "warning",
|
|
694
|
+
text: "Matching ids with different learning text were found; inspect manually before importing or restoring.",
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
if (comparisonOnly.length > 0) {
|
|
698
|
+
recommendations.push({
|
|
699
|
+
level: "info",
|
|
700
|
+
text: "Run `design-ai learn --import --from-file <profile.json> --dry-run` to preview adding comparison-only entries.",
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
if (profileOnly.length > 0) {
|
|
704
|
+
recommendations.push({
|
|
705
|
+
level: "info",
|
|
706
|
+
text: "Profile-only entries would be absent if the comparison profile were used as the restore source.",
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
if (
|
|
710
|
+
profileOnly.length === 0
|
|
711
|
+
&& comparisonOnly.length === 0
|
|
712
|
+
&& metadataChanged.length === 0
|
|
713
|
+
&& idConflicts.length === 0
|
|
714
|
+
) {
|
|
715
|
+
recommendations.push({
|
|
716
|
+
level: "info",
|
|
717
|
+
text: "No learning profile diff action is needed.",
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
file: resolvedFile,
|
|
723
|
+
source: resolvedSource,
|
|
724
|
+
generatedAt: now.toISOString(),
|
|
725
|
+
profileExists,
|
|
726
|
+
profileUpdatedAt: profile.updatedAt,
|
|
727
|
+
comparisonUpdatedAt: comparison.updatedAt,
|
|
728
|
+
profileCount: profile.entries.length,
|
|
729
|
+
comparisonCount: comparison.entries.length,
|
|
730
|
+
profileAuditSummary: profileAudit.summary,
|
|
731
|
+
comparisonAuditSummary: comparisonAudit.summary,
|
|
732
|
+
sameTextCount: commonKeys.length,
|
|
733
|
+
profileOnlyCount: profileOnly.length,
|
|
734
|
+
comparisonOnlyCount: comparisonOnly.length,
|
|
735
|
+
metadataChangedCount: metadataChanged.length,
|
|
736
|
+
idConflictCount: idConflicts.length,
|
|
737
|
+
profileOnly,
|
|
738
|
+
comparisonOnly,
|
|
739
|
+
metadataChanged,
|
|
740
|
+
idConflicts,
|
|
741
|
+
recommendations,
|
|
742
|
+
privacy: {
|
|
743
|
+
storesRawBriefText: false,
|
|
744
|
+
exposesEntryTextPreview: true,
|
|
745
|
+
mutatesProfile: false,
|
|
746
|
+
},
|
|
747
|
+
};
|
|
748
|
+
}
|