@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
package/cli/commands/pack.mjs
CHANGED
|
@@ -14,12 +14,12 @@ import {
|
|
|
14
14
|
import { writeOutputFile } from "../lib/output.mjs";
|
|
15
15
|
|
|
16
16
|
function printHelp() {
|
|
17
|
-
console.log("Usage: design-ai pack <brief> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--max-bytes N] [--json] [--out file] [--force]");
|
|
18
|
-
console.log(" design-ai pack --from-file brief.md [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--max-bytes N] [--json] [--out file] [--force]");
|
|
17
|
+
console.log("Usage: design-ai pack <brief> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--with-recall] [--recall-limit N] [--max-bytes N] [--json] [--out file] [--force]");
|
|
18
|
+
console.log(" design-ai pack --from-file brief.md [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--with-recall] [--recall-limit N] [--max-bytes N] [--json] [--out file] [--force]");
|
|
19
19
|
console.log(" design-ai pack --eval-template [--json] [--out file] [--force]");
|
|
20
20
|
console.log(" design-ai pack --eval --from-file pack-eval.json [--strict] [--json] [--out file] [--force]");
|
|
21
21
|
console.log(" cat pack-eval.json | design-ai pack --eval --stdin [--strict] [--json]");
|
|
22
|
-
console.log(" cat brief.md | design-ai pack --stdin [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--max-bytes N] [--json]\n");
|
|
22
|
+
console.log(" cat brief.md | design-ai pack --stdin [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--with-recall] [--recall-limit N] [--max-bytes N] [--json]\n");
|
|
23
23
|
console.log("Generates a ready-to-use prompt plus bounded context file contents, summary, and warnings.");
|
|
24
24
|
console.log("Pack eval is read-only and checks generated prompt-pack context contracts.\n");
|
|
25
25
|
console.log("Options:");
|
|
@@ -29,6 +29,8 @@ function printHelp() {
|
|
|
29
29
|
console.log(" --with-learning Include brief-relevant local learning preferences and record local usage metadata");
|
|
30
30
|
console.log(" --learning-category kind Include only one learning category; requires --with-learning");
|
|
31
31
|
console.log(" --learning-limit N Limit included learning entries, 1-100; requires --with-learning");
|
|
32
|
+
console.log(" --with-recall Include brief-relevant corpus knowledge files recalled from the shipped design corpus");
|
|
33
|
+
console.log(" --recall-limit N Limit recalled corpus knowledge files, 1-20; requires --with-recall");
|
|
32
34
|
console.log(" --max-bytes N Maximum context bytes to include, 1000-1000000. Default: 120000");
|
|
33
35
|
console.log(" --eval-template Generate a runnable pack eval checkpoint JSON template");
|
|
34
36
|
console.log(" --eval Run deterministic prompt-pack checkpoint cases");
|
|
@@ -163,6 +165,8 @@ export async function runPack(args) {
|
|
|
163
165
|
withLearning: parsed.withLearning,
|
|
164
166
|
learningCategory: parsed.learningCategory,
|
|
165
167
|
learningLimit: parsed.learningLimit,
|
|
168
|
+
withRecall: parsed.withRecall,
|
|
169
|
+
recallLimit: parsed.recallLimit,
|
|
166
170
|
});
|
|
167
171
|
const learningUsage = parsed.withLearning
|
|
168
172
|
? recordLearningUsage({
|
package/cli/commands/prompt.mjs
CHANGED
|
@@ -14,12 +14,12 @@ import {
|
|
|
14
14
|
import { writeOutputFile } from "../lib/output.mjs";
|
|
15
15
|
|
|
16
16
|
function printHelp() {
|
|
17
|
-
console.log("Usage: design-ai prompt <brief> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--json] [--out file] [--force]");
|
|
18
|
-
console.log(" design-ai prompt --from-file brief.md [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--json] [--out file] [--force]");
|
|
17
|
+
console.log("Usage: design-ai prompt <brief> [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--with-recall] [--recall-limit N] [--json] [--out file] [--force]");
|
|
18
|
+
console.log(" design-ai prompt --from-file brief.md [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--with-recall] [--recall-limit N] [--json] [--out file] [--force]");
|
|
19
19
|
console.log(" design-ai prompt --eval-template [--json] [--out file] [--force]");
|
|
20
20
|
console.log(" design-ai prompt --eval --from-file prompt-eval.json [--strict] [--json] [--out file] [--force]");
|
|
21
21
|
console.log(" cat prompt-eval.json | design-ai prompt --eval --stdin [--strict] [--json]");
|
|
22
|
-
console.log(" cat brief.md | design-ai prompt --stdin [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--json]\n");
|
|
22
|
+
console.log(" cat brief.md | design-ai prompt --stdin [--route id] [--with-learning] [--learning-category kind] [--learning-limit N] [--with-recall] [--recall-limit N] [--json]\n");
|
|
23
23
|
console.log("Builds a ready-to-use prompt from route recommendations and required design-ai files.");
|
|
24
24
|
console.log("Prompt eval is read-only and checks generated prompt-plan contracts.\n");
|
|
25
25
|
console.log("Options:");
|
|
@@ -29,6 +29,8 @@ function printHelp() {
|
|
|
29
29
|
console.log(" --with-learning Include brief-relevant local learning preferences and record local usage metadata");
|
|
30
30
|
console.log(" --learning-category kind Include only one learning category; requires --with-learning");
|
|
31
31
|
console.log(" --learning-limit N Limit included learning entries, 1-100; requires --with-learning");
|
|
32
|
+
console.log(" --with-recall Include brief-relevant corpus knowledge files recalled from the shipped design corpus");
|
|
33
|
+
console.log(" --recall-limit N Limit recalled corpus knowledge files, 1-20; requires --with-recall");
|
|
32
34
|
console.log(" --eval-template Generate a runnable prompt eval checkpoint JSON template");
|
|
33
35
|
console.log(" --eval Run deterministic prompt-plan checkpoint cases");
|
|
34
36
|
console.log(" --strict With --eval, exit non-zero on warning or failure");
|
|
@@ -161,6 +163,8 @@ export async function runPrompt(args) {
|
|
|
161
163
|
withLearning: parsed.withLearning,
|
|
162
164
|
learningCategory: parsed.learningCategory,
|
|
163
165
|
learningLimit: parsed.learningLimit,
|
|
166
|
+
withRecall: parsed.withRecall,
|
|
167
|
+
recallLimit: parsed.recallLimit,
|
|
164
168
|
});
|
|
165
169
|
const learningUsage = parsed.withLearning
|
|
166
170
|
? recordLearningUsage({
|
package/cli/commands/search.mjs
CHANGED
|
@@ -2,18 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
import { DESIGN_AI_HOME } from "../lib/paths.mjs";
|
|
4
4
|
import { dim, header, info, warn } from "../lib/log.mjs";
|
|
5
|
+
import { configuredEmbeddingProvider, defaultConfigFile } from "../lib/local-config.mjs";
|
|
6
|
+
import { embeddingRerankSearch, rankedSearchCorpus } from "../lib/search-ranked.mjs";
|
|
5
7
|
import { formatSearchJson, parseSearchArgs, searchCorpus } from "../lib/search.mjs";
|
|
6
8
|
|
|
7
9
|
function printHelp() {
|
|
8
|
-
console.log("Usage: design-ai search <query> [--limit N] [--dir kind] [--json]\n");
|
|
10
|
+
console.log("Usage: design-ai search <query> [--limit N] [--dir kind] [--ranked] [--embeddings [--provider \"cmd args\"]] [--json]\n");
|
|
9
11
|
console.log("Searches markdown files across knowledge, examples, skills, docs, agents, and commands.\n");
|
|
10
12
|
console.log("Options:");
|
|
11
13
|
console.log(" --limit N Maximum hits to return, 1-500. Default: 20");
|
|
12
14
|
console.log(" --dir kind Restrict to one corpus directory. Repeatable.");
|
|
13
15
|
console.log(" kind: knowledge, examples, skills, docs, agents, commands");
|
|
16
|
+
console.log(" --ranked Rank results with the deterministic lexical (BM25-style) scorer");
|
|
17
|
+
console.log(" --embeddings Rerank ranked results with the opt-in local embedding backend.");
|
|
18
|
+
console.log(" Requires --ranked and a configured provider; falls back to the");
|
|
19
|
+
console.log(" lexical ranking (with a notice) if no provider or sidecar is available");
|
|
20
|
+
console.log(" --provider \"cmd args\" Embedding provider command for this invocation (overrides config)");
|
|
14
21
|
console.log(" --json Emit machine-readable results");
|
|
15
22
|
}
|
|
16
23
|
|
|
24
|
+
function parseProviderFlag(value) {
|
|
25
|
+
const parts = String(value || "").trim().split(/\s+/).filter(Boolean);
|
|
26
|
+
if (parts.length === 0) return null;
|
|
27
|
+
return { command: parts[0], args: parts.slice(1) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveProvider({ flagProvider, configFile }) {
|
|
31
|
+
if (flagProvider) return parseProviderFlag(flagProvider);
|
|
32
|
+
return configuredEmbeddingProvider(configFile);
|
|
33
|
+
}
|
|
34
|
+
|
|
17
35
|
function printHits(hits) {
|
|
18
36
|
for (const hit of hits) {
|
|
19
37
|
console.log(`${hit.relPath}:${hit.lineNumber}`);
|
|
@@ -21,6 +39,12 @@ function printHits(hits) {
|
|
|
21
39
|
}
|
|
22
40
|
}
|
|
23
41
|
|
|
42
|
+
function printRankedHits(query, hits, backend) {
|
|
43
|
+
header("design-ai search", `${query} (ranked, ${backend})`);
|
|
44
|
+
info(`Source: ${DESIGN_AI_HOME}`);
|
|
45
|
+
info(`Hits: ${hits.length}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
24
48
|
export async function runSearch(args) {
|
|
25
49
|
const parsed = parseSearchArgs(args);
|
|
26
50
|
if (parsed.help) {
|
|
@@ -28,12 +52,81 @@ export async function runSearch(args) {
|
|
|
28
52
|
return;
|
|
29
53
|
}
|
|
30
54
|
|
|
55
|
+
if (parsed.embeddings && !parsed.ranked) {
|
|
56
|
+
throw new Error("--embeddings requires --ranked");
|
|
57
|
+
}
|
|
58
|
+
|
|
31
59
|
if (!parsed.query) {
|
|
32
60
|
printHelp();
|
|
33
61
|
process.exitCode = 1;
|
|
34
62
|
return;
|
|
35
63
|
}
|
|
36
64
|
|
|
65
|
+
if (parsed.ranked) {
|
|
66
|
+
let backend = "lexical";
|
|
67
|
+
let notice = "";
|
|
68
|
+
let hits;
|
|
69
|
+
|
|
70
|
+
if (parsed.embeddings) {
|
|
71
|
+
const provider = resolveProvider({ flagProvider: parsed.provider, configFile: defaultConfigFile() });
|
|
72
|
+
const reranked = embeddingRerankSearch({
|
|
73
|
+
query: parsed.query,
|
|
74
|
+
provider,
|
|
75
|
+
designAiPath: DESIGN_AI_HOME,
|
|
76
|
+
dirs: parsed.dirs,
|
|
77
|
+
limit: parsed.limit,
|
|
78
|
+
});
|
|
79
|
+
if (reranked.fallback) {
|
|
80
|
+
notice = `embeddings unavailable, using lexical ranking: ${reranked.notice}`;
|
|
81
|
+
const lexical = rankedSearchCorpus({
|
|
82
|
+
query: parsed.query,
|
|
83
|
+
designAiPath: DESIGN_AI_HOME,
|
|
84
|
+
dirs: parsed.dirs,
|
|
85
|
+
limit: parsed.limit,
|
|
86
|
+
});
|
|
87
|
+
hits = lexical.hits;
|
|
88
|
+
} else {
|
|
89
|
+
backend = "embeddings";
|
|
90
|
+
hits = reranked.hits;
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
const lexical = rankedSearchCorpus({
|
|
94
|
+
query: parsed.query,
|
|
95
|
+
designAiPath: DESIGN_AI_HOME,
|
|
96
|
+
dirs: parsed.dirs,
|
|
97
|
+
limit: parsed.limit,
|
|
98
|
+
});
|
|
99
|
+
hits = lexical.hits;
|
|
100
|
+
notice = lexical.notice;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (parsed.json) {
|
|
104
|
+
const payload = { query: parsed.query, ranked: true, notice, hits };
|
|
105
|
+
if (parsed.embeddings) payload.backend = backend;
|
|
106
|
+
console.log(formatSearchJson(payload));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (parsed.embeddings) {
|
|
111
|
+
printRankedHits(parsed.query, hits, backend);
|
|
112
|
+
} else {
|
|
113
|
+
header("design-ai search", `${parsed.query} (ranked)`);
|
|
114
|
+
info(`Source: ${DESIGN_AI_HOME}`);
|
|
115
|
+
info(`Hits: ${hits.length}`);
|
|
116
|
+
}
|
|
117
|
+
if (notice) info(notice);
|
|
118
|
+
console.log();
|
|
119
|
+
if (hits.length === 0) {
|
|
120
|
+
warn("No matches found.");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
for (const hit of hits) {
|
|
124
|
+
console.log(`${hit.relPath} (score ${hit.score}, matched: ${hit.matchedTokens.join(", ")})`);
|
|
125
|
+
console.log(` ${dim(hit.preview)}`);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
37
130
|
const hits = searchCorpus({
|
|
38
131
|
query: parsed.query,
|
|
39
132
|
designAiPath: DESIGN_AI_HOME,
|
package/cli/lib/dispatch.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { runPrompt } from "../commands/prompt.mjs";
|
|
|
20
20
|
import { runPack } from "../commands/pack.mjs";
|
|
21
21
|
import { runVersion } from "../commands/version.mjs";
|
|
22
22
|
import { runHelp } from "../commands/help.mjs";
|
|
23
|
+
import { runIndex } from "../commands/index.mjs";
|
|
23
24
|
import { hasHelpFlag } from "./help-flags.mjs";
|
|
24
25
|
import { suggestNearest } from "./suggest.mjs";
|
|
25
26
|
|
|
@@ -52,6 +53,7 @@ const commands = {
|
|
|
52
53
|
mcp: runMcp,
|
|
53
54
|
search: runSearch,
|
|
54
55
|
find: runSearch,
|
|
56
|
+
index: runIndex,
|
|
55
57
|
show: runShow,
|
|
56
58
|
cat: runShow,
|
|
57
59
|
route: runRoute,
|
|
@@ -75,6 +77,7 @@ export const CANONICAL_COMMANDS = [
|
|
|
75
77
|
"status",
|
|
76
78
|
"list",
|
|
77
79
|
"search",
|
|
80
|
+
"index",
|
|
78
81
|
"show",
|
|
79
82
|
"route",
|
|
80
83
|
"routes",
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// Local embedding sidecar for Phase B (docs/AI-LEARNING-PHASE2.md).
|
|
2
|
+
//
|
|
3
|
+
// Sits next to the Phase A retrieval index under the same index directory. Never a
|
|
4
|
+
// source of truth on its own: it records the same corpus/learning source identity as
|
|
5
|
+
// the Phase A index (reusing retrieval-index.mjs helpers) so staleness/identity rules
|
|
6
|
+
// stay identical (FU-2). Building requires a working provider (cli/lib/embedding-provider.mjs);
|
|
7
|
+
// there is no network call anywhere in this module.
|
|
8
|
+
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
|
|
12
|
+
import { embedDocuments } from "./embedding-provider.mjs";
|
|
13
|
+
import { PACKAGE_ROOT } from "./paths.mjs";
|
|
14
|
+
import {
|
|
15
|
+
collectCorpusDocuments,
|
|
16
|
+
computeCorpusDigest,
|
|
17
|
+
computeLearningDigest,
|
|
18
|
+
defaultIndexDir,
|
|
19
|
+
learningEntryDocuments,
|
|
20
|
+
} from "./retrieval-index.mjs";
|
|
21
|
+
import { auditLearningProfile, loadLearningProfile } from "./learn-profile.mjs";
|
|
22
|
+
import { defaultLearningFile } from "./learn-shared.mjs";
|
|
23
|
+
import { DEFAULT_SEARCH_DIRS } from "./search.mjs";
|
|
24
|
+
|
|
25
|
+
export const EMBEDDING_INDEX_VERSION = 1;
|
|
26
|
+
|
|
27
|
+
// Learning ids are prefixed in the embedding sidecar ONLY, so they can never collide
|
|
28
|
+
// with corpus document ids (corpus ids are relative file paths and never contain ":").
|
|
29
|
+
export const LEARNING_ID_PREFIX = "learning:";
|
|
30
|
+
|
|
31
|
+
export function embeddingIndexFile(indexDir = defaultIndexDir()) {
|
|
32
|
+
return path.join(indexDir, "embedding-index.json");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function roundVector(vector) {
|
|
36
|
+
return vector.map((n) => Number(n.toFixed(6)));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Builds the embedding sidecar by embedding both the corpus documents and the
|
|
40
|
+
// learning entry documents (learning ids prefixed with "learning:" here only —
|
|
41
|
+
// the Phase A learning index and learning.json itself are untouched).
|
|
42
|
+
export function buildEmbeddingIndex({
|
|
43
|
+
provider,
|
|
44
|
+
designAiPath = PACKAGE_ROOT,
|
|
45
|
+
dirs = DEFAULT_SEARCH_DIRS,
|
|
46
|
+
learningFile = defaultLearningFile(),
|
|
47
|
+
now = new Date(),
|
|
48
|
+
spawnRunner,
|
|
49
|
+
} = {}) {
|
|
50
|
+
if (!provider || typeof provider.command !== "string" || provider.command.trim() === "") {
|
|
51
|
+
return { ok: false, error: "no embedding provider configured (pass --provider or set embeddings.provider in ~/.design-ai/config.json)" };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const corpusDocuments = collectCorpusDocuments({ designAiPath, dirs });
|
|
55
|
+
const profile = loadLearningProfile(learningFile);
|
|
56
|
+
const learningDocuments = learningEntryDocuments(profile).map((doc) => ({
|
|
57
|
+
id: `${LEARNING_ID_PREFIX}${doc.id}`,
|
|
58
|
+
text: doc.text,
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
const allDocuments = [...corpusDocuments, ...learningDocuments].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
62
|
+
|
|
63
|
+
const embedArgs = { provider, documents: allDocuments };
|
|
64
|
+
if (spawnRunner) embedArgs.spawnRunner = spawnRunner;
|
|
65
|
+
const embedded = embedDocuments(embedArgs);
|
|
66
|
+
if (!embedded.ok) return { ok: false, error: embedded.error };
|
|
67
|
+
|
|
68
|
+
const vectors = allDocuments
|
|
69
|
+
.map((doc) => ({ id: doc.id, v: roundVector(embedded.vectorsById.get(doc.id)) }))
|
|
70
|
+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
71
|
+
|
|
72
|
+
const { digest: learningDigest } = computeLearningDigest(learningFile);
|
|
73
|
+
const audit = auditLearningProfile({ filePath: learningFile });
|
|
74
|
+
|
|
75
|
+
const payload = {
|
|
76
|
+
version: EMBEDDING_INDEX_VERSION,
|
|
77
|
+
kind: "embedding-index",
|
|
78
|
+
generatedAt: now.toISOString(),
|
|
79
|
+
provider: {
|
|
80
|
+
command: provider.command,
|
|
81
|
+
args: Array.isArray(provider.args) ? [...provider.args] : [],
|
|
82
|
+
modelLabel: provider.modelLabel || "",
|
|
83
|
+
dimensions: embedded.dimensions,
|
|
84
|
+
},
|
|
85
|
+
source: {
|
|
86
|
+
designAiPath: path.resolve(designAiPath),
|
|
87
|
+
corpusDirs: [...dirs],
|
|
88
|
+
corpusDigest: computeCorpusDigest(corpusDocuments),
|
|
89
|
+
learningFile: path.resolve(learningFile),
|
|
90
|
+
learningDigest,
|
|
91
|
+
auditStatus: audit.status || "unknown",
|
|
92
|
+
},
|
|
93
|
+
vectors,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return { ok: true, payload };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function writeEmbeddingIndex({ indexDir = defaultIndexDir(), payload }) {
|
|
100
|
+
mkdirSync(indexDir, { recursive: true });
|
|
101
|
+
const file = embeddingIndexFile(indexDir);
|
|
102
|
+
writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
103
|
+
return { file };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Mirrors loadIndexFile's validation shape/version contract from retrieval-index.mjs.
|
|
107
|
+
export function loadEmbeddingIndexFile(file) {
|
|
108
|
+
if (!existsSync(file)) return { present: false, payload: null, error: "" };
|
|
109
|
+
try {
|
|
110
|
+
const payload = JSON.parse(readFileSync(file, "utf8"));
|
|
111
|
+
if (!payload || typeof payload !== "object" || payload.version !== EMBEDDING_INDEX_VERSION || payload.kind !== "embedding-index") {
|
|
112
|
+
return { present: true, payload: null, error: "unsupported embedding index payload shape or version" };
|
|
113
|
+
}
|
|
114
|
+
return { present: true, payload, error: "" };
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return { present: true, payload: null, error: `unreadable embedding index file: ${error.message}` };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Status mirrors retrieval-index.mjs's statusEntry rules: fresh requires both digests
|
|
121
|
+
// to match AND both source identities (designAiPath, learningFile) to match — a
|
|
122
|
+
// different checkout or a different learning file is "not my index" (same FU-2 rule).
|
|
123
|
+
export function embeddingIndexStatus({
|
|
124
|
+
designAiPath = PACKAGE_ROOT,
|
|
125
|
+
dirs = DEFAULT_SEARCH_DIRS,
|
|
126
|
+
indexDir = defaultIndexDir(),
|
|
127
|
+
learningFile = defaultLearningFile(),
|
|
128
|
+
} = {}) {
|
|
129
|
+
const file = embeddingIndexFile(indexDir);
|
|
130
|
+
const loaded = loadEmbeddingIndexFile(file);
|
|
131
|
+
|
|
132
|
+
if (!loaded.present) {
|
|
133
|
+
return {
|
|
134
|
+
file,
|
|
135
|
+
present: false,
|
|
136
|
+
fresh: false,
|
|
137
|
+
sourceMatch: false,
|
|
138
|
+
generatedAt: "",
|
|
139
|
+
documentCount: 0,
|
|
140
|
+
provider: null,
|
|
141
|
+
storedDigest: "",
|
|
142
|
+
currentDigest: "",
|
|
143
|
+
error: "",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const currentCorpusDigest = computeCorpusDigest(collectCorpusDocuments({ designAiPath, dirs }));
|
|
148
|
+
const currentLearningDigest = computeLearningDigest(learningFile).digest;
|
|
149
|
+
|
|
150
|
+
if (loaded.error) {
|
|
151
|
+
return {
|
|
152
|
+
file,
|
|
153
|
+
present: true,
|
|
154
|
+
fresh: false,
|
|
155
|
+
sourceMatch: false,
|
|
156
|
+
generatedAt: "",
|
|
157
|
+
documentCount: 0,
|
|
158
|
+
provider: null,
|
|
159
|
+
storedDigest: "",
|
|
160
|
+
currentDigest: currentCorpusDigest,
|
|
161
|
+
error: loaded.error,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const storedSource = loaded.payload.source || {};
|
|
166
|
+
const storedCorpusDigest = storedSource.corpusDigest || "";
|
|
167
|
+
const storedLearningDigest = storedSource.learningDigest || "";
|
|
168
|
+
const sourceMatch =
|
|
169
|
+
storedSource.designAiPath === path.resolve(designAiPath) &&
|
|
170
|
+
storedSource.learningFile === path.resolve(learningFile);
|
|
171
|
+
const digestMatch = storedCorpusDigest === currentCorpusDigest && storedLearningDigest === currentLearningDigest;
|
|
172
|
+
const fresh = sourceMatch && digestMatch;
|
|
173
|
+
|
|
174
|
+
const provider = loaded.payload.provider || null;
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
file,
|
|
178
|
+
present: true,
|
|
179
|
+
fresh,
|
|
180
|
+
sourceMatch,
|
|
181
|
+
generatedAt: loaded.payload.generatedAt || "",
|
|
182
|
+
documentCount: Array.isArray(loaded.payload.vectors) ? loaded.payload.vectors.length : 0,
|
|
183
|
+
provider: provider ? { command: provider.command, modelLabel: provider.modelLabel || "", dimensions: provider.dimensions } : null,
|
|
184
|
+
// storedDigest/currentDigest track the corpus digest, matching the Phase A status
|
|
185
|
+
// entry shape; learning drift is folded into `fresh`/`sourceMatch` above since the
|
|
186
|
+
// embedding sidecar has two source digests (corpus + learning) but one file.
|
|
187
|
+
storedDigest: storedCorpusDigest,
|
|
188
|
+
currentDigest: currentCorpusDigest,
|
|
189
|
+
error: "",
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function vectorsById(payload) {
|
|
194
|
+
const map = new Map();
|
|
195
|
+
for (const entry of payload?.vectors || []) {
|
|
196
|
+
map.set(entry.id, entry.v);
|
|
197
|
+
}
|
|
198
|
+
return map;
|
|
199
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Local embedding provider protocol for Phase B (docs/AI-LEARNING-PHASE2.md).
|
|
2
|
+
//
|
|
3
|
+
// Protocol: design-ai spawns the user-configured provider command as a local child
|
|
4
|
+
// process (spawnSync, no shell — args are passed as an argv array, never interpolated
|
|
5
|
+
// into a shell string). design-ai writes JSON Lines to the provider's stdin: one line
|
|
6
|
+
// per input document, `{"id": string, "text": string}`, in sorted-id order. design-ai
|
|
7
|
+
// reads JSON Lines from the provider's stdout: one line per input, `{"id": string,
|
|
8
|
+
// "vector": number[]}`, in ANY order — outputs are matched back to inputs by id, not
|
|
9
|
+
// position. The provider is expected to emit exactly one output line per input id.
|
|
10
|
+
//
|
|
11
|
+
// Failure handling: a non-zero exit code, unparseable stdout, a missing id, or vectors
|
|
12
|
+
// with an empty or inconsistent dimension across the batch all collapse to a single
|
|
13
|
+
// descriptive failure — `embedDocuments` never throws to its caller; it always returns
|
|
14
|
+
// `{ ok: false, error }` so callers (index build, ranked search) can fall back to the
|
|
15
|
+
// deterministic lexical path. No network access is made by this module; the provider
|
|
16
|
+
// is a local executable chosen by the user, invoked exactly as configured.
|
|
17
|
+
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
|
|
20
|
+
function toJsonLines(documents) {
|
|
21
|
+
return documents
|
|
22
|
+
.map((doc) => `${JSON.stringify({ id: doc.id, text: doc.text })}\n`)
|
|
23
|
+
.join("");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseOutputLines(stdout) {
|
|
27
|
+
const vectors = new Map();
|
|
28
|
+
const lines = stdout.split("\n").filter((line) => line.trim() !== "");
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
let parsed;
|
|
31
|
+
try {
|
|
32
|
+
parsed = JSON.parse(line);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return { error: `provider emitted unparseable output line: ${error.message}` };
|
|
35
|
+
}
|
|
36
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.id !== "string") {
|
|
37
|
+
return { error: "provider output line is missing a string id" };
|
|
38
|
+
}
|
|
39
|
+
if (!Array.isArray(parsed.vector) || !parsed.vector.every((n) => typeof n === "number" && Number.isFinite(n))) {
|
|
40
|
+
return { error: `provider output for id "${parsed.id}" is missing a numeric vector` };
|
|
41
|
+
}
|
|
42
|
+
vectors.set(parsed.id, parsed.vector);
|
|
43
|
+
}
|
|
44
|
+
return { vectors };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Default runner: spawns the provider as a local process only (no shell). Injectable
|
|
48
|
+
// for tests, matching the mcp-server.mjs pattern of a default-parameter runner swap.
|
|
49
|
+
function defaultSpawnRunner({ command, args, input }) {
|
|
50
|
+
const result = spawnSync(command, args, {
|
|
51
|
+
input,
|
|
52
|
+
encoding: "utf8",
|
|
53
|
+
shell: false,
|
|
54
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
status: result.status,
|
|
58
|
+
stdout: result.stdout || "",
|
|
59
|
+
stderr: result.stderr || "",
|
|
60
|
+
error: result.error || null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// documents: [{ id, text }]. provider: { command, args }.
|
|
65
|
+
// Returns { ok: true, vectorsById: Map<id, number[]>, dimensions } or { ok: false, error }.
|
|
66
|
+
export function embedDocuments({ provider, documents, spawnRunner = defaultSpawnRunner }) {
|
|
67
|
+
if (!provider || typeof provider.command !== "string" || provider.command.trim() === "") {
|
|
68
|
+
return { ok: false, error: "no embedding provider command configured" };
|
|
69
|
+
}
|
|
70
|
+
const args = Array.isArray(provider.args) ? provider.args : [];
|
|
71
|
+
const sortedDocs = [...documents].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
72
|
+
|
|
73
|
+
if (sortedDocs.length === 0) {
|
|
74
|
+
return { ok: true, vectorsById: new Map(), dimensions: 0 };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let result;
|
|
78
|
+
try {
|
|
79
|
+
result = spawnRunner({ command: provider.command, args, input: toJsonLines(sortedDocs) });
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return { ok: false, error: `failed to spawn embedding provider "${provider.command}": ${error.message}` };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (result.error) {
|
|
85
|
+
return { ok: false, error: `failed to spawn embedding provider "${provider.command}": ${result.error.message}` };
|
|
86
|
+
}
|
|
87
|
+
if (result.status !== 0) {
|
|
88
|
+
const detail = (result.stderr || "").trim();
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
error: `embedding provider "${provider.command}" exited with code ${result.status}${detail ? `: ${detail}` : ""}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const parsed = parseOutputLines(result.stdout);
|
|
96
|
+
if (parsed.error) return { ok: false, error: parsed.error };
|
|
97
|
+
|
|
98
|
+
const missing = sortedDocs.filter((doc) => !parsed.vectors.has(doc.id)).map((doc) => doc.id);
|
|
99
|
+
if (missing.length > 0) {
|
|
100
|
+
return { ok: false, error: `embedding provider did not return vectors for id(s): ${missing.slice(0, 5).join(", ")}` };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let dimensions = 0;
|
|
104
|
+
for (const doc of sortedDocs) {
|
|
105
|
+
const vector = parsed.vectors.get(doc.id);
|
|
106
|
+
if (vector.length === 0) {
|
|
107
|
+
return { ok: false, error: `embedding provider returned an empty vector for id "${doc.id}"` };
|
|
108
|
+
}
|
|
109
|
+
if (dimensions === 0) {
|
|
110
|
+
dimensions = vector.length;
|
|
111
|
+
} else if (vector.length !== dimensions) {
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
error: `embedding provider returned inconsistent vector dimensions (expected ${dimensions}, got ${vector.length} for id "${doc.id}")`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const vectorsById = new Map(sortedDocs.map((doc) => [doc.id, parsed.vectors.get(doc.id)]));
|
|
120
|
+
return { ok: true, vectorsById, dimensions };
|
|
121
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Deterministic cosine-similarity rerank for Phase B (docs/AI-LEARNING-PHASE2.md).
|
|
2
|
+
//
|
|
3
|
+
// design-ai's side of Phase B stays fully deterministic: candidate generation is the
|
|
4
|
+
// existing Phase A lexical ranking, and this module only computes cosine similarity
|
|
5
|
+
// against provider-supplied vectors and sorts with a fixed tie-break rule. Vector
|
|
6
|
+
// values depend on the user's provider, but everything design-ai does with them here
|
|
7
|
+
// is pure and order-stable.
|
|
8
|
+
|
|
9
|
+
function dotProduct(a, b) {
|
|
10
|
+
let sum = 0;
|
|
11
|
+
for (let i = 0; i < a.length; i += 1) sum += a[i] * b[i];
|
|
12
|
+
return sum;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function magnitude(vector) {
|
|
16
|
+
return Math.sqrt(dotProduct(vector, vector));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function cosineSimilarity(a, b) {
|
|
20
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length || a.length === 0) return 0;
|
|
21
|
+
const magA = magnitude(a);
|
|
22
|
+
const magB = magnitude(b);
|
|
23
|
+
if (magA === 0 || magB === 0) return 0;
|
|
24
|
+
return dotProduct(a, b) / (magA * magB);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// candidates: [{ id, ... }] in Phase A lexical order (used as the tie-break/placement
|
|
28
|
+
// order for anything without a vector). vectorsById: Map<id, number[]>. queryVector:
|
|
29
|
+
// number[]. Candidates missing a vector are NOT scored — they keep their lexical
|
|
30
|
+
// order and are placed AFTER every scored (embedded) candidate, deterministically.
|
|
31
|
+
export function cosineRerank({ queryVector, candidates, vectorsById, limit }) {
|
|
32
|
+
const scored = [];
|
|
33
|
+
const unscored = [];
|
|
34
|
+
|
|
35
|
+
for (const candidate of candidates) {
|
|
36
|
+
const vector = vectorsById.get(candidate.id);
|
|
37
|
+
if (!vector) {
|
|
38
|
+
unscored.push(candidate);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const score = cosineSimilarity(queryVector, vector);
|
|
42
|
+
scored.push({ ...candidate, score: Number(score.toFixed(6)) });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
scored.sort((a, b) => {
|
|
46
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
47
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const ordered = [...scored, ...unscored];
|
|
51
|
+
return Number.isInteger(limit) ? ordered.slice(0, limit) : ordered;
|
|
52
|
+
}
|