relay-dsh-plugin-manager 0.2.6 → 0.3.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/CHANGELOG.md +31 -0
- package/README.md +21 -4
- package/README.zh.md +14 -3
- package/SPEC.md +24 -6
- package/docs/acceptance/live-dsh-task-solution-2026-09-11.md +61 -0
- package/docs/acceptance/search-scenarios-2026-09-11.md +32 -0
- package/docs/acceptance/task-role-model-2026-09-11.md +45 -0
- package/docs/acceptance/task-solutions-2026-09-11.md +42 -0
- package/docs/acceptance.md +5 -1
- package/docs/technical-design.md +57 -4
- package/lib/errors-BGx4K2uq.js.map +1 -1
- package/lib/index.d.ts +136 -3
- package/lib/index.js +506 -37
- package/lib/index.js.map +1 -1
- package/lib/{search-runtime-DsmnI673.d.ts → search-runtime-gzUZ5Yb3.d.ts} +17 -3
- package/lib/search-runtime.d.ts +1 -1
- package/lib/search-runtime.js.map +1 -1
- package/package.json +3 -1
package/lib/index.js
CHANGED
|
@@ -74,13 +74,15 @@ function registerConversationSurface(ctx, manager) {
|
|
|
74
74
|
const confirmations = /* @__PURE__ */ new Map();
|
|
75
75
|
ctx.tools.register(defineTool({
|
|
76
76
|
name: "plugin_discover",
|
|
77
|
-
description: "Read-only DSH plugin discovery.
|
|
77
|
+
description: "Read-only DSH plugin discovery. Use search for one capability, exact identity, or GitHub owner:NAME. For a task needing multiple responsibilities, use search_roles: decompose the task into the smallest set of mutually distinct required/optional coverage roles, even when one plugin may ultimately cover several roles. Keep coherent operations on the same resource together: browsing, filtering, and previewing files are one file-browser responsibility. Every ongoing observation that finishes later is the deliberate exception and needs separate roles for source-specific state/event reading and for durable waiting/scheduling that resumes the original session; never merge those coverage responsibilities. Give each role a focused capability query and declare unresolved user choices as ambiguities. It returns inspected candidates grouped by role but does not claim they are relevant until reviewed. Exclude unrelated candidates, retain materially different alternatives, then call assess_solution with only reviewed candidate identities from each role. Assessment verifies role membership, merges one solution used by multiple roles, and reports the smallest complete answer only when every required role is selected and no ambiguity remains; never repeat npm/GitHub aliases, pad results, or silently impose a fixed top count. Ranking and directory placement are relevance evidence, not compatibility, security, or installation approval. Search result repository and recommendedSource values can be passed directly to inspect and plan. This tool never changes the profile.",
|
|
78
78
|
parameters: {
|
|
79
79
|
action: {
|
|
80
80
|
type: "string",
|
|
81
81
|
enum: [
|
|
82
82
|
"list",
|
|
83
83
|
"search",
|
|
84
|
+
"search_roles",
|
|
85
|
+
"assess_solution",
|
|
84
86
|
"inspect",
|
|
85
87
|
"status"
|
|
86
88
|
],
|
|
@@ -89,7 +91,7 @@ function registerConversationSurface(ctx, manager) {
|
|
|
89
91
|
},
|
|
90
92
|
query: {
|
|
91
93
|
type: "string",
|
|
92
|
-
description: "Natural-language query or GitHub owner:NAME search."
|
|
94
|
+
description: "Natural-language query or GitHub owner:NAME search. Required for search and search_roles; for search_roles this is the complete user task and must contain 1 to 1000 printable characters."
|
|
93
95
|
},
|
|
94
96
|
target: {
|
|
95
97
|
type: "string",
|
|
@@ -102,6 +104,85 @@ function registerConversationSurface(ctx, manager) {
|
|
|
102
104
|
maxResults: {
|
|
103
105
|
type: "integer",
|
|
104
106
|
description: "Ranked result-page size from 1 to 20. Use 20 for ordinary need-based searches unless the user explicitly asks for fewer."
|
|
107
|
+
},
|
|
108
|
+
maxResultsPerRole: {
|
|
109
|
+
type: "integer",
|
|
110
|
+
description: "Candidate-pool size from 1 to 20 for each search_roles role. Defaults to 20."
|
|
111
|
+
},
|
|
112
|
+
roles: {
|
|
113
|
+
type: "array",
|
|
114
|
+
items: {
|
|
115
|
+
type: "object",
|
|
116
|
+
additionalProperties: false,
|
|
117
|
+
properties: {
|
|
118
|
+
id: {
|
|
119
|
+
type: "string",
|
|
120
|
+
required: true,
|
|
121
|
+
description: "Stable lowercase role id."
|
|
122
|
+
},
|
|
123
|
+
label: {
|
|
124
|
+
type: "string",
|
|
125
|
+
required: true,
|
|
126
|
+
description: "Short user-facing responsibility label containing 1 to 80 printable characters."
|
|
127
|
+
},
|
|
128
|
+
query: {
|
|
129
|
+
type: "string",
|
|
130
|
+
required: true,
|
|
131
|
+
description: "Focused capability query for this role, without unrelated responsibilities; must contain 1 to 120 printable characters."
|
|
132
|
+
},
|
|
133
|
+
required: {
|
|
134
|
+
type: "boolean",
|
|
135
|
+
description: "False only when the user explicitly made this role optional."
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
description: "For search_roles, the smallest mutually distinct coverage responsibilities needed to complete the task; keep roles distinct even if one plugin may cover several."
|
|
140
|
+
},
|
|
141
|
+
ambiguities: {
|
|
142
|
+
type: "array",
|
|
143
|
+
items: {
|
|
144
|
+
type: "object",
|
|
145
|
+
additionalProperties: false,
|
|
146
|
+
properties: {
|
|
147
|
+
id: {
|
|
148
|
+
type: "string",
|
|
149
|
+
required: true
|
|
150
|
+
},
|
|
151
|
+
question: {
|
|
152
|
+
type: "string",
|
|
153
|
+
required: true
|
|
154
|
+
},
|
|
155
|
+
options: {
|
|
156
|
+
type: "array",
|
|
157
|
+
items: { type: "string" },
|
|
158
|
+
required: true
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
description: "Material unresolved user choices. Any ambiguity prevents a complete assessment."
|
|
163
|
+
},
|
|
164
|
+
solutionId: {
|
|
165
|
+
type: "string",
|
|
166
|
+
description: "Short-lived id returned by search_roles."
|
|
167
|
+
},
|
|
168
|
+
selections: {
|
|
169
|
+
type: "array",
|
|
170
|
+
items: {
|
|
171
|
+
type: "object",
|
|
172
|
+
additionalProperties: false,
|
|
173
|
+
properties: {
|
|
174
|
+
roleId: {
|
|
175
|
+
type: "string",
|
|
176
|
+
required: true
|
|
177
|
+
},
|
|
178
|
+
candidateIdentities: {
|
|
179
|
+
type: "array",
|
|
180
|
+
items: { type: "string" },
|
|
181
|
+
required: true
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
description: "For assess_solution, reviewed candidates grouped by role; first is primary and remaining entries are materially different alternatives."
|
|
105
186
|
}
|
|
106
187
|
},
|
|
107
188
|
output: {
|
|
@@ -734,6 +815,30 @@ function candidateSources(provider, rows) {
|
|
|
734
815
|
}
|
|
735
816
|
return output;
|
|
736
817
|
}
|
|
818
|
+
function candidateMatchReasons(match) {
|
|
819
|
+
if (match?.kind === "github-owner") return [];
|
|
820
|
+
if (match?.kind === "exact-identifier") return [`Exact identifier: ${match.value}`];
|
|
821
|
+
if (match?.kind !== "registry") return [];
|
|
822
|
+
return [
|
|
823
|
+
...match.exactIdentifier ? ["Exact Registry identifier"] : [],
|
|
824
|
+
...match.canonicalPath.length === 0 ? [] : [`Semantic directory: ${match.canonicalPath.join(" / ")}`],
|
|
825
|
+
...match.matchedCapabilities.length === 0 ? [] : [`Matched capabilities: ${match.matchedCapabilities.join(", ")}`]
|
|
826
|
+
];
|
|
827
|
+
}
|
|
828
|
+
function matchPriority(match, exactOwner) {
|
|
829
|
+
if (exactOwner || match?.kind === "exact-identifier" || match?.kind === "registry" && match.exactIdentifier) return 0;
|
|
830
|
+
return 1;
|
|
831
|
+
}
|
|
832
|
+
function semanticMatch(match) {
|
|
833
|
+
if (match?.kind !== "registry" || match.canonicalPath.length === 0 && match.matchedCapabilities.length === 0) return null;
|
|
834
|
+
return {
|
|
835
|
+
directoryVersion: match.directoryVersion ?? null,
|
|
836
|
+
canonicalPathKey: match.canonicalPathKey ?? null,
|
|
837
|
+
canonicalPath: [...match.canonicalPath],
|
|
838
|
+
matchedCapabilities: [...match.matchedCapabilities],
|
|
839
|
+
retrievalSources: [...match.retrievalSources]
|
|
840
|
+
};
|
|
841
|
+
}
|
|
737
842
|
async function searchPlugins(runtime, rawQuery, options = {}) {
|
|
738
843
|
const parsed = parseSearchQuery(rawQuery);
|
|
739
844
|
const maxResults = Math.max(1, Math.min(20, options.maxResults ?? 20));
|
|
@@ -770,17 +875,44 @@ async function searchPlugins(runtime, rawQuery, options = {}) {
|
|
|
770
875
|
return { ok: false };
|
|
771
876
|
}
|
|
772
877
|
}));
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
878
|
+
const accepted = inspected.flatMap((result) => result.ok ? [result] : []);
|
|
879
|
+
const parent = accepted.map((_, index) => index);
|
|
880
|
+
const root = (index) => {
|
|
881
|
+
let current = index;
|
|
882
|
+
while (parent[current] !== current) current = parent[current];
|
|
883
|
+
while (parent[index] !== index) {
|
|
884
|
+
const next = parent[index];
|
|
885
|
+
parent[index] = current;
|
|
886
|
+
index = next;
|
|
887
|
+
}
|
|
888
|
+
return current;
|
|
889
|
+
};
|
|
890
|
+
const join = (left, right) => {
|
|
891
|
+
const leftRoot = root(left);
|
|
892
|
+
const rightRoot = root(right);
|
|
893
|
+
if (leftRoot === rightRoot) return;
|
|
894
|
+
parent[Math.max(leftRoot, rightRoot)] = Math.min(leftRoot, rightRoot);
|
|
895
|
+
};
|
|
896
|
+
const aliasOwner = /* @__PURE__ */ new Map();
|
|
897
|
+
for (const [index, result] of accepted.entries()) {
|
|
898
|
+
const aliases = [`package:${result.inspection.packageName.toLowerCase()}`, ...result.inspection.repository === null ? [] : [`repository:${result.inspection.repository.toLowerCase()}`]];
|
|
899
|
+
for (const alias of aliases) {
|
|
900
|
+
const owner = aliasOwner.get(alias);
|
|
901
|
+
if (owner === void 0) aliasOwner.set(alias, index);
|
|
902
|
+
else join(index, owner);
|
|
779
903
|
}
|
|
904
|
+
}
|
|
905
|
+
const projects = /* @__PURE__ */ new Map();
|
|
906
|
+
const rejectedCandidates = inspected.length - accepted.length;
|
|
907
|
+
for (const [acceptedIndex, result] of accepted.entries()) {
|
|
908
|
+
const project = root(acceptedIndex);
|
|
780
909
|
const identity = inspectionIdentity(result.inspection);
|
|
781
910
|
const repositoryOwner = /^github\.com\/([^/]+)\//iu.exec(result.inspection.repository ?? "")?.[1]?.toLowerCase() ?? null;
|
|
782
|
-
const
|
|
783
|
-
const
|
|
911
|
+
const exactOwnerValue = result.item.match?.kind === "github-owner" ? result.item.match.value : null;
|
|
912
|
+
const exactOwner = exactOwnerValue !== null && repositoryOwner === exactOwnerValue.toLowerCase();
|
|
913
|
+
const reasons = candidateMatchReasons(result.item.match);
|
|
914
|
+
const semantic = semanticMatch(result.item.match);
|
|
915
|
+
const existing = projects.get(project) ?? {
|
|
784
916
|
identity,
|
|
785
917
|
packageName: result.inspection.packageName,
|
|
786
918
|
description: result.inspection.description,
|
|
@@ -788,16 +920,19 @@ async function searchPlugins(runtime, rawQuery, options = {}) {
|
|
|
788
920
|
repositoryOwner,
|
|
789
921
|
providers: [],
|
|
790
922
|
matchReasons: [],
|
|
923
|
+
semanticMatches: [],
|
|
791
924
|
sources: [],
|
|
792
925
|
recommendedSource: result.inspection.installSpec,
|
|
793
926
|
rank: result.item.rank,
|
|
794
|
-
matchPriority: exactOwner
|
|
927
|
+
matchPriority: matchPriority(result.item.match, exactOwner)
|
|
795
928
|
};
|
|
796
929
|
if (!existing.providers.includes(result.item.provider)) existing.providers.push(result.item.provider);
|
|
797
930
|
if (exactOwner) {
|
|
798
|
-
const reason = `Exact GitHub owner: ${
|
|
931
|
+
const reason = `Exact GitHub owner: ${exactOwnerValue}`;
|
|
799
932
|
if (!existing.matchReasons.includes(reason)) existing.matchReasons.push(reason);
|
|
800
933
|
}
|
|
934
|
+
for (const reason of reasons) if (!existing.matchReasons.includes(reason)) existing.matchReasons.push(reason);
|
|
935
|
+
if (semantic !== null && !existing.semanticMatches.some((item) => item.directoryVersion === semantic.directoryVersion && item.canonicalPathKey === semantic.canonicalPathKey)) existing.semanticMatches.push(semantic);
|
|
801
936
|
const sameSource = existing.sources.find((source) => source.inspection.installSpec === result.inspection.installSpec);
|
|
802
937
|
if (sameSource === void 0) existing.sources.push({
|
|
803
938
|
inspection: result.inspection,
|
|
@@ -809,9 +944,9 @@ async function searchPlugins(runtime, rawQuery, options = {}) {
|
|
|
809
944
|
for (const evidence of result.item.evidence) if (!sameSource.evidence.includes(evidence)) sameSource.evidence.push(evidence);
|
|
810
945
|
}
|
|
811
946
|
existing.rank = Math.min(existing.rank, result.item.rank);
|
|
812
|
-
existing.matchPriority = Math.min(existing.matchPriority, exactOwner
|
|
947
|
+
existing.matchPriority = Math.min(existing.matchPriority, matchPriority(result.item.match, exactOwner));
|
|
813
948
|
existing.recommendedSource = existing.sources.find((source) => source.inspection.sourceType === "npm")?.inspection.installSpec ?? existing.sources[0].inspection.installSpec;
|
|
814
|
-
projects.set(
|
|
949
|
+
projects.set(project, existing);
|
|
815
950
|
}
|
|
816
951
|
const candidates = [...projects.values()].sort((left, right) => left.matchPriority - right.matchPriority || left.rank - right.rank || left.packageName.localeCompare(right.packageName)).slice(0, maxResults).map(({ rank: _providerRank, matchPriority: _matchPriority, ...candidate }, index) => ({
|
|
817
952
|
...candidate,
|
|
@@ -825,8 +960,10 @@ async function searchPlugins(runtime, rawQuery, options = {}) {
|
|
|
825
960
|
order: "rank_ascending",
|
|
826
961
|
returnedCandidates: candidates.length,
|
|
827
962
|
requestedMaximum: maxResults,
|
|
828
|
-
|
|
963
|
+
includeEveryDistinctRelevantSolution: true,
|
|
829
964
|
excludeClearlyIrrelevant: true,
|
|
965
|
+
deduplicateEquivalentSources: true,
|
|
966
|
+
padToRequestedMaximum: false,
|
|
830
967
|
silentTopNTruncation: false
|
|
831
968
|
},
|
|
832
969
|
providerErrors,
|
|
@@ -1205,6 +1342,177 @@ var OperationTracker = class {
|
|
|
1205
1342
|
}
|
|
1206
1343
|
};
|
|
1207
1344
|
//#endregion
|
|
1345
|
+
//#region src/task-solutions.ts
|
|
1346
|
+
const DEFAULT_TTL_MS = 600 * 1e3;
|
|
1347
|
+
const MAX_ROLES = 8;
|
|
1348
|
+
const MAX_AMBIGUITIES = 4;
|
|
1349
|
+
const MAX_OPTIONS = 8;
|
|
1350
|
+
const MAX_DRAFTS = 32;
|
|
1351
|
+
const ROLE_ID = /^[a-z][a-z0-9_]{0,63}$/u;
|
|
1352
|
+
function bounded(value, name, maximum) {
|
|
1353
|
+
const text = typeof value === "string" ? value.trim() : "";
|
|
1354
|
+
if (text === "" || text.length > maximum || /[\u0000-\u001f\u007f]/u.test(text)) fail("INVALID_TASK_SOLUTION", `${name} must contain 1 to ${String(maximum)} printable characters.`);
|
|
1355
|
+
return text;
|
|
1356
|
+
}
|
|
1357
|
+
function validateTaskSolutionPlan(taskValue, roleValues, ambiguityValues = []) {
|
|
1358
|
+
const task = bounded(taskValue, "Task", 1e3);
|
|
1359
|
+
if (!Array.isArray(roleValues) || roleValues.length < 1 || roleValues.length > MAX_ROLES) fail("INVALID_TASK_SOLUTION", `A task solution requires 1 to ${String(MAX_ROLES)} roles.`);
|
|
1360
|
+
const roleIds = /* @__PURE__ */ new Set();
|
|
1361
|
+
const roleLabels = /* @__PURE__ */ new Set();
|
|
1362
|
+
const roleQueries = /* @__PURE__ */ new Set();
|
|
1363
|
+
const roles = roleValues.map((value) => {
|
|
1364
|
+
const id = typeof value?.id === "string" ? value.id.trim() : "";
|
|
1365
|
+
if (!ROLE_ID.test(id)) fail("INVALID_TASK_SOLUTION", "Role ids must be lowercase stable identifiers.");
|
|
1366
|
+
if (roleIds.has(id)) fail("INVALID_TASK_SOLUTION", "Role ids must be unique.");
|
|
1367
|
+
roleIds.add(id);
|
|
1368
|
+
const label = bounded(value.label, "Role label", 80);
|
|
1369
|
+
const query = bounded(value.query, "Role query", 120);
|
|
1370
|
+
const labelKey = label.toLocaleLowerCase();
|
|
1371
|
+
const queryKey = query.toLocaleLowerCase().replace(/\s+/gu, " ");
|
|
1372
|
+
if (roleLabels.has(labelKey) || roleQueries.has(queryKey)) fail("INVALID_TASK_SOLUTION", "Role labels and focused queries must be unique.");
|
|
1373
|
+
roleLabels.add(labelKey);
|
|
1374
|
+
roleQueries.add(queryKey);
|
|
1375
|
+
return {
|
|
1376
|
+
id,
|
|
1377
|
+
label,
|
|
1378
|
+
query,
|
|
1379
|
+
required: value.required !== false
|
|
1380
|
+
};
|
|
1381
|
+
});
|
|
1382
|
+
if (!Array.isArray(ambiguityValues) || ambiguityValues.length > MAX_AMBIGUITIES) fail("INVALID_TASK_SOLUTION", `A task solution supports at most ${String(MAX_AMBIGUITIES)} unresolved ambiguities.`);
|
|
1383
|
+
const ambiguityIds = /* @__PURE__ */ new Set();
|
|
1384
|
+
return {
|
|
1385
|
+
task,
|
|
1386
|
+
roles,
|
|
1387
|
+
ambiguities: ambiguityValues.map((value) => {
|
|
1388
|
+
const id = typeof value?.id === "string" ? value.id.trim() : "";
|
|
1389
|
+
if (!ROLE_ID.test(id) || ambiguityIds.has(id) || roleIds.has(id)) fail("INVALID_TASK_SOLUTION", "Ambiguity ids must be unique lowercase stable identifiers and cannot reuse a role id.");
|
|
1390
|
+
ambiguityIds.add(id);
|
|
1391
|
+
if (!Array.isArray(value.options) || value.options.length < 2 || value.options.length > MAX_OPTIONS) fail("INVALID_TASK_SOLUTION", `Each ambiguity requires 2 to ${String(MAX_OPTIONS)} options.`);
|
|
1392
|
+
const options = value.options.map((option) => bounded(option, "Ambiguity option", 80));
|
|
1393
|
+
if (new Set(options).size !== options.length) fail("INVALID_TASK_SOLUTION", "Ambiguity options must be unique.");
|
|
1394
|
+
return {
|
|
1395
|
+
id,
|
|
1396
|
+
question: bounded(value.question, "Ambiguity question", 200),
|
|
1397
|
+
options
|
|
1398
|
+
};
|
|
1399
|
+
})
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
function candidateSummary(candidate) {
|
|
1403
|
+
const { sources: _sources, ...summary } = candidate;
|
|
1404
|
+
return structuredClone(summary);
|
|
1405
|
+
}
|
|
1406
|
+
var TaskSolutionStore = class {
|
|
1407
|
+
now;
|
|
1408
|
+
id;
|
|
1409
|
+
ttlMs;
|
|
1410
|
+
drafts = /* @__PURE__ */ new Map();
|
|
1411
|
+
constructor(options = {}) {
|
|
1412
|
+
this.now = options.now ?? Date.now;
|
|
1413
|
+
this.id = options.id ?? (() => `task-solution:${randomUUID()}`);
|
|
1414
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
1415
|
+
if (!Number.isSafeInteger(this.ttlMs) || this.ttlMs < 1e3 || this.ttlMs > 3600 * 1e3) throw new RangeError("Task solution ttlMs must be from 1 second to 1 hour.");
|
|
1416
|
+
}
|
|
1417
|
+
create(input) {
|
|
1418
|
+
const plan = validateTaskSolutionPlan(input.task, input.roles, input.ambiguities);
|
|
1419
|
+
const now = this.now();
|
|
1420
|
+
for (const [id, draft] of this.drafts) if (draft.expiresAt <= now) this.drafts.delete(id);
|
|
1421
|
+
while (this.drafts.size >= MAX_DRAFTS) this.drafts.delete(this.drafts.keys().next().value);
|
|
1422
|
+
const roles = plan.roles.map((role) => {
|
|
1423
|
+
const search = input.searches[role.id];
|
|
1424
|
+
if (search === void 0) fail("INVALID_TASK_SOLUTION", `Search results are missing for role ${role.id}.`);
|
|
1425
|
+
return {
|
|
1426
|
+
...role,
|
|
1427
|
+
candidates: search.candidates.map(candidateSummary),
|
|
1428
|
+
providerErrors: structuredClone(search.providerErrors),
|
|
1429
|
+
rejectedCandidates: search.rejectedCandidates
|
|
1430
|
+
};
|
|
1431
|
+
});
|
|
1432
|
+
const requiredCandidateMissing = roles.some((role) => role.required && role.candidates.length === 0);
|
|
1433
|
+
const draft = {
|
|
1434
|
+
schemaVersion: "1.0.0",
|
|
1435
|
+
solutionId: bounded(this.id(), "Task solution id", 200),
|
|
1436
|
+
task: plan.task,
|
|
1437
|
+
status: plan.ambiguities.length > 0 ? "ambiguous" : requiredCandidateMissing ? "incomplete" : "needs_review",
|
|
1438
|
+
createdAt: now,
|
|
1439
|
+
expiresAt: now + this.ttlMs,
|
|
1440
|
+
roles,
|
|
1441
|
+
ambiguities: plan.ambiguities
|
|
1442
|
+
};
|
|
1443
|
+
if (this.drafts.has(draft.solutionId)) fail("INVALID_TASK_SOLUTION", "Task solution id must be unique.");
|
|
1444
|
+
this.drafts.set(draft.solutionId, draft);
|
|
1445
|
+
return structuredClone(draft);
|
|
1446
|
+
}
|
|
1447
|
+
assess(solutionIdValue, selectionValues) {
|
|
1448
|
+
const solutionId = bounded(solutionIdValue, "Task solution id", 200);
|
|
1449
|
+
const draft = this.drafts.get(solutionId);
|
|
1450
|
+
if (draft === void 0) fail("TASK_SOLUTION_NOT_FOUND", "Task solution draft was not found.");
|
|
1451
|
+
if (draft.expiresAt <= this.now()) {
|
|
1452
|
+
this.drafts.delete(solutionId);
|
|
1453
|
+
fail("TASK_SOLUTION_EXPIRED", "Task solution draft has expired; search the roles again.");
|
|
1454
|
+
}
|
|
1455
|
+
if (!Array.isArray(selectionValues) || selectionValues.length > draft.roles.length) fail("INVALID_TASK_SOLUTION", "Task solution selections must contain at most one row per role.");
|
|
1456
|
+
const roleById = new Map(draft.roles.map((role) => [role.id, role]));
|
|
1457
|
+
const selections = /* @__PURE__ */ new Map();
|
|
1458
|
+
for (const value of selectionValues) {
|
|
1459
|
+
const roleId = typeof value?.roleId === "string" ? value.roleId.trim() : "";
|
|
1460
|
+
const role = roleById.get(roleId);
|
|
1461
|
+
if (role === void 0) fail("INVALID_TASK_SOLUTION", `Unknown task solution role ${roleId}.`);
|
|
1462
|
+
if (selections.has(roleId)) fail("INVALID_TASK_SOLUTION", `Role ${roleId} has duplicate selection rows.`);
|
|
1463
|
+
if (!Array.isArray(value.candidateIdentities) || value.candidateIdentities.length > 20) fail("INVALID_TASK_SOLUTION", `Role ${roleId} has too many candidate identities.`);
|
|
1464
|
+
const identities = value.candidateIdentities.map((identity) => bounded(identity, "Candidate identity", 500));
|
|
1465
|
+
if (new Set(identities).size !== identities.length) fail("INVALID_TASK_SOLUTION", `Role ${roleId} contains a duplicate candidate.`);
|
|
1466
|
+
const available = new Set(role.candidates.map((candidate) => candidate.identity));
|
|
1467
|
+
for (const identity of identities) if (!available.has(identity)) fail("INVALID_TASK_SOLUTION", `${identity} is not a candidate for role ${roleId}.`);
|
|
1468
|
+
selections.set(roleId, identities);
|
|
1469
|
+
}
|
|
1470
|
+
const solutions = /* @__PURE__ */ new Map();
|
|
1471
|
+
const roles = draft.roles.map((role) => {
|
|
1472
|
+
const identities = selections.get(role.id) ?? [];
|
|
1473
|
+
for (const identity of identities) {
|
|
1474
|
+
const candidate = role.candidates.find((item) => item.identity === identity);
|
|
1475
|
+
const existing = solutions.get(identity);
|
|
1476
|
+
if (existing === void 0) solutions.set(identity, {
|
|
1477
|
+
...structuredClone(candidate),
|
|
1478
|
+
roleIds: [role.id]
|
|
1479
|
+
});
|
|
1480
|
+
else if (!existing.roleIds.includes(role.id)) existing.roleIds.push(role.id);
|
|
1481
|
+
}
|
|
1482
|
+
return {
|
|
1483
|
+
id: role.id,
|
|
1484
|
+
label: role.label,
|
|
1485
|
+
query: role.query,
|
|
1486
|
+
required: role.required,
|
|
1487
|
+
status: identities.length > 0 ? "covered" : role.required ? "missing_required" : "missing_optional",
|
|
1488
|
+
primaryCandidateIdentity: identities[0] ?? null,
|
|
1489
|
+
alternativeCandidateIdentities: identities.slice(1)
|
|
1490
|
+
};
|
|
1491
|
+
});
|
|
1492
|
+
const required = roles.filter((role) => role.required);
|
|
1493
|
+
const optional = roles.filter((role) => !role.required);
|
|
1494
|
+
const missingRequiredRoleIds = required.filter((role) => role.status !== "covered").map((role) => role.id);
|
|
1495
|
+
const complete = missingRequiredRoleIds.length === 0 && draft.ambiguities.length === 0;
|
|
1496
|
+
return {
|
|
1497
|
+
schemaVersion: "1.0.0",
|
|
1498
|
+
solutionId,
|
|
1499
|
+
task: draft.task,
|
|
1500
|
+
status: draft.ambiguities.length > 0 ? "ambiguous" : complete ? "complete" : "incomplete",
|
|
1501
|
+
roles,
|
|
1502
|
+
ambiguities: structuredClone(draft.ambiguities),
|
|
1503
|
+
solutions: [...solutions.values()],
|
|
1504
|
+
coverage: {
|
|
1505
|
+
requiredRoles: required.length,
|
|
1506
|
+
coveredRequiredRoles: required.filter((role) => role.status === "covered").length,
|
|
1507
|
+
optionalRoles: optional.length,
|
|
1508
|
+
coveredOptionalRoles: optional.filter((role) => role.status === "covered").length,
|
|
1509
|
+
missingRequiredRoleIds,
|
|
1510
|
+
complete
|
|
1511
|
+
}
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
//#endregion
|
|
1208
1516
|
//#region src/manager.ts
|
|
1209
1517
|
const MAX_INSTALL_MANY_SOURCES = 20;
|
|
1210
1518
|
const FIBER_PHASE = {
|
|
@@ -1258,7 +1566,10 @@ const TELEMETRY_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
|
1258
1566
|
"DSH_COMMAND_FAILED",
|
|
1259
1567
|
"BATCH_INSTALL_FAILED",
|
|
1260
1568
|
"POSTCONDITION_FAILED",
|
|
1261
|
-
"RESTART_UNAVAILABLE"
|
|
1569
|
+
"RESTART_UNAVAILABLE",
|
|
1570
|
+
"INVALID_TASK_SOLUTION",
|
|
1571
|
+
"TASK_SOLUTION_NOT_FOUND",
|
|
1572
|
+
"TASK_SOLUTION_EXPIRED"
|
|
1262
1573
|
]);
|
|
1263
1574
|
function safePackageName(value) {
|
|
1264
1575
|
const name = value?.trim() ?? "";
|
|
@@ -1315,6 +1626,7 @@ var PluginManager = class {
|
|
|
1315
1626
|
fetchOptions;
|
|
1316
1627
|
hmrTimeoutMs;
|
|
1317
1628
|
telemetry;
|
|
1629
|
+
taskSolutions;
|
|
1318
1630
|
constructor(dependencies) {
|
|
1319
1631
|
this.profileDir = dependencies.profileDir;
|
|
1320
1632
|
this.searchRuntime = dependencies.searchRuntime;
|
|
@@ -1328,6 +1640,7 @@ var PluginManager = class {
|
|
|
1328
1640
|
this.fetchOptions = dependencies.fetchOptions ?? {};
|
|
1329
1641
|
this.hmrTimeoutMs = dependencies.hmrTimeoutMs ?? 5e3;
|
|
1330
1642
|
this.telemetry = dependencies.telemetry ?? { capture() {} };
|
|
1643
|
+
this.taskSolutions = dependencies.taskSolutions ?? new TaskSolutionStore();
|
|
1331
1644
|
}
|
|
1332
1645
|
capture(event, properties = {}) {
|
|
1333
1646
|
try {
|
|
@@ -1356,7 +1669,7 @@ var PluginManager = class {
|
|
|
1356
1669
|
this.capture("plugin_manager_used", {
|
|
1357
1670
|
surface: "discover",
|
|
1358
1671
|
action: request.action,
|
|
1359
|
-
...request.action === "search" ? {
|
|
1672
|
+
...request.action === "search" || request.action === "search_roles" ? {
|
|
1360
1673
|
has_query: (request.query?.trim().length ?? 0) > 0,
|
|
1361
1674
|
query_length_bucket: queryLengthBucket(request.query)
|
|
1362
1675
|
} : {}
|
|
@@ -1374,6 +1687,27 @@ var PluginManager = class {
|
|
|
1374
1687
|
};
|
|
1375
1688
|
return await searchPlugins(this.searchRuntime, request.query ?? "", options);
|
|
1376
1689
|
}
|
|
1690
|
+
if (request.action === "search_roles") {
|
|
1691
|
+
const plan = validateTaskSolutionPlan(request.query ?? "", request.roles ?? [], request.ambiguities ?? []);
|
|
1692
|
+
const maxResultsPerRole = request.maxResultsPerRole ?? 20;
|
|
1693
|
+
if (!Number.isInteger(maxResultsPerRole) || maxResultsPerRole < 1 || maxResultsPerRole > 20) fail("INVALID_TASK_SOLUTION", "maxResultsPerRole must be an integer from 1 to 20.");
|
|
1694
|
+
const searches = await Promise.all(plan.roles.map(async (role) => [role.id, await searchPlugins(this.searchRuntime, role.query, {
|
|
1695
|
+
...this.fetchOptions,
|
|
1696
|
+
signal,
|
|
1697
|
+
maxResults: maxResultsPerRole,
|
|
1698
|
+
inspect: this.inspect
|
|
1699
|
+
})]));
|
|
1700
|
+
return this.taskSolutions.create({
|
|
1701
|
+
task: plan.task,
|
|
1702
|
+
roles: plan.roles,
|
|
1703
|
+
ambiguities: plan.ambiguities,
|
|
1704
|
+
searches: Object.fromEntries(searches)
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
if (request.action === "assess_solution") {
|
|
1708
|
+
if (request.solutionId === void 0) fail("INVALID_TASK_SOLUTION", "A task solution id is required for assessment.");
|
|
1709
|
+
return this.taskSolutions.assess(request.solutionId, request.selections ?? []);
|
|
1710
|
+
}
|
|
1377
1711
|
if (request.action === "inspect") {
|
|
1378
1712
|
if (request.target === void 0) fail("INVALID_SOURCE", "A plugin source is required for inspection.");
|
|
1379
1713
|
return await this.inspect(request.target, {
|
|
@@ -1795,7 +2129,14 @@ var PluginManager = class {
|
|
|
1795
2129
|
//#endregion
|
|
1796
2130
|
//#region src/providers.ts
|
|
1797
2131
|
const MAX_PROVIDER_RESULTS = 20;
|
|
2132
|
+
const REGISTRY_KEYWORD_CHALLENGER_POOL = 21;
|
|
2133
|
+
const REGISTRY_DIRECTORY_POOL = 50;
|
|
2134
|
+
const RECIPROCAL_RANK_OFFSET = 20;
|
|
2135
|
+
const KEYWORD_RANK_WEIGHT = .1;
|
|
2136
|
+
const DIRECTORY_RANK_WEIGHT = .2;
|
|
2137
|
+
const IDENTITY_TERM_BOOST = .01;
|
|
1798
2138
|
const REGISTRY_SNAPSHOT_ID = /^discovery\.[a-z0-9.-]+$/u;
|
|
2139
|
+
const REGISTRY_DIRECTORY_VERSION = /^[a-z0-9][a-z0-9._-]{0,127}$/u;
|
|
1799
2140
|
function query(value) {
|
|
1800
2141
|
const normalized = value.trim();
|
|
1801
2142
|
if (normalized === "" || normalized.length > 120 || /[\u0000-\u001f\u007f]/u.test(normalized)) throw new Error("Search query must contain 1 to 120 printable characters.");
|
|
@@ -1836,7 +2177,11 @@ function npmSearchProvider(fetchImpl = globalThis.fetch) {
|
|
|
1836
2177
|
package: text
|
|
1837
2178
|
}],
|
|
1838
2179
|
score: Number.MAX_SAFE_INTEGER,
|
|
1839
|
-
evidence: ["Exact npm package-name query"]
|
|
2180
|
+
evidence: ["Exact npm package-name query"],
|
|
2181
|
+
match: {
|
|
2182
|
+
kind: "exact-identifier",
|
|
2183
|
+
value: text
|
|
2184
|
+
}
|
|
1840
2185
|
}, ...searched];
|
|
1841
2186
|
}
|
|
1842
2187
|
};
|
|
@@ -1911,7 +2256,7 @@ function githubSearchProvider(fetchImpl = globalThis.fetch, env = process.env) {
|
|
|
1911
2256
|
}
|
|
1912
2257
|
};
|
|
1913
2258
|
}
|
|
1914
|
-
function registryEndpoint(value) {
|
|
2259
|
+
function registryEndpoint(value, operation) {
|
|
1915
2260
|
let url;
|
|
1916
2261
|
try {
|
|
1917
2262
|
url = new URL(value);
|
|
@@ -1921,7 +2266,7 @@ function registryEndpoint(value) {
|
|
|
1921
2266
|
const local = url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "::1";
|
|
1922
2267
|
if (url.protocol !== "https:" && !(local && url.protocol === "http:")) throw new Error("Registry URL must use HTTPS, except for an explicit local development endpoint.");
|
|
1923
2268
|
if (url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "") throw new Error("Registry URL cannot contain credentials, query parameters, or a fragment.");
|
|
1924
|
-
url.pathname = `${url.pathname.replace(/\/$/u, "")}/v1/plugins
|
|
2269
|
+
url.pathname = `${url.pathname.replace(/\/$/u, "")}/v1/plugins:${operation}`;
|
|
1925
2270
|
return url.href;
|
|
1926
2271
|
}
|
|
1927
2272
|
function boundedText(value, maximum = 4e3) {
|
|
@@ -1930,7 +2275,19 @@ function boundedText(value, maximum = 4e3) {
|
|
|
1930
2275
|
function queryLocale(value) {
|
|
1931
2276
|
return /\p{Script=Han}/u.test(value) ? "zh-CN" : "en";
|
|
1932
2277
|
}
|
|
1933
|
-
function
|
|
2278
|
+
function safeCodes(value, maximum = 20) {
|
|
2279
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string" && /^[a-z0-9._-]+$/u.test(item)).slice(0, maximum) : [];
|
|
2280
|
+
}
|
|
2281
|
+
function safePath(value, locale) {
|
|
2282
|
+
if (!Array.isArray(value)) return [];
|
|
2283
|
+
return value.flatMap((item) => {
|
|
2284
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) return [];
|
|
2285
|
+
const path = item;
|
|
2286
|
+
const label = locale === "en" ? boundedText(path.en, 200) ?? boundedText(path.zh_CN, 200) : boundedText(path.zh_CN, 200) ?? boundedText(path.en, 200);
|
|
2287
|
+
return label === void 0 ? [] : [label];
|
|
2288
|
+
}).slice(0, 8);
|
|
2289
|
+
}
|
|
2290
|
+
function registryCandidate(value, metadata) {
|
|
1934
2291
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
1935
2292
|
const candidate = value;
|
|
1936
2293
|
const entry = candidate.entry;
|
|
@@ -1961,7 +2318,12 @@ function registryCandidate(value, snapshotId) {
|
|
|
1961
2318
|
const zh = boundedText(entry.imported_content?.description?.["zh-CN"]);
|
|
1962
2319
|
const en = boundedText(entry.imported_content?.description?.en);
|
|
1963
2320
|
const repository = boundedText(entry.identity?.repository_url, 500);
|
|
1964
|
-
const reasonCodes =
|
|
2321
|
+
const reasonCodes = safeCodes(candidate.match?.reason_codes ?? candidate.match?.keyword_reason_codes, 8);
|
|
2322
|
+
const retrievalSources = safeCodes(candidate.match?.retrieval_sources, 8);
|
|
2323
|
+
const canonicalPathKey = boundedText(candidate.match?.canonical_path_key, 500);
|
|
2324
|
+
const canonicalPath = safePath(candidate.match?.canonical_primary_path, metadata.locale);
|
|
2325
|
+
const matchedCapabilities = safeCodes(candidate.match?.matched_capabilities, 8);
|
|
2326
|
+
const exactIdentifier = reasonCodes.includes("exact_identifier");
|
|
1965
2327
|
const score = typeof candidate.match?.score === "number" && Number.isFinite(candidate.match.score) && candidate.match.score >= 0 ? candidate.match.score : void 0;
|
|
1966
2328
|
return {
|
|
1967
2329
|
id: `registry:${entry.entry_id}`,
|
|
@@ -1974,19 +2336,123 @@ function registryCandidate(value, snapshotId) {
|
|
|
1974
2336
|
sources,
|
|
1975
2337
|
...score === void 0 ? {} : { score },
|
|
1976
2338
|
evidence: [
|
|
1977
|
-
`DSH Registry source snapshot: ${snapshotId}`,
|
|
2339
|
+
`DSH Registry source snapshot: ${metadata.snapshotId}`,
|
|
2340
|
+
...metadata.directoryVersion === void 0 ? [] : [`DSH Registry directory version: ${metadata.directoryVersion}`],
|
|
2341
|
+
...canonicalPath.length === 0 ? [] : [`Semantic directory: ${canonicalPath.join(" / ")}`],
|
|
2342
|
+
...matchedCapabilities.length === 0 ? [] : [`Matched capabilities: ${matchedCapabilities.join(", ")}`],
|
|
1978
2343
|
"Registry discovery record only; compatibility and security not tested",
|
|
1979
2344
|
...reasonCodes.map((code) => `Registry match: ${code}`)
|
|
1980
|
-
]
|
|
2345
|
+
],
|
|
2346
|
+
match: {
|
|
2347
|
+
kind: "registry",
|
|
2348
|
+
strategy: metadata.strategy,
|
|
2349
|
+
snapshotId: metadata.snapshotId,
|
|
2350
|
+
...metadata.directoryVersion === void 0 ? {} : { directoryVersion: metadata.directoryVersion },
|
|
2351
|
+
retrievalSources,
|
|
2352
|
+
keywordReasonCodes: reasonCodes,
|
|
2353
|
+
...canonicalPathKey === void 0 ? {} : { canonicalPathKey },
|
|
2354
|
+
canonicalPath,
|
|
2355
|
+
matchedCapabilities,
|
|
2356
|
+
exactIdentifier
|
|
2357
|
+
}
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2360
|
+
async function parseRegistryResponse(response, strategy, locale) {
|
|
2361
|
+
if (!response.ok) throw new Error(`DSH Registry ${strategy === "keyword" ? "search" : "directory route"} returned HTTP ${response.status}`);
|
|
2362
|
+
const data = await response.json();
|
|
2363
|
+
const directoryVersion = strategy === "keyword-plus-semantic-directory-v1" && typeof data.directory_version === "string" && REGISTRY_DIRECTORY_VERSION.test(data.directory_version) ? data.directory_version : void 0;
|
|
2364
|
+
if (typeof data.snapshot_id !== "string" || !REGISTRY_SNAPSHOT_ID.test(data.snapshot_id) || !Array.isArray(data.candidates) || data.is_final_recommendation === true || data.grants_install_approval === true || strategy === "keyword-plus-semantic-directory-v1" && directoryVersion === void 0) throw new Error("DSH Registry search returned an invalid discovery response.");
|
|
2365
|
+
const metadata = {
|
|
2366
|
+
snapshotId: data.snapshot_id,
|
|
2367
|
+
strategy,
|
|
2368
|
+
locale,
|
|
2369
|
+
...directoryVersion === void 0 ? {} : { directoryVersion }
|
|
2370
|
+
};
|
|
2371
|
+
return {
|
|
2372
|
+
metadata,
|
|
2373
|
+
candidates: data.candidates.flatMap((candidate) => {
|
|
2374
|
+
const normalized = registryCandidate(candidate, metadata);
|
|
2375
|
+
return normalized === null ? [] : [normalized];
|
|
2376
|
+
})
|
|
1981
2377
|
};
|
|
1982
2378
|
}
|
|
1983
|
-
|
|
1984
|
-
|
|
2379
|
+
const IDENTITY_STOP_TERMS = /* @__PURE__ */ new Set([
|
|
2380
|
+
"and",
|
|
2381
|
+
"dsh",
|
|
2382
|
+
"for",
|
|
2383
|
+
"from",
|
|
2384
|
+
"inside",
|
|
2385
|
+
"into",
|
|
2386
|
+
"plugin",
|
|
2387
|
+
"plugins",
|
|
2388
|
+
"the",
|
|
2389
|
+
"use",
|
|
2390
|
+
"using",
|
|
2391
|
+
"with"
|
|
2392
|
+
]);
|
|
2393
|
+
function identityTermCoverage(searchText, candidate) {
|
|
2394
|
+
const terms = [...new Set(searchText.toLowerCase().match(/[a-z0-9@]+/gu) ?? [])].filter((term) => term.length >= 3 && !IDENTITY_STOP_TERMS.has(term));
|
|
2395
|
+
if (terms.length === 0) return 0;
|
|
2396
|
+
const identityTerms = new Set(`${candidate.title} ${candidate.repository ?? ""}`.toLowerCase().split(/[^a-z0-9@]+/gu).filter(Boolean));
|
|
2397
|
+
return terms.filter((term) => identityTerms.has(term)).length / terms.length;
|
|
2398
|
+
}
|
|
2399
|
+
function mergeRegistryRankings(searchText, keyword, directory, limit) {
|
|
2400
|
+
const combined = /* @__PURE__ */ new Map();
|
|
2401
|
+
for (const [source, response, weight] of [[
|
|
2402
|
+
"keyword",
|
|
2403
|
+
keyword,
|
|
2404
|
+
KEYWORD_RANK_WEIGHT
|
|
2405
|
+
], [
|
|
2406
|
+
"directory",
|
|
2407
|
+
directory,
|
|
2408
|
+
DIRECTORY_RANK_WEIGHT
|
|
2409
|
+
]]) {
|
|
2410
|
+
if (response === null) continue;
|
|
2411
|
+
response.candidates.forEach((candidate, index) => {
|
|
2412
|
+
if (source === "directory" && keyword !== null && !combined.has(candidate.id)) return;
|
|
2413
|
+
const current = combined.get(candidate.id) ?? {
|
|
2414
|
+
candidate,
|
|
2415
|
+
score: 0
|
|
2416
|
+
};
|
|
2417
|
+
current.score += weight / (RECIPROCAL_RANK_OFFSET + index + 1);
|
|
2418
|
+
if (source === "keyword") current.keywordRank = index + 1;
|
|
2419
|
+
else {
|
|
2420
|
+
current.directoryRank = index + 1;
|
|
2421
|
+
current.candidate = candidate;
|
|
2422
|
+
}
|
|
2423
|
+
combined.set(candidate.id, current);
|
|
2424
|
+
});
|
|
2425
|
+
}
|
|
2426
|
+
return [...combined.values()].map((item) => {
|
|
2427
|
+
const keywordMatch = keyword?.candidates.find((candidate) => candidate.id === item.candidate.id)?.match;
|
|
2428
|
+
const directoryMatch = directory?.candidates.find((candidate) => candidate.id === item.candidate.id)?.match;
|
|
2429
|
+
const exactIdentifier = keywordMatch?.kind === "registry" && keywordMatch.exactIdentifier || directoryMatch?.kind === "registry" && directoryMatch.exactIdentifier;
|
|
2430
|
+
const registryMatch = directoryMatch?.kind === "registry" ? directoryMatch : keywordMatch?.kind === "registry" ? keywordMatch : null;
|
|
2431
|
+
const score = item.score + IDENTITY_TERM_BOOST * identityTermCoverage(searchText, item.candidate) + (exactIdentifier ? 1 : 0);
|
|
2432
|
+
return {
|
|
2433
|
+
...item.candidate,
|
|
2434
|
+
score,
|
|
2435
|
+
...registryMatch === null ? {} : { match: {
|
|
2436
|
+
...registryMatch,
|
|
2437
|
+
strategy: directory === null ? "keyword" : "keyword-plus-semantic-directory-v1",
|
|
2438
|
+
keywordReasonCodes: keywordMatch?.kind === "registry" ? keywordMatch.keywordReasonCodes : registryMatch.keywordReasonCodes,
|
|
2439
|
+
exactIdentifier
|
|
2440
|
+
} },
|
|
2441
|
+
evidence: [...item.candidate.evidence ?? [], `Registry rank fusion: keyword=${String(item.keywordRank ?? "none")}, directory=${String(item.directoryRank ?? "none")}`]
|
|
2442
|
+
};
|
|
2443
|
+
}).sort((left, right) => (right.score ?? 0) - (left.score ?? 0) || left.id.localeCompare(right.id)).slice(0, limit);
|
|
2444
|
+
}
|
|
2445
|
+
function registrySearchProvider(baseUrl, fetchImpl = globalThis.fetch, options = {}) {
|
|
2446
|
+
const keywordEndpoint = registryEndpoint(baseUrl, "search");
|
|
2447
|
+
const directoryEndpoint = registryEndpoint(baseUrl, "route");
|
|
2448
|
+
const strategy = options.strategy ?? "hybrid";
|
|
1985
2449
|
return {
|
|
1986
2450
|
id: "dsh-registry",
|
|
1987
2451
|
async search(request) {
|
|
1988
2452
|
const text = query(request.query);
|
|
1989
|
-
const
|
|
2453
|
+
const locale = queryLocale(text);
|
|
2454
|
+
const outputLimit = Math.min(request.maxResults, MAX_PROVIDER_RESULTS);
|
|
2455
|
+
const requestEndpoint = async (endpoint, responseStrategy, limit) => parseRegistryResponse(await fetchImpl(endpoint, {
|
|
1990
2456
|
method: "POST",
|
|
1991
2457
|
signal: request.signal,
|
|
1992
2458
|
headers: {
|
|
@@ -1996,17 +2462,20 @@ function registrySearchProvider(baseUrl, fetchImpl = globalThis.fetch) {
|
|
|
1996
2462
|
body: JSON.stringify({
|
|
1997
2463
|
schema_version: "1.0.0",
|
|
1998
2464
|
query: text,
|
|
1999
|
-
locale
|
|
2000
|
-
limit
|
|
2465
|
+
locale,
|
|
2466
|
+
limit
|
|
2001
2467
|
})
|
|
2002
|
-
});
|
|
2003
|
-
if (
|
|
2004
|
-
const
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2468
|
+
}), responseStrategy, locale);
|
|
2469
|
+
if (strategy === "keyword") return (await requestEndpoint(keywordEndpoint, "keyword", outputLimit)).candidates.slice(0, outputLimit);
|
|
2470
|
+
const [keywordResult, directoryResult] = await Promise.allSettled([requestEndpoint(keywordEndpoint, "keyword", REGISTRY_KEYWORD_CHALLENGER_POOL), requestEndpoint(directoryEndpoint, "keyword-plus-semantic-directory-v1", REGISTRY_DIRECTORY_POOL)]);
|
|
2471
|
+
const keyword = keywordResult.status === "fulfilled" ? keywordResult.value : null;
|
|
2472
|
+
const directory = directoryResult.status === "fulfilled" ? directoryResult.value : null;
|
|
2473
|
+
if (keyword === null && directory === null) {
|
|
2474
|
+
const reasons = [keywordResult, directoryResult].map((result) => result.status === "rejected" ? result.reason instanceof Error ? result.reason.message : String(result.reason) : "").filter(Boolean);
|
|
2475
|
+
throw new Error(`DSH Registry search failed: ${reasons.join("; ")}`);
|
|
2476
|
+
}
|
|
2477
|
+
if (keyword !== null && directory !== null && keyword.metadata.snapshotId !== directory.metadata.snapshotId) throw new Error("DSH Registry keyword and directory responses reference different snapshots.");
|
|
2478
|
+
return mergeRegistryRankings(text, keyword, directory, outputLimit);
|
|
2010
2479
|
}
|
|
2011
2480
|
};
|
|
2012
2481
|
}
|