@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,223 @@
|
|
|
1
|
+
// Learning entry selection and prompt-context rendering for `design-ai learn`.
|
|
2
|
+
|
|
3
|
+
import { normalizeCategory } from "./learn-args.mjs";
|
|
4
|
+
import { auditLearningProfile, loadLearningProfile } from "./learn-profile.mjs";
|
|
5
|
+
import { cleanNoteText, defaultLearningFile } from "./learn-shared.mjs";
|
|
6
|
+
import { bm25Score, buildLexicalStats, lexicalQueryTokens } from "./lexical.mjs";
|
|
7
|
+
|
|
8
|
+
function learningEntryTime(entry) {
|
|
9
|
+
const time = Date.parse(entry.createdAt || "");
|
|
10
|
+
return Number.isNaN(time) ? 0 : time;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Shared lexical (BM25-style) scorer from lexical.mjs — the same auditable algorithm
|
|
14
|
+
// that ranks `design-ai search --ranked` (docs/AI-LEARNING-PHASE2.md, Phase A).
|
|
15
|
+
function rankLearningEntries(entries, { query = "" } = {}) {
|
|
16
|
+
const queryTokens = lexicalQueryTokens(query);
|
|
17
|
+
const stats = buildLexicalStats(entries.map((entry, index) => ({
|
|
18
|
+
id: String(index),
|
|
19
|
+
text: cleanNoteText(`${entry.category || ""} ${entry.text || ""}`),
|
|
20
|
+
})));
|
|
21
|
+
const docByIndex = new Map(stats.documents.map((doc) => [doc.id, doc]));
|
|
22
|
+
const ranked = entries.map((entry, index) => {
|
|
23
|
+
const doc = docByIndex.get(String(index));
|
|
24
|
+
const relevance = queryTokens.length === 0 || !doc
|
|
25
|
+
? { score: 0, matchedTokens: [] }
|
|
26
|
+
: bm25Score(queryTokens, doc, stats);
|
|
27
|
+
return {
|
|
28
|
+
entry,
|
|
29
|
+
index,
|
|
30
|
+
score: Number(relevance.score.toFixed(6)),
|
|
31
|
+
matchedTokens: relevance.matchedTokens,
|
|
32
|
+
time: learningEntryTime(entry),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (queryTokens.length === 0) {
|
|
37
|
+
return {
|
|
38
|
+
entries,
|
|
39
|
+
ranked,
|
|
40
|
+
query: "",
|
|
41
|
+
mode: "recency",
|
|
42
|
+
candidateCount: entries.length,
|
|
43
|
+
matchedCount: 0,
|
|
44
|
+
queryTokenCount: 0,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ranked.sort((a, b) => (
|
|
49
|
+
b.score - a.score
|
|
50
|
+
|| b.time - a.time
|
|
51
|
+
|| b.index - a.index
|
|
52
|
+
));
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
entries: ranked.map((item) => item.entry),
|
|
56
|
+
ranked,
|
|
57
|
+
query: String(query || "").trim(),
|
|
58
|
+
mode: "brief-relevance",
|
|
59
|
+
candidateCount: entries.length,
|
|
60
|
+
matchedCount: ranked.filter((item) => item.score > 0).length,
|
|
61
|
+
queryTokenCount: queryTokens.length,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function learningSelectionReason(item, mode) {
|
|
66
|
+
if (mode !== "brief-relevance") return "recency";
|
|
67
|
+
return item.score > 0 ? "brief-match" : "recency-fallback";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function learningSelectionItem(item, mode) {
|
|
71
|
+
return {
|
|
72
|
+
id: item.entry.id,
|
|
73
|
+
category: item.entry.category,
|
|
74
|
+
score: item.score,
|
|
75
|
+
matchedTokens: item.matchedTokens,
|
|
76
|
+
reason: learningSelectionReason(item, mode),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function selectLearningEntrySet(profile, {
|
|
81
|
+
category = "",
|
|
82
|
+
limit = 0,
|
|
83
|
+
query = "",
|
|
84
|
+
includeFallback = true,
|
|
85
|
+
} = {}) {
|
|
86
|
+
const normalizedCategory = category ? normalizeCategory(category) : "";
|
|
87
|
+
const entries = [...profile.entries].filter((entry) => (
|
|
88
|
+
entry.text && (!normalizedCategory || entry.category === normalizedCategory)
|
|
89
|
+
));
|
|
90
|
+
const ranked = rankLearningEntries(entries, { query });
|
|
91
|
+
const rankedItems = ranked.mode === "brief-relevance" && !includeFallback
|
|
92
|
+
? ranked.ranked.filter((item) => item.score > 0)
|
|
93
|
+
: ranked.ranked;
|
|
94
|
+
const selectedItems = Number.isInteger(limit) && limit > 0
|
|
95
|
+
? ranked.mode === "brief-relevance"
|
|
96
|
+
? rankedItems.slice(0, limit)
|
|
97
|
+
: rankedItems.slice(-limit)
|
|
98
|
+
: rankedItems;
|
|
99
|
+
const selected = selectedItems.map((item) => item.entry);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
entries: selected,
|
|
103
|
+
selection: {
|
|
104
|
+
mode: ranked.mode,
|
|
105
|
+
query: ranked.query,
|
|
106
|
+
candidateCount: ranked.candidateCount,
|
|
107
|
+
matchedCount: ranked.matchedCount,
|
|
108
|
+
queryTokenCount: ranked.queryTokenCount,
|
|
109
|
+
fallbackEnabled: ranked.mode === "brief-relevance" ? includeFallback : false,
|
|
110
|
+
selectedCount: selected.length,
|
|
111
|
+
fallbackCount: ranked.mode === "brief-relevance"
|
|
112
|
+
? selectedItems.filter((item) => item.score === 0).length
|
|
113
|
+
: 0,
|
|
114
|
+
selected: selectedItems.map((item) => learningSelectionItem(item, ranked.mode)),
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function selectLearningEntries(profile, {
|
|
120
|
+
category = "",
|
|
121
|
+
limit = 0,
|
|
122
|
+
query = "",
|
|
123
|
+
includeFallback = true,
|
|
124
|
+
} = {}) {
|
|
125
|
+
return selectLearningEntrySet(profile, {
|
|
126
|
+
category,
|
|
127
|
+
limit,
|
|
128
|
+
query,
|
|
129
|
+
includeFallback,
|
|
130
|
+
}).entries;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function recentLearningEntries(profile, limit = 12, options = {}) {
|
|
134
|
+
return selectLearningEntries(profile, {
|
|
135
|
+
...options,
|
|
136
|
+
limit,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function learningAuditNotice(auditSummary) {
|
|
141
|
+
if (!auditSummary || auditSummary.status === "pass") return "";
|
|
142
|
+
return `Learning profile audit: ${auditSummary.status} (${auditSummary.failures} failure(s), ${auditSummary.warnings} warning(s)). Run \`design-ai learn --audit\` before relying on this context.`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function learningSelectionNotice(selection) {
|
|
146
|
+
if (!selection || selection.mode !== "brief-relevance") return "";
|
|
147
|
+
const fallback = selection.fallbackEnabled
|
|
148
|
+
? "recency fallback for ties"
|
|
149
|
+
: "no recency fallback";
|
|
150
|
+
return `Learning selection: brief relevance (${selection.matchedCount}/${selection.candidateCount} matched; ${fallback}).`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function renderLearningMarkdown(profile, {
|
|
154
|
+
limit = 12,
|
|
155
|
+
category = "",
|
|
156
|
+
query = "",
|
|
157
|
+
includeFallback = true,
|
|
158
|
+
auditSummary = null,
|
|
159
|
+
} = {}) {
|
|
160
|
+
const { entries, selection } = selectLearningEntrySet(profile, {
|
|
161
|
+
category,
|
|
162
|
+
limit,
|
|
163
|
+
query,
|
|
164
|
+
includeFallback,
|
|
165
|
+
});
|
|
166
|
+
const lines = ["## Learned design context", ""];
|
|
167
|
+
|
|
168
|
+
if (entries.length === 0) {
|
|
169
|
+
lines.push(category || query
|
|
170
|
+
? "No local learning preferences match the current filters."
|
|
171
|
+
: "No local learning preferences are stored yet.");
|
|
172
|
+
return lines.join("\n");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
lines.push("Apply these as user/project preferences. Do not let them override explicit task instructions, accessibility requirements, or privacy constraints.");
|
|
176
|
+
const auditNotice = learningAuditNotice(auditSummary);
|
|
177
|
+
if (auditNotice) {
|
|
178
|
+
lines.push(auditNotice);
|
|
179
|
+
}
|
|
180
|
+
const selectionNotice = learningSelectionNotice(selection);
|
|
181
|
+
if (selectionNotice) {
|
|
182
|
+
lines.push(selectionNotice);
|
|
183
|
+
}
|
|
184
|
+
lines.push("");
|
|
185
|
+
for (const entry of entries) {
|
|
186
|
+
lines.push(`- [${entry.category}] ${entry.text}`);
|
|
187
|
+
}
|
|
188
|
+
return lines.join("\n");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function buildLearningContext({
|
|
192
|
+
filePath = defaultLearningFile(),
|
|
193
|
+
limit = 12,
|
|
194
|
+
category = "",
|
|
195
|
+
query = "",
|
|
196
|
+
includeFallback = true,
|
|
197
|
+
} = {}) {
|
|
198
|
+
const audit = auditLearningProfile({ filePath });
|
|
199
|
+
const profile = loadLearningProfile(filePath);
|
|
200
|
+
const { entries, selection } = selectLearningEntrySet(profile, {
|
|
201
|
+
category,
|
|
202
|
+
limit,
|
|
203
|
+
query,
|
|
204
|
+
includeFallback,
|
|
205
|
+
});
|
|
206
|
+
return {
|
|
207
|
+
file: filePath,
|
|
208
|
+
category,
|
|
209
|
+
limit,
|
|
210
|
+
query: String(query || "").trim(),
|
|
211
|
+
selection,
|
|
212
|
+
entries,
|
|
213
|
+
empty: entries.length === 0,
|
|
214
|
+
auditSummary: audit.summary,
|
|
215
|
+
markdown: renderLearningMarkdown(profile, {
|
|
216
|
+
limit,
|
|
217
|
+
category,
|
|
218
|
+
query,
|
|
219
|
+
includeFallback,
|
|
220
|
+
auditSummary: audit.summary,
|
|
221
|
+
}),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Shared internal helpers for the `design-ai learn` module family.
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
mkdirSync,
|
|
5
|
+
writeFileSync,
|
|
6
|
+
} from "node:fs";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_LEARNING_FILE = path.join(homedir(), ".design-ai", "learning.json");
|
|
12
|
+
export const LEARNING_SENSITIVE_PATTERNS = [
|
|
13
|
+
{
|
|
14
|
+
code: "private-key",
|
|
15
|
+
label: "a private key block",
|
|
16
|
+
pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/i,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
code: "secret-assignment",
|
|
20
|
+
label: "a secret-like assignment",
|
|
21
|
+
pattern: /\b(?:api[_-]?key|secret|token|password)\b\s*[:=]/i,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
code: "openai-secret-key",
|
|
25
|
+
label: "an OpenAI-style secret key",
|
|
26
|
+
pattern: /\bsk-[A-Za-z0-9_-]{20,}\b/,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
code: "email-address",
|
|
30
|
+
label: "an email address",
|
|
31
|
+
pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
code: "kr-phone-number",
|
|
35
|
+
label: "a Korean mobile phone number",
|
|
36
|
+
pattern: /\b01[016789]-?\d{3,4}-?\d{4}\b/,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export function defaultLearningFile() {
|
|
41
|
+
return process.env.DESIGN_AI_LEARNING_FILE || DEFAULT_LEARNING_FILE;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function defaultLearningUsageFile(filePath = defaultLearningFile()) {
|
|
45
|
+
if (process.env.DESIGN_AI_LEARNING_USAGE_FILE) {
|
|
46
|
+
return process.env.DESIGN_AI_LEARNING_USAGE_FILE;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const parsed = path.parse(filePath);
|
|
50
|
+
const ext = parsed.ext || ".json";
|
|
51
|
+
return path.join(parsed.dir, `${parsed.name}.usage${ext}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function defaultLearningRestoreBackupFile(filePath = defaultLearningFile(), now = new Date()) {
|
|
55
|
+
const resolvedFile = path.resolve(filePath);
|
|
56
|
+
const parsed = path.parse(resolvedFile);
|
|
57
|
+
const ext = parsed.ext || ".json";
|
|
58
|
+
const timestamp = (now instanceof Date ? now : new Date(now))
|
|
59
|
+
.toISOString()
|
|
60
|
+
.replace(/[-:]/g, "")
|
|
61
|
+
.replace(/\./g, "");
|
|
62
|
+
return path.join(parsed.dir, `${parsed.name}.restore-backup-${timestamp}${ext}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function shellQuote(value) {
|
|
66
|
+
const text = String(value);
|
|
67
|
+
if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(text)) return text;
|
|
68
|
+
return `'${text.replace(/'/g, "'\\''")}'`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function commandFromArgs(args) {
|
|
72
|
+
return args.map(shellQuote).join(" ");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function writeLearningProfile(filePath, profile) {
|
|
76
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
77
|
+
writeFileSync(filePath, `${JSON.stringify(profile, null, 2)}\n`, "utf8");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function shortEntryId({ text, category, createdAt }) {
|
|
81
|
+
const input = `${createdAt}\n${category}\n${text}`;
|
|
82
|
+
return createHash("sha256").update(input).digest("hex").slice(0, 10);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function shortHash(value) {
|
|
86
|
+
return createHash("sha256").update(String(value || "")).digest("hex").slice(0, 16);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function cleanNoteText(text) {
|
|
90
|
+
return String(text || "").trim().replace(/\s+/g, " ");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function learningEntryMergeKey(entry) {
|
|
94
|
+
return `${entry.category}\n${cleanNoteText(entry.text).toLowerCase()}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function uniqueImportedEntryId(entry, usedIds) {
|
|
98
|
+
if (entry.id && !usedIds.has(entry.id)) return entry.id;
|
|
99
|
+
|
|
100
|
+
for (let attempt = 0; attempt < 1000; attempt += 1) {
|
|
101
|
+
const suffix = attempt === 0 ? "" : `:${attempt}`;
|
|
102
|
+
const candidate = `learn-${shortEntryId({
|
|
103
|
+
text: entry.text,
|
|
104
|
+
category: entry.category,
|
|
105
|
+
createdAt: `${entry.createdAt}:${entry.source}${suffix}`,
|
|
106
|
+
})}`;
|
|
107
|
+
if (!usedIds.has(candidate)) return candidate;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
throw new Error("Could not allocate a unique learning entry id during import");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function parseLearningProfilePayload(importText, label = "Learning import") {
|
|
114
|
+
let payload = null;
|
|
115
|
+
try {
|
|
116
|
+
payload = JSON.parse(importText);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
throw new Error(`${label} is not valid JSON`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
122
|
+
throw new Error(`${label} must be a JSON object with an entries array`);
|
|
123
|
+
}
|
|
124
|
+
if (!Array.isArray(payload.entries)) {
|
|
125
|
+
throw new Error(`${label} must include an entries array`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return payload;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function countBy(items, getKey) {
|
|
132
|
+
const counts = {};
|
|
133
|
+
for (const item of items) {
|
|
134
|
+
const key = String(getKey(item) || "").trim();
|
|
135
|
+
if (key) counts[key] = (counts[key] || 0) + 1;
|
|
136
|
+
}
|
|
137
|
+
return counts;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function previewText(text, maxChars = 96) {
|
|
141
|
+
const cleaned = cleanNoteText(text);
|
|
142
|
+
if (cleaned.length <= maxChars) return cleaned;
|
|
143
|
+
return `${cleaned.slice(0, maxChars - 3)}...`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function statsEntry(entry) {
|
|
147
|
+
return {
|
|
148
|
+
id: entry.id,
|
|
149
|
+
category: entry.category,
|
|
150
|
+
source: entry.source,
|
|
151
|
+
createdAt: entry.createdAt,
|
|
152
|
+
textPreview: previewText(entry.text),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function formatLearningJson(payload) {
|
|
157
|
+
return JSON.stringify(payload, null, 2);
|
|
158
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// Shared test helpers for the learn.* test suite.
|
|
2
|
+
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
export function withTempDir(fn) {
|
|
8
|
+
const dir = mkdtempSync(path.join(tmpdir(), "design-ai-learn-test-"));
|
|
9
|
+
try {
|
|
10
|
+
return fn(dir);
|
|
11
|
+
} finally {
|
|
12
|
+
rmSync(dir, { recursive: true, force: true });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function withTempDirAsync(fn) {
|
|
17
|
+
const dir = mkdtempSync(path.join(tmpdir(), "design-ai-learn-test-"));
|
|
18
|
+
try {
|
|
19
|
+
return await fn(dir);
|
|
20
|
+
} finally {
|
|
21
|
+
rmSync(dir, { recursive: true, force: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function captureStdout(fn) {
|
|
26
|
+
const lines = [];
|
|
27
|
+
const originalLog = console.log;
|
|
28
|
+
console.log = (...args) => {
|
|
29
|
+
lines.push(args.join(" "));
|
|
30
|
+
};
|
|
31
|
+
try {
|
|
32
|
+
await fn();
|
|
33
|
+
} finally {
|
|
34
|
+
console.log = originalLog;
|
|
35
|
+
}
|
|
36
|
+
return lines.join("\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function withLearningEnv({ learningFile, usageFile }, fn) {
|
|
40
|
+
const previousLearningFile = process.env.DESIGN_AI_LEARNING_FILE;
|
|
41
|
+
const previousUsageFile = process.env.DESIGN_AI_LEARNING_USAGE_FILE;
|
|
42
|
+
process.env.DESIGN_AI_LEARNING_FILE = learningFile;
|
|
43
|
+
process.env.DESIGN_AI_LEARNING_USAGE_FILE = usageFile;
|
|
44
|
+
try {
|
|
45
|
+
return await fn();
|
|
46
|
+
} finally {
|
|
47
|
+
if (previousLearningFile === undefined) {
|
|
48
|
+
delete process.env.DESIGN_AI_LEARNING_FILE;
|
|
49
|
+
} else {
|
|
50
|
+
process.env.DESIGN_AI_LEARNING_FILE = previousLearningFile;
|
|
51
|
+
}
|
|
52
|
+
if (previousUsageFile === undefined) {
|
|
53
|
+
delete process.env.DESIGN_AI_LEARNING_USAGE_FILE;
|
|
54
|
+
} else {
|
|
55
|
+
process.env.DESIGN_AI_LEARNING_USAGE_FILE = previousUsageFile;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
import assert from "node:assert/strict";
|
|
61
|
+
|
|
62
|
+
import { runLearn } from "../commands/learn.mjs";
|
|
63
|
+
import { defaultLearningUsageFile } from "./learn.mjs";
|
|
64
|
+
import {
|
|
65
|
+
buildSkillEvolutionProposals,
|
|
66
|
+
buildSkillProposalApplyPlan,
|
|
67
|
+
} from "./skill-proposals.mjs";
|
|
68
|
+
|
|
69
|
+
export function writeStrictProposalsFixture(dir) {
|
|
70
|
+
const filePath = path.join(dir, "learning.json");
|
|
71
|
+
const usageFile = defaultLearningUsageFile(filePath);
|
|
72
|
+
writeFileSync(filePath, JSON.stringify({
|
|
73
|
+
version: 1,
|
|
74
|
+
updatedAt: "2026-06-10T00:00:02.000Z",
|
|
75
|
+
entries: [
|
|
76
|
+
{
|
|
77
|
+
id: "learn-check-a",
|
|
78
|
+
category: "workflow",
|
|
79
|
+
text: "Improve future outputs by addressing Responsive behavior: No mobile behavior note detected.",
|
|
80
|
+
source: "check:website-improvement",
|
|
81
|
+
createdAt: "2026-06-10T00:00:01.000Z",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "learn-check-b",
|
|
85
|
+
category: "workflow",
|
|
86
|
+
text: "Improve future outputs by addressing Responsive behavior: No tablet behavior note detected.",
|
|
87
|
+
source: "check:website-improvement",
|
|
88
|
+
createdAt: "2026-06-10T00:00:02.000Z",
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
}), "utf8");
|
|
92
|
+
writeFileSync(usageFile, JSON.stringify({
|
|
93
|
+
version: 1,
|
|
94
|
+
updatedAt: "2026-06-10T00:00:02.000Z",
|
|
95
|
+
profileFile: filePath,
|
|
96
|
+
events: [],
|
|
97
|
+
}), "utf8");
|
|
98
|
+
const before = readFileSync(filePath, "utf8");
|
|
99
|
+
const candidateSkillPath = path.resolve("skills/website-improvement/SKILL.md");
|
|
100
|
+
const candidateSkillBefore = readFileSync(candidateSkillPath, "utf8");
|
|
101
|
+
return { filePath, usageFile, before, candidateSkillPath, candidateSkillBefore };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function writePendingReviewFile(dir, strictPayload) {
|
|
105
|
+
const reviewFile = path.join(dir, "skill-proposals.review.json");
|
|
106
|
+
writeFileSync(reviewFile, JSON.stringify({
|
|
107
|
+
version: 1,
|
|
108
|
+
decisions: [
|
|
109
|
+
{
|
|
110
|
+
proposalId: strictPayload.proposals[0].id,
|
|
111
|
+
status: "applied",
|
|
112
|
+
reviewedAt: "2026-06-10T00:05:00.000Z",
|
|
113
|
+
reviewer: "local-operator",
|
|
114
|
+
note: "Instruction delta was manually reviewed and applied.",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
proposalId: "skill-proposal-stale",
|
|
118
|
+
status: "rejected",
|
|
119
|
+
reviewedAt: "2026-06-09T00:00:00.000Z",
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
}), "utf8");
|
|
123
|
+
return reviewFile;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function writeCleanReviewFile(dir, strictPayload) {
|
|
127
|
+
const cleanReviewFile = path.join(dir, "skill-proposals.clean.review.json");
|
|
128
|
+
writeFileSync(cleanReviewFile, JSON.stringify({
|
|
129
|
+
version: 1,
|
|
130
|
+
decisions: [
|
|
131
|
+
{
|
|
132
|
+
proposalId: strictPayload.proposals[0].id,
|
|
133
|
+
status: "applied",
|
|
134
|
+
reviewedAt: "2026-06-10T00:06:00.000Z",
|
|
135
|
+
reviewer: "local-operator",
|
|
136
|
+
note: "Instruction delta was manually reviewed and applied.",
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
}), "utf8");
|
|
140
|
+
return cleanReviewFile;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function buildAcceptedApplyPlanFixture(dir, filePath, usageFile, strictPayload) {
|
|
144
|
+
const acceptedReviewFile = path.join(dir, "skill-proposals.accepted.review.json");
|
|
145
|
+
writeFileSync(acceptedReviewFile, JSON.stringify({
|
|
146
|
+
version: 1,
|
|
147
|
+
decisions: [
|
|
148
|
+
{
|
|
149
|
+
proposalId: strictPayload.proposals[0].id,
|
|
150
|
+
status: "accepted",
|
|
151
|
+
reviewedAt: "2026-06-10T00:07:00.000Z",
|
|
152
|
+
reviewer: "local-operator",
|
|
153
|
+
note: "Instruction delta is accepted for manual skill editing.",
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
}), "utf8");
|
|
157
|
+
const acceptedReviewBefore = readFileSync(acceptedReviewFile, "utf8");
|
|
158
|
+
const acceptedProposalPayload = buildSkillEvolutionProposals({
|
|
159
|
+
filePath,
|
|
160
|
+
usageFile,
|
|
161
|
+
signalSource: dir,
|
|
162
|
+
root: dir,
|
|
163
|
+
reviewFile: acceptedReviewFile,
|
|
164
|
+
signalRegistryProvider: ({ signalSource }) => ({
|
|
165
|
+
status: "pass",
|
|
166
|
+
signalSource: path.resolve(signalSource),
|
|
167
|
+
}),
|
|
168
|
+
});
|
|
169
|
+
const applyPlan = buildSkillProposalApplyPlan(acceptedProposalPayload, {
|
|
170
|
+
generatedAt: new Date("2026-06-10T00:12:00.000Z"),
|
|
171
|
+
});
|
|
172
|
+
assert.equal(applyPlan.kind, "skill-proposal-apply-plan");
|
|
173
|
+
assert.equal(applyPlan.status, "warn");
|
|
174
|
+
assert.equal(applyPlan.acceptedCount, 1);
|
|
175
|
+
assert.equal(applyPlan.tasks[0].proposalId, strictPayload.proposals[0].id);
|
|
176
|
+
assert.equal(applyPlan.tasks[0].candidateSkillPath, "skills/website-improvement/SKILL.md");
|
|
177
|
+
assert.match(applyPlan.tasks[0].manualSteps.join("\n"), /update the review decision from `accepted` to `applied`/);
|
|
178
|
+
assert.match(applyPlan.commands.reviewCheckJson, /--file /);
|
|
179
|
+
assert.match(applyPlan.commands.reviewCheckJson, /--usage-file /);
|
|
180
|
+
assert.match(applyPlan.commands.reviewCheckJson, /--from-file /);
|
|
181
|
+
assert.match(applyPlan.commands.reviewCheckJson, /--review-check --json/);
|
|
182
|
+
const applyPlanContextArgs = [
|
|
183
|
+
"design-ai",
|
|
184
|
+
"learn",
|
|
185
|
+
"--propose-skills",
|
|
186
|
+
"--file",
|
|
187
|
+
filePath,
|
|
188
|
+
"--usage-file",
|
|
189
|
+
usageFile,
|
|
190
|
+
"--from-file",
|
|
191
|
+
dir,
|
|
192
|
+
"--review-file",
|
|
193
|
+
acceptedReviewFile,
|
|
194
|
+
];
|
|
195
|
+
return {
|
|
196
|
+
acceptedReviewFile,
|
|
197
|
+
acceptedReviewBefore,
|
|
198
|
+
acceptedProposalPayload,
|
|
199
|
+
applyPlan,
|
|
200
|
+
applyPlanContextArgs,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function runStrictProposalsPayload({ dir, filePath, usageFile }) {
|
|
205
|
+
const strictOutput = await captureStdout(() => runLearn([
|
|
206
|
+
"--propose-skills",
|
|
207
|
+
"--file",
|
|
208
|
+
filePath,
|
|
209
|
+
"--usage-file",
|
|
210
|
+
usageFile,
|
|
211
|
+
"--from-file",
|
|
212
|
+
dir,
|
|
213
|
+
"--strict",
|
|
214
|
+
"--json",
|
|
215
|
+
]));
|
|
216
|
+
process.exitCode = 0;
|
|
217
|
+
return JSON.parse(strictOutput);
|
|
218
|
+
}
|