@lmzhen/dsh-evolution-skill-discovery 0.0.0-stage → 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.
Files changed (37) hide show
  1. package/README.md +16 -2
  2. package/lib/index.js +2457 -0
  3. package/lib/types/adapter.d.ts +48 -0
  4. package/lib/types/assignment-queue.d.ts +24 -0
  5. package/lib/types/assignment-sidecar.d.ts +30 -0
  6. package/lib/types/assignment.d.ts +131 -0
  7. package/lib/types/awareness.d.ts +6 -0
  8. package/lib/types/domain-bootstrap.d.ts +14 -0
  9. package/lib/types/domain-description.d.ts +19 -0
  10. package/lib/types/domain-generation-prompt.d.ts +14 -0
  11. package/lib/types/domain-generation-service.d.ts +23 -0
  12. package/lib/types/domain-generation.d.ts +48 -0
  13. package/lib/types/domain-map-store.d.ts +39 -0
  14. package/lib/types/domain-map.d.ts +52 -0
  15. package/lib/types/domain-metrics.d.ts +41 -0
  16. package/lib/types/domain-policy.d.ts +34 -0
  17. package/lib/types/domain-projection.d.ts +43 -0
  18. package/lib/types/domain-quality.d.ts +17 -0
  19. package/lib/types/domain-view.d.ts +15 -0
  20. package/lib/types/features.d.ts +11 -0
  21. package/lib/types/identity.d.ts +67 -0
  22. package/lib/types/index.d.ts +74 -0
  23. package/lib/types/maintenance.d.ts +62 -0
  24. package/lib/types/plugin.d.ts +37 -0
  25. package/lib/types/projection.d.ts +58 -0
  26. package/lib/types/runtime.d.ts +42 -0
  27. package/lib/types/search-judge.d.ts +26 -0
  28. package/lib/types/search.d.ts +84 -0
  29. package/lib/types/security.d.ts +14 -0
  30. package/lib/types/session-projection.d.ts +19 -0
  31. package/lib/types/session-store.d.ts +22 -0
  32. package/lib/types/shadow-evidence.d.ts +22 -0
  33. package/lib/types/stages.d.ts +6 -0
  34. package/lib/types/tool.d.ts +26 -0
  35. package/lib/types/ui.d.ts +69 -0
  36. package/lib/types/worker.d.ts +14 -0
  37. package/package.json +58 -4
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
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 };