@design-ai/cli 4.55.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 +75 -0
- package/README.ko.md +15 -32
- package/README.md +18 -31
- 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 +221 -13
- 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-analysis.mjs +297 -0
- package/cli/lib/site-args.mjs +433 -0
- package/cli/lib/site-bundle-build.mjs +127 -0
- package/cli/lib/site-bundle-check.mjs +454 -0
- package/cli/lib/site-bundle-commands.mjs +95 -0
- package/cli/lib/site-bundle-compare.mjs +157 -0
- package/cli/lib/site-bundle-contract.mjs +79 -0
- package/cli/lib/site-bundle-files.mjs +87 -0
- 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-actions.mjs +113 -0
- package/cli/lib/site-bundle-handoff-runbook-evidence-fields.mjs +164 -0
- package/cli/lib/site-bundle-handoff-runbook-evidence.mjs +334 -0
- package/cli/lib/site-bundle-handoff-runbook-format.mjs +31 -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-stage-metadata.mjs +84 -0
- package/cli/lib/site-bundle-handoff-runbook.mjs +444 -0
- package/cli/lib/site-bundle-handoff-summary.mjs +183 -0
- package/cli/lib/site-bundle-handoff.mjs +271 -0
- package/cli/lib/site-bundle-readme.mjs +98 -0
- package/cli/lib/site-bundle-repair-report.mjs +143 -0
- package/cli/lib/site-bundle-repair.mjs +68 -0
- package/cli/lib/site-content.mjs +399 -0
- package/cli/lib/site-evidence.mjs +35 -0
- package/cli/lib/site-mcp-commands.mjs +28 -0
- package/cli/lib/site-mcp-probes.mjs +159 -0
- package/cli/lib/site-mcp-readiness.mjs +157 -0
- package/cli/lib/site-mcp-report.mjs +324 -0
- package/cli/lib/site-next-actions.mjs +333 -0
- package/cli/lib/site-options.mjs +104 -0
- package/cli/lib/site-prompts.mjs +332 -0
- package/cli/lib/site-starter.mjs +153 -0
- package/cli/lib/site-strings.mjs +23 -0
- package/cli/lib/site-tasks.mjs +93 -0
- package/cli/lib/site-test-support.mjs +68 -0
- package/cli/lib/site-workflow-graph.mjs +309 -0
- package/cli/lib/site-workspace.mjs +492 -0
- package/cli/lib/site.mjs +108 -6617
- 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 +35 -6
- package/docs/DISTRIBUTION.md +35 -8
- package/docs/NEXT-SURFACE-DECISION.md +125 -0
- package/docs/PRODUCT-READINESS.md +3 -3
- package/docs/RELEASE-CHECKLIST.md +20 -3
- package/docs/RELEASE-GATES.ko.md +35 -0
- package/docs/RELEASE-GATES.md +234 -0
- package/docs/ROADMAP.md +2232 -8
- package/docs/external-status.md +20 -7
- package/docs/inspection-20260630.md +169 -0
- package/docs/integrations/design-ai-mcp-server.md +35 -3
- package/docs/integrations/vscode-walkthrough.ko.md +3 -3
- package/docs/integrations/vscode-walkthrough.md +3 -3
- 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/docs/site-overrides/main.html +1 -1
- 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 +717 -9
- package/tools/audit/registry-smoke.py +912 -10
- package/tools/audit/release-metadata.py +47 -1
- package/tools/audit/smoke_assertions.py +998 -35
- package/tools/migrations/refs-links-to-reference-pages.py +179 -0
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}`);
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Corpus knowledge recall (retrieval-augmentation) for `design-ai prompt`/`pack`
|
|
2
|
+
// --with-recall. Injects the most brief-relevant CORPUS KNOWLEDGE FILES, ranked by
|
|
3
|
+
// the SAME shipped deterministic lexical (BM25-style) scorer that powers
|
|
4
|
+
// `design-ai search --ranked` and learning selection (docs/AI-LEARNING-PHASE2.md,
|
|
5
|
+
// Phase A). Sibling of learning selection: learning recalls LOCAL preferences,
|
|
6
|
+
// recall recalls SHIPPED corpus knowledge. Zero-dependency and deterministic.
|
|
7
|
+
//
|
|
8
|
+
// NO embeddings in this path: the pack/prompt recall stays lexical-only. Embedding
|
|
9
|
+
// rerank remains a search-only opt-in for now (docs/AI-LEARNING-PHASE2.md, Phase B);
|
|
10
|
+
// there is deliberately no --embeddings recall variant.
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
|
|
15
|
+
import { rankedSearchCorpus, rankedPreview } from "./search-ranked.mjs";
|
|
16
|
+
import { collectCorpusDocuments } from "./retrieval-index.mjs";
|
|
17
|
+
import { loadLearningProfile } from "./learn-profile.mjs";
|
|
18
|
+
import { selectLearningEntrySet } from "./learn-select.mjs";
|
|
19
|
+
import { defaultLearningFile } from "./learn-shared.mjs";
|
|
20
|
+
import { DESIGN_AI_HOME } from "./paths.mjs";
|
|
21
|
+
import { DEFAULT_SEARCH_DIRS } from "./search.mjs";
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_RECALL_LIMIT = 5;
|
|
24
|
+
|
|
25
|
+
// Mirrors parseLearningLimit but bounded 1-20 (recall injects whole corpus files, so a
|
|
26
|
+
// tighter cap than learning's 1-100 keeps the recall section budget-friendly).
|
|
27
|
+
export function parseRecallLimit(rawLimit) {
|
|
28
|
+
const limit = Number(rawLimit);
|
|
29
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 20) {
|
|
30
|
+
throw new Error("--recall-limit expects an integer from 1 to 20");
|
|
31
|
+
}
|
|
32
|
+
return limit;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Cap each recalled file's rendered block so the total recall section stays bounded.
|
|
36
|
+
// The pack's takeUtf8(maxBytes) truncation still applies to the WHOLE pack as-is, so
|
|
37
|
+
// recall respects the byte budget by being subject to the same cap; this per-file cap
|
|
38
|
+
// keeps any single file from crowding out the rest of the recall section.
|
|
39
|
+
const RECALL_BLOCK_CHAR_CAP = 400;
|
|
40
|
+
|
|
41
|
+
// First Markdown `# ` heading, else the first non-empty line — a short, deterministic
|
|
42
|
+
// citation label for the recalled file.
|
|
43
|
+
function firstHeadingOrLine(text) {
|
|
44
|
+
const lines = String(text || "").split("\n");
|
|
45
|
+
for (const line of lines) {
|
|
46
|
+
const trimmed = line.trim();
|
|
47
|
+
if (trimmed.startsWith("# ")) return trimmed.slice(2).trim();
|
|
48
|
+
}
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
if (trimmed.length > 0) return trimmed;
|
|
52
|
+
}
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readCorpusText(designAiPath, relPath) {
|
|
57
|
+
try {
|
|
58
|
+
return readFileSync(path.join(designAiPath, relPath), "utf8");
|
|
59
|
+
} catch {
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Bounded per-file block: relPath citation + heading/first line + matched excerpt.
|
|
65
|
+
// Capped to ~RECALL_BLOCK_CHAR_CAP chars so no single file dominates the section.
|
|
66
|
+
function renderRecallBlock({ id, text, matchedTokens }) {
|
|
67
|
+
const heading = firstHeadingOrLine(text);
|
|
68
|
+
const excerpt = rankedPreview(text, matchedTokens);
|
|
69
|
+
const parts = [`- ${id}`];
|
|
70
|
+
if (heading) parts.push(` - ${heading}`);
|
|
71
|
+
if (excerpt) parts.push(` - ${excerpt}`);
|
|
72
|
+
const block = parts.join("\n");
|
|
73
|
+
return block.length <= RECALL_BLOCK_CHAR_CAP
|
|
74
|
+
? block
|
|
75
|
+
: `${block.slice(0, RECALL_BLOCK_CHAR_CAP)}...`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function renderRecallMarkdown(selected, textById) {
|
|
79
|
+
const lines = ["## Recalled design knowledge", ""];
|
|
80
|
+
if (selected.length === 0) {
|
|
81
|
+
lines.push("No corpus knowledge files matched this brief.");
|
|
82
|
+
return lines.join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
lines.push("Cite these shipped corpus knowledge files when they inform a design decision.");
|
|
86
|
+
lines.push("");
|
|
87
|
+
for (const item of selected) {
|
|
88
|
+
lines.push(renderRecallBlock({
|
|
89
|
+
id: item.id,
|
|
90
|
+
text: textById.get(item.id) || "",
|
|
91
|
+
matchedTokens: item.matchedTokens,
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
return lines.join("\n");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Returns a recall context mirroring learning's selection-item shape (id = corpus
|
|
98
|
+
// relPath). REUSES rankedSearchCorpus for scoring — no reimplemented ranking. The
|
|
99
|
+
// ranker already orders fully by score desc, id asc, so the result is deterministic.
|
|
100
|
+
// Empty brief or no lexical hits -> selectedCount 0 and a "no corpus knowledge"
|
|
101
|
+
// markdown line.
|
|
102
|
+
export function buildRecallContext({
|
|
103
|
+
brief = "",
|
|
104
|
+
recallLimit = DEFAULT_RECALL_LIMIT,
|
|
105
|
+
designAiPath = DESIGN_AI_HOME,
|
|
106
|
+
dirs = DEFAULT_SEARCH_DIRS,
|
|
107
|
+
} = {}) {
|
|
108
|
+
const query = String(brief || "").trim();
|
|
109
|
+
const candidateCount = collectCorpusDocuments({ designAiPath, dirs }).length;
|
|
110
|
+
|
|
111
|
+
const hits = query
|
|
112
|
+
? rankedSearchCorpus({ query, designAiPath, dirs, limit: recallLimit }).hits
|
|
113
|
+
: [];
|
|
114
|
+
|
|
115
|
+
const selected = hits.map((hit) => ({
|
|
116
|
+
id: hit.relPath,
|
|
117
|
+
score: hit.score,
|
|
118
|
+
matchedTokens: hit.matchedTokens,
|
|
119
|
+
}));
|
|
120
|
+
|
|
121
|
+
const textById = new Map(
|
|
122
|
+
selected.map((item) => [item.id, readCorpusText(designAiPath, item.id)]),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
query,
|
|
127
|
+
mode: "lexical",
|
|
128
|
+
candidateCount,
|
|
129
|
+
selectedCount: selected.length,
|
|
130
|
+
selected,
|
|
131
|
+
markdown: renderRecallMarkdown(selected, textById),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Read-side companion to `pack --with-recall`: a unified "what does design-ai
|
|
136
|
+
// recall for this query" view. Combines (1) the top corpus knowledge files ranked
|
|
137
|
+
// by rankedSearchCorpus with (2) the top local learning-profile entries ranked by
|
|
138
|
+
// selectLearningEntrySet — BOTH using the same shipped deterministic lexical scorer.
|
|
139
|
+
// Read-only: never writes the profile or any file. Empty query -> zero hits on both
|
|
140
|
+
// sides. `limit` applies to BOTH lists; `category` scopes ONLY the learning list.
|
|
141
|
+
export function buildLearnRecall({
|
|
142
|
+
query = "",
|
|
143
|
+
limit = DEFAULT_RECALL_LIMIT,
|
|
144
|
+
category = "",
|
|
145
|
+
designAiPath = DESIGN_AI_HOME,
|
|
146
|
+
learningFilePath = defaultLearningFile(),
|
|
147
|
+
dirs = DEFAULT_SEARCH_DIRS,
|
|
148
|
+
} = {}) {
|
|
149
|
+
const normalizedQuery = String(query || "").trim();
|
|
150
|
+
|
|
151
|
+
const corpusCandidateCount = collectCorpusDocuments({ designAiPath, dirs }).length;
|
|
152
|
+
const corpusHits = normalizedQuery
|
|
153
|
+
? rankedSearchCorpus({ query: normalizedQuery, designAiPath, dirs, limit }).hits
|
|
154
|
+
: [];
|
|
155
|
+
const corpusSelected = corpusHits.map((hit) => ({
|
|
156
|
+
id: hit.relPath,
|
|
157
|
+
score: hit.score,
|
|
158
|
+
matchedTokens: hit.matchedTokens,
|
|
159
|
+
}));
|
|
160
|
+
|
|
161
|
+
const profile = loadLearningProfile(learningFilePath);
|
|
162
|
+
const textById = new Map(profile.entries.map((entry) => [entry.id, entry.text || ""]));
|
|
163
|
+
const { selection } = selectLearningEntrySet(profile, {
|
|
164
|
+
query: normalizedQuery,
|
|
165
|
+
limit,
|
|
166
|
+
category,
|
|
167
|
+
includeFallback: false,
|
|
168
|
+
});
|
|
169
|
+
const learningSelected = normalizedQuery
|
|
170
|
+
? selection.selected.map((item) => ({
|
|
171
|
+
id: item.id,
|
|
172
|
+
category: item.category,
|
|
173
|
+
score: item.score,
|
|
174
|
+
matchedTokens: item.matchedTokens,
|
|
175
|
+
text: textById.get(item.id) || "",
|
|
176
|
+
}))
|
|
177
|
+
: [];
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
query: normalizedQuery,
|
|
181
|
+
corpus: {
|
|
182
|
+
candidateCount: corpusCandidateCount,
|
|
183
|
+
selectedCount: corpusSelected.length,
|
|
184
|
+
selected: corpusSelected,
|
|
185
|
+
},
|
|
186
|
+
learning: {
|
|
187
|
+
mode: selection.mode,
|
|
188
|
+
candidateCount: selection.candidateCount,
|
|
189
|
+
selectedCount: learningSelected.length,
|
|
190
|
+
selected: learningSelected,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Ranked (BM25-style) corpus search for `design-ai search --ranked`, plus the
|
|
2
|
+
// opt-in Phase B embedding rerank (docs/AI-LEARNING-PHASE2.md, Phase B).
|
|
3
|
+
// Deterministic: ranks a live corpus scan, then reports retrieval-index staleness
|
|
4
|
+
// instead of ever serving stale index text (docs/AI-LEARNING-PHASE2.md, Phase A).
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
|
+
import { embeddingIndexFile, loadEmbeddingIndexFile, vectorsById as embeddingVectorsById } from "./embedding-index.mjs";
|
|
10
|
+
import { embedDocuments } from "./embedding-provider.mjs";
|
|
11
|
+
import { cosineRerank } from "./embedding-rerank.mjs";
|
|
12
|
+
import { buildLexicalStats, rankLexical } from "./lexical.mjs";
|
|
13
|
+
import { PACKAGE_ROOT } from "./paths.mjs";
|
|
14
|
+
import {
|
|
15
|
+
collectCorpusDocuments,
|
|
16
|
+
computeCorpusDigest,
|
|
17
|
+
computeLearningDigest,
|
|
18
|
+
corpusIndexFile,
|
|
19
|
+
defaultIndexDir,
|
|
20
|
+
loadIndexFile,
|
|
21
|
+
} from "./retrieval-index.mjs";
|
|
22
|
+
import { defaultLearningFile } from "./learn-shared.mjs";
|
|
23
|
+
import { DEFAULT_SEARCH_DIRS } from "./search.mjs";
|
|
24
|
+
|
|
25
|
+
const RANKED_PREVIEW_LEN = 120;
|
|
26
|
+
|
|
27
|
+
// N = max(limit*5, 25): the number of top lexical candidates handed to the embedding
|
|
28
|
+
// reranker. Documented constant (docs/AI-LEARNING-PHASE2.md, Phase B CLI wiring).
|
|
29
|
+
export function embeddingCandidateCount(limit) {
|
|
30
|
+
return Math.max(limit * 5, 25);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function rankedPreview(text, matchedTokens) {
|
|
34
|
+
const lines = text.split("\n");
|
|
35
|
+
for (const token of matchedTokens) {
|
|
36
|
+
for (const line of lines) {
|
|
37
|
+
const trimmed = line.trim();
|
|
38
|
+
if (trimmed && trimmed.toLowerCase().includes(token)) {
|
|
39
|
+
return trimmed.length <= RANKED_PREVIEW_LEN
|
|
40
|
+
? trimmed
|
|
41
|
+
: `${trimmed.slice(0, RANKED_PREVIEW_LEN)}...`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const firstLine = lines.map((line) => line.trim()).find((line) => line.length > 0) || "";
|
|
46
|
+
return firstLine.length <= RANKED_PREVIEW_LEN ? firstLine : `${firstLine.slice(0, RANKED_PREVIEW_LEN)}...`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function corpusIndexNotice({ indexDir, currentDigest }) {
|
|
50
|
+
const loaded = loadIndexFile(corpusIndexFile(indexDir));
|
|
51
|
+
if (!loaded.present) {
|
|
52
|
+
return "no corpus index built yet; ranked results come from a live corpus scan (design-ai index --build)";
|
|
53
|
+
}
|
|
54
|
+
if (loaded.error) {
|
|
55
|
+
return `corpus index is unreadable (${loaded.error}); ranked results come from a live corpus scan (design-ai index --build)`;
|
|
56
|
+
}
|
|
57
|
+
if ((loaded.payload.source?.corpusDigest || "") !== currentDigest) {
|
|
58
|
+
return "corpus index is stale; ranked results come from a live corpus scan (design-ai index --build)";
|
|
59
|
+
}
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function rankedSearchCorpus({
|
|
64
|
+
query,
|
|
65
|
+
designAiPath = PACKAGE_ROOT,
|
|
66
|
+
dirs = DEFAULT_SEARCH_DIRS,
|
|
67
|
+
limit = 20,
|
|
68
|
+
indexDir = defaultIndexDir(),
|
|
69
|
+
} = {}) {
|
|
70
|
+
const documents = collectCorpusDocuments({ designAiPath, dirs });
|
|
71
|
+
const stats = buildLexicalStats(documents.map(({ id, text }) => ({ id, text })));
|
|
72
|
+
const textById = new Map(documents.map((doc) => [doc.id, doc.text]));
|
|
73
|
+
|
|
74
|
+
const hits = rankLexical(query, stats, { limit }).map((hit) => ({
|
|
75
|
+
relPath: hit.id,
|
|
76
|
+
file: path.join(designAiPath, hit.id),
|
|
77
|
+
score: hit.score,
|
|
78
|
+
matchedTokens: hit.matchedTokens,
|
|
79
|
+
preview: rankedPreview(textById.get(hit.id) || "", hit.matchedTokens),
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
hits,
|
|
84
|
+
notice: corpusIndexNotice({ indexDir, currentDigest: computeCorpusDigest(documents) }),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function embeddingSidecarFreshness({ indexDir, designAiPath, learningFile, corpusDigest }) {
|
|
89
|
+
const file = embeddingIndexFile(indexDir);
|
|
90
|
+
const loaded = loadEmbeddingIndexFile(file);
|
|
91
|
+
if (!loaded.present) return { ok: false, reason: "no embedding index built yet (design-ai index --build --embeddings)" };
|
|
92
|
+
if (loaded.error) return { ok: false, reason: `embedding index is unreadable (${loaded.error})` };
|
|
93
|
+
|
|
94
|
+
const source = loaded.payload.source || {};
|
|
95
|
+
const sourceMatch =
|
|
96
|
+
source.designAiPath === path.resolve(designAiPath) && source.learningFile === path.resolve(learningFile);
|
|
97
|
+
if (!sourceMatch) {
|
|
98
|
+
return { ok: false, reason: "embedding index was built from a different checkout or learning file" };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const { digest: currentLearningDigest } = computeLearningDigest(learningFile);
|
|
102
|
+
const digestMatch = source.corpusDigest === corpusDigest && source.learningDigest === currentLearningDigest;
|
|
103
|
+
if (!digestMatch) {
|
|
104
|
+
return { ok: false, reason: "embedding index is stale (design-ai index --build --embeddings)" };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { ok: true, payload: loaded.payload };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Opt-in Phase B rerank: take the top-N lexical candidates, embed the QUERY via the
|
|
111
|
+
// provider, cosine-rerank against the stored sidecar vectors, return the top `limit`.
|
|
112
|
+
// ANY failure (no provider, no sidecar, stale sidecar, provider error) is reported
|
|
113
|
+
// via `fallback: true` + a specific `notice` — callers must fall back to the lexical
|
|
114
|
+
// path and exit 0; a stale sidecar must never silently serve embedding results.
|
|
115
|
+
export function embeddingRerankSearch({
|
|
116
|
+
query,
|
|
117
|
+
provider,
|
|
118
|
+
designAiPath = PACKAGE_ROOT,
|
|
119
|
+
dirs = DEFAULT_SEARCH_DIRS,
|
|
120
|
+
limit = 20,
|
|
121
|
+
indexDir = defaultIndexDir(),
|
|
122
|
+
learningFile = defaultLearningFile(),
|
|
123
|
+
spawnRunner,
|
|
124
|
+
} = {}) {
|
|
125
|
+
if (!provider || typeof provider.command !== "string" || provider.command.trim() === "") {
|
|
126
|
+
return { fallback: true, notice: "no embedding provider configured (pass --provider or configure ~/.design-ai/config.json)" };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Freshness is checked against the FULL corpus (the same dirs `index --build
|
|
130
|
+
// --embeddings` always embeds), independent of this search's --dir filter — a
|
|
131
|
+
// scoped `--dir knowledge` search must not be compared against a digest of only
|
|
132
|
+
// the knowledge dir, or every scoped search would spuriously report the sidecar
|
|
133
|
+
// stale. Candidate generation below still respects the caller's `dirs` filter.
|
|
134
|
+
const fullCorpusDocuments = collectCorpusDocuments({ designAiPath, dirs: DEFAULT_SEARCH_DIRS });
|
|
135
|
+
const corpusDigest = computeCorpusDigest(fullCorpusDocuments);
|
|
136
|
+
const freshness = embeddingSidecarFreshness({ indexDir, designAiPath, learningFile, corpusDigest });
|
|
137
|
+
if (!freshness.ok) return { fallback: true, notice: freshness.reason };
|
|
138
|
+
|
|
139
|
+
const documents = dirs === DEFAULT_SEARCH_DIRS ? fullCorpusDocuments : collectCorpusDocuments({ designAiPath, dirs });
|
|
140
|
+
const candidateCount = embeddingCandidateCount(limit);
|
|
141
|
+
const stats = buildLexicalStats(documents.map(({ id, text }) => ({ id, text })));
|
|
142
|
+
const textById = new Map(documents.map((doc) => [doc.id, doc.text]));
|
|
143
|
+
const candidates = rankLexical(query, stats, { limit: candidateCount });
|
|
144
|
+
if (candidates.length === 0) {
|
|
145
|
+
return { fallback: true, notice: "no lexical candidates matched the query; nothing to rerank" };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const embedArgs = { provider, documents: [{ id: "__query__", text: query }] };
|
|
149
|
+
if (spawnRunner) embedArgs.spawnRunner = spawnRunner;
|
|
150
|
+
const embedded = embedDocuments(embedArgs);
|
|
151
|
+
if (!embedded.ok) return { fallback: true, notice: `embedding provider failed: ${embedded.error}` };
|
|
152
|
+
|
|
153
|
+
const queryVector = embedded.vectorsById.get("__query__");
|
|
154
|
+
const vectors = embeddingVectorsById(freshness.payload);
|
|
155
|
+
const reranked = cosineRerank({ queryVector, candidates, vectorsById: vectors, limit });
|
|
156
|
+
|
|
157
|
+
const hits = reranked.map((hit) => ({
|
|
158
|
+
relPath: hit.id,
|
|
159
|
+
file: path.join(designAiPath, hit.id),
|
|
160
|
+
// `score` is a cosine similarity for embedded candidates; a candidate the
|
|
161
|
+
// provider had no vector for (placed after all embedded ones, see
|
|
162
|
+
// embedding-rerank.mjs) keeps its lexical BM25 score here as a documented,
|
|
163
|
+
// deterministic fallback value rather than an undefined/null field.
|
|
164
|
+
score: hit.score,
|
|
165
|
+
matchedTokens: hit.matchedTokens,
|
|
166
|
+
preview: rankedPreview(textById.get(hit.id) || "", hit.matchedTokens),
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
return { fallback: false, hits };
|
|
170
|
+
}
|
package/cli/lib/search.mjs
CHANGED
|
@@ -20,7 +20,7 @@ export const DEFAULT_SEARCH_DIRS = [
|
|
|
20
20
|
|
|
21
21
|
const PREVIEW_LEN = 120;
|
|
22
22
|
const PREVIEW_BEFORE = 50;
|
|
23
|
-
const SEARCH_OPTIONS = ["-h", "--help", "--json", "--limit", "--dir"];
|
|
23
|
+
const SEARCH_OPTIONS = ["-h", "--help", "--json", "--limit", "--dir", "--ranked", "--embeddings", "--provider"];
|
|
24
24
|
|
|
25
25
|
function exists(p) {
|
|
26
26
|
try {
|
|
@@ -115,6 +115,9 @@ export function parseSearchArgs(args) {
|
|
|
115
115
|
dirs: [],
|
|
116
116
|
limit: 20,
|
|
117
117
|
json: false,
|
|
118
|
+
ranked: false,
|
|
119
|
+
embeddings: false,
|
|
120
|
+
provider: "",
|
|
118
121
|
help: false,
|
|
119
122
|
};
|
|
120
123
|
|
|
@@ -124,6 +127,13 @@ export function parseSearchArgs(args) {
|
|
|
124
127
|
out.help = true;
|
|
125
128
|
} else if (arg === "--json") {
|
|
126
129
|
out.json = true;
|
|
130
|
+
} else if (arg === "--ranked") {
|
|
131
|
+
out.ranked = true;
|
|
132
|
+
} else if (arg === "--embeddings") {
|
|
133
|
+
out.embeddings = true;
|
|
134
|
+
} else if (arg === "--provider") {
|
|
135
|
+
out.provider = args[i + 1] || "";
|
|
136
|
+
i += 1;
|
|
127
137
|
} else if (arg === "--limit") {
|
|
128
138
|
const next = args[i + 1];
|
|
129
139
|
const limit = Number(next);
|