@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,763 @@
|
|
|
1
|
+
// Learning profile core: load/normalize/audit/mutate for `design-ai learn`.
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
LEARNING_CATEGORIES,
|
|
7
|
+
normalizeCategory,
|
|
8
|
+
normalizeFeedbackOutcome,
|
|
9
|
+
} from "./learn-args.mjs";
|
|
10
|
+
import {
|
|
11
|
+
LEARNING_SENSITIVE_PATTERNS,
|
|
12
|
+
cleanNoteText,
|
|
13
|
+
commandFromArgs,
|
|
14
|
+
defaultLearningFile,
|
|
15
|
+
learningEntryMergeKey,
|
|
16
|
+
shortEntryId,
|
|
17
|
+
statsEntry,
|
|
18
|
+
uniqueImportedEntryId,
|
|
19
|
+
writeLearningProfile,
|
|
20
|
+
} from "./learn-shared.mjs";
|
|
21
|
+
|
|
22
|
+
export const LEARNING_INIT_SOURCE = "init:local-dogfood";
|
|
23
|
+
export const LEARNING_INIT_ENTRIES = [
|
|
24
|
+
{
|
|
25
|
+
category: "preference",
|
|
26
|
+
text: "Prefer concise, evidence-led design recommendations with one best path and explicit tradeoffs.",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
category: "workflow",
|
|
30
|
+
text: "For implementation work, inspect repository context first, keep edits scoped, and run meaningful verification before handoff.",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
category: "accessibility",
|
|
34
|
+
text: "For non-trivial UI, include keyboard navigation, visible focus, screen-reader behavior, and WCAG 2.1 AA contrast notes.",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
category: "korean",
|
|
38
|
+
text: "When Korean users or Korean copy are involved, use Pretendard, Korean typography line-height, dense mobile conventions, and a consistent honorific level.",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
category: "brand",
|
|
42
|
+
text: "Use restrained product UI language for internal tools and avoid decorative marketing phrasing unless explicitly requested.",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
category: "constraint",
|
|
46
|
+
text: "Do not add external AI APIs, embeddings, telemetry, or fine-tuning behavior without explicit approval.",
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
const DEFAULT_AUDIT_MAX_ENTRY_CHARS = 800;
|
|
50
|
+
export function emptyLearningProfile() {
|
|
51
|
+
return {
|
|
52
|
+
version: 1,
|
|
53
|
+
updatedAt: "",
|
|
54
|
+
entries: [],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeEntry(entry) {
|
|
59
|
+
if (!entry || typeof entry !== "object") return null;
|
|
60
|
+
const text = String(entry.text || "").trim();
|
|
61
|
+
if (!text) return null;
|
|
62
|
+
const createdAt = String(entry.createdAt || "").trim();
|
|
63
|
+
return {
|
|
64
|
+
id: String(entry.id || `learn-${shortEntryId({ text, category: entry.category || "preference", createdAt })}`).trim(),
|
|
65
|
+
category: normalizeCategory(entry.category || "preference"),
|
|
66
|
+
text,
|
|
67
|
+
source: String(entry.source || "cli").trim() || "cli",
|
|
68
|
+
createdAt,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function normalizeLearningProfile(rawProfile) {
|
|
73
|
+
const profile = rawProfile && typeof rawProfile === "object" ? rawProfile : {};
|
|
74
|
+
const entries = Array.isArray(profile.entries)
|
|
75
|
+
? profile.entries.map(normalizeEntry).filter(Boolean)
|
|
76
|
+
: [];
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
version: Number.isInteger(profile.version) ? profile.version : 1,
|
|
80
|
+
updatedAt: String(profile.updatedAt || "").trim(),
|
|
81
|
+
entries,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function loadLearningProfile(filePath = defaultLearningFile()) {
|
|
86
|
+
if (!existsSync(filePath)) return emptyLearningProfile();
|
|
87
|
+
const raw = readFileSync(filePath, "utf8");
|
|
88
|
+
try {
|
|
89
|
+
return normalizeLearningProfile(JSON.parse(raw));
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw new Error(`Learning profile is not valid JSON: ${filePath}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function learningAuditIssue({ level = "warning", code, entryId = "", message }) {
|
|
96
|
+
return {
|
|
97
|
+
level,
|
|
98
|
+
code,
|
|
99
|
+
...(entryId ? { entryId } : {}),
|
|
100
|
+
message,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function summarizeLearningAudit(issues) {
|
|
105
|
+
const failures = issues.filter((issue) => issue.level === "failure").length;
|
|
106
|
+
const warnings = issues.filter((issue) => issue.level === "warning").length;
|
|
107
|
+
return {
|
|
108
|
+
status: failures > 0 ? "fail" : warnings > 0 ? "warn" : "pass",
|
|
109
|
+
failures,
|
|
110
|
+
warnings,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function forgetCommand({ filePath, entryId }) {
|
|
115
|
+
return ["design-ai", "learn", "--file", filePath, "--forget", entryId, "--yes"];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function commandSuggestion({ issue, action, message, commandArgs = [] }) {
|
|
119
|
+
return {
|
|
120
|
+
issueCode: issue.code,
|
|
121
|
+
...(issue.entryId ? { entryId: issue.entryId } : {}),
|
|
122
|
+
action,
|
|
123
|
+
message,
|
|
124
|
+
...(commandArgs.length > 0 ? {
|
|
125
|
+
commandArgs,
|
|
126
|
+
command: commandFromArgs(commandArgs),
|
|
127
|
+
} : {}),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function learningAuditSuggestion(issue, filePath, { ambiguousEntryIds = new Set() } = {}) {
|
|
132
|
+
const hasStableEntryTarget = issue.entryId && ![
|
|
133
|
+
"missing-entry-id",
|
|
134
|
+
"duplicate-entry-id",
|
|
135
|
+
"invalid-entry",
|
|
136
|
+
"empty-entry-text",
|
|
137
|
+
"invalid-category",
|
|
138
|
+
].includes(issue.code) && !ambiguousEntryIds.has(issue.entryId);
|
|
139
|
+
const commandArgs = hasStableEntryTarget
|
|
140
|
+
? forgetCommand({ filePath, entryId: issue.entryId })
|
|
141
|
+
: [];
|
|
142
|
+
|
|
143
|
+
if (issue.code === "duplicate-entry-text") {
|
|
144
|
+
return commandSuggestion({
|
|
145
|
+
issue,
|
|
146
|
+
action: "remove-duplicate",
|
|
147
|
+
message: "Remove the duplicate entry, or rewrite it if it captures a distinct constraint.",
|
|
148
|
+
commandArgs,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (issue.code.startsWith("sensitive-")) {
|
|
153
|
+
return commandSuggestion({
|
|
154
|
+
issue,
|
|
155
|
+
action: "remove-or-redact-sensitive-content",
|
|
156
|
+
message: "Remove this entry or re-add a redacted preference before using --with-learning.",
|
|
157
|
+
commandArgs,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (issue.code === "long-entry-text") {
|
|
162
|
+
return commandSuggestion({
|
|
163
|
+
issue,
|
|
164
|
+
action: "split-or-rewrite",
|
|
165
|
+
message: "Split this note into smaller preference entries, or remove and re-add a focused version.",
|
|
166
|
+
commandArgs,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (["missing-created-at", "invalid-created-at"].includes(issue.code)) {
|
|
171
|
+
return commandSuggestion({
|
|
172
|
+
issue,
|
|
173
|
+
action: "refresh-entry-metadata",
|
|
174
|
+
message: "Remove and re-add this entry if recency ordering matters for prompt personalization.",
|
|
175
|
+
commandArgs,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (issue.code === "missing-entry-id") {
|
|
180
|
+
return commandSuggestion({
|
|
181
|
+
issue,
|
|
182
|
+
action: "re-add-for-stable-id",
|
|
183
|
+
message: "Re-add this note through `design-ai learn --remember` if you need a stable id for deletion and audit trails.",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (issue.code === "duplicate-entry-id") {
|
|
188
|
+
return commandSuggestion({
|
|
189
|
+
issue,
|
|
190
|
+
action: "manual-profile-edit",
|
|
191
|
+
message: "Inspect the profile manually before deleting because duplicate ids make id-based deletion ambiguous.",
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (issue.level === "failure") {
|
|
196
|
+
return commandSuggestion({
|
|
197
|
+
issue,
|
|
198
|
+
action: "manual-profile-repair",
|
|
199
|
+
message: "Repair the local learning JSON manually, or clear the profile only after reviewing the file.",
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return commandSuggestion({
|
|
204
|
+
issue,
|
|
205
|
+
action: "manual-review",
|
|
206
|
+
message: "Review this warning before exporting or injecting local learning context.",
|
|
207
|
+
commandArgs,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function learningAuditSuggestions(issues, { filePath }) {
|
|
212
|
+
const ambiguousEntryIds = new Set(
|
|
213
|
+
issues
|
|
214
|
+
.filter((issue) => issue.code === "duplicate-entry-id" && issue.entryId)
|
|
215
|
+
.map((issue) => issue.entryId),
|
|
216
|
+
);
|
|
217
|
+
return issues.map((issue) => learningAuditSuggestion(issue, filePath, { ambiguousEntryIds }));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function finalizeLearningAudit(payload) {
|
|
221
|
+
const summary = summarizeLearningAudit(payload.issues);
|
|
222
|
+
return {
|
|
223
|
+
...payload,
|
|
224
|
+
summary,
|
|
225
|
+
suggestions: learningAuditSuggestions(payload.issues, { filePath: payload.file }),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function entryAuditId(entry, index) {
|
|
230
|
+
return String(entry?.id || `entry-${index + 1}`).trim() || `entry-${index + 1}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function inspectLearningEntry({
|
|
234
|
+
entry,
|
|
235
|
+
index,
|
|
236
|
+
issues,
|
|
237
|
+
categoryCounts,
|
|
238
|
+
seenIds,
|
|
239
|
+
seenNotes,
|
|
240
|
+
maxEntryChars,
|
|
241
|
+
}) {
|
|
242
|
+
const entryId = entryAuditId(entry, index);
|
|
243
|
+
|
|
244
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
245
|
+
issues.push(learningAuditIssue({
|
|
246
|
+
level: "failure",
|
|
247
|
+
code: "invalid-entry",
|
|
248
|
+
entryId,
|
|
249
|
+
message: "Entry must be an object with text, category, and metadata.",
|
|
250
|
+
}));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!String(entry.id || "").trim()) {
|
|
255
|
+
issues.push(learningAuditIssue({
|
|
256
|
+
code: "missing-entry-id",
|
|
257
|
+
entryId,
|
|
258
|
+
message: "Entry is missing an id; list and export will generate one at runtime.",
|
|
259
|
+
}));
|
|
260
|
+
} else if (seenIds.has(entryId)) {
|
|
261
|
+
issues.push(learningAuditIssue({
|
|
262
|
+
code: "duplicate-entry-id",
|
|
263
|
+
entryId,
|
|
264
|
+
message: `Entry id duplicates ${seenIds.get(entryId)} and can make deletion ambiguous.`,
|
|
265
|
+
}));
|
|
266
|
+
} else {
|
|
267
|
+
seenIds.set(entryId, entryId);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const text = String(entry.text || "").trim();
|
|
271
|
+
if (!text) {
|
|
272
|
+
issues.push(learningAuditIssue({
|
|
273
|
+
level: "failure",
|
|
274
|
+
code: "empty-entry-text",
|
|
275
|
+
entryId,
|
|
276
|
+
message: "Entry text is empty and will be ignored by prompt personalization.",
|
|
277
|
+
}));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const rawCategory = String(entry.category || "preference").trim().toLowerCase();
|
|
282
|
+
if (!LEARNING_CATEGORIES.includes(rawCategory)) {
|
|
283
|
+
issues.push(learningAuditIssue({
|
|
284
|
+
level: "failure",
|
|
285
|
+
code: "invalid-category",
|
|
286
|
+
entryId,
|
|
287
|
+
message: `Category must be one of: ${LEARNING_CATEGORIES.join(", ")}.`,
|
|
288
|
+
}));
|
|
289
|
+
} else {
|
|
290
|
+
categoryCounts[rawCategory] = (categoryCounts[rawCategory] || 0) + 1;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const noteKey = `${rawCategory}\n${cleanNoteText(text).toLowerCase()}`;
|
|
294
|
+
if (seenNotes.has(noteKey)) {
|
|
295
|
+
issues.push(learningAuditIssue({
|
|
296
|
+
code: "duplicate-entry-text",
|
|
297
|
+
entryId,
|
|
298
|
+
message: `Entry duplicates ${seenNotes.get(noteKey)} in the same category.`,
|
|
299
|
+
}));
|
|
300
|
+
} else {
|
|
301
|
+
seenNotes.set(noteKey, entryId);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const createdAt = String(entry.createdAt || "").trim();
|
|
305
|
+
if (!createdAt) {
|
|
306
|
+
issues.push(learningAuditIssue({
|
|
307
|
+
code: "missing-created-at",
|
|
308
|
+
entryId,
|
|
309
|
+
message: "Entry is missing createdAt metadata, so recency is unclear.",
|
|
310
|
+
}));
|
|
311
|
+
} else if (Number.isNaN(Date.parse(createdAt))) {
|
|
312
|
+
issues.push(learningAuditIssue({
|
|
313
|
+
code: "invalid-created-at",
|
|
314
|
+
entryId,
|
|
315
|
+
message: "Entry createdAt metadata is not a parseable date.",
|
|
316
|
+
}));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (text.length > maxEntryChars) {
|
|
320
|
+
issues.push(learningAuditIssue({
|
|
321
|
+
code: "long-entry-text",
|
|
322
|
+
entryId,
|
|
323
|
+
message: `Entry is ${text.length} characters; keep learning notes under ${maxEntryChars} characters when possible.`,
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
for (const sensitivePattern of LEARNING_SENSITIVE_PATTERNS) {
|
|
328
|
+
if (sensitivePattern.pattern.test(text)) {
|
|
329
|
+
issues.push(learningAuditIssue({
|
|
330
|
+
code: `sensitive-${sensitivePattern.code}`,
|
|
331
|
+
entryId,
|
|
332
|
+
message: `Entry may contain ${sensitivePattern.label}; remove or rewrite it before using --with-learning.`,
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function auditLearningProfileObject(rawProfile, {
|
|
339
|
+
filePath = defaultLearningFile(),
|
|
340
|
+
exists = true,
|
|
341
|
+
maxEntryChars = DEFAULT_AUDIT_MAX_ENTRY_CHARS,
|
|
342
|
+
} = {}) {
|
|
343
|
+
const payload = {
|
|
344
|
+
file: filePath,
|
|
345
|
+
exists,
|
|
346
|
+
version: null,
|
|
347
|
+
updatedAt: "",
|
|
348
|
+
count: 0,
|
|
349
|
+
categoryCounts: {},
|
|
350
|
+
issues: [],
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
if (!payload.exists) return finalizeLearningAudit(payload);
|
|
354
|
+
|
|
355
|
+
if (!rawProfile || typeof rawProfile !== "object" || Array.isArray(rawProfile)) {
|
|
356
|
+
payload.issues.push(learningAuditIssue({
|
|
357
|
+
level: "failure",
|
|
358
|
+
code: "invalid-profile",
|
|
359
|
+
message: "Learning profile root must be a JSON object.",
|
|
360
|
+
}));
|
|
361
|
+
return finalizeLearningAudit(payload);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
payload.version = Number.isInteger(rawProfile.version) ? rawProfile.version : 1;
|
|
365
|
+
payload.updatedAt = String(rawProfile.updatedAt || "").trim();
|
|
366
|
+
|
|
367
|
+
if (rawProfile.version !== undefined && !Number.isInteger(rawProfile.version)) {
|
|
368
|
+
payload.issues.push(learningAuditIssue({
|
|
369
|
+
code: "invalid-version",
|
|
370
|
+
message: "Profile version is not an integer; version 1 will be assumed.",
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (!Array.isArray(rawProfile.entries)) {
|
|
375
|
+
payload.issues.push(learningAuditIssue({
|
|
376
|
+
level: "failure",
|
|
377
|
+
code: "invalid-entries",
|
|
378
|
+
message: "Learning profile entries must be an array.",
|
|
379
|
+
}));
|
|
380
|
+
return finalizeLearningAudit(payload);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
payload.count = rawProfile.entries.length;
|
|
384
|
+
|
|
385
|
+
const seenIds = new Map();
|
|
386
|
+
const seenNotes = new Map();
|
|
387
|
+
for (let index = 0; index < rawProfile.entries.length; index += 1) {
|
|
388
|
+
inspectLearningEntry({
|
|
389
|
+
entry: rawProfile.entries[index],
|
|
390
|
+
index,
|
|
391
|
+
issues: payload.issues,
|
|
392
|
+
categoryCounts: payload.categoryCounts,
|
|
393
|
+
seenIds,
|
|
394
|
+
seenNotes,
|
|
395
|
+
maxEntryChars,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
payload.categoryCounts = Object.fromEntries(
|
|
400
|
+
LEARNING_CATEGORIES
|
|
401
|
+
.filter((category) => payload.categoryCounts[category])
|
|
402
|
+
.map((category) => [category, payload.categoryCounts[category]]),
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
return finalizeLearningAudit(payload);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function auditLearningProfile({
|
|
409
|
+
filePath = defaultLearningFile(),
|
|
410
|
+
maxEntryChars = DEFAULT_AUDIT_MAX_ENTRY_CHARS,
|
|
411
|
+
} = {}) {
|
|
412
|
+
if (!existsSync(filePath)) {
|
|
413
|
+
return auditLearningProfileObject(emptyLearningProfile(), {
|
|
414
|
+
filePath,
|
|
415
|
+
exists: false,
|
|
416
|
+
maxEntryChars,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
let rawText = "";
|
|
421
|
+
try {
|
|
422
|
+
rawText = readFileSync(filePath, "utf8");
|
|
423
|
+
} catch {
|
|
424
|
+
return finalizeLearningAudit({
|
|
425
|
+
file: filePath,
|
|
426
|
+
exists: true,
|
|
427
|
+
version: null,
|
|
428
|
+
updatedAt: "",
|
|
429
|
+
count: 0,
|
|
430
|
+
categoryCounts: {},
|
|
431
|
+
issues: [
|
|
432
|
+
learningAuditIssue({
|
|
433
|
+
level: "failure",
|
|
434
|
+
code: "read-error",
|
|
435
|
+
message: "Learning profile could not be read from disk.",
|
|
436
|
+
}),
|
|
437
|
+
],
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
let rawProfile = null;
|
|
442
|
+
try {
|
|
443
|
+
rawProfile = JSON.parse(rawText);
|
|
444
|
+
} catch {
|
|
445
|
+
return finalizeLearningAudit({
|
|
446
|
+
file: filePath,
|
|
447
|
+
exists: true,
|
|
448
|
+
version: null,
|
|
449
|
+
updatedAt: "",
|
|
450
|
+
count: 0,
|
|
451
|
+
categoryCounts: {},
|
|
452
|
+
issues: [
|
|
453
|
+
learningAuditIssue({
|
|
454
|
+
level: "failure",
|
|
455
|
+
code: "invalid-json",
|
|
456
|
+
message: "Learning profile is not valid JSON.",
|
|
457
|
+
}),
|
|
458
|
+
],
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return auditLearningProfileObject(rawProfile, {
|
|
463
|
+
filePath,
|
|
464
|
+
exists: true,
|
|
465
|
+
maxEntryChars,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function rememberLearning({
|
|
470
|
+
text,
|
|
471
|
+
category = "preference",
|
|
472
|
+
filePath = defaultLearningFile(),
|
|
473
|
+
now = new Date(),
|
|
474
|
+
source = "cli",
|
|
475
|
+
}) {
|
|
476
|
+
const note = cleanNoteText(text);
|
|
477
|
+
if (!note) throw new Error("Learning note is empty");
|
|
478
|
+
|
|
479
|
+
const normalizedCategory = normalizeCategory(category);
|
|
480
|
+
const createdAt = now.toISOString();
|
|
481
|
+
const entry = {
|
|
482
|
+
id: `learn-${shortEntryId({ text: note, category: normalizedCategory, createdAt })}`,
|
|
483
|
+
category: normalizedCategory,
|
|
484
|
+
text: note,
|
|
485
|
+
source,
|
|
486
|
+
createdAt,
|
|
487
|
+
};
|
|
488
|
+
const profile = loadLearningProfile(filePath);
|
|
489
|
+
const nextProfile = {
|
|
490
|
+
version: 1,
|
|
491
|
+
updatedAt: createdAt,
|
|
492
|
+
entries: [...profile.entries, entry],
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
writeLearningProfile(filePath, nextProfile);
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
file: filePath,
|
|
499
|
+
entry,
|
|
500
|
+
profile: nextProfile,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function feedbackInstruction({ outcome, text }) {
|
|
505
|
+
if (outcome === "keep") return `Repeat in future outputs: ${text}`;
|
|
506
|
+
if (outcome === "avoid") return `Avoid in future outputs: ${text}`;
|
|
507
|
+
return `Improve future outputs by: ${text}`;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export function recordLearningFeedback({
|
|
511
|
+
text,
|
|
512
|
+
outcome = "improve",
|
|
513
|
+
category = "workflow",
|
|
514
|
+
filePath = defaultLearningFile(),
|
|
515
|
+
now = new Date(),
|
|
516
|
+
}) {
|
|
517
|
+
const normalizedOutcome = normalizeFeedbackOutcome(outcome);
|
|
518
|
+
const feedbackText = cleanNoteText(text);
|
|
519
|
+
if (!feedbackText) throw new Error("Learning feedback is empty");
|
|
520
|
+
|
|
521
|
+
return rememberLearning({
|
|
522
|
+
text: feedbackInstruction({ outcome: normalizedOutcome, text: feedbackText }),
|
|
523
|
+
category,
|
|
524
|
+
filePath,
|
|
525
|
+
now,
|
|
526
|
+
source: `feedback:${normalizedOutcome}`,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function offsetIsoDate(now, offsetMs) {
|
|
531
|
+
const base = now instanceof Date ? now : new Date(now);
|
|
532
|
+
return new Date(base.getTime() + offsetMs).toISOString();
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export function captureLearningEntries({
|
|
536
|
+
entries = [],
|
|
537
|
+
source = "cli",
|
|
538
|
+
filePath = defaultLearningFile(),
|
|
539
|
+
dryRun = true,
|
|
540
|
+
now = new Date(),
|
|
541
|
+
} = {}) {
|
|
542
|
+
const profile = loadLearningProfile(filePath);
|
|
543
|
+
const existingKeys = new Set(profile.entries.map(learningEntryMergeKey));
|
|
544
|
+
const usedIds = new Set(profile.entries.map((entry) => entry.id).filter(Boolean));
|
|
545
|
+
const added = [];
|
|
546
|
+
const skipped = [];
|
|
547
|
+
|
|
548
|
+
const candidates = entries.map((entry, index) => {
|
|
549
|
+
const text = cleanNoteText(entry?.text);
|
|
550
|
+
if (!text) throw new Error(`Learning capture entry ${index + 1} has empty text`);
|
|
551
|
+
const category = normalizeCategory(entry?.category || "workflow");
|
|
552
|
+
const createdAt = offsetIsoDate(now, index);
|
|
553
|
+
const entrySource = String(entry?.source || source || "cli").trim() || "cli";
|
|
554
|
+
return {
|
|
555
|
+
id: `learn-${shortEntryId({ text, category, createdAt })}`,
|
|
556
|
+
category,
|
|
557
|
+
text,
|
|
558
|
+
source: entrySource,
|
|
559
|
+
createdAt,
|
|
560
|
+
};
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
for (const candidate of candidates) {
|
|
564
|
+
const mergeKey = learningEntryMergeKey(candidate);
|
|
565
|
+
if (existingKeys.has(mergeKey)) {
|
|
566
|
+
skipped.push({
|
|
567
|
+
...statsEntry(candidate),
|
|
568
|
+
reason: "duplicate-entry-text",
|
|
569
|
+
});
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const entry = {
|
|
574
|
+
...candidate,
|
|
575
|
+
id: uniqueImportedEntryId(candidate, usedIds),
|
|
576
|
+
};
|
|
577
|
+
usedIds.add(entry.id);
|
|
578
|
+
existingKeys.add(mergeKey);
|
|
579
|
+
added.push(entry);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const updatedAt = added.length > 0 ? added[added.length - 1].createdAt : profile.updatedAt;
|
|
583
|
+
const nextProfile = {
|
|
584
|
+
version: 1,
|
|
585
|
+
updatedAt,
|
|
586
|
+
entries: [...profile.entries, ...added],
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
if (!dryRun && added.length > 0) {
|
|
590
|
+
writeLearningProfile(filePath, nextProfile);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return {
|
|
594
|
+
file: filePath,
|
|
595
|
+
dryRun,
|
|
596
|
+
applied: !dryRun,
|
|
597
|
+
source,
|
|
598
|
+
candidateCount: candidates.length,
|
|
599
|
+
addedCount: added.length,
|
|
600
|
+
skippedCount: skipped.length,
|
|
601
|
+
count: nextProfile.entries.length,
|
|
602
|
+
entries: added,
|
|
603
|
+
skipped,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export function initializeLearningProfile({
|
|
608
|
+
filePath = defaultLearningFile(),
|
|
609
|
+
dryRun = true,
|
|
610
|
+
now = new Date(),
|
|
611
|
+
} = {}) {
|
|
612
|
+
return captureLearningEntries({
|
|
613
|
+
entries: LEARNING_INIT_ENTRIES,
|
|
614
|
+
source: LEARNING_INIT_SOURCE,
|
|
615
|
+
filePath,
|
|
616
|
+
dryRun,
|
|
617
|
+
now,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function resolveLearningEntryTarget(profile, target) {
|
|
622
|
+
const normalizedTarget = String(target || "").trim();
|
|
623
|
+
if (!normalizedTarget) throw new Error("--forget expects an entry id or list number");
|
|
624
|
+
|
|
625
|
+
if (/^\d+$/.test(normalizedTarget)) {
|
|
626
|
+
const index = Number(normalizedTarget) - 1;
|
|
627
|
+
if (index >= 0 && index < profile.entries.length) {
|
|
628
|
+
return { index, entry: profile.entries[index] };
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const index = profile.entries.findIndex((entry) => entry.id === normalizedTarget);
|
|
633
|
+
if (index >= 0) return { index, entry: profile.entries[index] };
|
|
634
|
+
|
|
635
|
+
throw new Error(`Learning entry not found: ${normalizedTarget}`);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
export function forgetLearning({
|
|
639
|
+
target,
|
|
640
|
+
filePath = defaultLearningFile(),
|
|
641
|
+
now = new Date(),
|
|
642
|
+
}) {
|
|
643
|
+
const profile = loadLearningProfile(filePath);
|
|
644
|
+
const { index, entry } = resolveLearningEntryTarget(profile, target);
|
|
645
|
+
const updatedAt = now.toISOString();
|
|
646
|
+
const nextProfile = {
|
|
647
|
+
version: 1,
|
|
648
|
+
updatedAt,
|
|
649
|
+
entries: profile.entries.filter((_, entryIndex) => entryIndex !== index),
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
writeLearningProfile(filePath, nextProfile);
|
|
653
|
+
|
|
654
|
+
return {
|
|
655
|
+
file: filePath,
|
|
656
|
+
removed: entry,
|
|
657
|
+
count: nextProfile.entries.length,
|
|
658
|
+
profile: nextProfile,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function fixableLearningSuggestions(audit) {
|
|
663
|
+
const byEntryId = new Map();
|
|
664
|
+
const skipped = [];
|
|
665
|
+
|
|
666
|
+
for (const suggestion of audit.suggestions || []) {
|
|
667
|
+
if (!suggestion.entryId || !Array.isArray(suggestion.commandArgs) || suggestion.commandArgs.length === 0) {
|
|
668
|
+
skipped.push({
|
|
669
|
+
issueCode: suggestion.issueCode,
|
|
670
|
+
...(suggestion.entryId ? { entryId: suggestion.entryId } : {}),
|
|
671
|
+
action: suggestion.action,
|
|
672
|
+
reason: "manual-review-required",
|
|
673
|
+
message: suggestion.message,
|
|
674
|
+
});
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (!byEntryId.has(suggestion.entryId)) {
|
|
679
|
+
byEntryId.set(suggestion.entryId, {
|
|
680
|
+
entryId: suggestion.entryId,
|
|
681
|
+
issueCodes: [],
|
|
682
|
+
actions: [],
|
|
683
|
+
commandArgs: suggestion.commandArgs,
|
|
684
|
+
command: suggestion.command,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const fix = byEntryId.get(suggestion.entryId);
|
|
689
|
+
if (!fix.issueCodes.includes(suggestion.issueCode)) fix.issueCodes.push(suggestion.issueCode);
|
|
690
|
+
if (!fix.actions.includes(suggestion.action)) fix.actions.push(suggestion.action);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
return {
|
|
694
|
+
fixes: [...byEntryId.values()],
|
|
695
|
+
skipped,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
export function applyLearningAuditFixes({
|
|
700
|
+
filePath = defaultLearningFile(),
|
|
701
|
+
dryRun = true,
|
|
702
|
+
now = new Date(),
|
|
703
|
+
} = {}) {
|
|
704
|
+
const beforeAudit = auditLearningProfile({ filePath });
|
|
705
|
+
const { fixes, skipped } = fixableLearningSuggestions(beforeAudit);
|
|
706
|
+
const payload = {
|
|
707
|
+
file: filePath,
|
|
708
|
+
dryRun,
|
|
709
|
+
applied: !dryRun,
|
|
710
|
+
before: beforeAudit.summary,
|
|
711
|
+
cleanupCount: fixes.length,
|
|
712
|
+
cleanup: fixes,
|
|
713
|
+
skipped,
|
|
714
|
+
removed: [],
|
|
715
|
+
after: null,
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
if (dryRun || fixes.length === 0) {
|
|
719
|
+
return payload;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const targetIds = new Set(fixes.map((fix) => fix.entryId));
|
|
723
|
+
const profile = loadLearningProfile(filePath);
|
|
724
|
+
const removed = [];
|
|
725
|
+
const remaining = [];
|
|
726
|
+
|
|
727
|
+
for (const entry of profile.entries) {
|
|
728
|
+
if (targetIds.has(entry.id)) {
|
|
729
|
+
removed.push(statsEntry(entry));
|
|
730
|
+
} else {
|
|
731
|
+
remaining.push(entry);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const removedIds = new Set(removed.map((entry) => entry.id));
|
|
736
|
+
for (const fix of fixes) {
|
|
737
|
+
if (!removedIds.has(fix.entryId)) {
|
|
738
|
+
skipped.push({
|
|
739
|
+
entryId: fix.entryId,
|
|
740
|
+
action: fix.actions.join(","),
|
|
741
|
+
reason: "entry-not-found",
|
|
742
|
+
message: "The entry was not present when applying audit cleanup.",
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const updatedAt = now.toISOString();
|
|
748
|
+
writeLearningProfile(filePath, {
|
|
749
|
+
version: 1,
|
|
750
|
+
updatedAt,
|
|
751
|
+
entries: remaining,
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
const afterAudit = auditLearningProfile({ filePath });
|
|
755
|
+
return {
|
|
756
|
+
...payload,
|
|
757
|
+
cleanupCount: removed.length,
|
|
758
|
+
cleanup: fixes.filter((fix) => removedIds.has(fix.entryId)),
|
|
759
|
+
skipped,
|
|
760
|
+
removed,
|
|
761
|
+
after: afterAudit.summary,
|
|
762
|
+
};
|
|
763
|
+
}
|