@lmzhen/dsh-evolution-skill-discovery 0.0.0 → 0.30.40
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/README.md +16 -2
- package/lib/index.js +2457 -0
- package/lib/types/adapter.d.ts +48 -0
- package/lib/types/assignment-queue.d.ts +24 -0
- package/lib/types/assignment-sidecar.d.ts +30 -0
- package/lib/types/assignment.d.ts +131 -0
- package/lib/types/awareness.d.ts +6 -0
- package/lib/types/domain-bootstrap.d.ts +14 -0
- package/lib/types/domain-description.d.ts +19 -0
- package/lib/types/domain-generation-prompt.d.ts +14 -0
- package/lib/types/domain-generation-service.d.ts +23 -0
- package/lib/types/domain-generation.d.ts +48 -0
- package/lib/types/domain-map-store.d.ts +39 -0
- package/lib/types/domain-map.d.ts +52 -0
- package/lib/types/domain-metrics.d.ts +41 -0
- package/lib/types/domain-policy.d.ts +34 -0
- package/lib/types/domain-projection.d.ts +43 -0
- package/lib/types/domain-quality.d.ts +17 -0
- package/lib/types/domain-view.d.ts +15 -0
- package/lib/types/features.d.ts +11 -0
- package/lib/types/identity.d.ts +67 -0
- package/lib/types/index.d.ts +74 -0
- package/lib/types/maintenance.d.ts +62 -0
- package/lib/types/plugin.d.ts +37 -0
- package/lib/types/projection.d.ts +58 -0
- package/lib/types/runtime.d.ts +42 -0
- package/lib/types/search-judge.d.ts +26 -0
- package/lib/types/search.d.ts +84 -0
- package/lib/types/security.d.ts +14 -0
- package/lib/types/session-projection.d.ts +19 -0
- package/lib/types/session-store.d.ts +22 -0
- package/lib/types/shadow-evidence.d.ts +22 -0
- package/lib/types/stages.d.ts +6 -0
- package/lib/types/tool.d.ts +26 -0
- package/lib/types/ui.d.ts +69 -0
- package/lib/types/worker.d.ts +14 -0
- package/package.json +54 -3
package/lib/index.js
ADDED
|
@@ -0,0 +1,2457 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
4
|
+
import { SessionSeq } from "@deepseek-ai/dsh-session";
|
|
5
|
+
import MiniSearch from "minisearch";
|
|
6
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
7
|
+
import { authorityFrom, evolutionIoAdapter, newSkillLibrary, parseFrontmatter } from "@lmzhen/dsh-evolution-core";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
//#region lib/types/identity.js
|
|
10
|
+
function sha256(value) {
|
|
11
|
+
return createHash("sha256").update(value).digest("hex");
|
|
12
|
+
}
|
|
13
|
+
function canonical(value) {
|
|
14
|
+
if (value instanceof Set) return { $set: [...value].map(canonical).sort((left, right) => JSON.stringify(left) < JSON.stringify(right) ? -1 : JSON.stringify(left) > JSON.stringify(right) ? 1 : 0) };
|
|
15
|
+
if (value instanceof Map) return { $map: [...value.entries()].map(([key, entry]) => [canonical(key), canonical(entry)]).sort((left, right) => JSON.stringify(left) < JSON.stringify(right) ? -1 : JSON.stringify(left) > JSON.stringify(right) ? 1 : 0) };
|
|
16
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
17
|
+
if (typeof value === "object" && value !== null) return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, entry]) => [key, canonical(entry)]));
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function digest$1(value) {
|
|
21
|
+
return sha256(JSON.stringify(canonical(value)));
|
|
22
|
+
}
|
|
23
|
+
/** @returns stable key for the winning provider/source/name tuple. */
|
|
24
|
+
function skillKeyOf(summary) {
|
|
25
|
+
return JSON.stringify([
|
|
26
|
+
summary.provider,
|
|
27
|
+
summary.source,
|
|
28
|
+
summary.name
|
|
29
|
+
]);
|
|
30
|
+
}
|
|
31
|
+
/** @returns stable hash of the canonical cwd and optional scope key. */
|
|
32
|
+
function viewKeyOf(input) {
|
|
33
|
+
return digest$1([resolve(input.cwd), input.scopeKey ?? null]);
|
|
34
|
+
}
|
|
35
|
+
/** @returns hash of the three fields used by discovery/retrieval. */
|
|
36
|
+
function threeFieldHashOf(input) {
|
|
37
|
+
return digest$1([
|
|
38
|
+
input.name,
|
|
39
|
+
input.description,
|
|
40
|
+
input.whenToUse ?? null
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
43
|
+
/** @returns normalized row for one skill summary. */
|
|
44
|
+
function skillIndexRowOf(summary, managed, protectedSkill = false, pinned = false) {
|
|
45
|
+
return {
|
|
46
|
+
skillKey: skillKeyOf(summary),
|
|
47
|
+
name: summary.name,
|
|
48
|
+
description: summary.description,
|
|
49
|
+
...summary.whenToUse === void 0 ? {} : { whenToUse: summary.whenToUse },
|
|
50
|
+
provider: summary.provider,
|
|
51
|
+
source: summary.source,
|
|
52
|
+
managed,
|
|
53
|
+
modelInvocable: summary.invocation.modelInvocable,
|
|
54
|
+
protected: protectedSkill,
|
|
55
|
+
pinned,
|
|
56
|
+
threeFieldHash: threeFieldHashOf(summary)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** @returns digest of the sorted rows plus completeness. */
|
|
60
|
+
function catalogRevisionOf(rows, complete) {
|
|
61
|
+
return digest$1([complete, rows.map((row) => [
|
|
62
|
+
row.skillKey,
|
|
63
|
+
row.name,
|
|
64
|
+
row.description,
|
|
65
|
+
row.whenToUse ?? null,
|
|
66
|
+
row.provider,
|
|
67
|
+
row.source,
|
|
68
|
+
row.managed,
|
|
69
|
+
row.modelInvocable,
|
|
70
|
+
row.protected,
|
|
71
|
+
row.pinned,
|
|
72
|
+
row.threeFieldHash
|
|
73
|
+
])]);
|
|
74
|
+
}
|
|
75
|
+
/** @returns digest of a concrete per-view snapshot. */
|
|
76
|
+
function viewCatalogDigestOf(viewKey, rows, complete) {
|
|
77
|
+
return digest$1([viewKey, catalogRevisionOf(rows, complete)]);
|
|
78
|
+
}
|
|
79
|
+
/** @returns digest of effective policy plus its revisions. */
|
|
80
|
+
function policyDigestOf(input) {
|
|
81
|
+
return digest$1([
|
|
82
|
+
input.globalPolicyRevision,
|
|
83
|
+
input.workspaceOverlayRevision ?? null,
|
|
84
|
+
input.scopeOverlayRevision ?? null,
|
|
85
|
+
input.effectivePolicy
|
|
86
|
+
]);
|
|
87
|
+
}
|
|
88
|
+
/** @returns view-cache identity for one projection. */
|
|
89
|
+
function projectionDigestOf(input) {
|
|
90
|
+
return digest$1([
|
|
91
|
+
input.viewKey,
|
|
92
|
+
input.mapVersion,
|
|
93
|
+
input.policyDigest,
|
|
94
|
+
input.viewCatalogDigest,
|
|
95
|
+
input.adapterVersion,
|
|
96
|
+
input.projectionSchemaVersion,
|
|
97
|
+
input.discoveryMode
|
|
98
|
+
]);
|
|
99
|
+
}
|
|
100
|
+
/** @returns view-cache identity for one search index. */
|
|
101
|
+
function searchIndexKeyOf(input) {
|
|
102
|
+
return digest$1([
|
|
103
|
+
input.viewKey,
|
|
104
|
+
input.viewCatalogDigest,
|
|
105
|
+
input.policyDigest,
|
|
106
|
+
input.tokenizerVersion,
|
|
107
|
+
input.indexSchemaVersion,
|
|
108
|
+
input.discoveryMode
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region lib/types/security.js
|
|
113
|
+
/**
|
|
114
|
+
* Treat description text as data: strip control characters, escape HTML
|
|
115
|
+
* delimiters, and cap length. It never turns data into instructions.
|
|
116
|
+
* @param value - untrusted domain/skill text.
|
|
117
|
+
* @param limit - maximum characters retained.
|
|
118
|
+
* @returns escaped text plus truncation flag.
|
|
119
|
+
*/
|
|
120
|
+
function sanitizeUntrustedText(value, limit = 2e3) {
|
|
121
|
+
const escaped = value.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
122
|
+
return escaped.length <= limit ? {
|
|
123
|
+
text: escaped,
|
|
124
|
+
truncated: false
|
|
125
|
+
} : {
|
|
126
|
+
text: escaped.slice(0, limit),
|
|
127
|
+
truncated: true
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region lib/types/projection.js
|
|
132
|
+
function escapeData$1(value) {
|
|
133
|
+
return sanitizeUntrustedText(value, 2e3).text;
|
|
134
|
+
}
|
|
135
|
+
function renderCards(cards) {
|
|
136
|
+
if (cards.length === 0) return "";
|
|
137
|
+
return [
|
|
138
|
+
"<system-reminder>",
|
|
139
|
+
"Domain cards summarize where skills cluster; they support awareness, not retrieval. skill_search matches task text to skill summaries, skill(name) loads an exact skill, and the full catalog remains the fallback.",
|
|
140
|
+
"<skill_domains>",
|
|
141
|
+
...cards.map((card) => [
|
|
142
|
+
"- ",
|
|
143
|
+
escapeData$1(card.id),
|
|
144
|
+
" ",
|
|
145
|
+
escapeData$1(card.title),
|
|
146
|
+
": ",
|
|
147
|
+
escapeData$1(card.purpose),
|
|
148
|
+
" triggers=",
|
|
149
|
+
card.triggers.map(escapeData$1).join(","),
|
|
150
|
+
" | managed=",
|
|
151
|
+
String(card.managedModelInvocableCount),
|
|
152
|
+
" external=",
|
|
153
|
+
String(card.externalCatalogCount),
|
|
154
|
+
" pending=",
|
|
155
|
+
String(card.pendingCount),
|
|
156
|
+
" state=",
|
|
157
|
+
card.state,
|
|
158
|
+
" descriptionStatus=",
|
|
159
|
+
card.descriptionStatus,
|
|
160
|
+
" asOf=",
|
|
161
|
+
escapeData$1(card.asOf),
|
|
162
|
+
" mapVersion=",
|
|
163
|
+
escapeData$1(card.mapVersion),
|
|
164
|
+
" mapStale=",
|
|
165
|
+
String(card.mapStale)
|
|
166
|
+
].join("")),
|
|
167
|
+
"</skill_domains>",
|
|
168
|
+
"</system-reminder>"
|
|
169
|
+
].join("\n");
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Pure catalog projection: hide only active managed entries, preserve every
|
|
173
|
+
* other entry, and render L1 cards. Incomplete input fails open.
|
|
174
|
+
* @param input - view rows, active managed keys, cards, and identity inputs.
|
|
175
|
+
* @returns projected entries/context/hidden names and digest.
|
|
176
|
+
*/
|
|
177
|
+
function projectCatalog(input) {
|
|
178
|
+
if (!input.complete) return {
|
|
179
|
+
catalogEntries: input.entries,
|
|
180
|
+
context: "",
|
|
181
|
+
hiddenNames: [],
|
|
182
|
+
scope: input.scope,
|
|
183
|
+
digest: projectionDigestOf({
|
|
184
|
+
viewKey: input.viewKey,
|
|
185
|
+
mapVersion: input.mapVersion,
|
|
186
|
+
policyDigest: input.policyDigest,
|
|
187
|
+
viewCatalogDigest: input.viewCatalogDigest,
|
|
188
|
+
adapterVersion: input.adapterVersion,
|
|
189
|
+
projectionSchemaVersion: input.projectionSchemaVersion,
|
|
190
|
+
discoveryMode: input.discoveryMode
|
|
191
|
+
}),
|
|
192
|
+
complete: false
|
|
193
|
+
};
|
|
194
|
+
const activeByName = /* @__PURE__ */ new Map();
|
|
195
|
+
for (const row of input.rows) if (row.managed && row.modelInvocable && !row.protected && input.activeManagedSkillKeys.has(row.skillKey)) activeByName.set(row.name, row);
|
|
196
|
+
const hiddenNames = input.entries.filter((entry) => activeByName.has(entry.name)).map((entry) => entry.name).sort();
|
|
197
|
+
const hidden = new Set(hiddenNames);
|
|
198
|
+
return {
|
|
199
|
+
catalogEntries: input.entries.filter((entry) => !hidden.has(entry.name)),
|
|
200
|
+
context: renderCards(input.cards),
|
|
201
|
+
hiddenNames,
|
|
202
|
+
scope: input.scope,
|
|
203
|
+
digest: projectionDigestOf({
|
|
204
|
+
viewKey: input.viewKey,
|
|
205
|
+
mapVersion: input.mapVersion,
|
|
206
|
+
policyDigest: input.policyDigest,
|
|
207
|
+
viewCatalogDigest: input.viewCatalogDigest,
|
|
208
|
+
adapterVersion: input.adapterVersion,
|
|
209
|
+
projectionSchemaVersion: input.projectionSchemaVersion,
|
|
210
|
+
discoveryMode: input.discoveryMode
|
|
211
|
+
}),
|
|
212
|
+
complete: true
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region lib/types/adapter.js
|
|
217
|
+
/** @returns true for the platform catalog message. */
|
|
218
|
+
function isCatalogMessage(message) {
|
|
219
|
+
return message.source.kind === "skill-catalog";
|
|
220
|
+
}
|
|
221
|
+
function escapeData(value) {
|
|
222
|
+
return sanitizeUntrustedText(value, 2e3).text;
|
|
223
|
+
}
|
|
224
|
+
function renderCatalog(entries) {
|
|
225
|
+
if (entries.length === 0) return [];
|
|
226
|
+
return [
|
|
227
|
+
"<system-reminder>",
|
|
228
|
+
"A skill is a reusable set of task-specific instructions. The following skills are available in this session:",
|
|
229
|
+
"<available_skills>",
|
|
230
|
+
...entries.map((entry) => "- " + escapeData(entry.name) + ": " + escapeData(entry.description)),
|
|
231
|
+
"</available_skills>",
|
|
232
|
+
"Call the skill tool with the exact name before acting.",
|
|
233
|
+
"</system-reminder>"
|
|
234
|
+
];
|
|
235
|
+
}
|
|
236
|
+
/** Build the plugin-owned replacement message from a complete projection. */
|
|
237
|
+
function projectionPresent(agent, digest) {
|
|
238
|
+
try {
|
|
239
|
+
const visible = new Set(agent.session.surface.nodes);
|
|
240
|
+
for (let index = agent.session.seq - 1; index >= 0; index -= 1) {
|
|
241
|
+
const event = agent.session.eventAt(SessionSeq(index));
|
|
242
|
+
if (event === void 0) return false;
|
|
243
|
+
if (event.type !== "user/message") continue;
|
|
244
|
+
const data = event.data;
|
|
245
|
+
if (data?.source?.kind !== "evolution-skill-projection" || data.source.digest !== digest) continue;
|
|
246
|
+
if (visible.has(event.seq)) return true;
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
function renderProjection(result) {
|
|
254
|
+
const text = [...renderCatalog(result.catalogEntries), ...result.context === "" ? [] : [result.context]].join("\n");
|
|
255
|
+
if (text === "") return void 0;
|
|
256
|
+
return createUserMessage({
|
|
257
|
+
content: [{
|
|
258
|
+
type: "text",
|
|
259
|
+
text
|
|
260
|
+
}],
|
|
261
|
+
source: {
|
|
262
|
+
kind: "evolution-skill-projection",
|
|
263
|
+
entries: result.catalogEntries,
|
|
264
|
+
hiddenNames: result.hiddenNames,
|
|
265
|
+
digest: result.digest
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Atomically replace one platform catalog message with the plugin projection.
|
|
271
|
+
* @param decision - the waterfall decision; only enter decisions are changed.
|
|
272
|
+
* @param result - a complete projection; incomplete results are ignored.
|
|
273
|
+
* @returns a new decision, or the original when no safe replacement exists.
|
|
274
|
+
*/
|
|
275
|
+
function applyCatalogProjection(decision, result) {
|
|
276
|
+
if (decision.kind !== "enter" || !result.complete) return decision;
|
|
277
|
+
const index = decision.messages.findIndex(isCatalogMessage);
|
|
278
|
+
if (index < 0) return decision;
|
|
279
|
+
const replacement = renderProjection(result);
|
|
280
|
+
const messages = [...decision.messages];
|
|
281
|
+
if (replacement === void 0) messages.splice(index, 1);
|
|
282
|
+
else messages[index] = replacement;
|
|
283
|
+
return {
|
|
284
|
+
...decision,
|
|
285
|
+
messages
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Install the thin pre-step adapter. The listener is outer; it always calls
|
|
290
|
+
* next, only touches the platform catalog message, and fails open.
|
|
291
|
+
* @param ctx - plugin context.
|
|
292
|
+
* @param provider - projection provider backed by SkillIndex/DomainMap.
|
|
293
|
+
* @returns disposer from ctx.on.
|
|
294
|
+
*/
|
|
295
|
+
function installCatalogProjection(ctx, provider, digestStore) {
|
|
296
|
+
const projectedDigest = /* @__PURE__ */ new Map();
|
|
297
|
+
return ctx.on("agent/pre-step", async ({ agent }, next) => {
|
|
298
|
+
const decision = await next();
|
|
299
|
+
if (decision.kind !== "enter") return decision;
|
|
300
|
+
const catalog = decision.messages.find(isCatalogMessage);
|
|
301
|
+
if (catalog === void 0) return decision;
|
|
302
|
+
try {
|
|
303
|
+
if (provider.shouldProject !== void 0 && !await provider.shouldProject(agent)) return decision;
|
|
304
|
+
const result = await provider.project({
|
|
305
|
+
agent,
|
|
306
|
+
catalogEntries: catalog.source.entries
|
|
307
|
+
});
|
|
308
|
+
if (!result.complete) return decision;
|
|
309
|
+
const agentKey = String(agent.id);
|
|
310
|
+
if ((projectedDigest.get(agentKey) ?? await digestStore?.get(agentKey)) === result.digest && projectionPresent(agent, result.digest)) return {
|
|
311
|
+
...decision,
|
|
312
|
+
messages: decision.messages.filter((message) => !isCatalogMessage(message))
|
|
313
|
+
};
|
|
314
|
+
const nextDecision = applyCatalogProjection(decision, result);
|
|
315
|
+
if (nextDecision !== decision) {
|
|
316
|
+
projectedDigest.set(agentKey, result.digest);
|
|
317
|
+
await digestStore?.set(agentKey, result.digest);
|
|
318
|
+
}
|
|
319
|
+
return nextDecision;
|
|
320
|
+
} catch {
|
|
321
|
+
return decision;
|
|
322
|
+
}
|
|
323
|
+
}, { prepend: true });
|
|
324
|
+
}
|
|
325
|
+
//#endregion
|
|
326
|
+
//#region lib/types/features.js
|
|
327
|
+
const V0_DISCOVERY_FEATURES = Object.freeze({
|
|
328
|
+
projectionEnabled: false,
|
|
329
|
+
searchEnabled: false,
|
|
330
|
+
createClassificationEnabled: false,
|
|
331
|
+
discoveryMode: "off"
|
|
332
|
+
});
|
|
333
|
+
function canClassifyAtCreate(config, hasCreateDomainCards) {
|
|
334
|
+
return config.createClassificationEnabled && hasCreateDomainCards;
|
|
335
|
+
}
|
|
336
|
+
//#endregion
|
|
337
|
+
//#region lib/types/stages.js
|
|
338
|
+
/** Ordered implementation stages from PLAN-DISC-001. */
|
|
339
|
+
const DISCOVERY_STAGES = [
|
|
340
|
+
"G0-projection",
|
|
341
|
+
"G1-domain",
|
|
342
|
+
"G2-projection",
|
|
343
|
+
"G3-search",
|
|
344
|
+
"G4-assignment",
|
|
345
|
+
"G5-maintenance",
|
|
346
|
+
"G6-ui",
|
|
347
|
+
"G7-migration",
|
|
348
|
+
"G8-release"
|
|
349
|
+
];
|
|
350
|
+
function stageIndex(stage) {
|
|
351
|
+
return DISCOVERY_STAGES.indexOf(stage);
|
|
352
|
+
}
|
|
353
|
+
function mayAdvance(current, next) {
|
|
354
|
+
return stageIndex(next) === stageIndex(current) + 1;
|
|
355
|
+
}
|
|
356
|
+
//#endregion
|
|
357
|
+
//#region lib/types/search.js
|
|
358
|
+
/** Tokenizer version; changing the algorithm requires rebuilding indexes. */
|
|
359
|
+
const SKILL_SEARCH_TOKENIZER_VERSION = "cjk-bigram-v1";
|
|
360
|
+
/** Fields indexed by skill search; no body/path. */
|
|
361
|
+
const SKILL_SEARCH_FIELDS = [
|
|
362
|
+
"name",
|
|
363
|
+
"description",
|
|
364
|
+
"whenToUse"
|
|
365
|
+
];
|
|
366
|
+
/** Index schema version; bump when indexed fields/options change. */
|
|
367
|
+
const SKILL_SEARCH_INDEX_SCHEMA_VERSION = "skill-search-v1";
|
|
368
|
+
const CJK = String.raw`[[\p{scx=Han}\p{scx=Hira}\p{scx=Kana}\p{scx=Hang}]&&[\p{L}\p{M}\p{N}]]`;
|
|
369
|
+
const RUN = new RegExp(String.raw`${CJK}+|[[\p{L}\p{M}\p{N}]--${CJK}]+`, "gv");
|
|
370
|
+
const CJK_START = new RegExp(String.raw`^${CJK}`, "v");
|
|
371
|
+
/**
|
|
372
|
+
* Split NFKC text into CJK overlapping bigrams plus tail, and ASCII/other runs.
|
|
373
|
+
* Parity source: plugin-market/src/search.ts searchTerms.
|
|
374
|
+
* @param text - indexed or queried text.
|
|
375
|
+
* @returns terms in text order; callers lowercase.
|
|
376
|
+
*/
|
|
377
|
+
function searchTerms(text) {
|
|
378
|
+
return [...text.normalize("NFKC").matchAll(RUN)].flatMap(([run]) => {
|
|
379
|
+
if (!CJK_START.test(run)) return [run];
|
|
380
|
+
const chars = Array.from(run);
|
|
381
|
+
return [...chars.slice(1).map((char, position) => chars[position] + char), chars.at(-1)];
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
function clampLimit(limit) {
|
|
385
|
+
if (limit === void 0) return 3;
|
|
386
|
+
return Math.min(20, Math.max(1, Math.trunc(limit)));
|
|
387
|
+
}
|
|
388
|
+
function build(rows) {
|
|
389
|
+
const engine = new MiniSearch({
|
|
390
|
+
fields: [...SKILL_SEARCH_FIELDS],
|
|
391
|
+
storeFields: [...SKILL_SEARCH_FIELDS],
|
|
392
|
+
tokenize: searchTerms,
|
|
393
|
+
processTerm: (term) => term.toLowerCase(),
|
|
394
|
+
searchOptions: {
|
|
395
|
+
boost: {
|
|
396
|
+
name: 3,
|
|
397
|
+
whenToUse: 2
|
|
398
|
+
},
|
|
399
|
+
prefix: true,
|
|
400
|
+
combineWith: "AND",
|
|
401
|
+
fuzzy: (term) => term.length >= 4 && !CJK_START.test(term) ? .2 : false,
|
|
402
|
+
maxFuzzy: 2
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
const seen = /* @__PURE__ */ new Set();
|
|
406
|
+
const documents = [];
|
|
407
|
+
for (const row of rows) {
|
|
408
|
+
if (!row.modelInvocable || row.protected || row.pinned || seen.has(row.skillKey)) continue;
|
|
409
|
+
seen.add(row.skillKey);
|
|
410
|
+
documents.push({
|
|
411
|
+
id: row.skillKey,
|
|
412
|
+
name: row.name,
|
|
413
|
+
description: row.description,
|
|
414
|
+
whenToUse: row.whenToUse ?? ""
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
engine.addAll(documents);
|
|
418
|
+
return engine;
|
|
419
|
+
}
|
|
420
|
+
/** Per-view MiniSearch index; no global engine and no projection-only filtering. */
|
|
421
|
+
var SkillSearchIndex = class SkillSearchIndex {
|
|
422
|
+
engine;
|
|
423
|
+
staleState = false;
|
|
424
|
+
viewKey;
|
|
425
|
+
constructor(viewKey, engine) {
|
|
426
|
+
this.viewKey = viewKey;
|
|
427
|
+
this.engine = engine;
|
|
428
|
+
}
|
|
429
|
+
/** @returns an index over every modelInvocable row in this view. */
|
|
430
|
+
static fromRows(viewKey, rows) {
|
|
431
|
+
return new SkillSearchIndex(viewKey, build(rows));
|
|
432
|
+
}
|
|
433
|
+
/** Mark the current index stale before a rebuild finishes. */
|
|
434
|
+
markStale() {
|
|
435
|
+
this.staleState = true;
|
|
436
|
+
}
|
|
437
|
+
/** Rebuild from this view's full rows; a failed rebuild keeps the old engine stale. */
|
|
438
|
+
async rebuild(rows) {
|
|
439
|
+
this.staleState = true;
|
|
440
|
+
try {
|
|
441
|
+
this.engine = build(rows);
|
|
442
|
+
this.staleState = false;
|
|
443
|
+
return true;
|
|
444
|
+
} catch {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/** @returns hits, truncation, and whether the index is stale. */
|
|
449
|
+
async search(query, options) {
|
|
450
|
+
const limit = clampLimit(query.limit);
|
|
451
|
+
const terms = searchTerms(query.query);
|
|
452
|
+
if (terms.length === 0) return {
|
|
453
|
+
results: [],
|
|
454
|
+
truncated: false,
|
|
455
|
+
stale: this.staleState
|
|
456
|
+
};
|
|
457
|
+
let fallback = false;
|
|
458
|
+
let hits = this.engine.search(query.query, { combineWith: "AND" });
|
|
459
|
+
if (hits.length === 0 && terms.length > 1) {
|
|
460
|
+
const relaxed = this.engine.search(query.query, { combineWith: "OR" });
|
|
461
|
+
fallback = relaxed.length > 0;
|
|
462
|
+
hits = relaxed;
|
|
463
|
+
}
|
|
464
|
+
const allowed = options?.allowedSkillKeys;
|
|
465
|
+
const filtered = allowed === void 0 ? hits : hits.filter((hit) => allowed.has(String(hit.id)));
|
|
466
|
+
return {
|
|
467
|
+
results: filtered.slice(0, limit).map((hit) => {
|
|
468
|
+
const fields = new Set(Object.values(hit.match).flat());
|
|
469
|
+
return {
|
|
470
|
+
name: String(hit.name),
|
|
471
|
+
description: String(hit.description),
|
|
472
|
+
...hit.whenToUse === void 0 || hit.whenToUse === "" ? {} : { whenToUse: String(hit.whenToUse) },
|
|
473
|
+
matchedBy: fields.has("name") ? "name" : fields.has("whenToUse") ? "whenToUse" : "description"
|
|
474
|
+
};
|
|
475
|
+
}),
|
|
476
|
+
truncated: filtered.length > limit,
|
|
477
|
+
...fallback ? { fallback: true } : {},
|
|
478
|
+
stale: this.staleState
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
/** Per-view cache keyed by searchIndexKey; never shares one engine across views. */
|
|
483
|
+
var SkillSearchCache = class {
|
|
484
|
+
entries = /* @__PURE__ */ new Map();
|
|
485
|
+
/**
|
|
486
|
+
* Get or build one view index. A failed rebuild retains the previous engine
|
|
487
|
+
* and marks it stale; no previous engine means undefined.
|
|
488
|
+
* @param key - identity inputs for searchIndexKey.
|
|
489
|
+
* @param rows - current full rows for the view.
|
|
490
|
+
* @returns index plus stale state, or undefined when no index is available.
|
|
491
|
+
*/
|
|
492
|
+
async get(key, rows) {
|
|
493
|
+
const cacheKey = searchIndexKeyOf({
|
|
494
|
+
viewKey: key.viewKey,
|
|
495
|
+
viewCatalogDigest: key.viewCatalogDigest,
|
|
496
|
+
policyDigest: key.policyDigest,
|
|
497
|
+
tokenizerVersion: SKILL_SEARCH_TOKENIZER_VERSION,
|
|
498
|
+
indexSchemaVersion: SKILL_SEARCH_INDEX_SCHEMA_VERSION,
|
|
499
|
+
discoveryMode: key.discoveryMode
|
|
500
|
+
});
|
|
501
|
+
const existing = this.entries.get(cacheKey);
|
|
502
|
+
if (existing !== void 0) return {
|
|
503
|
+
index: existing.index,
|
|
504
|
+
stale: false
|
|
505
|
+
};
|
|
506
|
+
try {
|
|
507
|
+
const index = SkillSearchIndex.fromRows(key.viewKey, rows);
|
|
508
|
+
this.entries.set(cacheKey, {
|
|
509
|
+
key: cacheKey,
|
|
510
|
+
viewKey: key.viewKey,
|
|
511
|
+
index
|
|
512
|
+
});
|
|
513
|
+
return {
|
|
514
|
+
index,
|
|
515
|
+
stale: false
|
|
516
|
+
};
|
|
517
|
+
} catch {
|
|
518
|
+
const old = [...this.entries.values()].find((entry) => entry.viewKey === key.viewKey);
|
|
519
|
+
if (old === void 0) return void 0;
|
|
520
|
+
old.index.markStale();
|
|
521
|
+
return {
|
|
522
|
+
index: old.index,
|
|
523
|
+
stale: true
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/** Drop every index for one view. */
|
|
528
|
+
invalidate(viewKey) {
|
|
529
|
+
for (const [cacheKey, entry] of this.entries) if (entry.viewKey === viewKey) this.entries.delete(cacheKey);
|
|
530
|
+
}
|
|
531
|
+
/** Drop every cached view index. */
|
|
532
|
+
clear() {
|
|
533
|
+
this.entries.clear();
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
//#endregion
|
|
537
|
+
//#region lib/types/domain-map-store.js
|
|
538
|
+
/** File schema version supported by this store. */
|
|
539
|
+
const DOMAIN_MAP_SCHEMA_VERSION = 1;
|
|
540
|
+
/** Typed failure for an invalid persisted file. */
|
|
541
|
+
var DomainMapCorruptError = class extends Error {
|
|
542
|
+
code = "domain-map-corrupt";
|
|
543
|
+
constructor(message) {
|
|
544
|
+
super(message);
|
|
545
|
+
this.name = "DomainMapCorruptError";
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
/** Typed failure for a CAS mismatch. */
|
|
549
|
+
var DomainMapVersionConflict = class extends Error {
|
|
550
|
+
code = "domain-map-version-conflict";
|
|
551
|
+
expected;
|
|
552
|
+
actual;
|
|
553
|
+
constructor(expected, actual) {
|
|
554
|
+
super("domain map version conflict: expected " + expected + ", found " + actual);
|
|
555
|
+
this.expected = expected;
|
|
556
|
+
this.actual = actual;
|
|
557
|
+
this.name = "DomainMapVersionConflict";
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
/** Typed failure for an invalid write. */
|
|
561
|
+
var DomainMapValidationError = class extends Error {
|
|
562
|
+
code = "domain-map-invalid";
|
|
563
|
+
constructor(message) {
|
|
564
|
+
super(message);
|
|
565
|
+
this.name = "DomainMapValidationError";
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
function emptySnapshot() {
|
|
569
|
+
return {
|
|
570
|
+
mapVersion: "0",
|
|
571
|
+
domains: [],
|
|
572
|
+
edges: []
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
function isRecord(value) {
|
|
576
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
577
|
+
}
|
|
578
|
+
function assertNode(node) {
|
|
579
|
+
if (!isRecord(node) || typeof node.id !== "string" || node.id.length === 0) throw new DomainMapCorruptError("domain node id is missing");
|
|
580
|
+
if (typeof node.name !== "string" || typeof node.purpose !== "string" || typeof node.description !== "string") throw new DomainMapCorruptError("domain node text fields are missing");
|
|
581
|
+
if (!Array.isArray(node.inScope) || !Array.isArray(node.outOfScope) || !Array.isArray(node.triggers) || !Array.isArray(node.parentIds)) throw new DomainMapCorruptError("domain node arrays are invalid");
|
|
582
|
+
if (node.status !== "active" && node.status !== "provisional" && node.status !== "deprecated") throw new DomainMapCorruptError("domain node status is invalid");
|
|
583
|
+
}
|
|
584
|
+
function assertEdge(edge) {
|
|
585
|
+
if (!isRecord(edge) || typeof edge.skillKey !== "string" || typeof edge.domainId !== "string") throw new DomainMapCorruptError("skill edge identity is missing");
|
|
586
|
+
if (edge.role !== "primary" && edge.role !== "secondary") throw new DomainMapCorruptError("skill edge role is invalid");
|
|
587
|
+
if (edge.status !== "active" && edge.status !== "stale" && edge.status !== "pending") throw new DomainMapCorruptError("skill edge status is invalid");
|
|
588
|
+
if (typeof edge.confidence !== "number" || edge.confidence < 0 || edge.confidence > 1) throw new DomainMapCorruptError("skill edge confidence is invalid");
|
|
589
|
+
}
|
|
590
|
+
function validateWrite(next) {
|
|
591
|
+
if (next.mapVersion.length === 0) throw new DomainMapValidationError("mapVersion must be non-empty");
|
|
592
|
+
const ids = /* @__PURE__ */ new Set();
|
|
593
|
+
for (const node of next.domains) {
|
|
594
|
+
assertNode(node);
|
|
595
|
+
if (ids.has(node.id)) throw new DomainMapValidationError("duplicate domain id " + node.id);
|
|
596
|
+
ids.add(node.id);
|
|
597
|
+
}
|
|
598
|
+
for (const node of next.domains) for (const parent of node.parentIds) {
|
|
599
|
+
if (parent === node.id) throw new DomainMapValidationError("domain self-parent " + node.id);
|
|
600
|
+
if (!ids.has(parent)) throw new DomainMapValidationError("unknown parent domain " + parent);
|
|
601
|
+
}
|
|
602
|
+
const edgeKeys = /* @__PURE__ */ new Set();
|
|
603
|
+
const primaries = /* @__PURE__ */ new Map();
|
|
604
|
+
for (const edge of next.edges) {
|
|
605
|
+
assertEdge(edge);
|
|
606
|
+
if (!ids.has(edge.domainId)) throw new DomainMapValidationError("unknown edge domain " + edge.domainId);
|
|
607
|
+
const key = edge.skillKey + "\0" + edge.domainId;
|
|
608
|
+
if (edgeKeys.has(key)) throw new DomainMapValidationError("duplicate skill/domain edge");
|
|
609
|
+
edgeKeys.add(key);
|
|
610
|
+
if (edge.role === "primary") {
|
|
611
|
+
const count = (primaries.get(edge.skillKey) ?? 0) + 1;
|
|
612
|
+
if (count > 1) throw new DomainMapValidationError("more than one primary domain for " + edge.skillKey);
|
|
613
|
+
primaries.set(edge.skillKey, count);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* JSON-backed DomainMap owner. One file, schemaVersion, CAS mapVersion, and a
|
|
619
|
+
* best-effort .bak for rollback; no evolution-state tables or graph DB.
|
|
620
|
+
*/
|
|
621
|
+
var DomainMapFileStore = class {
|
|
622
|
+
queue = Promise.resolve();
|
|
623
|
+
io;
|
|
624
|
+
path;
|
|
625
|
+
now;
|
|
626
|
+
constructor(io, path, now = Date.now) {
|
|
627
|
+
this.io = io;
|
|
628
|
+
this.path = path;
|
|
629
|
+
this.now = now;
|
|
630
|
+
}
|
|
631
|
+
get backupPath() {
|
|
632
|
+
return this.path + ".bak";
|
|
633
|
+
}
|
|
634
|
+
parse(content) {
|
|
635
|
+
if (content === null) return emptySnapshot();
|
|
636
|
+
let value;
|
|
637
|
+
try {
|
|
638
|
+
value = JSON.parse(content);
|
|
639
|
+
} catch {
|
|
640
|
+
throw new DomainMapCorruptError("domain map is not valid JSON");
|
|
641
|
+
}
|
|
642
|
+
if (!isRecord(value) || value.schemaVersion !== 1 || typeof value.mapVersion !== "string" || !Array.isArray(value.domains) || !Array.isArray(value.edges)) throw new DomainMapCorruptError("domain map schema is unsupported");
|
|
643
|
+
for (const node of value.domains) assertNode(node);
|
|
644
|
+
for (const edge of value.edges) assertEdge(edge);
|
|
645
|
+
return {
|
|
646
|
+
mapVersion: value.mapVersion,
|
|
647
|
+
domains: value.domains,
|
|
648
|
+
edges: value.edges
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
async read() {
|
|
652
|
+
return this.parse(await this.io.readText(this.path));
|
|
653
|
+
}
|
|
654
|
+
async commitNow(current, expected, next) {
|
|
655
|
+
const snapshot = this.parse(current);
|
|
656
|
+
if (snapshot.mapVersion !== expected) throw new DomainMapVersionConflict(expected, snapshot.mapVersion);
|
|
657
|
+
if (next.mapVersion === expected) throw new DomainMapValidationError("mapVersion must change on commit");
|
|
658
|
+
validateWrite(next);
|
|
659
|
+
if (current !== null) await this.io.writeText(this.backupPath, current);
|
|
660
|
+
const file = {
|
|
661
|
+
schemaVersion: 1,
|
|
662
|
+
mapVersion: next.mapVersion,
|
|
663
|
+
updatedAt: this.now(),
|
|
664
|
+
domains: next.domains,
|
|
665
|
+
edges: next.edges
|
|
666
|
+
};
|
|
667
|
+
return JSON.stringify(file);
|
|
668
|
+
}
|
|
669
|
+
async commit(expectedMapVersion, next) {
|
|
670
|
+
const run = async () => {
|
|
671
|
+
if (this.io.transact !== void 0) await this.io.transact(this.path, (current) => this.commitNow(current, expectedMapVersion, next));
|
|
672
|
+
else {
|
|
673
|
+
const current = await this.io.readText(this.path);
|
|
674
|
+
await this.io.writeText(this.path, await this.commitNow(current, expectedMapVersion, next));
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
const pending = this.queue.then(run, run);
|
|
678
|
+
this.queue = pending.catch(() => {});
|
|
679
|
+
await pending;
|
|
680
|
+
return this.read();
|
|
681
|
+
}
|
|
682
|
+
async rollback() {
|
|
683
|
+
const backup = await this.io.readText(this.backupPath);
|
|
684
|
+
if (backup === null) throw new DomainMapCorruptError("domain map backup is missing");
|
|
685
|
+
const snapshot = this.parse(backup);
|
|
686
|
+
await this.io.writeText(this.path, backup);
|
|
687
|
+
return snapshot;
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
//#endregion
|
|
691
|
+
//#region lib/types/domain-view.js
|
|
692
|
+
/**
|
|
693
|
+
* Count visible skills by active domain membership.
|
|
694
|
+
* @param snapshot - global domain facts.
|
|
695
|
+
* @param visibleSkillKeys - skill keys visible in this view.
|
|
696
|
+
* @returns per-domain member counts plus the unclassified virtual count.
|
|
697
|
+
*/
|
|
698
|
+
function domainViewCounts(snapshot, visibleSkillKeys) {
|
|
699
|
+
const byDomain = /* @__PURE__ */ new Map();
|
|
700
|
+
const classified = /* @__PURE__ */ new Set();
|
|
701
|
+
for (const edge of snapshot.edges) {
|
|
702
|
+
if (edge.status !== "active" || !visibleSkillKeys.has(edge.skillKey)) continue;
|
|
703
|
+
const set = byDomain.get(edge.domainId) ?? /* @__PURE__ */ new Set();
|
|
704
|
+
set.add(edge.skillKey);
|
|
705
|
+
byDomain.set(edge.domainId, set);
|
|
706
|
+
if (edge.role === "primary") classified.add(edge.skillKey);
|
|
707
|
+
}
|
|
708
|
+
const counts = /* @__PURE__ */ new Map();
|
|
709
|
+
for (const [domainId, members] of byDomain) counts.set(domainId, members.size);
|
|
710
|
+
return {
|
|
711
|
+
byDomain: counts,
|
|
712
|
+
unclassified: [...visibleSkillKeys].filter((key) => !classified.has(key)).length
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
//#endregion
|
|
716
|
+
//#region lib/types/domain-policy.js
|
|
717
|
+
/** Invalid policy layer (e.g. attempts to edit taxonomy). */
|
|
718
|
+
var DomainPolicyValidationError = class extends Error {
|
|
719
|
+
code = "domain-policy-invalid";
|
|
720
|
+
constructor(message) {
|
|
721
|
+
super(message);
|
|
722
|
+
this.name = "DomainPolicyValidationError";
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
const ALLOWED = new Set([
|
|
726
|
+
"mode",
|
|
727
|
+
"pinDomains",
|
|
728
|
+
"disableDomains",
|
|
729
|
+
"revision"
|
|
730
|
+
]);
|
|
731
|
+
const MODES$1 = new Set([
|
|
732
|
+
"off",
|
|
733
|
+
"auto",
|
|
734
|
+
"on"
|
|
735
|
+
]);
|
|
736
|
+
function checkLayer(layer) {
|
|
737
|
+
for (const key of Object.keys(layer)) if (!ALLOWED.has(key)) throw new DomainPolicyValidationError("unsupported policy field " + key);
|
|
738
|
+
if (layer.mode !== void 0 && !MODES$1.has(layer.mode)) throw new DomainPolicyValidationError("unsupported policy mode");
|
|
739
|
+
}
|
|
740
|
+
function knownOnly(values, known, degraded) {
|
|
741
|
+
const result = /* @__PURE__ */ new Set();
|
|
742
|
+
for (const value of values ?? []) {
|
|
743
|
+
if (known !== void 0 && !known.has(value)) {
|
|
744
|
+
degraded.add(value);
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
result.add(value);
|
|
748
|
+
}
|
|
749
|
+
return result;
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Merge global -> workspace -> scope policy. Disable wins over pin; unknown
|
|
753
|
+
* domains are ignored and reported as degraded.
|
|
754
|
+
* @param global - mandatory global layer.
|
|
755
|
+
* @param workspace - optional workspace overlay.
|
|
756
|
+
* @param scope - optional scope overlay.
|
|
757
|
+
* @param knownDomainIds - optional known ids for validation.
|
|
758
|
+
* @returns immutable effective policy.
|
|
759
|
+
*/
|
|
760
|
+
function mergeDomainPolicy(global, workspace, scope, knownDomainIds) {
|
|
761
|
+
checkLayer(global);
|
|
762
|
+
if (workspace) checkLayer(workspace);
|
|
763
|
+
if (scope) checkLayer(scope);
|
|
764
|
+
const degraded = /* @__PURE__ */ new Set();
|
|
765
|
+
const blocked = new Set([
|
|
766
|
+
...knownOnly(global.disableDomains, knownDomainIds, degraded),
|
|
767
|
+
...knownOnly(workspace?.disableDomains, knownDomainIds, degraded),
|
|
768
|
+
...knownOnly(scope?.disableDomains, knownDomainIds, degraded)
|
|
769
|
+
]);
|
|
770
|
+
const pinned = new Set([
|
|
771
|
+
...knownOnly(global.pinDomains, knownDomainIds, degraded),
|
|
772
|
+
...knownOnly(workspace?.pinDomains, knownDomainIds, degraded),
|
|
773
|
+
...knownOnly(scope?.pinDomains, knownDomainIds, degraded)
|
|
774
|
+
]);
|
|
775
|
+
for (const id of blocked) pinned.delete(id);
|
|
776
|
+
return {
|
|
777
|
+
mode: scope?.mode ?? workspace?.mode ?? global.mode ?? "off",
|
|
778
|
+
pinned,
|
|
779
|
+
disabled: blocked,
|
|
780
|
+
degraded: [...degraded].sort(),
|
|
781
|
+
globalRevision: global.revision,
|
|
782
|
+
workspaceRevision: workspace?.revision,
|
|
783
|
+
scopeRevision: scope?.revision
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
//#endregion
|
|
787
|
+
//#region lib/types/domain-generation.js
|
|
788
|
+
/** @returns stable id derived from title, not the title itself. */
|
|
789
|
+
function mintDomainId(title) {
|
|
790
|
+
return "domain-" + createHash("sha256").update(title).digest("hex").slice(0, 12);
|
|
791
|
+
}
|
|
792
|
+
/** @returns direct for <=100 skills; prechunk-eligible above 100. */
|
|
793
|
+
function domainGenerationMode(skillCount) {
|
|
794
|
+
if (!Number.isSafeInteger(skillCount) || skillCount < 0) throw new RangeError("skillCount must be a non-negative safe integer");
|
|
795
|
+
return skillCount <= 100 ? "direct" : "prechunk-eligible";
|
|
796
|
+
}
|
|
797
|
+
function nonEmpty(value) {
|
|
798
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
799
|
+
}
|
|
800
|
+
function nonEmptyList(value) {
|
|
801
|
+
return Array.isArray(value) && value.length > 0 && value.every(nonEmpty);
|
|
802
|
+
}
|
|
803
|
+
function forbiddenKeys(value) {
|
|
804
|
+
return [
|
|
805
|
+
"primaryParent",
|
|
806
|
+
"primaryParentId",
|
|
807
|
+
"parent",
|
|
808
|
+
"embedding",
|
|
809
|
+
"embeddingVector",
|
|
810
|
+
"taxonomyMethod"
|
|
811
|
+
].filter((key) => key in value);
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Validate an initial generation proposal and mint ids. Does not call an LLM
|
|
815
|
+
* and does not read skill bodies; embedding taxonomy is rejected.
|
|
816
|
+
* @param input - skills the proposal was generated from.
|
|
817
|
+
* @param proposal - LLM output.
|
|
818
|
+
* @returns normalized domains or stable errors.
|
|
819
|
+
*/
|
|
820
|
+
function validateDomainGeneration(input, proposal) {
|
|
821
|
+
const errors = [];
|
|
822
|
+
if (proposal.domains.length < 8 || proposal.domains.length > 12) errors.push("domain count must be 8-12");
|
|
823
|
+
const known = new Set(input.skills.map((skill) => skill.name));
|
|
824
|
+
const ids = /* @__PURE__ */ new Set();
|
|
825
|
+
const titles = /* @__PURE__ */ new Set();
|
|
826
|
+
const domains = [];
|
|
827
|
+
for (const candidate of proposal.domains) {
|
|
828
|
+
const bad = forbiddenKeys(candidate);
|
|
829
|
+
if (bad.length > 0) errors.push("forbidden fields: " + bad.join(","));
|
|
830
|
+
if (!nonEmpty(candidate.title) || !nonEmpty(candidate.purpose)) errors.push("domain title/purpose is empty");
|
|
831
|
+
if (!nonEmptyList(candidate.inScope) || !nonEmptyList(candidate.outOfScope) || !nonEmptyList(candidate.triggers)) errors.push("domain scope/triggers are empty");
|
|
832
|
+
if (!Array.isArray(candidate.evidenceSkillKeys) || candidate.evidenceSkillKeys.length === 0) errors.push("domain evidence is empty");
|
|
833
|
+
if (candidate.evidenceSkillKeys?.some((key) => !known.has(key)) === true) errors.push("domain evidence references unknown skill");
|
|
834
|
+
const id = mintDomainId(candidate.title);
|
|
835
|
+
if (ids.has(id)) errors.push("duplicate minted domain id");
|
|
836
|
+
if (titles.has(candidate.title)) errors.push("duplicate domain title");
|
|
837
|
+
ids.add(id);
|
|
838
|
+
titles.add(candidate.title);
|
|
839
|
+
domains.push({
|
|
840
|
+
...candidate,
|
|
841
|
+
id
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
if ("embedding" in proposal || "taxonomyMethod" in proposal) errors.push("embedding taxonomy is not allowed");
|
|
845
|
+
return errors.length === 0 ? {
|
|
846
|
+
ok: true,
|
|
847
|
+
domains
|
|
848
|
+
} : {
|
|
849
|
+
ok: false,
|
|
850
|
+
errors
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
//#endregion
|
|
854
|
+
//#region lib/types/domain-description.js
|
|
855
|
+
/** @returns true only for structural/definition events; normal skill add does not rewrite. */
|
|
856
|
+
function shouldRegenerateDescription(event) {
|
|
857
|
+
return event !== "skill-add";
|
|
858
|
+
}
|
|
859
|
+
/** @returns true when the existing description may still serve. */
|
|
860
|
+
function descriptionIsServable(status) {
|
|
861
|
+
return status === "fresh" || status === "stale";
|
|
862
|
+
}
|
|
863
|
+
/** @returns hash of child ids/description versions, never member skill names. */
|
|
864
|
+
function descriptionHashOf(domain, children) {
|
|
865
|
+
const childRefs = children.map((child) => JSON.stringify([child.id, child.descriptionVersion])).sort();
|
|
866
|
+
return createHash("sha256").update(JSON.stringify([
|
|
867
|
+
domain.id,
|
|
868
|
+
domain.descriptionVersion,
|
|
869
|
+
childRefs
|
|
870
|
+
])).digest("hex");
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Render a domain description from definition fields only. It never appends
|
|
874
|
+
* evidence/member skill names and is capped to 600 characters.
|
|
875
|
+
* @param domain - domain definition.
|
|
876
|
+
* @returns description text.
|
|
877
|
+
*/
|
|
878
|
+
function renderDomainDescription(domain) {
|
|
879
|
+
return [
|
|
880
|
+
domain.name + ":" + domain.purpose,
|
|
881
|
+
"适用:" + domain.inScope.join(";"),
|
|
882
|
+
"不适用:" + domain.outOfScope.join(";"),
|
|
883
|
+
"触发:" + domain.triggers.join(";")
|
|
884
|
+
].join("\n").slice(0, 600);
|
|
885
|
+
}
|
|
886
|
+
//#endregion
|
|
887
|
+
//#region lib/types/domain-quality.js
|
|
888
|
+
function tokens(values) {
|
|
889
|
+
return new Set(values.join(" ").toLowerCase().split(/[^\p{Letter}\p{Number}]+/u).filter((token) => token.length > 0));
|
|
890
|
+
}
|
|
891
|
+
function jaccard(left, right) {
|
|
892
|
+
if (left.size === 0 && right.size === 0) return 1;
|
|
893
|
+
let overlap = 0;
|
|
894
|
+
for (const token of left) if (right.has(token)) overlap += 1;
|
|
895
|
+
return overlap / (left.size + right.size - overlap);
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Evaluate whether a domain definition is specific, triggered, and distinct.
|
|
899
|
+
* @param domain - candidate domain.
|
|
900
|
+
* @param peers - other visible domains.
|
|
901
|
+
* @returns auditable issues; does not mutate the candidate.
|
|
902
|
+
*/
|
|
903
|
+
function evaluateDomainQuality(domain, peers) {
|
|
904
|
+
const issues = [];
|
|
905
|
+
if (domain.purpose.trim().length === 0) issues.push("empty-purpose");
|
|
906
|
+
else if (domain.purpose.trim().length < 8) issues.push("vague-purpose");
|
|
907
|
+
if (domain.inScope.length === 0) issues.push("empty-in-scope");
|
|
908
|
+
if (domain.outOfScope.length === 0) issues.push("empty-out-of-scope");
|
|
909
|
+
if (domain.triggers.length === 0) issues.push("empty-triggers");
|
|
910
|
+
const own = tokens([...domain.inScope, ...domain.triggers]);
|
|
911
|
+
for (const peer of peers) {
|
|
912
|
+
if (domain.name.trim().toLowerCase() === peer.name.trim().toLowerCase() || domain.purpose.trim().toLowerCase() === peer.purpose.trim().toLowerCase()) {
|
|
913
|
+
issues.push("indistinguishable");
|
|
914
|
+
break;
|
|
915
|
+
}
|
|
916
|
+
if (jaccard(own, tokens([...peer.inScope, ...peer.triggers])) > .8) {
|
|
917
|
+
issues.push("overlaps-peer");
|
|
918
|
+
break;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
const unique = [...new Set(issues)];
|
|
922
|
+
return {
|
|
923
|
+
passed: unique.length === 0,
|
|
924
|
+
issues: unique,
|
|
925
|
+
score: Math.max(0, 1 - unique.length * .2)
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
//#endregion
|
|
929
|
+
//#region lib/types/domain-metrics.js
|
|
930
|
+
/**
|
|
931
|
+
* @param snapshot - domain facts.
|
|
932
|
+
* @param visibleSkillKeys - skills visible in this view.
|
|
933
|
+
* @param labels - optional assignment labels.
|
|
934
|
+
* @returns metrics; null means the denominator is zero/unmeasured.
|
|
935
|
+
*/
|
|
936
|
+
function computeDomainMetrics(snapshot, visibleSkillKeys, labels, counters) {
|
|
937
|
+
const visible = visibleSkillKeys.size;
|
|
938
|
+
const primary = /* @__PURE__ */ new Set();
|
|
939
|
+
const domainsWithActiveMembers = /* @__PURE__ */ new Set();
|
|
940
|
+
let activeEdges = 0;
|
|
941
|
+
let pendingEdges = 0;
|
|
942
|
+
const memberships = /* @__PURE__ */ new Map();
|
|
943
|
+
for (const edge of snapshot.edges) {
|
|
944
|
+
if (edge.status === "pending") pendingEdges += 1;
|
|
945
|
+
if (edge.status !== "active") continue;
|
|
946
|
+
activeEdges += 1;
|
|
947
|
+
if (!visibleSkillKeys.has(edge.skillKey)) continue;
|
|
948
|
+
if (edge.role === "primary") primary.add(edge.skillKey);
|
|
949
|
+
domainsWithActiveMembers.add(edge.domainId);
|
|
950
|
+
memberships.set(edge.skillKey, (memberships.get(edge.skillKey) ?? 0) + 1);
|
|
951
|
+
}
|
|
952
|
+
const crossDomain = [...memberships.values()].filter((count) => count > 1).length;
|
|
953
|
+
const emptyDomainCount = snapshot.domains.filter((domain) => !domainsWithActiveMembers.has(domain.id)).length;
|
|
954
|
+
const assignmentAccuracy = labels === void 0 ? null : accuracy(labels);
|
|
955
|
+
const staleDescriptions = snapshot.domains.filter((domain) => domain.descriptionStatus === "stale").length;
|
|
956
|
+
const overAssigned = counters?.maxDomainsPerSkill === void 0 ? null : [...memberships.values()].filter((count) => count > counters.maxDomainsPerSkill).length;
|
|
957
|
+
return {
|
|
958
|
+
measured: visible > 0,
|
|
959
|
+
visibleSkills: visible,
|
|
960
|
+
coverage: visible === 0 ? null : primary.size / visible,
|
|
961
|
+
unclassifiedRatio: visible === 0 ? null : (visible - primary.size) / visible,
|
|
962
|
+
emptyDomainCount,
|
|
963
|
+
crossDomainRatio: primary.size === 0 ? null : crossDomain / primary.size,
|
|
964
|
+
pendingRate: activeEdges + pendingEdges === 0 ? null : pendingEdges / (activeEdges + pendingEdges),
|
|
965
|
+
assignmentAccuracy,
|
|
966
|
+
newDomainPrecision: labels?.newDomainIds === void 0 || labels.predictedNewDomainIds === void 0 || labels.newDomainIds.length === 0 ? null : precision(labels.newDomainIds, labels.predictedNewDomainIds),
|
|
967
|
+
descriptionRewriteRate: counters?.descriptionEligible === void 0 || counters.descriptionEligible === 0 ? null : (counters.descriptionRewritten ?? 0) / counters.descriptionEligible,
|
|
968
|
+
staleDescriptionRatio: snapshot.domains.length === 0 ? null : staleDescriptions / snapshot.domains.length,
|
|
969
|
+
overAssignmentRate: overAssigned === null || visible === 0 ? null : overAssigned / visible,
|
|
970
|
+
refusalRate: counters?.assignmentAttempts === void 0 || counters.assignmentAttempts === 0 ? null : (counters.refusedAssignments ?? 0) / counters.assignmentAttempts
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
function accuracy(labels) {
|
|
974
|
+
const keys = Object.keys(labels.expectedBySkill).filter((key) => labels.predictedBySkill[key] !== void 0);
|
|
975
|
+
if (keys.length === 0) return null;
|
|
976
|
+
return keys.filter((key) => labels.expectedBySkill[key] === labels.predictedBySkill[key]).length / keys.length;
|
|
977
|
+
}
|
|
978
|
+
function precision(expected, predicted) {
|
|
979
|
+
if (predicted.length === 0) return null;
|
|
980
|
+
const wanted = new Set(expected);
|
|
981
|
+
return predicted.filter((id) => wanted.has(id)).length / predicted.length;
|
|
982
|
+
}
|
|
983
|
+
//#endregion
|
|
984
|
+
//#region lib/types/domain-projection.js
|
|
985
|
+
function qualityPasses(node, domains) {
|
|
986
|
+
return evaluateDomainQuality(node, domains.filter((peer) => peer.id !== node.id)).passed;
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Build L1 cards and the hidden set for one view, then delegate the pure
|
|
990
|
+
* catalog rewrite. Structural staleness fails open; description staleness does not.
|
|
991
|
+
* @param input - rows, domain facts, policy, and identity inputs.
|
|
992
|
+
* @returns projection result with cards and hidden names.
|
|
993
|
+
*/
|
|
994
|
+
function hiddenRecoverable(hiddenNames, searchResultNames) {
|
|
995
|
+
return hiddenNames.every((name) => searchResultNames.has(name));
|
|
996
|
+
}
|
|
997
|
+
function buildDomainProjection(input) {
|
|
998
|
+
const mode = input.mode ?? "on";
|
|
999
|
+
if (mode === "off") return projectCatalog({
|
|
1000
|
+
entries: input.entries,
|
|
1001
|
+
rows: input.rows,
|
|
1002
|
+
activeManagedSkillKeys: /* @__PURE__ */ new Set(),
|
|
1003
|
+
cards: [],
|
|
1004
|
+
viewKey: input.viewKey,
|
|
1005
|
+
scope: input.scope,
|
|
1006
|
+
mapVersion: input.mapVersion,
|
|
1007
|
+
policyDigest: input.policyDigest,
|
|
1008
|
+
viewCatalogDigest: input.viewCatalogDigest,
|
|
1009
|
+
adapterVersion: input.adapterVersion,
|
|
1010
|
+
projectionSchemaVersion: input.projectionSchemaVersion,
|
|
1011
|
+
discoveryMode: input.discoveryMode,
|
|
1012
|
+
complete: false
|
|
1013
|
+
});
|
|
1014
|
+
const staleByAge = input.staleAfterHours !== void 0 && input.snapshot.domains.some((node) => input.now !== void 0 && input.now - node.updatedAt > input.staleAfterHours * 36e5);
|
|
1015
|
+
const mapStale = input.mapStale === true || staleByAge;
|
|
1016
|
+
if (!(input.complete && !mapStale && input.searchAvailable !== false)) return projectCatalog({
|
|
1017
|
+
entries: input.entries,
|
|
1018
|
+
rows: input.rows,
|
|
1019
|
+
activeManagedSkillKeys: /* @__PURE__ */ new Set(),
|
|
1020
|
+
cards: [],
|
|
1021
|
+
viewKey: input.viewKey,
|
|
1022
|
+
scope: input.scope,
|
|
1023
|
+
mapVersion: input.mapVersion,
|
|
1024
|
+
policyDigest: input.policyDigest,
|
|
1025
|
+
viewCatalogDigest: input.viewCatalogDigest,
|
|
1026
|
+
adapterVersion: input.adapterVersion,
|
|
1027
|
+
projectionSchemaVersion: input.projectionSchemaVersion,
|
|
1028
|
+
discoveryMode: input.discoveryMode,
|
|
1029
|
+
complete: false
|
|
1030
|
+
});
|
|
1031
|
+
const rowByKey = new Map(input.rows.map((row) => [row.skillKey, row]));
|
|
1032
|
+
const domains = input.snapshot.domains.filter((node) => node.status === "active" || node.status === "provisional");
|
|
1033
|
+
const cardDomains = domains.filter((node) => node.parentIds.length <= (input.maxVisibleDepth ?? 3) - 1 && node.parentIds.length === 0).filter((node) => !input.policy.disabled.has(node.id)).filter((node) => input.policy.pinned.has(node.id) || qualityPasses(node, domains)).sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0).slice(0, Math.max(1, input.maxCards ?? 12));
|
|
1034
|
+
const domainById = new Map(domains.map((node) => [node.id, node]));
|
|
1035
|
+
const cards = cardDomains.map((node) => {
|
|
1036
|
+
let managedModelInvocableCount = 0;
|
|
1037
|
+
let externalCatalogCount = 0;
|
|
1038
|
+
let pendingCount = 0;
|
|
1039
|
+
for (const edge of input.snapshot.edges) {
|
|
1040
|
+
if (edge.domainId !== node.id) continue;
|
|
1041
|
+
if (edge.status === "pending") {
|
|
1042
|
+
pendingCount += 1;
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1045
|
+
if (edge.status !== "active") continue;
|
|
1046
|
+
const row = rowByKey.get(edge.skillKey);
|
|
1047
|
+
if (row === void 0) continue;
|
|
1048
|
+
if (row.managed && row.modelInvocable && !row.protected && !row.pinned) managedModelInvocableCount += 1;
|
|
1049
|
+
else if (!row.managed) externalCatalogCount += 1;
|
|
1050
|
+
}
|
|
1051
|
+
return {
|
|
1052
|
+
id: node.id,
|
|
1053
|
+
title: node.name,
|
|
1054
|
+
purpose: node.purpose.slice(0, 80),
|
|
1055
|
+
descriptionStatus: node.descriptionStatus,
|
|
1056
|
+
triggers: node.triggers.slice(0, 3),
|
|
1057
|
+
managedModelInvocableCount,
|
|
1058
|
+
externalCatalogCount,
|
|
1059
|
+
managedCount: managedModelInvocableCount,
|
|
1060
|
+
externalCount: externalCatalogCount,
|
|
1061
|
+
pendingCount,
|
|
1062
|
+
state: node.status === "provisional" ? "provisional" : "active",
|
|
1063
|
+
asOf: new Date(node.updatedAt).toISOString(),
|
|
1064
|
+
mapVersion: input.mapVersion,
|
|
1065
|
+
mapStale
|
|
1066
|
+
};
|
|
1067
|
+
});
|
|
1068
|
+
const budget = input.tokenBudget;
|
|
1069
|
+
if (budget !== void 0 && budget > 0) while (cards.length > 1 && Math.ceil(JSON.stringify(cards).length / 4) > budget) cards.pop();
|
|
1070
|
+
const cardIds = new Set(cards.map((card) => card.id));
|
|
1071
|
+
const wouldHide = /* @__PURE__ */ new Set();
|
|
1072
|
+
for (const edge of input.snapshot.edges) {
|
|
1073
|
+
if (edge.status !== "active" || edge.role !== "primary") continue;
|
|
1074
|
+
const node = domainById.get(edge.domainId);
|
|
1075
|
+
const row = rowByKey.get(edge.skillKey);
|
|
1076
|
+
if (node === void 0 || row === void 0) continue;
|
|
1077
|
+
if (node.status !== "active" || !cardIds.has(node.id) || !row.managed || !row.modelInvocable || row.protected || row.pinned) continue;
|
|
1078
|
+
if (input.policy.disabled.has(node.id)) continue;
|
|
1079
|
+
wouldHide.add(edge.skillKey);
|
|
1080
|
+
}
|
|
1081
|
+
const activeManaged = mode === "on" ? wouldHide : /* @__PURE__ */ new Set();
|
|
1082
|
+
const projected = projectCatalog({
|
|
1083
|
+
entries: input.entries,
|
|
1084
|
+
rows: input.rows,
|
|
1085
|
+
activeManagedSkillKeys: activeManaged,
|
|
1086
|
+
cards,
|
|
1087
|
+
viewKey: input.viewKey,
|
|
1088
|
+
scope: input.scope,
|
|
1089
|
+
mapVersion: input.mapVersion,
|
|
1090
|
+
policyDigest: input.policyDigest,
|
|
1091
|
+
viewCatalogDigest: input.viewCatalogDigest,
|
|
1092
|
+
adapterVersion: input.adapterVersion,
|
|
1093
|
+
projectionSchemaVersion: input.projectionSchemaVersion,
|
|
1094
|
+
discoveryMode: input.discoveryMode,
|
|
1095
|
+
complete: true
|
|
1096
|
+
});
|
|
1097
|
+
return mode === "on" ? projected : {
|
|
1098
|
+
...projected,
|
|
1099
|
+
shadowHiddenNames: [...wouldHide],
|
|
1100
|
+
shadowRecoveredNames: [...wouldHide]
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
/** @returns true when shadow evidence is sufficient to promote to on. */
|
|
1104
|
+
function canPromoteShadow(input) {
|
|
1105
|
+
return input.samples >= 20 && input.hiddenRecoverableRate >= 1 && input.silentMisses === 0;
|
|
1106
|
+
}
|
|
1107
|
+
//#endregion
|
|
1108
|
+
//#region lib/types/session-projection.js
|
|
1109
|
+
/** @returns true only for a new, fully-enabled session in the current epoch. */
|
|
1110
|
+
function shouldHideCatalog(meta, currentFeatureEpoch) {
|
|
1111
|
+
return meta?.discoveryMode === "on" && meta.featureEpoch === currentFeatureEpoch;
|
|
1112
|
+
}
|
|
1113
|
+
/** @returns true when awareness may be emitted (shadow or on). */
|
|
1114
|
+
function shouldEmitAwareness(meta, currentFeatureEpoch) {
|
|
1115
|
+
return (meta?.discoveryMode === "on" || meta?.discoveryMode === "shadow") && meta.featureEpoch === currentFeatureEpoch;
|
|
1116
|
+
}
|
|
1117
|
+
/** @returns false when the same digest was already emitted in this session. */
|
|
1118
|
+
function shouldEmitDigest(previousDigest, nextDigest) {
|
|
1119
|
+
return previousDigest !== nextDigest;
|
|
1120
|
+
}
|
|
1121
|
+
/** @returns off at <=2200 tokens, on at >=3000, shadow between. */
|
|
1122
|
+
function projectionBudgetMode(catalogTokens, promoted = false) {
|
|
1123
|
+
if (catalogTokens <= 2200) return "off";
|
|
1124
|
+
if (promoted && catalogTokens >= 3e3) return "on";
|
|
1125
|
+
return "shadow";
|
|
1126
|
+
}
|
|
1127
|
+
/** @returns true when a resumed session already enabled discovery. */
|
|
1128
|
+
function shouldInstallAwarenessOnResume(record, featureEpoch) {
|
|
1129
|
+
return record !== void 0 && record.discoveryMode !== "off" && record.featureEpoch === featureEpoch;
|
|
1130
|
+
}
|
|
1131
|
+
//#endregion
|
|
1132
|
+
//#region lib/types/tool.js
|
|
1133
|
+
/** Static model-facing description; no dynamic domain list/tree. */
|
|
1134
|
+
const SKILL_SEARCH_DESCRIPTION = "Search skill summaries by name, description, or whenToUse when coverage is uncertain. Results are summaries, not bodies; a matching name can be loaded directly. Empty results are inconclusive; the full catalog remains available.";
|
|
1135
|
+
/** @returns response or a degraded hint; never silent empty on failure. */
|
|
1136
|
+
async function searchWithDegraded(provider, query, allowedSkillKeys, agent) {
|
|
1137
|
+
if (provider === void 0) return {
|
|
1138
|
+
results: [],
|
|
1139
|
+
truncated: false,
|
|
1140
|
+
degraded: true,
|
|
1141
|
+
hint: "Skill search is unavailable. Use the full catalog or load an exact skill by name with skill(name)."
|
|
1142
|
+
};
|
|
1143
|
+
try {
|
|
1144
|
+
return await provider.search(query, allowedSkillKeys === void 0 ? void 0 : { allowedSkillKeys }, agent);
|
|
1145
|
+
} catch {
|
|
1146
|
+
return {
|
|
1147
|
+
results: [],
|
|
1148
|
+
truncated: false,
|
|
1149
|
+
degraded: true,
|
|
1150
|
+
hint: "Skill search failed. Use the full catalog or load an exact skill by name with skill(name)."
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
function toToolResponse(response) {
|
|
1155
|
+
return {
|
|
1156
|
+
results: response.results.map((hit) => ({
|
|
1157
|
+
name: hit.name,
|
|
1158
|
+
description: hit.description,
|
|
1159
|
+
...hit.whenToUse === void 0 ? {} : { whenToUse: hit.whenToUse },
|
|
1160
|
+
...hit.matchedBy === void 0 ? {} : { matchedBy: hit.matchedBy }
|
|
1161
|
+
})),
|
|
1162
|
+
truncated: response.truncated,
|
|
1163
|
+
...response.fallback === void 0 ? {} : { fallback: response.fallback },
|
|
1164
|
+
...response.stale === void 0 ? {} : { stale: response.stale },
|
|
1165
|
+
...response.degraded === void 0 ? {} : { degraded: response.degraded },
|
|
1166
|
+
...response.hint === void 0 ? {} : { hint: response.hint }
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Register skill_search on ctx.tools. The description/schema are static; the
|
|
1171
|
+
* provider is the only dynamic dependency.
|
|
1172
|
+
* @param ctx - plugin context.
|
|
1173
|
+
* @param provider - search service or undefined when unavailable.
|
|
1174
|
+
* @returns tool disposer.
|
|
1175
|
+
*/
|
|
1176
|
+
function registerSkillSearchTool(ctx, provider) {
|
|
1177
|
+
return ctx.tools.register(defineTool({
|
|
1178
|
+
name: "skill_search",
|
|
1179
|
+
description: SKILL_SEARCH_DESCRIPTION,
|
|
1180
|
+
parameters: {
|
|
1181
|
+
query: {
|
|
1182
|
+
type: "string",
|
|
1183
|
+
required: true,
|
|
1184
|
+
description: "Search terms; CJK and ASCII are supported."
|
|
1185
|
+
},
|
|
1186
|
+
limit: {
|
|
1187
|
+
type: "integer",
|
|
1188
|
+
description: "Maximum results; defaults to 3, capped at 20."
|
|
1189
|
+
}
|
|
1190
|
+
},
|
|
1191
|
+
output: {
|
|
1192
|
+
schema: {
|
|
1193
|
+
type: "object",
|
|
1194
|
+
additionalProperties: false,
|
|
1195
|
+
properties: {
|
|
1196
|
+
results: {
|
|
1197
|
+
type: "array",
|
|
1198
|
+
required: true,
|
|
1199
|
+
items: {
|
|
1200
|
+
type: "object",
|
|
1201
|
+
additionalProperties: false,
|
|
1202
|
+
properties: {
|
|
1203
|
+
name: {
|
|
1204
|
+
type: "string",
|
|
1205
|
+
required: true
|
|
1206
|
+
},
|
|
1207
|
+
description: {
|
|
1208
|
+
type: "string",
|
|
1209
|
+
required: true
|
|
1210
|
+
},
|
|
1211
|
+
whenToUse: { type: "string" },
|
|
1212
|
+
matchedBy: { type: "string" }
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
},
|
|
1216
|
+
truncated: {
|
|
1217
|
+
type: "boolean",
|
|
1218
|
+
required: true
|
|
1219
|
+
},
|
|
1220
|
+
fallback: { type: "boolean" },
|
|
1221
|
+
stale: { type: "boolean" },
|
|
1222
|
+
degraded: { type: "boolean" },
|
|
1223
|
+
hint: { type: "string" }
|
|
1224
|
+
}
|
|
1225
|
+
},
|
|
1226
|
+
render: (_args, value) => [{
|
|
1227
|
+
type: "text",
|
|
1228
|
+
text: value.degraded === true ? "Skill search is degraded: " + (value.hint ?? "use the full catalog or skill(name).") : "Found " + value.results.length + " skill result(s)" + (value.fallback === true ? " (OR fallback)." : ".") + (value.stale === true ? " (stale index)." : "") + "\n" + value.results.map((result) => "- " + result.name + ": " + result.description + (result.whenToUse === void 0 ? "" : " | when: " + result.whenToUse)).join("\n")
|
|
1229
|
+
}]
|
|
1230
|
+
},
|
|
1231
|
+
async execute(args, exec) {
|
|
1232
|
+
const allowed = await provider?.allowedSkillKeys?.(exec.agent);
|
|
1233
|
+
return toToolResponse(await searchWithDegraded(provider, {
|
|
1234
|
+
query: args.query,
|
|
1235
|
+
limit: args.limit
|
|
1236
|
+
}, allowed, exec.agent));
|
|
1237
|
+
}
|
|
1238
|
+
}));
|
|
1239
|
+
}
|
|
1240
|
+
/** @returns true only for a search implementation; used by composition wiring. */
|
|
1241
|
+
function isSkillSearch(value) {
|
|
1242
|
+
return typeof value === "object" && value !== null && typeof value.search === "function";
|
|
1243
|
+
}
|
|
1244
|
+
//#endregion
|
|
1245
|
+
//#region lib/types/assignment.js
|
|
1246
|
+
/** In-memory idempotent queue; production adapter can persist by key. */
|
|
1247
|
+
var MemoryAssignmentQueue = class {
|
|
1248
|
+
entries = /* @__PURE__ */ new Map();
|
|
1249
|
+
async enqueue(input) {
|
|
1250
|
+
const existing = this.entries.get(input.key);
|
|
1251
|
+
if (existing !== void 0) return {
|
|
1252
|
+
entry: existing,
|
|
1253
|
+
duplicate: true
|
|
1254
|
+
};
|
|
1255
|
+
const entry = {
|
|
1256
|
+
...input,
|
|
1257
|
+
status: "queued"
|
|
1258
|
+
};
|
|
1259
|
+
this.entries.set(input.key, entry);
|
|
1260
|
+
return {
|
|
1261
|
+
entry,
|
|
1262
|
+
duplicate: false
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
async update(key, status, pendingReason) {
|
|
1266
|
+
const entry = this.entries.get(key);
|
|
1267
|
+
if (entry !== void 0) {
|
|
1268
|
+
entry.status = status;
|
|
1269
|
+
entry.pendingReason = pendingReason;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
list() {
|
|
1273
|
+
return [...this.entries.values()];
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
function digest(value) {
|
|
1277
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
1278
|
+
}
|
|
1279
|
+
/** @returns idempotency key for one proposal observation. */
|
|
1280
|
+
function assignmentKey(input) {
|
|
1281
|
+
return digest([
|
|
1282
|
+
input.skillKey,
|
|
1283
|
+
input.threeFieldHash,
|
|
1284
|
+
input.proposalDigest,
|
|
1285
|
+
input.mapVersion
|
|
1286
|
+
]);
|
|
1287
|
+
}
|
|
1288
|
+
/** @returns stable proposal digest; undefined proposal is digest of null. */
|
|
1289
|
+
function proposalDigest(proposal) {
|
|
1290
|
+
return digest(proposal ?? null);
|
|
1291
|
+
}
|
|
1292
|
+
/** @returns a closed-set pending reason or undefined when the proposal can enqueue. */
|
|
1293
|
+
function validateProposal(proposal) {
|
|
1294
|
+
if (proposal.mode !== "existing" && proposal.mode !== "new") return "schema-invalid";
|
|
1295
|
+
if (typeof proposal.confidence !== "number" || proposal.confidence < 0 || proposal.confidence > 1 || typeof proposal.reason !== "string" || typeof proposal.mapVersion !== "string") return "schema-invalid";
|
|
1296
|
+
if (proposal.mode === "existing" && (typeof proposal.id !== "string" || proposal.id.length === 0)) return "invalid-id";
|
|
1297
|
+
if (proposal.mode === "new") {
|
|
1298
|
+
const proposed = proposal.proposed;
|
|
1299
|
+
if (proposed === void 0 || proposed.title.trim().length === 0 || proposed.purpose.trim().length === 0 || proposed.inScope.length === 0 || proposed.outOfScope.length === 0 || proposed.triggers.length === 0) return "new-domain-conflict";
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
/** @returns true only when whenToUse is covered by in/out/triggers; names alone do not count. */
|
|
1303
|
+
function coveredByDomain(whenToUse, domain) {
|
|
1304
|
+
const text = whenToUse.toLowerCase();
|
|
1305
|
+
if (text.length === 0) return false;
|
|
1306
|
+
if (domain.outOfScope.some((value) => value.length > 0 && text.includes(value.toLowerCase()))) return false;
|
|
1307
|
+
return domain.triggers.some((value) => value.length > 0 && text.includes(value.toLowerCase())) || domain.inScope.some((value) => value.length > 0 && text.includes(value.toLowerCase()));
|
|
1308
|
+
}
|
|
1309
|
+
/** One assignment sidecar service; the only map write path for create. */
|
|
1310
|
+
var DomainAssignmentService = class {
|
|
1311
|
+
queue;
|
|
1312
|
+
store;
|
|
1313
|
+
options;
|
|
1314
|
+
constructor(queue, store, options) {
|
|
1315
|
+
this.queue = queue;
|
|
1316
|
+
this.store = store;
|
|
1317
|
+
this.options = options;
|
|
1318
|
+
}
|
|
1319
|
+
/** Stage a create proposal without queueing it. */
|
|
1320
|
+
stage(submission) {
|
|
1321
|
+
return {
|
|
1322
|
+
id: digest([
|
|
1323
|
+
"staged",
|
|
1324
|
+
submission.skillKey,
|
|
1325
|
+
submission.threeFieldHash
|
|
1326
|
+
]),
|
|
1327
|
+
submission
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
/** Approve a staged create; only this call enqueues. */
|
|
1331
|
+
async approve(staged) {
|
|
1332
|
+
return this.submit(staged.submission);
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Submit a create proposal after SkillLibrary.create succeeded.
|
|
1336
|
+
* @param submission - skill identity, three-field hash, permission, proposal.
|
|
1337
|
+
* @returns queue/pending/unclassified; never rolls back create.
|
|
1338
|
+
*/
|
|
1339
|
+
async submit(submission) {
|
|
1340
|
+
if (!this.options.createClassificationEnabled) return {
|
|
1341
|
+
kind: "unclassified",
|
|
1342
|
+
reason: "feature-off"
|
|
1343
|
+
};
|
|
1344
|
+
if (!submission.modelInvocable) return {
|
|
1345
|
+
kind: "unclassified",
|
|
1346
|
+
reason: "not-model-invocable"
|
|
1347
|
+
};
|
|
1348
|
+
if (submission.proposal === void 0) {
|
|
1349
|
+
if (this.options.createCardsAvailable === true) {
|
|
1350
|
+
const reason = "schema-invalid";
|
|
1351
|
+
return {
|
|
1352
|
+
kind: "pending",
|
|
1353
|
+
entry: {
|
|
1354
|
+
key: digest([
|
|
1355
|
+
"missing-proposal",
|
|
1356
|
+
submission.skillKey,
|
|
1357
|
+
submission.threeFieldHash
|
|
1358
|
+
]),
|
|
1359
|
+
submission,
|
|
1360
|
+
proposalDigest: proposalDigest(void 0),
|
|
1361
|
+
createdAt: this.now(),
|
|
1362
|
+
status: "pending",
|
|
1363
|
+
pendingReason: reason
|
|
1364
|
+
},
|
|
1365
|
+
reason
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
return {
|
|
1369
|
+
kind: "unclassified",
|
|
1370
|
+
reason: "no-proposal"
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
const invalid = validateProposal(submission.proposal);
|
|
1374
|
+
const pdigest = proposalDigest(submission.proposal);
|
|
1375
|
+
const key = assignmentKey({
|
|
1376
|
+
skillKey: submission.skillKey,
|
|
1377
|
+
threeFieldHash: submission.threeFieldHash,
|
|
1378
|
+
proposalDigest: pdigest,
|
|
1379
|
+
mapVersion: submission.proposal.mapVersion
|
|
1380
|
+
});
|
|
1381
|
+
let queued;
|
|
1382
|
+
try {
|
|
1383
|
+
queued = await this.queue.enqueue({
|
|
1384
|
+
key,
|
|
1385
|
+
submission,
|
|
1386
|
+
proposalDigest: pdigest,
|
|
1387
|
+
createdAt: submission.now ?? this.now()
|
|
1388
|
+
});
|
|
1389
|
+
} catch {
|
|
1390
|
+
return {
|
|
1391
|
+
kind: "pending",
|
|
1392
|
+
entry: {
|
|
1393
|
+
key,
|
|
1394
|
+
submission,
|
|
1395
|
+
proposalDigest: pdigest,
|
|
1396
|
+
createdAt: submission.now ?? this.now(),
|
|
1397
|
+
status: "pending",
|
|
1398
|
+
pendingReason: "queue-unavailable"
|
|
1399
|
+
},
|
|
1400
|
+
reason: "queue-unavailable"
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
if (invalid !== void 0) {
|
|
1404
|
+
await this.queue.update(key, "pending", invalid);
|
|
1405
|
+
return {
|
|
1406
|
+
kind: "pending",
|
|
1407
|
+
entry: {
|
|
1408
|
+
...queued.entry,
|
|
1409
|
+
status: "pending",
|
|
1410
|
+
pendingReason: invalid
|
|
1411
|
+
},
|
|
1412
|
+
reason: invalid
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
return {
|
|
1416
|
+
kind: "queued",
|
|
1417
|
+
entry: queued.entry,
|
|
1418
|
+
duplicate: queued.duplicate
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
/** Process one queued entry with the current map; CAS failure becomes pending. */
|
|
1422
|
+
async process(entry) {
|
|
1423
|
+
const proposal = entry.submission.proposal;
|
|
1424
|
+
if (proposal === void 0) return {
|
|
1425
|
+
kind: "pending",
|
|
1426
|
+
entry,
|
|
1427
|
+
reason: "schema-invalid"
|
|
1428
|
+
};
|
|
1429
|
+
const current = await this.store.read();
|
|
1430
|
+
if (proposal.mapVersion !== current.mapVersion) {
|
|
1431
|
+
await this.queue.update(entry.key, "pending", "map-version-stale");
|
|
1432
|
+
return {
|
|
1433
|
+
kind: "pending",
|
|
1434
|
+
entry: {
|
|
1435
|
+
...entry,
|
|
1436
|
+
status: "pending",
|
|
1437
|
+
pendingReason: "map-version-stale"
|
|
1438
|
+
},
|
|
1439
|
+
reason: "map-version-stale"
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
try {
|
|
1443
|
+
const next = buildNext(current, entry, proposal, this.options.provisionalThreshold ?? 3);
|
|
1444
|
+
const committed = await this.store.commit(current.mapVersion, {
|
|
1445
|
+
mapVersion: next.mapVersion,
|
|
1446
|
+
domains: next.domains,
|
|
1447
|
+
edges: next.edges
|
|
1448
|
+
});
|
|
1449
|
+
const status = next.provisional ? "provisional" : "applied";
|
|
1450
|
+
await this.queue.update(entry.key, status);
|
|
1451
|
+
return {
|
|
1452
|
+
kind: status,
|
|
1453
|
+
entry: {
|
|
1454
|
+
...entry,
|
|
1455
|
+
status
|
|
1456
|
+
},
|
|
1457
|
+
mapVersion: committed.mapVersion
|
|
1458
|
+
};
|
|
1459
|
+
} catch (error) {
|
|
1460
|
+
if (error instanceof DomainMapVersionConflict) {
|
|
1461
|
+
await this.queue.update(entry.key, "pending", "map-version-stale");
|
|
1462
|
+
return {
|
|
1463
|
+
kind: "pending",
|
|
1464
|
+
entry: {
|
|
1465
|
+
...entry,
|
|
1466
|
+
status: "pending",
|
|
1467
|
+
pendingReason: "map-version-stale"
|
|
1468
|
+
},
|
|
1469
|
+
reason: "map-version-stale"
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
const reason = error instanceof Error && (error.message === "unknown-domain" || error.message === "new-domain-conflict" || error.message === "schema-invalid") ? error.message : "queue-unavailable";
|
|
1473
|
+
await this.queue.update(entry.key, "pending", reason);
|
|
1474
|
+
return {
|
|
1475
|
+
kind: "pending",
|
|
1476
|
+
entry: {
|
|
1477
|
+
...entry,
|
|
1478
|
+
status: "pending",
|
|
1479
|
+
pendingReason: reason
|
|
1480
|
+
},
|
|
1481
|
+
reason
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
/** Manually confirm one provisional domain; no semantic reclassification. */
|
|
1486
|
+
async confirm(domainId) {
|
|
1487
|
+
const current = await this.store.read();
|
|
1488
|
+
const domain = current.domains.find((node) => node.id === domainId);
|
|
1489
|
+
if (domain === void 0 || domain.status !== "provisional") return false;
|
|
1490
|
+
const domains = current.domains.map((node) => node.id === domainId ? {
|
|
1491
|
+
...node,
|
|
1492
|
+
status: "active",
|
|
1493
|
+
updatedAt: this.now()
|
|
1494
|
+
} : node);
|
|
1495
|
+
await this.store.commit(current.mapVersion, {
|
|
1496
|
+
mapVersion: nextMapVersion(current.mapVersion),
|
|
1497
|
+
domains,
|
|
1498
|
+
edges: current.edges
|
|
1499
|
+
});
|
|
1500
|
+
return true;
|
|
1501
|
+
}
|
|
1502
|
+
now() {
|
|
1503
|
+
return this.options.now?.() ?? Date.now();
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
function nextMapVersion(version) {
|
|
1507
|
+
const numeric = Number(version);
|
|
1508
|
+
return Number.isSafeInteger(numeric) && numeric >= 0 ? String(numeric + 1) : version + "+1";
|
|
1509
|
+
}
|
|
1510
|
+
function buildNext(current, entry, proposal, threshold) {
|
|
1511
|
+
const edge = {
|
|
1512
|
+
skillKey: entry.submission.skillKey,
|
|
1513
|
+
domainId: "",
|
|
1514
|
+
role: "primary",
|
|
1515
|
+
confidence: proposal.confidence,
|
|
1516
|
+
source: "create",
|
|
1517
|
+
skillHash: entry.submission.threeFieldHash,
|
|
1518
|
+
mapVersion: nextMapVersion(current.mapVersion),
|
|
1519
|
+
status: "active",
|
|
1520
|
+
assignedAt: entry.createdAt
|
|
1521
|
+
};
|
|
1522
|
+
const secondaries = (proposal.secondaryDomainIds ?? []).map((id) => {
|
|
1523
|
+
const secondary = current.domains.find((node) => node.id === id);
|
|
1524
|
+
if (secondary === void 0) throw new Error("unknown-domain");
|
|
1525
|
+
return {
|
|
1526
|
+
...edge,
|
|
1527
|
+
domainId: secondary.id,
|
|
1528
|
+
role: "secondary"
|
|
1529
|
+
};
|
|
1530
|
+
});
|
|
1531
|
+
if (proposal.mode === "existing") {
|
|
1532
|
+
const domain = current.domains.find((node) => node.id === proposal.id);
|
|
1533
|
+
if (domain === void 0) throw new Error("unknown-domain");
|
|
1534
|
+
if (!coveredByDomain(entry.submission.whenToUse ?? "", domain)) throw new Error("new-domain-conflict");
|
|
1535
|
+
const members = new Set(current.edges.filter((item) => item.domainId === domain.id).map((item) => item.skillKey));
|
|
1536
|
+
members.add(entry.submission.skillKey);
|
|
1537
|
+
const promote = domain.status === "provisional" && members.size >= threshold;
|
|
1538
|
+
const domains = promote ? current.domains.map((node) => node.id === domain.id ? {
|
|
1539
|
+
...node,
|
|
1540
|
+
status: "active",
|
|
1541
|
+
updatedAt: entry.createdAt
|
|
1542
|
+
} : node) : current.domains;
|
|
1543
|
+
return {
|
|
1544
|
+
mapVersion: nextMapVersion(current.mapVersion),
|
|
1545
|
+
domains,
|
|
1546
|
+
edges: [
|
|
1547
|
+
...current.edges,
|
|
1548
|
+
{
|
|
1549
|
+
...edge,
|
|
1550
|
+
domainId: domain.id
|
|
1551
|
+
},
|
|
1552
|
+
...secondaries.filter((secondary) => secondary.domainId !== domain.id)
|
|
1553
|
+
],
|
|
1554
|
+
provisional: domain.status === "provisional" && !promote
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
const proposed = proposal.proposed;
|
|
1558
|
+
if (proposed === void 0) throw new Error("schema-invalid");
|
|
1559
|
+
const normalizedTitle = proposed.title.trim().toLowerCase();
|
|
1560
|
+
const existing = current.domains.find((node) => node.name.trim().toLowerCase() === normalizedTitle);
|
|
1561
|
+
if (existing !== void 0 && existing.status !== "provisional") throw new Error("new-domain-conflict");
|
|
1562
|
+
const id = existing?.id ?? mintDomainId(proposed.title);
|
|
1563
|
+
if (current.domains.some((node) => node.id !== id && node.triggers.some((trigger) => proposed.triggers.includes(trigger)))) throw new Error("new-domain-conflict");
|
|
1564
|
+
const domain = existing ?? {
|
|
1565
|
+
id,
|
|
1566
|
+
name: proposed.title,
|
|
1567
|
+
purpose: proposed.purpose,
|
|
1568
|
+
description: proposed.purpose,
|
|
1569
|
+
inScope: proposed.inScope,
|
|
1570
|
+
outOfScope: proposed.outOfScope,
|
|
1571
|
+
triggers: proposed.triggers,
|
|
1572
|
+
parentIds: [],
|
|
1573
|
+
status: "provisional",
|
|
1574
|
+
version: 1,
|
|
1575
|
+
createdAt: entry.createdAt,
|
|
1576
|
+
updatedAt: entry.createdAt,
|
|
1577
|
+
descriptionHash: "pending",
|
|
1578
|
+
descriptionVersion: 1,
|
|
1579
|
+
descriptionStatus: "fresh",
|
|
1580
|
+
provenance: { source: "create" },
|
|
1581
|
+
quality: {}
|
|
1582
|
+
};
|
|
1583
|
+
const members = new Set(current.edges.filter((item) => item.domainId === id).map((item) => item.skillKey));
|
|
1584
|
+
members.add(entry.submission.skillKey);
|
|
1585
|
+
const promote = members.size >= threshold;
|
|
1586
|
+
const finalDomain = promote ? {
|
|
1587
|
+
...domain,
|
|
1588
|
+
status: "active",
|
|
1589
|
+
updatedAt: entry.createdAt
|
|
1590
|
+
} : domain;
|
|
1591
|
+
return {
|
|
1592
|
+
mapVersion: nextMapVersion(current.mapVersion),
|
|
1593
|
+
domains: existing === void 0 ? [...current.domains, finalDomain] : current.domains.map((node) => node.id === id ? finalDomain : node),
|
|
1594
|
+
edges: [
|
|
1595
|
+
...current.edges,
|
|
1596
|
+
{
|
|
1597
|
+
...edge,
|
|
1598
|
+
domainId: id
|
|
1599
|
+
},
|
|
1600
|
+
...secondaries.filter((secondary) => secondary.domainId !== id)
|
|
1601
|
+
],
|
|
1602
|
+
provisional: !promote
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
//#endregion
|
|
1606
|
+
//#region lib/types/maintenance.js
|
|
1607
|
+
/** Maintenance defaults from PLAN/MAP-009. */
|
|
1608
|
+
const MAINTENANCE_DEFAULTS = Object.freeze({
|
|
1609
|
+
splitSkillThreshold: 25,
|
|
1610
|
+
coarsenDomainCount: 12,
|
|
1611
|
+
maxVisibleDepth: 3,
|
|
1612
|
+
minChildren: 2,
|
|
1613
|
+
minMembersPerChild: 3,
|
|
1614
|
+
coarsenMinDomains: 3,
|
|
1615
|
+
coarsenMaxDomains: 8
|
|
1616
|
+
});
|
|
1617
|
+
function directMembers(snapshot, domainId) {
|
|
1618
|
+
return new Set(snapshot.edges.filter((edge) => edge.status === "active" && edge.domainId === domainId).map((edge) => edge.skillKey));
|
|
1619
|
+
}
|
|
1620
|
+
function descendants(snapshot, domainId) {
|
|
1621
|
+
const result = /* @__PURE__ */ new Set();
|
|
1622
|
+
const queue = [domainId];
|
|
1623
|
+
while (queue.length > 0) {
|
|
1624
|
+
const current = queue.shift();
|
|
1625
|
+
for (const node of snapshot.domains) {
|
|
1626
|
+
if (!node.parentIds.includes(current) || result.has(node.id)) continue;
|
|
1627
|
+
result.add(node.id);
|
|
1628
|
+
queue.push(node.id);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
return result;
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Audit domain definitions/structure without mutating the map.
|
|
1635
|
+
* @param snapshot - domain facts.
|
|
1636
|
+
* @param visibleSkillKeys - optional view skills for unclassified count.
|
|
1637
|
+
* @returns per-domain audit plus map-level counts.
|
|
1638
|
+
*/
|
|
1639
|
+
function auditDomainMap(snapshot, visibleSkillKeys) {
|
|
1640
|
+
const ids = new Set(snapshot.domains.map((node) => node.id));
|
|
1641
|
+
const primary = /* @__PURE__ */ new Set();
|
|
1642
|
+
for (const edge of snapshot.edges) if (edge.status === "active" && edge.role === "primary") primary.add(edge.skillKey);
|
|
1643
|
+
const domains = snapshot.domains.map((node) => {
|
|
1644
|
+
const direct = directMembers(snapshot, node.id);
|
|
1645
|
+
const indirect = /* @__PURE__ */ new Set();
|
|
1646
|
+
for (const child of descendants(snapshot, node.id)) for (const member of directMembers(snapshot, child)) indirect.add(member);
|
|
1647
|
+
const definitionComplete = node.purpose.trim().length > 0 && node.inScope.length > 0 && node.outOfScope.length > 0 && node.triggers.length > 0;
|
|
1648
|
+
return {
|
|
1649
|
+
domainId: node.id,
|
|
1650
|
+
definitionComplete,
|
|
1651
|
+
childCount: snapshot.domains.filter((child) => child.parentIds.includes(node.id)).length,
|
|
1652
|
+
directSkillCount: direct.size,
|
|
1653
|
+
indirectSkillCount: indirect.size,
|
|
1654
|
+
empty: direct.size === 0,
|
|
1655
|
+
orphan: node.parentIds.some((parent) => !ids.has(parent)),
|
|
1656
|
+
lastUpdated: node.updatedAt
|
|
1657
|
+
};
|
|
1658
|
+
});
|
|
1659
|
+
const unclassifiedCount = [...visibleSkillKeys ?? new Set(snapshot.edges.map((edge) => edge.skillKey))].filter((key) => !primary.has(key)).length;
|
|
1660
|
+
return {
|
|
1661
|
+
domains,
|
|
1662
|
+
domainCount: domains.length,
|
|
1663
|
+
unclassifiedCount
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Find split candidates; no child domains are created and no skills are reassigned.
|
|
1668
|
+
* @param snapshot - domain facts.
|
|
1669
|
+
* @param threshold - minimum direct members to consider.
|
|
1670
|
+
* @returns candidates with blockers.
|
|
1671
|
+
*/
|
|
1672
|
+
function splitCandidates(snapshot, threshold = MAINTENANCE_DEFAULTS.splitSkillThreshold) {
|
|
1673
|
+
return snapshot.domains.flatMap((node) => {
|
|
1674
|
+
const direct = directMembers(snapshot, node.id).size;
|
|
1675
|
+
if (direct < threshold) return [];
|
|
1676
|
+
const children = Math.min(MAINTENANCE_DEFAULTS.coarsenMaxDomains, Math.floor(direct / MAINTENANCE_DEFAULTS.minMembersPerChild));
|
|
1677
|
+
const blockers = [];
|
|
1678
|
+
if (children < MAINTENANCE_DEFAULTS.minChildren) blockers.push("fewer-than-2-children");
|
|
1679
|
+
return [{
|
|
1680
|
+
domainId: node.id,
|
|
1681
|
+
directSkillCount: direct,
|
|
1682
|
+
children,
|
|
1683
|
+
blockers
|
|
1684
|
+
}];
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
/**
|
|
1688
|
+
* Find coarsen groups; only structure candidates, never edge rewrites.
|
|
1689
|
+
* @param snapshot - domain facts.
|
|
1690
|
+
* @param maxDomains - above this count coarsening is considered.
|
|
1691
|
+
* @returns groups of 3–8 domains, at least 2 groups.
|
|
1692
|
+
*/
|
|
1693
|
+
function coarsenCandidates(snapshot, maxDomains = MAINTENANCE_DEFAULTS.coarsenDomainCount) {
|
|
1694
|
+
if (snapshot.domains.length <= maxDomains) return [];
|
|
1695
|
+
const ids = snapshot.domains.map((node) => node.id).sort();
|
|
1696
|
+
const size = Math.min(MAINTENANCE_DEFAULTS.coarsenMaxDomains, Math.max(MAINTENANCE_DEFAULTS.coarsenMinDomains, Math.ceil(ids.length / 2)));
|
|
1697
|
+
const groups = [];
|
|
1698
|
+
for (let index = 0; index < ids.length; index += size) groups.push({ domainIds: ids.slice(index, index + size) });
|
|
1699
|
+
return groups.length >= 2 ? groups : [];
|
|
1700
|
+
}
|
|
1701
|
+
//#endregion
|
|
1702
|
+
//#region lib/types/ui.js
|
|
1703
|
+
const POLICY_KEYS = new Set([
|
|
1704
|
+
"mode",
|
|
1705
|
+
"maxCards",
|
|
1706
|
+
"tokenBudget",
|
|
1707
|
+
"staleAfterHours",
|
|
1708
|
+
"pinDomains",
|
|
1709
|
+
"disableDomains"
|
|
1710
|
+
]);
|
|
1711
|
+
const MODES = new Set([
|
|
1712
|
+
"auto",
|
|
1713
|
+
"off",
|
|
1714
|
+
"on"
|
|
1715
|
+
]);
|
|
1716
|
+
/** @returns true when the policy only edits supported fields. */
|
|
1717
|
+
function isDiscoveryUserPolicy(value) {
|
|
1718
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1719
|
+
const record = value;
|
|
1720
|
+
if (Object.keys(record).some((key) => !POLICY_KEYS.has(key))) return false;
|
|
1721
|
+
if (record.mode !== void 0 && (typeof record.mode !== "string" || !MODES.has(record.mode))) return false;
|
|
1722
|
+
if (record.maxCards !== void 0 && (typeof record.maxCards !== "number" || !Number.isSafeInteger(record.maxCards) || record.maxCards < 1)) return false;
|
|
1723
|
+
if (record.tokenBudget !== void 0 && (typeof record.tokenBudget !== "number" || !Number.isSafeInteger(record.tokenBudget) || record.tokenBudget < 1)) return false;
|
|
1724
|
+
if (record.staleAfterHours !== void 0 && (typeof record.staleAfterHours !== "number" || record.staleAfterHours <= 0)) return false;
|
|
1725
|
+
if (record.pinDomains !== void 0 && !Array.isArray(record.pinDomains)) return false;
|
|
1726
|
+
if (record.disableDomains !== void 0 && !Array.isArray(record.disableDomains)) return false;
|
|
1727
|
+
return true;
|
|
1728
|
+
}
|
|
1729
|
+
/** Static skill_manage description; no dynamic domain list/count/tree. */
|
|
1730
|
+
const SKILL_MANAGE_DESCRIPTION = "Manage skill content. create may include an optional domain proposal when create-side domain cards are available; no proposal means unclassified. Domain assignment never blocks skill creation and is stored as a sidecar.";
|
|
1731
|
+
/** @returns recomputable behavior metrics; null means no denominator. */
|
|
1732
|
+
function computeBehaviorMetrics(observations) {
|
|
1733
|
+
const total = observations.length;
|
|
1734
|
+
const shown = observations.filter((item) => item.awarenessShown).length;
|
|
1735
|
+
const searched = observations.filter((item) => item.searched).length;
|
|
1736
|
+
const successes = observations.filter((item) => item.taskSuccess).length;
|
|
1737
|
+
return {
|
|
1738
|
+
awarenessRecall: shown === 0 ? null : observations.filter((item) => item.awarenessShown && item.awarenessRecalled).length / shown,
|
|
1739
|
+
silentMissRate: shown === 0 ? null : observations.filter((item) => item.silentMiss).length / shown,
|
|
1740
|
+
overSearchRate: searched === 0 ? null : observations.filter((item) => item.searched && item.overSearch).length / searched,
|
|
1741
|
+
underSearchRate: searched === 0 ? null : observations.filter((item) => item.searched && item.underSearch).length / searched,
|
|
1742
|
+
wrongLoadRate: searched === 0 ? null : observations.filter((item) => item.searched && item.wrongLoad).length / searched,
|
|
1743
|
+
taskSuccess: total === 0 ? null : successes / total,
|
|
1744
|
+
tokens: observations.reduce((sum, item) => sum + item.tokens, 0),
|
|
1745
|
+
fallbackRate: searched === 0 ? null : observations.filter((item) => item.searched && item.fallback).length / searched,
|
|
1746
|
+
total
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
//#endregion
|
|
1750
|
+
//#region lib/types/session-store.js
|
|
1751
|
+
/** Plugin-side session metadata; platform SessionHeader has no discovery fields. */
|
|
1752
|
+
var DiscoverySessionStore = class {
|
|
1753
|
+
io;
|
|
1754
|
+
path;
|
|
1755
|
+
constructor(io, path) {
|
|
1756
|
+
this.io = io;
|
|
1757
|
+
this.path = path;
|
|
1758
|
+
}
|
|
1759
|
+
async read() {
|
|
1760
|
+
const content = await this.io.readText(this.path);
|
|
1761
|
+
if (content === null) return {
|
|
1762
|
+
schemaVersion: 1,
|
|
1763
|
+
sessions: {}
|
|
1764
|
+
};
|
|
1765
|
+
try {
|
|
1766
|
+
const parsed = JSON.parse(content);
|
|
1767
|
+
return parsed.schemaVersion === 1 && typeof parsed.sessions === "object" && parsed.sessions !== null ? {
|
|
1768
|
+
schemaVersion: 1,
|
|
1769
|
+
sessions: parsed.sessions
|
|
1770
|
+
} : {
|
|
1771
|
+
schemaVersion: 1,
|
|
1772
|
+
sessions: {}
|
|
1773
|
+
};
|
|
1774
|
+
} catch {
|
|
1775
|
+
return {
|
|
1776
|
+
schemaVersion: 1,
|
|
1777
|
+
sessions: {}
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
/** @returns persisted metadata for one session. */
|
|
1782
|
+
async get(sessionId) {
|
|
1783
|
+
const entry = (await this.read()).sessions[sessionId];
|
|
1784
|
+
return entry === void 0 ? void 0 : {
|
|
1785
|
+
sessionId,
|
|
1786
|
+
...entry
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
/** Persist one session's mode/epoch; later writes win. */
|
|
1790
|
+
async put(record) {
|
|
1791
|
+
const current = await this.read();
|
|
1792
|
+
const previous = current.sessions[record.sessionId];
|
|
1793
|
+
const projectionDigest = record.projectionDigest ?? previous?.projectionDigest;
|
|
1794
|
+
const sessions = {
|
|
1795
|
+
...current.sessions,
|
|
1796
|
+
[record.sessionId]: {
|
|
1797
|
+
discoveryMode: record.discoveryMode,
|
|
1798
|
+
featureEpoch: record.featureEpoch,
|
|
1799
|
+
...projectionDigest === void 0 ? {} : { projectionDigest }
|
|
1800
|
+
}
|
|
1801
|
+
};
|
|
1802
|
+
await this.io.writeText(this.path, JSON.stringify({
|
|
1803
|
+
schemaVersion: 1,
|
|
1804
|
+
sessions
|
|
1805
|
+
}));
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
/** @returns true only for a persisted enabled session in the current feature epoch. */
|
|
1809
|
+
async function shouldProjectPersistedSession(store, sessionId, currentFeatureEpoch) {
|
|
1810
|
+
return shouldHideCatalog(await store.get(sessionId), currentFeatureEpoch);
|
|
1811
|
+
}
|
|
1812
|
+
//#endregion
|
|
1813
|
+
//#region lib/types/runtime.js
|
|
1814
|
+
/** G2 adapter provider wired from ports; no global platform dependency. */
|
|
1815
|
+
var DiscoveryRuntime = class {
|
|
1816
|
+
ports;
|
|
1817
|
+
constructor(ports) {
|
|
1818
|
+
this.ports = ports;
|
|
1819
|
+
}
|
|
1820
|
+
/** @returns true only when persisted session metadata allows projection. */
|
|
1821
|
+
async shouldProject(agent) {
|
|
1822
|
+
return shouldEmitAwareness(await this.ports.sessionStore.get(agent.session.id), this.ports.featureEpoch);
|
|
1823
|
+
}
|
|
1824
|
+
/** @returns a complete projection for this agent's catalog entries. */
|
|
1825
|
+
async project(input) {
|
|
1826
|
+
const view = await new SkillIndex(this.ports.skillIndexSource).read({
|
|
1827
|
+
cwd: input.agent.session.header.cwd ?? "",
|
|
1828
|
+
scopeKey: String(input.agent.id),
|
|
1829
|
+
scope: input.agent
|
|
1830
|
+
});
|
|
1831
|
+
const snapshot = await this.ports.map.read();
|
|
1832
|
+
const estimatedTokens = Math.ceil(input.catalogEntries.reduce((sum, entry) => sum + entry.name.length + entry.description.length, 0) / 4);
|
|
1833
|
+
const promoted = typeof this.ports.promoted === "function" ? this.ports.promoted() : this.ports.promoted === true;
|
|
1834
|
+
const autoMode = this.ports.autoMode === true ? projectionBudgetMode(estimatedTokens, promoted) : this.ports.mode;
|
|
1835
|
+
const result = buildDomainProjection({
|
|
1836
|
+
entries: input.catalogEntries,
|
|
1837
|
+
rows: view.rows,
|
|
1838
|
+
complete: view.complete,
|
|
1839
|
+
snapshot,
|
|
1840
|
+
policy: this.ports.policy,
|
|
1841
|
+
viewKey: view.viewKey,
|
|
1842
|
+
scope: String(input.agent.id),
|
|
1843
|
+
mapVersion: snapshot.mapVersion,
|
|
1844
|
+
policyDigest: this.ports.policyDigest,
|
|
1845
|
+
viewCatalogDigest: view.viewCatalogDigest,
|
|
1846
|
+
adapterVersion: this.ports.adapterVersion,
|
|
1847
|
+
projectionSchemaVersion: this.ports.projectionSchemaVersion,
|
|
1848
|
+
discoveryMode: this.ports.autoMode === true ? autoMode ?? "shadow" : this.ports.discoveryMode,
|
|
1849
|
+
maxCards: this.ports.maxCards,
|
|
1850
|
+
tokenBudget: this.ports.tokenBudget,
|
|
1851
|
+
staleAfterHours: this.ports.staleAfterHours,
|
|
1852
|
+
maxVisibleDepth: this.ports.maxVisibleDepth,
|
|
1853
|
+
now: Date.now(),
|
|
1854
|
+
mode: autoMode,
|
|
1855
|
+
mapStale: this.ports.mapStale,
|
|
1856
|
+
searchAvailable: this.ports.searchAvailable
|
|
1857
|
+
});
|
|
1858
|
+
this.ports.onProjection?.(result);
|
|
1859
|
+
return result;
|
|
1860
|
+
}
|
|
1861
|
+
};
|
|
1862
|
+
//#endregion
|
|
1863
|
+
//#region lib/types/assignment-sidecar.js
|
|
1864
|
+
function submitCreate(service, args, options) {
|
|
1865
|
+
if (typeof args.name !== "string" || args.name.length === 0) return void 0;
|
|
1866
|
+
const parsed = parseFrontmatter(typeof args.content === "string" ? args.content : "");
|
|
1867
|
+
const description = parsed?.frontmatter.description ?? "";
|
|
1868
|
+
const rawWhenToUse = parsed?.frontmatter.whenToUse;
|
|
1869
|
+
const whenToUse = typeof rawWhenToUse === "string" ? rawWhenToUse : void 0;
|
|
1870
|
+
const domain = args.domain;
|
|
1871
|
+
const proposal = typeof domain === "object" && domain !== null ? domain : void 0;
|
|
1872
|
+
return service.submit({
|
|
1873
|
+
skillKey: skillKeyOf({
|
|
1874
|
+
provider: options.provider ?? "dsh-evolution",
|
|
1875
|
+
source: options.source ?? "user-dsh",
|
|
1876
|
+
name: args.name
|
|
1877
|
+
}),
|
|
1878
|
+
threeFieldHash: threeFieldHashOf({
|
|
1879
|
+
name: args.name,
|
|
1880
|
+
description,
|
|
1881
|
+
...whenToUse === void 0 ? {} : { whenToUse }
|
|
1882
|
+
}),
|
|
1883
|
+
modelInvocable: parsed?.frontmatter["disable-model-invocation"] !== true,
|
|
1884
|
+
...whenToUse === void 0 ? {} : { whenToUse },
|
|
1885
|
+
...proposal === void 0 ? {} : { proposal }
|
|
1886
|
+
}).then(() => options.onSubmitted?.());
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Observe successful create calls and submit their optional domain proposal.
|
|
1890
|
+
* Direct creates submit immediately; staged creates submit only after approval.
|
|
1891
|
+
* @param ctx - plugin context with tools and approval events.
|
|
1892
|
+
* @param service - the one assignment service.
|
|
1893
|
+
* @param options - provider/source identity for skillKey.
|
|
1894
|
+
* @returns disposer for both listeners.
|
|
1895
|
+
*/
|
|
1896
|
+
function installAssignmentSidecar(ctx, service, options = {}) {
|
|
1897
|
+
const stopResult = ctx.on("tools/result", (exec, result) => {
|
|
1898
|
+
if (result.isError || exec.name !== "skill_manage" || exec.agent === void 0) return;
|
|
1899
|
+
if (result.value?.pending_id !== void 0) return;
|
|
1900
|
+
const args = exec.arguments;
|
|
1901
|
+
if (typeof args !== "object" || args === null) return;
|
|
1902
|
+
const record = args;
|
|
1903
|
+
if (record.action !== "create") return;
|
|
1904
|
+
submitCreate(service, record, options)?.catch(() => {});
|
|
1905
|
+
});
|
|
1906
|
+
const stopApproval = ctx.on("evolution/approval-decided", ({ record, action, ok }) => {
|
|
1907
|
+
if (!ok || action !== "approve" || record.kind !== "skill" || typeof record.args !== "object" || record.args === null) return;
|
|
1908
|
+
const raw = record.args;
|
|
1909
|
+
const operation = typeof raw.operation === "object" && raw.operation !== null ? raw.operation : raw;
|
|
1910
|
+
if (operation.action !== "create") return;
|
|
1911
|
+
submitCreate(service, operation, options)?.catch(() => {});
|
|
1912
|
+
});
|
|
1913
|
+
return () => {
|
|
1914
|
+
stopResult();
|
|
1915
|
+
stopApproval();
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
//#endregion
|
|
1919
|
+
//#region lib/types/search-judge.js
|
|
1920
|
+
/** Validate a judge packet without accepting partial query coverage. */
|
|
1921
|
+
function validateJudgeRecord(record, expectedQueryIds) {
|
|
1922
|
+
const errors = [];
|
|
1923
|
+
if (record.schemaVersion !== 1 || record.querySet !== "eval-010" || record.judgeKind !== "model" && record.judgeKind !== "human") errors.push("schema-invalid");
|
|
1924
|
+
if (record.judgeId.trim().length === 0) errors.push("judge-id-missing");
|
|
1925
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1926
|
+
for (const observation of record.observations) {
|
|
1927
|
+
if (!expectedQueryIds.includes(observation.queryId)) errors.push("unknown-query:" + observation.queryId);
|
|
1928
|
+
if (seen.has(observation.queryId)) errors.push("duplicate-query:" + observation.queryId);
|
|
1929
|
+
seen.add(observation.queryId);
|
|
1930
|
+
}
|
|
1931
|
+
for (const id of expectedQueryIds) if (!seen.has(id)) errors.push("missing-query:" + id);
|
|
1932
|
+
return errors;
|
|
1933
|
+
}
|
|
1934
|
+
/** @returns aggregate over model/human records; agreement only for shared queries. */
|
|
1935
|
+
function aggregateJudge(records, expectedQueryIds) {
|
|
1936
|
+
const model = records.find((record) => record.judgeKind === "model");
|
|
1937
|
+
const human = records.find((record) => record.judgeKind === "human");
|
|
1938
|
+
const modelLabels = new Map(model?.observations.map((item) => [item.queryId, item.relevant]) ?? []);
|
|
1939
|
+
const humanLabels = new Map(human?.observations.map((item) => [item.queryId, item.relevant]) ?? []);
|
|
1940
|
+
const union = [...new Set([...modelLabels.keys(), ...humanLabels.keys()])];
|
|
1941
|
+
const relevant = union.filter((id) => (modelLabels.get(id) ?? humanLabels.get(id)) === true).length;
|
|
1942
|
+
const shared = [...modelLabels.keys()].filter((id) => humanLabels.has(id));
|
|
1943
|
+
return {
|
|
1944
|
+
queryCount: expectedQueryIds.length,
|
|
1945
|
+
relevantRate: union.length === 0 ? null : relevant / union.length,
|
|
1946
|
+
agreementRate: shared.length === 0 ? null : shared.filter((id) => modelLabels.get(id) === humanLabels.get(id)).length / shared.length,
|
|
1947
|
+
hasModel: model !== void 0,
|
|
1948
|
+
hasHuman: human !== void 0
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1951
|
+
//#endregion
|
|
1952
|
+
//#region lib/types/awareness.js
|
|
1953
|
+
/** Static 1–2 sentence awareness; no domain list/tree. */
|
|
1954
|
+
const DISCOVERY_AWARENESS = "When skill coverage for a task is uncertain, the domain cards and skill_search can reduce it; search is optional, not a required step.";
|
|
1955
|
+
/** Register the static awareness section; absent systemPrompt is a no-op. */
|
|
1956
|
+
function installDiscoveryAwareness(ctx) {
|
|
1957
|
+
const prompt = ctx.get("systemPrompt");
|
|
1958
|
+
if (prompt === void 0) return () => {};
|
|
1959
|
+
return prompt.section({
|
|
1960
|
+
name: "evolution-skill-discovery-awareness",
|
|
1961
|
+
order: 40,
|
|
1962
|
+
text: DISCOVERY_AWARENESS,
|
|
1963
|
+
interpolate: false
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
//#endregion
|
|
1967
|
+
//#region lib/types/assignment-queue.js
|
|
1968
|
+
/** Durable assignment queue; process-local cache over one JSON file. */
|
|
1969
|
+
var JsonAssignmentQueue = class {
|
|
1970
|
+
entries = /* @__PURE__ */ new Map();
|
|
1971
|
+
loaded = false;
|
|
1972
|
+
io;
|
|
1973
|
+
path;
|
|
1974
|
+
constructor(io, path) {
|
|
1975
|
+
this.io = io;
|
|
1976
|
+
this.path = path;
|
|
1977
|
+
}
|
|
1978
|
+
async load() {
|
|
1979
|
+
if (this.loaded) return;
|
|
1980
|
+
this.loaded = true;
|
|
1981
|
+
const content = await this.io.readText(this.path);
|
|
1982
|
+
if (content === null) return;
|
|
1983
|
+
try {
|
|
1984
|
+
const parsed = JSON.parse(content);
|
|
1985
|
+
if (parsed.schemaVersion !== 1 || !Array.isArray(parsed.entries)) return;
|
|
1986
|
+
for (const entry of parsed.entries) this.entries.set(entry.key, entry);
|
|
1987
|
+
} catch {}
|
|
1988
|
+
}
|
|
1989
|
+
async persist() {
|
|
1990
|
+
await this.io.writeText(this.path, JSON.stringify({
|
|
1991
|
+
schemaVersion: 1,
|
|
1992
|
+
entries: [...this.entries.values()]
|
|
1993
|
+
}));
|
|
1994
|
+
}
|
|
1995
|
+
async enqueue(input) {
|
|
1996
|
+
await this.load();
|
|
1997
|
+
const existing = this.entries.get(input.key);
|
|
1998
|
+
if (existing !== void 0) return {
|
|
1999
|
+
entry: existing,
|
|
2000
|
+
duplicate: true
|
|
2001
|
+
};
|
|
2002
|
+
const entry = {
|
|
2003
|
+
...input,
|
|
2004
|
+
status: "queued"
|
|
2005
|
+
};
|
|
2006
|
+
this.entries.set(input.key, entry);
|
|
2007
|
+
await this.persist();
|
|
2008
|
+
return {
|
|
2009
|
+
entry,
|
|
2010
|
+
duplicate: false
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
async update(key, status, pendingReason) {
|
|
2014
|
+
await this.load();
|
|
2015
|
+
const entry = this.entries.get(key);
|
|
2016
|
+
if (entry === void 0) return;
|
|
2017
|
+
entry.status = status;
|
|
2018
|
+
entry.pendingReason = pendingReason;
|
|
2019
|
+
await this.persist();
|
|
2020
|
+
}
|
|
2021
|
+
list() {
|
|
2022
|
+
return [...this.entries.values()];
|
|
2023
|
+
}
|
|
2024
|
+
};
|
|
2025
|
+
//#endregion
|
|
2026
|
+
//#region lib/types/worker.js
|
|
2027
|
+
/** Drivable worker; the composition may call drain on a schedule. */
|
|
2028
|
+
var AssignmentWorker = class {
|
|
2029
|
+
service;
|
|
2030
|
+
queue;
|
|
2031
|
+
constructor(service, queue) {
|
|
2032
|
+
this.service = service;
|
|
2033
|
+
this.queue = queue;
|
|
2034
|
+
}
|
|
2035
|
+
/** Process queued entries once; returns processed count. */
|
|
2036
|
+
async drain(limit = 10) {
|
|
2037
|
+
const entries = this.queue.list().filter((entry) => entry.status === "queued").slice(0, Math.max(0, limit));
|
|
2038
|
+
for (const entry of entries) await this.service.process(entry);
|
|
2039
|
+
return entries.length;
|
|
2040
|
+
}
|
|
2041
|
+
};
|
|
2042
|
+
//#endregion
|
|
2043
|
+
//#region lib/types/domain-bootstrap.js
|
|
2044
|
+
/**
|
|
2045
|
+
* Write an initial LLM domain proposal into the map. Code owns IDs and
|
|
2046
|
+
* validation; no embedding taxonomy and no skill body is read.
|
|
2047
|
+
* @param store - domain map owner.
|
|
2048
|
+
* @param proposal - validated 8–12 domain proposal.
|
|
2049
|
+
* @param skillKeys - evidence skill name -> stable skillKey.
|
|
2050
|
+
* @param expectedMapVersion - CAS precondition.
|
|
2051
|
+
* @returns committed snapshot.
|
|
2052
|
+
*/
|
|
2053
|
+
async function bootstrapDomainMap(store, proposal, skillKeys, expectedMapVersion) {
|
|
2054
|
+
const checked = validateDomainGeneration({ skills: Object.keys(skillKeys).map((name) => ({
|
|
2055
|
+
name,
|
|
2056
|
+
description: name
|
|
2057
|
+
})) }, proposal);
|
|
2058
|
+
if (!checked.ok) throw new Error("invalid domain proposal: " + checked.errors.join("; "));
|
|
2059
|
+
const now = Date.now();
|
|
2060
|
+
const domains = checked.domains.map((domain) => ({
|
|
2061
|
+
id: domain.id,
|
|
2062
|
+
name: domain.title,
|
|
2063
|
+
purpose: domain.purpose,
|
|
2064
|
+
description: domain.purpose,
|
|
2065
|
+
inScope: domain.inScope,
|
|
2066
|
+
outOfScope: domain.outOfScope,
|
|
2067
|
+
triggers: domain.triggers,
|
|
2068
|
+
parentIds: [],
|
|
2069
|
+
status: "provisional",
|
|
2070
|
+
version: 1,
|
|
2071
|
+
createdAt: now,
|
|
2072
|
+
updatedAt: now,
|
|
2073
|
+
descriptionHash: "bootstrap",
|
|
2074
|
+
descriptionVersion: 1,
|
|
2075
|
+
descriptionStatus: "fresh",
|
|
2076
|
+
provenance: { source: "llm" },
|
|
2077
|
+
quality: {}
|
|
2078
|
+
}));
|
|
2079
|
+
const primary = /* @__PURE__ */ new Set();
|
|
2080
|
+
const edges = [];
|
|
2081
|
+
for (const [domainIndex, domain] of checked.domains.entries()) for (const name of domain.evidenceSkillKeys) {
|
|
2082
|
+
const skillKey = skillKeys[name];
|
|
2083
|
+
if (skillKey === void 0) continue;
|
|
2084
|
+
const role = primary.has(skillKey) ? "secondary" : "primary";
|
|
2085
|
+
if (role === "primary") primary.add(skillKey);
|
|
2086
|
+
edges.push({
|
|
2087
|
+
skillKey,
|
|
2088
|
+
domainId: checked.domains[domainIndex].id,
|
|
2089
|
+
role,
|
|
2090
|
+
confidence: 1,
|
|
2091
|
+
source: "create",
|
|
2092
|
+
skillHash: "bootstrap",
|
|
2093
|
+
mapVersion: expectedMapVersion,
|
|
2094
|
+
status: "active",
|
|
2095
|
+
assignedAt: now
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
return store.commit(expectedMapVersion, {
|
|
2099
|
+
mapVersion: String(Number(expectedMapVersion) + 1),
|
|
2100
|
+
domains,
|
|
2101
|
+
edges
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
2104
|
+
//#endregion
|
|
2105
|
+
//#region lib/types/domain-generation-prompt.js
|
|
2106
|
+
/** Frozen initial domain-generation prompt; caller owns schema/IDs. */
|
|
2107
|
+
const DOMAIN_GENERATION_PROMPT_VERSION = "v2";
|
|
2108
|
+
const DOMAIN_GENERATION_PROMPT_V1 = `Group the provided skills into first-level domains.
|
|
2109
|
+
Use only name, description, and whenToUse; do not infer from skill bodies.
|
|
2110
|
+
Produce 8–12 domains that are coherent, distinct, task-recognizable, and useful as an awareness layer. A single skill is not a domain. Each domain has a name, one-sentence purpose, in-scope, out-of-scope, triggers, and evidence skill names that actually support it.
|
|
2111
|
+
Prefer clear boundaries over exhaustive coverage. If the evidence is weak, leave the skill unassigned rather than forcing it into a domain. Together the domains should cover the library where evidence supports it. Domain IDs are assigned by the caller.
|
|
2112
|
+
Return only the structured result.`;
|
|
2113
|
+
/** @returns a parameterized initial-domain prompt; default 8–12. */
|
|
2114
|
+
function buildDomainGenerationPrompt(options = {}) {
|
|
2115
|
+
const requestedMin = options.minDomains ?? 8;
|
|
2116
|
+
const requestedMax = options.maxDomains ?? 12;
|
|
2117
|
+
const valid = Number.isSafeInteger(requestedMin) && Number.isSafeInteger(requestedMax) && requestedMin >= 2 && requestedMax >= requestedMin && requestedMax <= 20;
|
|
2118
|
+
return `You are designing a first-level domain map for a skill library.
|
|
2119
|
+
Input is limited to each skill's name, description, and whenToUse; do not infer from skill bodies.
|
|
2120
|
+
Choose ${valid ? requestedMin : 8}–${valid ? requestedMax : 12} domains that help a model decide where to look. Domains are an awareness layer, not a complete taxonomy.
|
|
2121
|
+
Each domain needs a name, one-sentence purpose, in-scope, out-of-scope, triggers, and evidence skill names copied from the input.
|
|
2122
|
+
Keep domains coherent, distinguishable, and recognizable from a task description; a single skill is not a domain. Do not invent skills or domains.
|
|
2123
|
+
Prefer clear boundaries over exhaustive coverage. Leave a skill unassigned when evidence is weak rather than forcing it. The caller assigns IDs.
|
|
2124
|
+
Return only the structured result.`;
|
|
2125
|
+
}
|
|
2126
|
+
const DOMAIN_GENERATION_PROMPT_V2 = buildDomainGenerationPrompt();
|
|
2127
|
+
const DOMAIN_GENERATION_PROMPT_HASH = createHash("sha256").update(DOMAIN_GENERATION_PROMPT_V2, "utf8").digest("hex");
|
|
2128
|
+
/** Active prompt; v1 remains frozen for traceability. */
|
|
2129
|
+
const DOMAIN_GENERATION_PROMPT = DOMAIN_GENERATION_PROMPT_V2;
|
|
2130
|
+
//#endregion
|
|
2131
|
+
//#region lib/types/domain-generation-service.js
|
|
2132
|
+
/**
|
|
2133
|
+
* Generate and persist the initial map only when the map has no domains.
|
|
2134
|
+
* @param store - map owner.
|
|
2135
|
+
* @param port - external LLM adapter.
|
|
2136
|
+
* @param skills - name/description/whenToUse facts.
|
|
2137
|
+
* @param skillKeys - evidence name -> stable key.
|
|
2138
|
+
* @param expectedMapVersion - CAS precondition.
|
|
2139
|
+
* @returns committed snapshot, or existing snapshot when already bootstrapped.
|
|
2140
|
+
*/
|
|
2141
|
+
async function initialDomainMap(store, port, skills, skillKeys, expectedMapVersion, signal, prompt = DOMAIN_GENERATION_PROMPT) {
|
|
2142
|
+
const current = await store.read();
|
|
2143
|
+
if (current.domains.length > 0) return current;
|
|
2144
|
+
return bootstrapDomainMap(store, await port.generate({
|
|
2145
|
+
skills,
|
|
2146
|
+
prompt,
|
|
2147
|
+
...signal === void 0 ? {} : { signal }
|
|
2148
|
+
}), skillKeys, expectedMapVersion);
|
|
2149
|
+
}
|
|
2150
|
+
//#endregion
|
|
2151
|
+
//#region lib/types/shadow-evidence.js
|
|
2152
|
+
/** Durable shadow-run evidence; every complete shadow projection is one sample. */
|
|
2153
|
+
var ShadowEvidenceStore = class {
|
|
2154
|
+
io;
|
|
2155
|
+
path;
|
|
2156
|
+
samples = 0;
|
|
2157
|
+
recovered = 0;
|
|
2158
|
+
hidden = 0;
|
|
2159
|
+
silentMisses = 0;
|
|
2160
|
+
loaded = false;
|
|
2161
|
+
constructor(io, path) {
|
|
2162
|
+
this.io = io;
|
|
2163
|
+
this.path = path;
|
|
2164
|
+
}
|
|
2165
|
+
async load() {
|
|
2166
|
+
if (this.loaded) return;
|
|
2167
|
+
this.loaded = true;
|
|
2168
|
+
const content = await this.io.readText(this.path);
|
|
2169
|
+
if (content === null) return;
|
|
2170
|
+
try {
|
|
2171
|
+
const parsed = JSON.parse(content);
|
|
2172
|
+
if (parsed.schemaVersion === 1 && Number.isSafeInteger(parsed.samples) && parsed.samples >= 0) {
|
|
2173
|
+
this.samples = parsed.samples;
|
|
2174
|
+
this.recovered = Number.isSafeInteger(parsed.recovered) && parsed.recovered >= 0 ? parsed.recovered : 0;
|
|
2175
|
+
this.hidden = Number.isSafeInteger(parsed.hidden) && parsed.hidden >= 0 ? parsed.hidden : 0;
|
|
2176
|
+
this.silentMisses = Number.isSafeInteger(parsed.silentMisses) && parsed.silentMisses >= 0 ? parsed.silentMisses : 0;
|
|
2177
|
+
}
|
|
2178
|
+
} catch {}
|
|
2179
|
+
}
|
|
2180
|
+
async record(result) {
|
|
2181
|
+
if (result.shadowHiddenNames === void 0 || result.shadowHiddenNames.length === 0) return;
|
|
2182
|
+
await this.load();
|
|
2183
|
+
this.samples += 1;
|
|
2184
|
+
this.hidden += result.shadowHiddenNames.length;
|
|
2185
|
+
this.recovered += result.shadowRecoveredNames?.length ?? 0;
|
|
2186
|
+
await this.io.writeText(this.path, JSON.stringify({
|
|
2187
|
+
schemaVersion: 1,
|
|
2188
|
+
samples: this.samples,
|
|
2189
|
+
recovered: this.recovered,
|
|
2190
|
+
hidden: this.hidden,
|
|
2191
|
+
silentMisses: this.silentMisses
|
|
2192
|
+
}));
|
|
2193
|
+
}
|
|
2194
|
+
evidence() {
|
|
2195
|
+
return {
|
|
2196
|
+
samples: this.samples,
|
|
2197
|
+
hiddenRecoverableRate: this.hidden > 0 ? this.recovered / this.hidden : 0,
|
|
2198
|
+
silentMisses: this.silentMisses
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
canPromote() {
|
|
2202
|
+
return canPromoteShadow(this.evidence());
|
|
2203
|
+
}
|
|
2204
|
+
};
|
|
2205
|
+
//#endregion
|
|
2206
|
+
//#region lib/types/plugin.js
|
|
2207
|
+
const name = "evolution-skill-discovery";
|
|
2208
|
+
const inject = [
|
|
2209
|
+
"skills",
|
|
2210
|
+
"evolutionIo",
|
|
2211
|
+
"systemPrompt",
|
|
2212
|
+
"tools"
|
|
2213
|
+
];
|
|
2214
|
+
var PlatformSkillIndexSource = class {
|
|
2215
|
+
ctx;
|
|
2216
|
+
library;
|
|
2217
|
+
constructor(ctx, library) {
|
|
2218
|
+
this.ctx = ctx;
|
|
2219
|
+
this.library = library;
|
|
2220
|
+
}
|
|
2221
|
+
async snapshot(query) {
|
|
2222
|
+
const snapshot = await this.ctx.skills.snapshot({
|
|
2223
|
+
cwd: query.cwd,
|
|
2224
|
+
...query.scope === void 0 ? {} : { scope: query.scope }
|
|
2225
|
+
});
|
|
2226
|
+
return {
|
|
2227
|
+
skills: snapshot.skills,
|
|
2228
|
+
complete: snapshot.complete
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
async managedSkillKeys() {
|
|
2232
|
+
const summaries = await this.library.list();
|
|
2233
|
+
return new Set(summaries.filter((summary) => summary.managed).map((summary) => skillKeyOf({
|
|
2234
|
+
provider: "dsh-evolution",
|
|
2235
|
+
source: "user-dsh",
|
|
2236
|
+
name: summary.name
|
|
2237
|
+
})));
|
|
2238
|
+
}
|
|
2239
|
+
async protectedSkillKeys() {
|
|
2240
|
+
const summaries = await this.library.list();
|
|
2241
|
+
return new Set(summaries.filter((summary) => summary.protectedBy !== null || summary.protectionUnknown).map((summary) => skillKeyOf({
|
|
2242
|
+
provider: "dsh-evolution",
|
|
2243
|
+
source: "user-dsh",
|
|
2244
|
+
name: summary.name
|
|
2245
|
+
})));
|
|
2246
|
+
}
|
|
2247
|
+
async pinnedSkillKeys() {
|
|
2248
|
+
const summaries = await this.library.list();
|
|
2249
|
+
return new Set(summaries.filter((summary) => summary.protectedBy === "pinned").map((summary) => skillKeyOf({
|
|
2250
|
+
provider: "dsh-evolution",
|
|
2251
|
+
source: "user-dsh",
|
|
2252
|
+
name: summary.name
|
|
2253
|
+
})));
|
|
2254
|
+
}
|
|
2255
|
+
};
|
|
2256
|
+
/** Compose discovery services from platform ports; no catalog/provider replacement. */
|
|
2257
|
+
function apply(ctx, config) {
|
|
2258
|
+
if (typeof config.featureEpoch !== "string" || config.featureEpoch.length === 0) throw new Error("evolution-skill-discovery: featureEpoch is required");
|
|
2259
|
+
if (config.provisionalThreshold !== void 0 && (!Number.isSafeInteger(config.provisionalThreshold) || config.provisionalThreshold < 1)) throw new Error("evolution-skill-discovery: provisionalThreshold must be a positive integer");
|
|
2260
|
+
const evolutionIo = ctx.evolutionIo;
|
|
2261
|
+
const io = evolutionIoAdapter(() => evolutionIo.provider());
|
|
2262
|
+
const source = new PlatformSkillIndexSource(ctx, newSkillLibrary({
|
|
2263
|
+
config: {},
|
|
2264
|
+
io,
|
|
2265
|
+
ctx,
|
|
2266
|
+
authority: authorityFrom({ origin: "foreground" })
|
|
2267
|
+
}));
|
|
2268
|
+
const dshHome = process.env.DSH_HOME ?? join(homedir(), ".dsh");
|
|
2269
|
+
const map = new DomainMapFileStore(io, config.domainMapPath ?? join(dshHome, "evolution", "domain-map.json"));
|
|
2270
|
+
if (config.domainGeneration !== void 0 && config.domainGenerationSkills !== void 0 && config.domainGenerationSkillKeys !== void 0) initialDomainMap(map, config.domainGeneration, config.domainGenerationSkills, config.domainGenerationSkillKeys, "0", void 0, buildDomainGenerationPrompt({
|
|
2271
|
+
...config.domainMinDomains === void 0 ? {} : { minDomains: config.domainMinDomains },
|
|
2272
|
+
...config.domainMaxDomains === void 0 ? {} : { maxDomains: config.domainMaxDomains }
|
|
2273
|
+
})).catch(() => {});
|
|
2274
|
+
if (config.bootstrapDomains !== void 0 && config.bootstrapSkillKeys !== void 0) bootstrapDomainMap(map, config.bootstrapDomains, config.bootstrapSkillKeys, "0").catch(() => {});
|
|
2275
|
+
const sessions = new DiscoverySessionStore(io, config.sessionPath ?? join(dshHome, "evolution", "discovery-sessions.json"));
|
|
2276
|
+
const policy = mergeDomainPolicy({
|
|
2277
|
+
mode: config.mode === "shadow" || config.mode === void 0 || config.mode === "auto" ? "auto" : config.mode,
|
|
2278
|
+
pinDomains: config.pinDomains,
|
|
2279
|
+
disableDomains: config.disableDomains
|
|
2280
|
+
});
|
|
2281
|
+
const shadowStore = new ShadowEvidenceStore(io, config.shadowEvidencePath ?? join(dshHome, "evolution", "shadow-evidence.json"));
|
|
2282
|
+
let promoted = config.shadowEvidence !== void 0 && canPromoteShadow(config.shadowEvidence);
|
|
2283
|
+
shadowStore.load().then(() => {
|
|
2284
|
+
if (shadowStore.canPromote()) promoted = true;
|
|
2285
|
+
}).catch(() => {});
|
|
2286
|
+
const policyDigest = policyDigestOf({
|
|
2287
|
+
globalPolicyRevision: "config",
|
|
2288
|
+
effectivePolicy: {
|
|
2289
|
+
mode: policy.mode,
|
|
2290
|
+
pinned: policy.pinned,
|
|
2291
|
+
disabled: policy.disabled
|
|
2292
|
+
}
|
|
2293
|
+
});
|
|
2294
|
+
const runtime = new DiscoveryRuntime({
|
|
2295
|
+
skillIndexSource: source,
|
|
2296
|
+
map,
|
|
2297
|
+
policy,
|
|
2298
|
+
sessionStore: sessions,
|
|
2299
|
+
featureEpoch: config.featureEpoch,
|
|
2300
|
+
mapVersion: "0",
|
|
2301
|
+
policyDigest,
|
|
2302
|
+
adapterVersion: "1",
|
|
2303
|
+
projectionSchemaVersion: "1",
|
|
2304
|
+
discoveryMode: config.mode === "auto" ? "shadow" : config.mode ?? "off",
|
|
2305
|
+
maxCards: config.maxCards,
|
|
2306
|
+
tokenBudget: config.tokenBudget ?? 1200,
|
|
2307
|
+
staleAfterHours: config.staleAfterHours,
|
|
2308
|
+
mode: config.mode === "auto" ? "shadow" : config.mode === "shadow" ? "shadow" : config.mode === "on" ? "on" : "off",
|
|
2309
|
+
autoMode: config.mode === "auto",
|
|
2310
|
+
promoted: () => promoted,
|
|
2311
|
+
onProjection: (result) => {
|
|
2312
|
+
shadowStore.record(result).then(() => {
|
|
2313
|
+
if (shadowStore.canPromote()) promoted = true;
|
|
2314
|
+
}).catch(() => {});
|
|
2315
|
+
},
|
|
2316
|
+
mapStale: config.mapStale ?? false,
|
|
2317
|
+
searchAvailable: config.searchAvailable ?? true
|
|
2318
|
+
});
|
|
2319
|
+
ctx.on("agent/created", async ({ agent, source }) => {
|
|
2320
|
+
try {
|
|
2321
|
+
if (source === "startup") {
|
|
2322
|
+
await sessions.put({
|
|
2323
|
+
sessionId: String(agent.session.id),
|
|
2324
|
+
discoveryMode: config.mode === "on" ? "on" : config.mode === "shadow" || config.mode === "auto" ? "shadow" : "off",
|
|
2325
|
+
featureEpoch: config.featureEpoch
|
|
2326
|
+
});
|
|
2327
|
+
if (config.mode !== "off") installDiscoveryAwareness(agent.ctx);
|
|
2328
|
+
} else if (shouldInstallAwarenessOnResume(await sessions.get(String(agent.session.id)), config.featureEpoch)) installDiscoveryAwareness(agent.ctx);
|
|
2329
|
+
} catch {}
|
|
2330
|
+
});
|
|
2331
|
+
installCatalogProjection(ctx, runtime, {
|
|
2332
|
+
async get(agentKey) {
|
|
2333
|
+
return (await sessions.get(agentKey))?.projectionDigest;
|
|
2334
|
+
},
|
|
2335
|
+
async set(agentKey, digest) {
|
|
2336
|
+
const record = await sessions.get(agentKey);
|
|
2337
|
+
if (record !== void 0) await sessions.put({
|
|
2338
|
+
...record,
|
|
2339
|
+
projectionDigest: digest
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
});
|
|
2343
|
+
const queue = new JsonAssignmentQueue(io, join(dshHome, "evolution", "assignment-queue.json"));
|
|
2344
|
+
const assignment = new DomainAssignmentService(queue, map, {
|
|
2345
|
+
createClassificationEnabled: config.createClassificationEnabled ?? false,
|
|
2346
|
+
...config.provisionalThreshold === void 0 ? {} : { provisionalThreshold: config.provisionalThreshold },
|
|
2347
|
+
now: Date.now
|
|
2348
|
+
});
|
|
2349
|
+
const worker = new AssignmentWorker(assignment, queue);
|
|
2350
|
+
queue.load().then(() => worker.drain()).catch(() => {});
|
|
2351
|
+
installAssignmentSidecar(ctx, assignment, { onSubmitted: () => {
|
|
2352
|
+
worker.drain().catch(() => {});
|
|
2353
|
+
} });
|
|
2354
|
+
const commandRuntime = ctx.get("commands");
|
|
2355
|
+
if (commandRuntime !== void 0) ctx.effect(() => commandRuntime.register({
|
|
2356
|
+
name: "skill-domain",
|
|
2357
|
+
description: "List provisional skill domains or confirm one by id.",
|
|
2358
|
+
async handler({ rawInput }) {
|
|
2359
|
+
const [action, domainId] = rawInput.trim().split(/\s+/);
|
|
2360
|
+
if (action === "list" || action === void 0) {
|
|
2361
|
+
const provisional = (await map.read()).domains.filter((node) => node.status === "provisional");
|
|
2362
|
+
return {
|
|
2363
|
+
kind: "success",
|
|
2364
|
+
text: provisional.length === 0 ? "No provisional domains." : "Provisional domains:\n" + provisional.map((node) => "- " + node.id + ": " + node.name).join("\n")
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
if (action === "confirm" && domainId !== void 0) return await assignment.confirm(domainId) ? {
|
|
2368
|
+
kind: "success",
|
|
2369
|
+
text: "Confirmed domain " + domainId + "."
|
|
2370
|
+
} : {
|
|
2371
|
+
kind: "error",
|
|
2372
|
+
text: "Unknown or non-provisional domain: " + domainId
|
|
2373
|
+
};
|
|
2374
|
+
return {
|
|
2375
|
+
kind: "error",
|
|
2376
|
+
text: "Usage: /skill-domain list | /skill-domain confirm <id>"
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
}));
|
|
2380
|
+
const searchCache = new SkillSearchCache();
|
|
2381
|
+
ctx.on("evolution/skill-mutated", () => {
|
|
2382
|
+
searchCache.clear();
|
|
2383
|
+
});
|
|
2384
|
+
registerSkillSearchTool(ctx, {
|
|
2385
|
+
async allowedSkillKeys(agent) {
|
|
2386
|
+
const view = await new SkillIndex(source).read({
|
|
2387
|
+
cwd: agent?.session.header.cwd ?? process.cwd(),
|
|
2388
|
+
scopeKey: agent === void 0 ? "tool" : String(agent.id),
|
|
2389
|
+
...agent === void 0 ? {} : { scope: agent }
|
|
2390
|
+
});
|
|
2391
|
+
const snapshot = await map.read();
|
|
2392
|
+
const blocked = new Set(snapshot.edges.filter((edge) => edge.status === "active" && policy.disabled.has(edge.domainId)).map((edge) => edge.skillKey));
|
|
2393
|
+
return new Set(view.rows.filter((row) => row.modelInvocable && !blocked.has(row.skillKey)).map((row) => row.skillKey));
|
|
2394
|
+
},
|
|
2395
|
+
async search(query, options, agent) {
|
|
2396
|
+
const cwd = agent?.session.header.cwd ?? process.cwd();
|
|
2397
|
+
const view = await new SkillIndex(source).read({
|
|
2398
|
+
cwd,
|
|
2399
|
+
scopeKey: agent === void 0 ? "tool" : String(agent.id),
|
|
2400
|
+
...agent === void 0 ? {} : { scope: agent }
|
|
2401
|
+
});
|
|
2402
|
+
const cacheKey = {
|
|
2403
|
+
viewKey: view.viewKey,
|
|
2404
|
+
viewCatalogDigest: view.viewCatalogDigest,
|
|
2405
|
+
policyDigest,
|
|
2406
|
+
discoveryMode: config.mode ?? "auto"
|
|
2407
|
+
};
|
|
2408
|
+
const cached = await searchCache.get(cacheKey, view.rows);
|
|
2409
|
+
if (cached === void 0) return {
|
|
2410
|
+
results: [],
|
|
2411
|
+
truncated: false,
|
|
2412
|
+
degraded: true,
|
|
2413
|
+
hint: "Skill search index is unavailable. Use skill(name) or the full catalog."
|
|
2414
|
+
};
|
|
2415
|
+
const result = await cached.index.search(query, options);
|
|
2416
|
+
return result.stale || cached.stale ? {
|
|
2417
|
+
...result,
|
|
2418
|
+
stale: true
|
|
2419
|
+
} : result;
|
|
2420
|
+
}
|
|
2421
|
+
});
|
|
2422
|
+
}
|
|
2423
|
+
//#endregion
|
|
2424
|
+
//#region lib/types/index.js
|
|
2425
|
+
function bySkillKey(left, right) {
|
|
2426
|
+
return left.skillKey < right.skillKey ? -1 : left.skillKey > right.skillKey ? 1 : 0;
|
|
2427
|
+
}
|
|
2428
|
+
/** Read-only view model; it does not cache or own a provider. */
|
|
2429
|
+
var SkillIndex = class {
|
|
2430
|
+
source;
|
|
2431
|
+
constructor(source) {
|
|
2432
|
+
this.source = source;
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* Read one view from the source.
|
|
2436
|
+
* @param query - cwd/scope and cancellation for this observation.
|
|
2437
|
+
* @returns normalized rows and completeness; incomplete data is not hidden.
|
|
2438
|
+
*/
|
|
2439
|
+
async read(query) {
|
|
2440
|
+
const [snapshot, managed, protectedKeys, pinnedKeys] = await Promise.all([
|
|
2441
|
+
this.source.snapshot(query),
|
|
2442
|
+
this.source.managedSkillKeys(),
|
|
2443
|
+
this.source.protectedSkillKeys?.() ?? Promise.resolve(/* @__PURE__ */ new Set()),
|
|
2444
|
+
this.source.pinnedSkillKeys?.() ?? Promise.resolve(/* @__PURE__ */ new Set())
|
|
2445
|
+
]);
|
|
2446
|
+
const rows = snapshot.skills.map((summary) => skillIndexRowOf(summary, managed.has(skillKeyOf(summary)), protectedKeys.has(skillKeyOf(summary)), pinnedKeys.has(skillKeyOf(summary)))).sort(bySkillKey);
|
|
2447
|
+
return {
|
|
2448
|
+
viewKey: viewKeyOf(query),
|
|
2449
|
+
catalogRevision: catalogRevisionOf(rows, snapshot.complete),
|
|
2450
|
+
viewCatalogDigest: viewCatalogDigestOf(viewKeyOf(query), rows, snapshot.complete),
|
|
2451
|
+
rows,
|
|
2452
|
+
complete: snapshot.complete
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
};
|
|
2456
|
+
//#endregion
|
|
2457
|
+
export { AssignmentWorker, DISCOVERY_AWARENESS, DISCOVERY_STAGES, DOMAIN_GENERATION_PROMPT, DOMAIN_GENERATION_PROMPT_HASH, DOMAIN_GENERATION_PROMPT_V1, DOMAIN_GENERATION_PROMPT_V2, DOMAIN_GENERATION_PROMPT_VERSION, DOMAIN_MAP_SCHEMA_VERSION, DiscoveryRuntime, DiscoverySessionStore, DomainAssignmentService, DomainMapCorruptError, DomainMapFileStore, DomainMapValidationError, DomainMapVersionConflict, DomainPolicyValidationError, JsonAssignmentQueue, MAINTENANCE_DEFAULTS, MemoryAssignmentQueue, SKILL_MANAGE_DESCRIPTION, SKILL_SEARCH_DESCRIPTION, SKILL_SEARCH_FIELDS, SKILL_SEARCH_INDEX_SCHEMA_VERSION, SKILL_SEARCH_TOKENIZER_VERSION, ShadowEvidenceStore, SkillIndex, SkillSearchCache, SkillSearchIndex, V0_DISCOVERY_FEATURES, aggregateJudge, apply, applyCatalogProjection, assignmentKey, auditDomainMap, bootstrapDomainMap, buildDomainGenerationPrompt, buildDomainProjection, canClassifyAtCreate, canPromoteShadow, catalogRevisionOf, coarsenCandidates, computeBehaviorMetrics, computeDomainMetrics, coveredByDomain, descriptionHashOf, descriptionIsServable, domainGenerationMode, domainViewCounts, evaluateDomainQuality, hiddenRecoverable, initialDomainMap, inject, installAssignmentSidecar, installCatalogProjection, installDiscoveryAwareness, isDiscoveryUserPolicy, isSkillSearch, mayAdvance, mergeDomainPolicy, mintDomainId, name, policyDigestOf, projectCatalog, projectionBudgetMode, projectionDigestOf, proposalDigest, registerSkillSearchTool, renderDomainDescription, sanitizeUntrustedText, searchIndexKeyOf, searchTerms, searchWithDegraded, shouldEmitAwareness, shouldEmitDigest, shouldHideCatalog, shouldInstallAwarenessOnResume, shouldProjectPersistedSession, shouldRegenerateDescription, skillIndexRowOf, skillKeyOf, splitCandidates, stageIndex, threeFieldHashOf, validateDomainGeneration, validateJudgeRecord, validateProposal, viewCatalogDigestOf, viewKeyOf };
|