@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,137 @@
|
|
|
1
|
+
// Shared deterministic lexical (BM25-style) scoring for corpus and learning retrieval.
|
|
2
|
+
// Phase A of docs/AI-LEARNING-PHASE2.md: zero-dependency, no randomness, no time-dependent scoring.
|
|
3
|
+
|
|
4
|
+
export const BM25_K1 = 1.2;
|
|
5
|
+
export const BM25_B = 0.75;
|
|
6
|
+
|
|
7
|
+
// Hangul syllable block (U+AC00–U+D7A3). Korean surface forms are written without
|
|
8
|
+
// spaces between stem and particle (버튼을, 접근성이), so a whitespace-delimited token
|
|
9
|
+
// is an atomic surface form. To let a stem query match particle-attached documents
|
|
10
|
+
// (and vice versa), we deterministically expand every Hangul run of length >= 2 into
|
|
11
|
+
// its overlapping character bigrams and emit them ALONGSIDE the original token.
|
|
12
|
+
// 버튼을 -> keep 버튼을, add 버튼, 튼을
|
|
13
|
+
// 접근성 -> keep 접근성, add 접근, 근성
|
|
14
|
+
// 버튼 -> the 2-char run's only bigram is itself; kept via dedupe
|
|
15
|
+
// Latin/number tokens and mixed tokens' non-Hangul parts are unchanged. The rule is
|
|
16
|
+
// pure and deterministic (no locale, no time, no randomness); order is normalized by
|
|
17
|
+
// the dedupe+sort in lexicalQueryTokens and by termFrequencies for stats.
|
|
18
|
+
const HANGUL_RUN = /[가-힣]{2,}/gu;
|
|
19
|
+
|
|
20
|
+
function hangulBigrams(token) {
|
|
21
|
+
const bigrams = [];
|
|
22
|
+
for (const run of token.match(HANGUL_RUN) || []) {
|
|
23
|
+
for (let i = 0; i + 2 <= run.length; i += 1) {
|
|
24
|
+
bigrams.push(run.slice(i, i + 2));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return bigrams;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Same base token model as learn-select.mjs: Unicode letters/numbers, lowercased,
|
|
31
|
+
// length >= 2. Hangul tokens additionally emit character bigrams (see HANGUL_RUN).
|
|
32
|
+
export function lexicalTokens(text) {
|
|
33
|
+
const base = (String(text || "")
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.match(/[\p{L}\p{N}]+/gu) || [])
|
|
36
|
+
.filter((token) => token.length >= 2);
|
|
37
|
+
|
|
38
|
+
const expanded = [];
|
|
39
|
+
for (const token of base) {
|
|
40
|
+
expanded.push(token);
|
|
41
|
+
for (const bigram of hangulBigrams(token)) {
|
|
42
|
+
// A 2-char Hangul run's only bigram is the token itself; do not re-emit it,
|
|
43
|
+
// which would double-count its term frequency and distort BM25.
|
|
44
|
+
if (bigram !== token) expanded.push(bigram);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return expanded;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function lexicalQueryTokens(query) {
|
|
51
|
+
return Array.from(new Set(lexicalTokens(query))).sort();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function termFrequencies(tokens) {
|
|
55
|
+
const terms = {};
|
|
56
|
+
for (const token of tokens) {
|
|
57
|
+
terms[token] = (terms[token] || 0) + 1;
|
|
58
|
+
}
|
|
59
|
+
const sorted = {};
|
|
60
|
+
for (const term of Object.keys(terms).sort()) {
|
|
61
|
+
sorted[term] = terms[term];
|
|
62
|
+
}
|
|
63
|
+
return sorted;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// documents: [{ id, text }] -> deterministic index stats with sorted ids and term keys.
|
|
67
|
+
export function buildLexicalStats(documents) {
|
|
68
|
+
const indexed = [...documents]
|
|
69
|
+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
|
70
|
+
.map((doc) => {
|
|
71
|
+
const tokens = lexicalTokens(doc.text);
|
|
72
|
+
return { id: doc.id, length: tokens.length, terms: termFrequencies(tokens) };
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const docFrequency = {};
|
|
76
|
+
for (const doc of indexed) {
|
|
77
|
+
for (const term of Object.keys(doc.terms)) {
|
|
78
|
+
docFrequency[term] = (docFrequency[term] || 0) + 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const sortedDocFrequency = {};
|
|
82
|
+
for (const term of Object.keys(docFrequency).sort()) {
|
|
83
|
+
sortedDocFrequency[term] = docFrequency[term];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const totalLength = indexed.reduce((sum, doc) => sum + doc.length, 0);
|
|
87
|
+
return {
|
|
88
|
+
documentCount: indexed.length,
|
|
89
|
+
termCount: Object.keys(sortedDocFrequency).length,
|
|
90
|
+
avgDocLength: indexed.length > 0 ? totalLength / indexed.length : 0,
|
|
91
|
+
docFrequency: sortedDocFrequency,
|
|
92
|
+
documents: indexed,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function inverseDocumentFrequency(documentCount, docFrequency) {
|
|
97
|
+
return Math.log(1 + (documentCount - docFrequency + 0.5) / (docFrequency + 0.5));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function bm25Score(queryTokens, doc, stats) {
|
|
101
|
+
if (stats.documentCount === 0 || doc.length === 0) return { score: 0, matchedTokens: [] };
|
|
102
|
+
|
|
103
|
+
let score = 0;
|
|
104
|
+
const matchedTokens = [];
|
|
105
|
+
for (const token of queryTokens) {
|
|
106
|
+
const tf = doc.terms[token] || 0;
|
|
107
|
+
if (tf === 0) continue;
|
|
108
|
+
const df = stats.docFrequency[token] || 0;
|
|
109
|
+
const idf = inverseDocumentFrequency(stats.documentCount, df);
|
|
110
|
+
const lengthNorm = 1 - BM25_B + BM25_B * (doc.length / (stats.avgDocLength || 1));
|
|
111
|
+
score += idf * ((tf * (BM25_K1 + 1)) / (tf + BM25_K1 * lengthNorm));
|
|
112
|
+
matchedTokens.push(token);
|
|
113
|
+
}
|
|
114
|
+
return { score, matchedTokens };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Fully ordered ranking: score desc, then id asc. Zero-score documents are excluded.
|
|
118
|
+
export function rankLexical(query, stats, { limit = 20 } = {}) {
|
|
119
|
+
const queryTokens = lexicalQueryTokens(query);
|
|
120
|
+
if (queryTokens.length === 0) return [];
|
|
121
|
+
|
|
122
|
+
const ranked = [];
|
|
123
|
+
for (const doc of stats.documents) {
|
|
124
|
+
const { score, matchedTokens } = bm25Score(queryTokens, doc, stats);
|
|
125
|
+
if (score > 0) {
|
|
126
|
+
ranked.push({ id: doc.id, score, matchedTokens });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
ranked.sort((a, b) => {
|
|
130
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
131
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
132
|
+
});
|
|
133
|
+
return ranked.slice(0, limit).map((hit) => ({
|
|
134
|
+
...hit,
|
|
135
|
+
score: Number(hit.score.toFixed(6)),
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Local, user-authored configuration for optional Phase B features
|
|
2
|
+
// (docs/AI-LEARNING-PHASE2.md, "Phase A implementation review", decision 5 / FU-4).
|
|
3
|
+
//
|
|
4
|
+
// design-ai never writes this file. It is durable, per-machine, user-authored
|
|
5
|
+
// configuration — the CLI only reads it. Presence of a configured provider is
|
|
6
|
+
// NOT enough to enable embeddings: every consumer still requires the explicit
|
|
7
|
+
// per-invocation --embeddings flag (config supplies the provider, the flag arms it).
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
|
|
13
|
+
export function defaultConfigFile() {
|
|
14
|
+
return process.env.DESIGN_AI_CONFIG_FILE || path.join(homedir(), ".design-ai", "config.json");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const TOP_LEVEL_KEYS = new Set(["version", "embeddings"]);
|
|
18
|
+
const EMBEDDINGS_KEYS = new Set(["provider", "modelLabel"]);
|
|
19
|
+
const PROVIDER_KEYS = new Set(["command", "args"]);
|
|
20
|
+
|
|
21
|
+
function fail(message) {
|
|
22
|
+
return { present: true, config: null, error: message };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isPlainObject(value) {
|
|
26
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function validateProvider(provider) {
|
|
30
|
+
if (!isPlainObject(provider)) return "embeddings.provider must be an object";
|
|
31
|
+
|
|
32
|
+
const unknown = Object.keys(provider).filter((key) => !PROVIDER_KEYS.has(key));
|
|
33
|
+
if (unknown.length > 0) return `embeddings.provider has unknown key(s): ${unknown.join(", ")}`;
|
|
34
|
+
|
|
35
|
+
if (typeof provider.command !== "string" || provider.command.trim() === "") {
|
|
36
|
+
return "embeddings.provider.command is required and must be a non-empty string";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (provider.args !== undefined) {
|
|
40
|
+
if (!Array.isArray(provider.args) || !provider.args.every((arg) => typeof arg === "string")) {
|
|
41
|
+
return "embeddings.provider.args must be an array of strings";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function validateEmbeddings(embeddings) {
|
|
49
|
+
if (!isPlainObject(embeddings)) return "embeddings must be an object";
|
|
50
|
+
|
|
51
|
+
const unknown = Object.keys(embeddings).filter((key) => !EMBEDDINGS_KEYS.has(key));
|
|
52
|
+
if (unknown.length > 0) return `embeddings has unknown key(s): ${unknown.join(", ")}`;
|
|
53
|
+
|
|
54
|
+
if (embeddings.provider === undefined) return "embeddings.provider is required";
|
|
55
|
+
const providerError = validateProvider(embeddings.provider);
|
|
56
|
+
if (providerError) return providerError;
|
|
57
|
+
|
|
58
|
+
if (embeddings.modelLabel !== undefined && typeof embeddings.modelLabel !== "string") {
|
|
59
|
+
return "embeddings.modelLabel must be a string";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function validateConfig(config) {
|
|
66
|
+
if (!isPlainObject(config)) return "config must be a JSON object";
|
|
67
|
+
|
|
68
|
+
const unknown = Object.keys(config).filter((key) => !TOP_LEVEL_KEYS.has(key));
|
|
69
|
+
if (unknown.length > 0) return `config has unknown top-level key(s): ${unknown.join(", ")}`;
|
|
70
|
+
|
|
71
|
+
if (config.version !== 1) return "config.version must be 1";
|
|
72
|
+
|
|
73
|
+
if (config.embeddings !== undefined) {
|
|
74
|
+
const embeddingsError = validateEmbeddings(config.embeddings);
|
|
75
|
+
if (embeddingsError) return embeddingsError;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Never writes; design-ai only reads this user-authored file.
|
|
82
|
+
export function loadLocalConfig(filePath = defaultConfigFile()) {
|
|
83
|
+
if (!existsSync(filePath)) return { present: false, config: null, error: "" };
|
|
84
|
+
|
|
85
|
+
let raw;
|
|
86
|
+
try {
|
|
87
|
+
raw = readFileSync(filePath, "utf8");
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return fail(`unreadable config file: ${error.message}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let parsed;
|
|
93
|
+
try {
|
|
94
|
+
parsed = JSON.parse(raw);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return fail(`config file is not valid JSON: ${error.message}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const error = validateConfig(parsed);
|
|
100
|
+
if (error) return fail(error);
|
|
101
|
+
|
|
102
|
+
return { present: true, config: parsed, error: "" };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Convenience accessor: the configured embedding provider, or null when absent
|
|
106
|
+
// or the config is malformed/missing. Callers still must gate on --embeddings.
|
|
107
|
+
export function configuredEmbeddingProvider(filePath = defaultConfigFile()) {
|
|
108
|
+
const { config } = loadLocalConfig(filePath);
|
|
109
|
+
return config?.embeddings?.provider || null;
|
|
110
|
+
}
|
package/cli/lib/mcp-server.mjs
CHANGED
|
@@ -50,7 +50,7 @@ export const MCP_TOOLS = [
|
|
|
50
50
|
{
|
|
51
51
|
name: "design_ai_prompt",
|
|
52
52
|
title: "Generate an agent prompt",
|
|
53
|
-
description: "Generate a ready-to-use design-ai agent prompt from a brief. Read-only unless withLearning is true, which records local usage metadata.",
|
|
53
|
+
description: "Generate a ready-to-use design-ai agent prompt from a brief. Read-only unless withLearning is true, which records local usage metadata. Supports opt-in recall augmentation via withRecall, which enriches the output with brief-relevant shipped corpus knowledge.",
|
|
54
54
|
inputSchema: {
|
|
55
55
|
type: "object",
|
|
56
56
|
properties: {
|
|
@@ -59,6 +59,8 @@ export const MCP_TOOLS = [
|
|
|
59
59
|
withLearning: optionalBoolean("Include local learning preferences and record local usage metadata."),
|
|
60
60
|
learningCategory: optionalString("Optional learning category when withLearning is true."),
|
|
61
61
|
learningLimit: optionalInteger({ description: "Maximum learning entries, 1-100.", minimum: 1, maximum: 100 }),
|
|
62
|
+
withRecall: optionalBoolean("Opt-in recall augmentation: enriches the output with brief-relevant shipped corpus knowledge ranked by the deterministic lexical scorer. Requires no index and makes no network call."),
|
|
63
|
+
recallLimit: optionalInteger({ description: "Maximum recall passages, 1-20.", minimum: 1, maximum: 20 }),
|
|
62
64
|
json: optionalBoolean("Return machine-readable prompt plan JSON instead of Markdown."),
|
|
63
65
|
},
|
|
64
66
|
required: ["brief"],
|
|
@@ -68,7 +70,7 @@ export const MCP_TOOLS = [
|
|
|
68
70
|
{
|
|
69
71
|
name: "design_ai_pack",
|
|
70
72
|
title: "Generate prompt plus bounded context",
|
|
71
|
-
description: "Generate a prompt pack with relevant design-ai context files. Read-only unless withLearning is true, which records local usage metadata.",
|
|
73
|
+
description: "Generate a prompt pack with relevant design-ai context files. Read-only unless withLearning is true, which records local usage metadata. Supports opt-in recall augmentation via withRecall, which enriches the output with brief-relevant shipped corpus knowledge.",
|
|
72
74
|
inputSchema: {
|
|
73
75
|
type: "object",
|
|
74
76
|
properties: {
|
|
@@ -78,6 +80,8 @@ export const MCP_TOOLS = [
|
|
|
78
80
|
withLearning: optionalBoolean("Include local learning preferences and record local usage metadata."),
|
|
79
81
|
learningCategory: optionalString("Optional learning category when withLearning is true."),
|
|
80
82
|
learningLimit: optionalInteger({ description: "Maximum learning entries, 1-100.", minimum: 1, maximum: 100 }),
|
|
83
|
+
withRecall: optionalBoolean("Opt-in recall augmentation: enriches the output with brief-relevant shipped corpus knowledge ranked by the deterministic lexical scorer. Requires no index and makes no network call."),
|
|
84
|
+
recallLimit: optionalInteger({ description: "Maximum recall passages, 1-20.", minimum: 1, maximum: 20 }),
|
|
81
85
|
json: optionalBoolean("Return machine-readable pack JSON instead of Markdown."),
|
|
82
86
|
},
|
|
83
87
|
required: ["brief"],
|
|
@@ -87,13 +91,14 @@ export const MCP_TOOLS = [
|
|
|
87
91
|
{
|
|
88
92
|
name: "design_ai_search",
|
|
89
93
|
title: "Search design-ai corpus",
|
|
90
|
-
description: "Search knowledge, examples, skills, docs, agents, or commands. Read-only.",
|
|
94
|
+
description: "Search knowledge, examples, skills, docs, agents, or commands. Read-only. Supports an opt-in ranked mode for deterministic BM25-style results.",
|
|
91
95
|
inputSchema: {
|
|
92
96
|
type: "object",
|
|
93
97
|
properties: {
|
|
94
98
|
query: { type: "string", minLength: 2, description: "Search query." },
|
|
95
99
|
dir: optionalString("Optional corpus directory: knowledge, examples, skills, docs, agents, commands."),
|
|
96
100
|
limit: optionalInteger({ description: "Maximum hits, 1-500.", minimum: 1, maximum: 500 }),
|
|
101
|
+
ranked: optionalBoolean("Opt-in deterministic BM25-style ranked results. Requires no index and never builds one; falls back to a live corpus scan when no index is present."),
|
|
97
102
|
},
|
|
98
103
|
required: ["query"],
|
|
99
104
|
additionalProperties: false,
|
|
@@ -281,6 +286,8 @@ export function buildCliInvocation(toolName, input = {}) {
|
|
|
281
286
|
maybeBool(args, "--with-learning", input.withLearning);
|
|
282
287
|
maybePush(args, "--learning-category", input.learningCategory);
|
|
283
288
|
maybePush(args, "--learning-limit", input.learningLimit);
|
|
289
|
+
maybeBool(args, "--with-recall", input.withRecall);
|
|
290
|
+
maybePush(args, "--recall-limit", input.recallLimit);
|
|
284
291
|
maybeBool(args, "--json", input.json);
|
|
285
292
|
return { args, stdin };
|
|
286
293
|
}
|
|
@@ -292,6 +299,8 @@ export function buildCliInvocation(toolName, input = {}) {
|
|
|
292
299
|
maybeBool(args, "--with-learning", input.withLearning);
|
|
293
300
|
maybePush(args, "--learning-category", input.learningCategory);
|
|
294
301
|
maybePush(args, "--learning-limit", input.learningLimit);
|
|
302
|
+
maybeBool(args, "--with-recall", input.withRecall);
|
|
303
|
+
maybePush(args, "--recall-limit", input.recallLimit);
|
|
295
304
|
maybeBool(args, "--json", input.json);
|
|
296
305
|
return { args, stdin };
|
|
297
306
|
}
|
|
@@ -300,6 +309,7 @@ export function buildCliInvocation(toolName, input = {}) {
|
|
|
300
309
|
args.push("search", assertString(input.query, "query"), "--json");
|
|
301
310
|
maybePush(args, "--dir", input.dir);
|
|
302
311
|
maybePush(args, "--limit", input.limit);
|
|
312
|
+
maybeBool(args, "--ranked", input.ranked);
|
|
303
313
|
return { args, stdin };
|
|
304
314
|
}
|
|
305
315
|
|
package/cli/lib/pack.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { readFileSync } from "node:fs";
|
|
|
4
4
|
|
|
5
5
|
import { parseBriefSourceFlag } from "./brief.mjs";
|
|
6
6
|
import { normalizeCategory, parseLearningLimit } from "./learn.mjs";
|
|
7
|
+
import { parseRecallLimit } from "./recall.mjs";
|
|
7
8
|
import { SYMLINK_PREFIX } from "./paths.mjs";
|
|
8
9
|
import { parseOutputFlags } from "./output.mjs";
|
|
9
10
|
import { readRouteManifestVersion } from "./route.mjs";
|
|
@@ -29,6 +30,8 @@ const PACK_OPTIONS = [
|
|
|
29
30
|
"--with-learning",
|
|
30
31
|
"--learning-category",
|
|
31
32
|
"--learning-limit",
|
|
33
|
+
"--with-recall",
|
|
34
|
+
"--recall-limit",
|
|
32
35
|
];
|
|
33
36
|
|
|
34
37
|
export function parsePackArgs(args) {
|
|
@@ -44,6 +47,8 @@ export function parsePackArgs(args) {
|
|
|
44
47
|
withLearning: false,
|
|
45
48
|
learningCategory: "",
|
|
46
49
|
learningLimit: 0,
|
|
50
|
+
withRecall: false,
|
|
51
|
+
recallLimit: 0,
|
|
47
52
|
evalTemplate: false,
|
|
48
53
|
eval: false,
|
|
49
54
|
strict: false,
|
|
@@ -58,6 +63,17 @@ export function parsePackArgs(args) {
|
|
|
58
63
|
out.json = true;
|
|
59
64
|
} else if (arg === "--with-learning") {
|
|
60
65
|
out.withLearning = true;
|
|
66
|
+
} else if (arg === "--with-recall") {
|
|
67
|
+
out.withRecall = true;
|
|
68
|
+
} else if (arg === "--recall-limit") {
|
|
69
|
+
const limit = args[i + 1];
|
|
70
|
+
if (!limit || limit.startsWith("--")) throw new Error("--recall-limit expects an integer from 1 to 20");
|
|
71
|
+
try {
|
|
72
|
+
out.recallLimit = parseRecallLimit(limit);
|
|
73
|
+
} catch {
|
|
74
|
+
throw new Error("--recall-limit expects an integer from 1 to 20");
|
|
75
|
+
}
|
|
76
|
+
i += 1;
|
|
61
77
|
} else if (arg === "--eval-template") {
|
|
62
78
|
out.evalTemplate = true;
|
|
63
79
|
} else if (arg === "--eval") {
|
|
@@ -108,20 +124,23 @@ export function parsePackArgs(args) {
|
|
|
108
124
|
if ((out.learningCategory || out.learningLimit) && !out.withLearning) {
|
|
109
125
|
throw new Error("--learning-category and --learning-limit require --with-learning");
|
|
110
126
|
}
|
|
127
|
+
if (out.recallLimit && !out.withRecall) {
|
|
128
|
+
throw new Error("--recall-limit requires --with-recall");
|
|
129
|
+
}
|
|
111
130
|
if (out.eval && out.evalTemplate) {
|
|
112
131
|
throw new Error("Choose either --eval-template or --eval, not both");
|
|
113
132
|
}
|
|
114
133
|
if (out.strict && !out.eval) {
|
|
115
134
|
throw new Error("--strict can only be used with --eval");
|
|
116
135
|
}
|
|
117
|
-
if (out.evalTemplate && (out.briefParts.length > 0 || out.fromFile || out.stdin || out.routeId || out.withLearning)) {
|
|
118
|
-
throw new Error("--eval-template cannot be combined with a brief, --from-file, --stdin, --route, or --with-
|
|
136
|
+
if (out.evalTemplate && (out.briefParts.length > 0 || out.fromFile || out.stdin || out.routeId || out.withLearning || out.withRecall)) {
|
|
137
|
+
throw new Error("--eval-template cannot be combined with a brief, --from-file, --stdin, --route, --with-learning, or --with-recall");
|
|
119
138
|
}
|
|
120
139
|
if (out.eval && (!out.fromFile && !out.stdin)) {
|
|
121
140
|
throw new Error("--eval requires --from-file or --stdin");
|
|
122
141
|
}
|
|
123
|
-
if (out.eval && (out.briefParts.length > 0 || out.routeId || out.withLearning)) {
|
|
124
|
-
throw new Error("--eval cannot be combined with an inline brief, --route, or --with-
|
|
142
|
+
if (out.eval && (out.briefParts.length > 0 || out.routeId || out.withLearning || out.withRecall)) {
|
|
143
|
+
throw new Error("--eval cannot be combined with an inline brief, --route, --with-learning, or --with-recall");
|
|
125
144
|
}
|
|
126
145
|
|
|
127
146
|
return {
|
|
@@ -200,6 +219,8 @@ export function buildPromptPack({
|
|
|
200
219
|
learningFilePath = "",
|
|
201
220
|
learningCategory = "",
|
|
202
221
|
learningLimit = 0,
|
|
222
|
+
withRecall = false,
|
|
223
|
+
recallLimit = 0,
|
|
203
224
|
}) {
|
|
204
225
|
const plan = buildPromptPlan({
|
|
205
226
|
brief,
|
|
@@ -210,6 +231,8 @@ export function buildPromptPack({
|
|
|
210
231
|
learningFilePath,
|
|
211
232
|
learningCategory,
|
|
212
233
|
learningLimit,
|
|
234
|
+
withRecall,
|
|
235
|
+
recallLimit,
|
|
213
236
|
});
|
|
214
237
|
const files = [];
|
|
215
238
|
let usedBytes = 0;
|
|
@@ -572,6 +595,10 @@ export function renderPromptPack({ plan, files, summary, warnings = [] }) {
|
|
|
572
595
|
lines.push("");
|
|
573
596
|
lines.push("## Prompt");
|
|
574
597
|
lines.push("");
|
|
598
|
+
// plan.prompt already carries the --with-recall "## Recalled design knowledge"
|
|
599
|
+
// section (rendered by renderPrompt). Recall respects the byte budget by being
|
|
600
|
+
// subject to the same whole-pack takeUtf8(maxBytes) truncation as everything else —
|
|
601
|
+
// there is no recall-specific budget carve-out.
|
|
575
602
|
lines.push(plan.prompt);
|
|
576
603
|
lines.push("");
|
|
577
604
|
lines.push("## Context Files");
|
package/cli/lib/prompt.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { existsSync } from "node:fs";
|
|
|
6
6
|
import { parseBriefSourceFlag } from "./brief.mjs";
|
|
7
7
|
import { listExamples } from "./examples.mjs";
|
|
8
8
|
import { buildLearningContext, normalizeCategory, parseLearningLimit } from "./learn.mjs";
|
|
9
|
+
import { buildRecallContext, DEFAULT_RECALL_LIMIT, parseRecallLimit } from "./recall.mjs";
|
|
9
10
|
import { SYMLINK_PREFIX } from "./paths.mjs";
|
|
10
11
|
import { parseOutputFlags } from "./output.mjs";
|
|
11
12
|
import { readRouteManifestVersion, routeBrief, routeById } from "./route.mjs";
|
|
@@ -26,6 +27,8 @@ const PROMPT_OPTIONS = [
|
|
|
26
27
|
"--with-learning",
|
|
27
28
|
"--learning-category",
|
|
28
29
|
"--learning-limit",
|
|
30
|
+
"--with-recall",
|
|
31
|
+
"--recall-limit",
|
|
29
32
|
];
|
|
30
33
|
const PROMPT_EVAL_VERSION = 1;
|
|
31
34
|
const PROMPT_EVAL_DEFAULT_LIMIT = 12;
|
|
@@ -42,6 +45,8 @@ export function parsePromptArgs(args) {
|
|
|
42
45
|
withLearning: false,
|
|
43
46
|
learningCategory: "",
|
|
44
47
|
learningLimit: 0,
|
|
48
|
+
withRecall: false,
|
|
49
|
+
recallLimit: 0,
|
|
45
50
|
evalTemplate: false,
|
|
46
51
|
eval: false,
|
|
47
52
|
strict: false,
|
|
@@ -57,6 +62,17 @@ export function parsePromptArgs(args) {
|
|
|
57
62
|
out.json = true;
|
|
58
63
|
} else if (arg === "--with-learning") {
|
|
59
64
|
out.withLearning = true;
|
|
65
|
+
} else if (arg === "--with-recall") {
|
|
66
|
+
out.withRecall = true;
|
|
67
|
+
} else if (arg === "--recall-limit") {
|
|
68
|
+
const limit = args[i + 1];
|
|
69
|
+
if (!limit || limit.startsWith("--")) throw new Error("--recall-limit expects an integer from 1 to 20");
|
|
70
|
+
try {
|
|
71
|
+
out.recallLimit = parseRecallLimit(limit);
|
|
72
|
+
} catch {
|
|
73
|
+
throw new Error("--recall-limit expects an integer from 1 to 20");
|
|
74
|
+
}
|
|
75
|
+
i += 1;
|
|
60
76
|
} else if (arg === "--eval-template") {
|
|
61
77
|
out.evalTemplate = true;
|
|
62
78
|
} else if (arg === "--eval") {
|
|
@@ -96,20 +112,23 @@ export function parsePromptArgs(args) {
|
|
|
96
112
|
if ((out.learningCategory || out.learningLimit) && !out.withLearning) {
|
|
97
113
|
throw new Error("--learning-category and --learning-limit require --with-learning");
|
|
98
114
|
}
|
|
115
|
+
if (out.recallLimit && !out.withRecall) {
|
|
116
|
+
throw new Error("--recall-limit requires --with-recall");
|
|
117
|
+
}
|
|
99
118
|
if (out.eval && out.evalTemplate) {
|
|
100
119
|
throw new Error("Choose either --eval-template or --eval, not both");
|
|
101
120
|
}
|
|
102
121
|
if (out.strict && !out.eval) {
|
|
103
122
|
throw new Error("--strict can only be used with --eval");
|
|
104
123
|
}
|
|
105
|
-
if (out.evalTemplate && (out.briefParts.length > 0 || out.fromFile || out.stdin || out.routeId || out.withLearning)) {
|
|
106
|
-
throw new Error("--eval-template cannot be combined with a brief, --from-file, --stdin, --route, or --with-
|
|
124
|
+
if (out.evalTemplate && (out.briefParts.length > 0 || out.fromFile || out.stdin || out.routeId || out.withLearning || out.withRecall)) {
|
|
125
|
+
throw new Error("--eval-template cannot be combined with a brief, --from-file, --stdin, --route, --with-learning, or --with-recall");
|
|
107
126
|
}
|
|
108
127
|
if (out.eval && (!out.fromFile && !out.stdin)) {
|
|
109
128
|
throw new Error("--eval requires --from-file or --stdin");
|
|
110
129
|
}
|
|
111
|
-
if (out.eval && (out.briefParts.length > 0 || out.routeId || out.withLearning)) {
|
|
112
|
-
throw new Error("--eval cannot be combined with an inline brief, --route, or --with-
|
|
130
|
+
if (out.eval && (out.briefParts.length > 0 || out.routeId || out.withLearning || out.withRecall)) {
|
|
131
|
+
throw new Error("--eval cannot be combined with an inline brief, --route, --with-learning, or --with-recall");
|
|
113
132
|
}
|
|
114
133
|
|
|
115
134
|
return {
|
|
@@ -209,6 +228,8 @@ export function buildPromptPlan({
|
|
|
209
228
|
learningFilePath = "",
|
|
210
229
|
learningCategory = "",
|
|
211
230
|
learningLimit = 0,
|
|
231
|
+
withRecall = false,
|
|
232
|
+
recallLimit = 0,
|
|
212
233
|
}) {
|
|
213
234
|
const route = routeId
|
|
214
235
|
? routeById({ routeId, sourceRoot })
|
|
@@ -243,6 +264,13 @@ export function buildPromptPlan({
|
|
|
243
264
|
query: brief,
|
|
244
265
|
})
|
|
245
266
|
: null;
|
|
267
|
+
const recallContext = withRecall
|
|
268
|
+
? buildRecallContext({
|
|
269
|
+
brief,
|
|
270
|
+
recallLimit: recallLimit || DEFAULT_RECALL_LIMIT,
|
|
271
|
+
designAiPath: sourceRoot,
|
|
272
|
+
})
|
|
273
|
+
: null;
|
|
246
274
|
|
|
247
275
|
return {
|
|
248
276
|
brief,
|
|
@@ -254,6 +282,7 @@ export function buildPromptPlan({
|
|
|
254
282
|
checklist,
|
|
255
283
|
qualityCommand,
|
|
256
284
|
...(learningContext ? { learningContext } : {}),
|
|
285
|
+
...(recallContext ? { recall: recallContext } : {}),
|
|
257
286
|
prompt: renderPrompt({
|
|
258
287
|
brief,
|
|
259
288
|
route,
|
|
@@ -263,6 +292,7 @@ export function buildPromptPlan({
|
|
|
263
292
|
checklist,
|
|
264
293
|
qualityCommand,
|
|
265
294
|
learningContext,
|
|
295
|
+
recallContext,
|
|
266
296
|
}),
|
|
267
297
|
};
|
|
268
298
|
}
|
|
@@ -526,6 +556,7 @@ export function renderPrompt({
|
|
|
526
556
|
checklist = checklistForRoute(route),
|
|
527
557
|
qualityCommand = qualityCommandForRoute(route.id),
|
|
528
558
|
learningContext = null,
|
|
559
|
+
recallContext = null,
|
|
529
560
|
}) {
|
|
530
561
|
const lines = [];
|
|
531
562
|
lines.push("# design-ai task prompt");
|
|
@@ -571,6 +602,13 @@ export function renderPrompt({
|
|
|
571
602
|
lines.push("");
|
|
572
603
|
}
|
|
573
604
|
|
|
605
|
+
if (recallContext) {
|
|
606
|
+
lines.push("Recalled corpus knowledge:");
|
|
607
|
+
lines.push("");
|
|
608
|
+
lines.push(recallContext.markdown);
|
|
609
|
+
lines.push("");
|
|
610
|
+
}
|
|
611
|
+
|
|
574
612
|
lines.push("Before producing the artifact, read these files in order:");
|
|
575
613
|
for (const file of filesToRead) {
|
|
576
614
|
lines.push(`- ${file}`);
|