@agentproto/corpus 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,3380 @@
1
+ export { systemClock } from './chunk-KYX2DTEH.mjs';
2
+ import { CandidatesSidecar } from './chunk-UPX26MYH.mjs';
3
+ export { CandidatesSidecar, SidecarDuplicateError, SidecarNotFoundError } from './chunk-UPX26MYH.mjs';
4
+ import matter from 'gray-matter';
5
+ import { createHash } from 'crypto';
6
+ import { z } from 'zod';
7
+ import { parse, stringify } from 'yaml';
8
+ import Ajv2020 from 'ajv/dist/2020.js';
9
+ import addFormats from 'ajv-formats';
10
+ import { createRegistry } from '@agentproto/registry';
11
+
12
+ /**
13
+ * @agentproto/corpus v0.1.0-alpha
14
+ * Composition of AIP-10/12/18/9/15/41 — autonomous knowledge-improvement kit.
15
+ */
16
+
17
+ // src/util/slug.ts
18
+ var SOURCE_SLUG = /^[a-z0-9][a-z0-9-]*$/;
19
+ var ENTRY_SLUG = /^[a-z][a-z0-9-]*[a-z0-9]$/;
20
+ function slugify(input, opts = {}) {
21
+ const maxLen = opts.maxLen ?? 96;
22
+ const fallback = opts.fallback ?? "item";
23
+ let s = input.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "");
24
+ if (opts.stripScheme) s = s.replace(/^https?:\/\//, "");
25
+ s = s.replace(/[^a-z0-9]+/g, "-").replace(/-{2,}/g, "-").slice(0, maxLen).replace(/^-+|-+$/g, "");
26
+ if (opts.leadingLetter && s && !/^[a-z]/.test(s)) {
27
+ s = `e-${s}`.slice(0, maxLen).replace(/-+$/g, "");
28
+ }
29
+ return s.length >= 2 ? s : fallback;
30
+ }
31
+ function uniqueSlug(base, seen, maxLen = 96) {
32
+ let slug = base;
33
+ let n = 2;
34
+ while (seen.has(slug)) {
35
+ slug = `${base}-${n++}`.slice(0, maxLen).replace(/-+$/g, "");
36
+ }
37
+ seen.add(slug);
38
+ return slug;
39
+ }
40
+ function isSourceSlug(s) {
41
+ return SOURCE_SLUG.test(s) && s.length >= 2 && s.length <= 96;
42
+ }
43
+ function isEntrySlug(s) {
44
+ return ENTRY_SLUG.test(s) && s.length >= 2 && s.length <= 96;
45
+ }
46
+
47
+ // src/util/language.ts
48
+ var NAME_TO_CODE = {
49
+ english: "en",
50
+ french: "fr",
51
+ spanish: "es",
52
+ german: "de",
53
+ italian: "it",
54
+ portuguese: "pt",
55
+ dutch: "nl",
56
+ russian: "ru",
57
+ japanese: "ja",
58
+ korean: "ko",
59
+ chinese: "zh",
60
+ arabic: "ar",
61
+ hindi: "hi",
62
+ turkish: "tr",
63
+ polish: "pl",
64
+ swedish: "sv",
65
+ norwegian: "no",
66
+ danish: "da",
67
+ finnish: "fi",
68
+ greek: "el",
69
+ hebrew: "he",
70
+ thai: "th",
71
+ vietnamese: "vi",
72
+ indonesian: "id",
73
+ ukrainian: "uk"
74
+ };
75
+ function normalizeLanguageTag(raw) {
76
+ if (!raw) return void 0;
77
+ const v = raw.trim().toLowerCase();
78
+ if (!v) return void 0;
79
+ const named = NAME_TO_CODE[v];
80
+ if (named) return named;
81
+ const parts = v.split(/[-_]/);
82
+ const primary = parts[0];
83
+ if (!primary || !/^[a-z]{2}$/.test(primary)) return void 0;
84
+ const region = parts.slice(1).find((p) => /^[a-z]{2}$/.test(p));
85
+ return region ? `${primary}-${region.toUpperCase()}` : primary;
86
+ }
87
+
88
+ // src/lifecycle/attestations.ts
89
+ function appendAttestation(frontmatter, attestation) {
90
+ const metaIn = frontmatter.metadata ?? {};
91
+ const corpusIn = metaIn.corpus ?? {};
92
+ const existing = corpusIn.attestations ?? [];
93
+ const last = existing[existing.length - 1];
94
+ if (last && last.kind === attestation.kind && last.identity === attestation.identity && last.at === attestation.at) {
95
+ return { ...frontmatter };
96
+ }
97
+ return {
98
+ ...frontmatter,
99
+ metadata: {
100
+ ...metaIn,
101
+ corpus: {
102
+ ...corpusIn,
103
+ attestations: [...existing, attestation]
104
+ }
105
+ }
106
+ };
107
+ }
108
+ function readAttestations(frontmatter) {
109
+ const meta = frontmatter.metadata;
110
+ const list = meta?.corpus?.attestations;
111
+ if (!Array.isArray(list)) return [];
112
+ return Object.freeze(list);
113
+ }
114
+ function makeAttestation(input) {
115
+ return Object.freeze({
116
+ kind: input.kind,
117
+ identity: input.identity,
118
+ at: input.at,
119
+ ...input.model ? { model: input.model } : {},
120
+ ...input.promptHash ? { promptHash: input.promptHash } : {},
121
+ ...input.note ? { note: input.note } : {}
122
+ });
123
+ }
124
+
125
+ // src/access/policy.ts
126
+ var DEFAULT_CLASSIFICATION = "internal";
127
+ function evaluateAccess(spec, caller, context = {}) {
128
+ const classification = spec?.classification ?? DEFAULT_CLASSIFICATION;
129
+ if (spec) {
130
+ if (matchesAllowed("roles", spec.allowedRoles, caller))
131
+ return permit(`allowedRoles match`, classification);
132
+ if (matchesAllowed("operators", spec.allowedOperators, caller))
133
+ return permit(`allowedOperators match`, classification);
134
+ if (matchesAllowed("users", spec.allowedUsers, caller))
135
+ return permit(`allowedUsers match`, classification);
136
+ if (matchesAllowed("guilds", spec.allowedGuilds, caller))
137
+ return permit(`allowedGuilds match`, classification);
138
+ if (matchesAllowed("orgs", spec.allowedOrgs, caller))
139
+ return permit(`allowedOrgs match`, classification);
140
+ }
141
+ switch (classification) {
142
+ case "public":
143
+ return {
144
+ permitted: true,
145
+ reason: "classification=public",
146
+ redactBytes: false
147
+ };
148
+ case "internal": {
149
+ if (!context.homeGuild) {
150
+ return {
151
+ permitted: false,
152
+ reason: "classification=internal but no homeGuild context \u2014 fail-closed",
153
+ redactBytes: false
154
+ };
155
+ }
156
+ const expected = `ws://guilds/${context.homeGuild}`;
157
+ const inHomeGuild = caller.identityTree.includes(expected);
158
+ return {
159
+ permitted: inHomeGuild,
160
+ reason: inHomeGuild ? `classification=internal + caller in ${expected}` : `classification=internal but caller not in ${expected}`,
161
+ redactBytes: false
162
+ };
163
+ }
164
+ case "restricted":
165
+ return {
166
+ permitted: false,
167
+ reason: "classification=restricted \u2014 caller not in any allowed list (roles/operators/users/guilds/orgs)",
168
+ redactBytes: false
169
+ };
170
+ case "secret":
171
+ return {
172
+ permitted: false,
173
+ reason: "classification=secret \u2014 caller not in any allowed list; bytes redacted",
174
+ redactBytes: true
175
+ };
176
+ default: {
177
+ return {
178
+ permitted: false,
179
+ reason: `unknown classification "${String(classification)}" \u2014 fail-closed`,
180
+ redactBytes: true
181
+ };
182
+ }
183
+ }
184
+ }
185
+ function permit(reason, classification) {
186
+ return {
187
+ permitted: true,
188
+ reason: `${reason} (classification=${classification})`,
189
+ redactBytes: false
190
+ };
191
+ }
192
+ function matchesAllowed(kind, list, caller) {
193
+ if (!list || list.length === 0) return false;
194
+ const prefix = `ws://${kind}/`;
195
+ for (const ref of list) {
196
+ const expanded = ref.startsWith("ws://") ? ref : prefix + ref;
197
+ if (caller.identityTree.includes(expanded)) return true;
198
+ if (expanded === `${prefix}*`) {
199
+ if (caller.identityTree.some((r) => r.startsWith(prefix))) return true;
200
+ }
201
+ }
202
+ return false;
203
+ }
204
+ function readAccessSpec(frontmatter) {
205
+ const meta = frontmatter.metadata;
206
+ const access = meta?.corpus?.access;
207
+ if (!access || typeof access !== "object") return void 0;
208
+ return access;
209
+ }
210
+
211
+ // src/access/capability.ts
212
+ var DEFAULT_RULES = Object.freeze({
213
+ "read": { allowedRoles: ["*"] },
214
+ "cite": { allowedRoles: ["*"] },
215
+ "flag-learning": {
216
+ allowedRoles: ["*"],
217
+ rateLimit: { perOperator: 20, window: "24h" }
218
+ },
219
+ "curate": { allowedRoles: ["corpus-curator", "admin"] },
220
+ "promote": { allowedRoles: ["corpus-curator", "admin"] },
221
+ "activate-playbook": {
222
+ allowedRoles: ["corpus-curator", "admin"],
223
+ requireApproval: true
224
+ },
225
+ "admin-reindex": { allowedRoles: ["admin"] },
226
+ "bypass-default-filters": {
227
+ allowedRoles: ["corpus-curator", "admin"],
228
+ audit: true
229
+ }
230
+ });
231
+ function evaluateCapability(capability, accessModes, caller) {
232
+ const rule = accessModes?.[capability] ?? DEFAULT_RULES[capability];
233
+ if (!rule) {
234
+ return {
235
+ permitted: false,
236
+ reason: `unknown capability "${capability}"`,
237
+ requireApproval: false,
238
+ audit: true
239
+ };
240
+ }
241
+ const permitted = matchesRoles(rule.allowedRoles, caller.identityTree);
242
+ return {
243
+ permitted,
244
+ reason: permitted ? `caller identity matches allowedRoles for "${capability}"` : `caller identity does NOT match allowedRoles for "${capability}"`,
245
+ rateLimit: rule.rateLimit,
246
+ requireApproval: !!rule.requireApproval,
247
+ audit: !!rule.audit
248
+ };
249
+ }
250
+ function matchesRoles(allowed, identityTree) {
251
+ if (allowed.includes("*")) return true;
252
+ for (const role of allowed) {
253
+ const expanded = role.startsWith("ws://") ? role : `ws://roles/${role}`;
254
+ if (identityTree.includes(expanded)) return true;
255
+ }
256
+ return false;
257
+ }
258
+ function readAccessModes(workspaceFrontmatter) {
259
+ const meta = workspaceFrontmatter.metadata;
260
+ const am = meta?.corpus?.accessModes;
261
+ if (!am || typeof am !== "object") return void 0;
262
+ return am;
263
+ }
264
+
265
+ // src/access/language.ts
266
+ function resolveLanguageFilter(input) {
267
+ const set = /* @__PURE__ */ new Set();
268
+ if (input.callerLocale) addExpansions(set, input.callerLocale);
269
+ if (input.workspaceDefaultLanguage)
270
+ addExpansions(set, input.workspaceDefaultLanguage);
271
+ return Object.freeze({
272
+ allowedLanguages: set,
273
+ // If a workspace default exists, entries without an explicit
274
+ // language inherit it → pass-through. If neither caller nor
275
+ // workspace declares one, the filter is effectively off (everyone
276
+ // surfaces; the helper isn't called for cross-language scoping).
277
+ allowUnspecified: set.size === 0 ? true : input.workspaceDefaultLanguage != null
278
+ });
279
+ }
280
+ function matchesLanguageFilter(entryLanguage, filter) {
281
+ if (filter.allowedLanguages.size === 0) return true;
282
+ if (!entryLanguage) return filter.allowUnspecified;
283
+ const lower = entryLanguage.toLowerCase();
284
+ if (filter.allowedLanguages.has(lower)) return true;
285
+ const baseLang = lower.split("-")[0];
286
+ if (baseLang && filter.allowedLanguages.has(baseLang)) return true;
287
+ for (const allowed of filter.allowedLanguages) {
288
+ if (lower.startsWith(allowed + "-")) return true;
289
+ }
290
+ return false;
291
+ }
292
+ function addExpansions(set, locale) {
293
+ const lower = locale.toLowerCase();
294
+ set.add(lower);
295
+ const base = lower.split("-")[0];
296
+ if (base) set.add(base);
297
+ }
298
+ function readOperatorLocale(operatorFrontmatter) {
299
+ const meta = operatorFrontmatter.metadata;
300
+ const l = meta?.corpus?.locale;
301
+ return typeof l === "string" ? l : void 0;
302
+ }
303
+ function readWorkspaceDefaultLanguage(workspaceFrontmatter) {
304
+ const meta = workspaceFrontmatter.metadata;
305
+ const def = meta?.corpus?.languages?.default;
306
+ return typeof def === "string" ? def : void 0;
307
+ }
308
+ function readEntryLanguage(frontmatter) {
309
+ const top = frontmatter.language;
310
+ if (typeof top === "string") return top;
311
+ const meta = frontmatter.metadata;
312
+ const corp = meta?.corpus?.language;
313
+ return typeof corp === "string" ? corp : void 0;
314
+ }
315
+
316
+ // src/events/emitter.ts
317
+ var CorpusEventEmitter = class {
318
+ constructor(opts) {
319
+ this.opts = opts;
320
+ }
321
+ /**
322
+ * Emit one event. Returns the event as written (including the
323
+ * ISO timestamp and actor resolved at emit time). The .md log file
324
+ * is created on first append if missing.
325
+ */
326
+ async emit(kind, payload) {
327
+ const identity = await this.opts.identity.resolve();
328
+ const event = {
329
+ kind,
330
+ at: this.opts.clock.now().toISOString(),
331
+ actor: identity.principal,
332
+ payload
333
+ };
334
+ const logPath = joinPath(this.opts.workspaceRoot, "_log.md");
335
+ const line = formatLine(event) + "\n";
336
+ if (!await this.opts.fs.exists(logPath)) {
337
+ await this.opts.fs.writeFile(
338
+ logPath,
339
+ "# Corpus activity log\n\nAppend-only AIP-10 log of corpus state transitions. Each line:\n`- <iso> <kind> by <actor> payload=<json>`\n\n"
340
+ );
341
+ }
342
+ await this.opts.fs.appendFile(logPath, line);
343
+ return event;
344
+ }
345
+ };
346
+ function formatLine(event) {
347
+ const payload = JSON.stringify(event.payload, [...Object.keys(event.payload).sort()]);
348
+ return `- ${event.at} ${event.kind} by ${event.actor} payload=${payload}`;
349
+ }
350
+ function joinPath(a, b) {
351
+ if (!a) return b;
352
+ return a.endsWith("/") ? a + b : a + "/" + b;
353
+ }
354
+ var CorpusWorkspaceReader = class {
355
+ constructor(opts) {
356
+ this.opts = opts;
357
+ }
358
+ /**
359
+ * Scan the workspace rooted at `root` and return a typed snapshot.
360
+ * `root` is workspace-relative (the host resolves to a backing
361
+ * storage); commonly "" for the workspace root itself.
362
+ */
363
+ async read(root) {
364
+ const files = await this.opts.fs.walk(root);
365
+ const mdFiles = files.filter((p) => p.endsWith(".md"));
366
+ const buckets = {
367
+ "knowledge-workspace": [],
368
+ "knowledge-source": [],
369
+ "knowledge-entry": [],
370
+ "collection-schema": [],
371
+ "collection-item": [],
372
+ "playbook": [],
373
+ "operator": [],
374
+ "workflow": [],
375
+ "routine": [],
376
+ "unknown": []
377
+ };
378
+ for (const path of mdFiles) {
379
+ const content = await this.opts.fs.readFile(path);
380
+ const parsed = matter(content);
381
+ const file = {
382
+ // Workspace-relative path. The walk returns paths anchored at
383
+ // the host's storage root (which already includes `root` for
384
+ // non-empty workspaces — see GuildWorkspaceFsAdapter.walk),
385
+ // so we strip `root` here to match the documented contract
386
+ // ("paths returned are workspace-relative"). Without this,
387
+ // call sites that re-join `workspacePath + file.path` (the
388
+ // playbook lifecycle, the evaluator, the promoter's CAS write)
389
+ // double-prefix the path and the next read/CAS misses,
390
+ // surfacing as CorpusVersionConflictError.
391
+ path: stripRoot(path, root),
392
+ kind: classify(path, root),
393
+ frontmatter: parsed.data ?? {},
394
+ body: parsed.content,
395
+ versionToken: sha256(content)
396
+ };
397
+ buckets[file.kind].push(file);
398
+ }
399
+ return {
400
+ root,
401
+ workspace: buckets["knowledge-workspace"][0] ?? null,
402
+ sources: freeze(buckets["knowledge-source"]),
403
+ entries: freeze(buckets["knowledge-entry"]),
404
+ collections: freeze(buckets["collection-schema"]),
405
+ collectionItems: freeze(buckets["collection-item"]),
406
+ playbooks: freeze(buckets["playbook"]),
407
+ operators: freeze(buckets["operator"]),
408
+ workflows: freeze(buckets["workflow"]),
409
+ routines: freeze(buckets["routine"]),
410
+ unknown: freeze(buckets["unknown"])
411
+ };
412
+ }
413
+ };
414
+ function classify(path, root) {
415
+ const rel = stripRoot(path, root);
416
+ const segs = rel.split("/").filter(Boolean);
417
+ const fname = segs[segs.length - 1] ?? "";
418
+ if (segs.length === 1 && fname === "KNOWLEDGE.md") return "knowledge-workspace";
419
+ const top = segs[0];
420
+ switch (top) {
421
+ case "sources":
422
+ return "knowledge-source";
423
+ case "entries":
424
+ return "knowledge-entry";
425
+ case "collections":
426
+ if (fname === "COLLECTION.md") return "collection-schema";
427
+ if (fname === "ITEM.md" || /^[a-z0-9][a-z0-9-]*\.md$/.test(fname))
428
+ return "collection-item";
429
+ return "unknown";
430
+ case "playbooks":
431
+ if (fname === "PLAYBOOK.md") return "playbook";
432
+ return "unknown";
433
+ case "operators":
434
+ if (fname === "OPERATOR.md") return "operator";
435
+ return "unknown";
436
+ case "workflows":
437
+ if (fname === "WORKFLOW.md") return "workflow";
438
+ return "unknown";
439
+ case "routines":
440
+ if (fname === "ROUTINE.md") return "routine";
441
+ return "unknown";
442
+ default:
443
+ return "unknown";
444
+ }
445
+ }
446
+ function stripRoot(path, root) {
447
+ if (!root) return path;
448
+ const prefix = root.endsWith("/") ? root : root + "/";
449
+ return path.startsWith(prefix) ? path.slice(prefix.length) : path;
450
+ }
451
+ function sha256(content) {
452
+ return "sha256:" + createHash("sha256").update(content).digest("hex");
453
+ }
454
+ function freeze(arr) {
455
+ return Object.freeze([...arr]);
456
+ }
457
+ var CorpusVersionConflictError = class extends Error {
458
+ constructor(path, expected, actual) {
459
+ super(
460
+ `CorpusVersionConflictError: ${path} versionToken mismatch \u2014 expected ${expected}, found ${actual}. Re-read the file and retry with the fresh token.`
461
+ );
462
+ this.path = path;
463
+ this.expected = expected;
464
+ this.actual = actual;
465
+ this.name = "CorpusVersionConflictError";
466
+ }
467
+ };
468
+ var CorpusWorkspaceWriter = class _CorpusWorkspaceWriter {
469
+ constructor(opts) {
470
+ this.opts = opts;
471
+ }
472
+ /**
473
+ * Compute the versionToken for raw file content. Exposed so the
474
+ * caller can stage a write and pass the same hash function used
475
+ * inside the writer.
476
+ */
477
+ static versionTokenOf(content) {
478
+ return "sha256:" + createHash("sha256").update(content).digest("hex");
479
+ }
480
+ /**
481
+ * Atomic write with optimistic concurrency check.
482
+ *
483
+ * - expected === undefined → unconditional overwrite (use sparingly)
484
+ * - expected === null → create-only (refuses if exists)
485
+ * - expected === string → CAS check (refuses if current hash differs)
486
+ *
487
+ * Returns the new versionToken on success.
488
+ */
489
+ async writeFile(path, content, expected) {
490
+ if (expected === null) {
491
+ if (await this.opts.fs.exists(path)) {
492
+ throw new CorpusVersionConflictError(path, "<must-not-exist>", "<exists>");
493
+ }
494
+ } else if (typeof expected === "string") {
495
+ const existing = await this.opts.fs.exists(path);
496
+ if (!existing) {
497
+ throw new CorpusVersionConflictError(path, expected, "<missing>");
498
+ }
499
+ const current = await this.opts.fs.readFile(path);
500
+ const actual = _CorpusWorkspaceWriter.versionTokenOf(current);
501
+ if (actual !== expected) {
502
+ throw new CorpusVersionConflictError(path, expected, actual);
503
+ }
504
+ }
505
+ await this.opts.fs.writeFile(path, content);
506
+ return _CorpusWorkspaceWriter.versionTokenOf(content);
507
+ }
508
+ /**
509
+ * Serialize a markdown doc (frontmatter + body) and write atomically.
510
+ * Frontmatter keys are stringified in a stable order so version
511
+ * tokens are deterministic across emitter rebuilds.
512
+ */
513
+ async writeMarkdown(path, doc, expected) {
514
+ const content = serializeMarkdown(doc);
515
+ return this.writeFile(path, content, expected);
516
+ }
517
+ /**
518
+ * Run `fn` while holding a workspace-scoped lock. Used for
519
+ * multi-file transactions (promote entry + update _index.md + log
520
+ * append). The lock path is host-specific; we default to
521
+ * `_log.md` because it's the file every transaction touches.
522
+ *
523
+ * `lockPath` is workspace-relative.
524
+ */
525
+ async transaction(lockPath, fn) {
526
+ let handle = null;
527
+ try {
528
+ handle = await this.opts.fs.lock(lockPath);
529
+ return await fn();
530
+ } finally {
531
+ if (handle) await handle.release();
532
+ }
533
+ }
534
+ };
535
+ function serializeMarkdown(doc) {
536
+ return matter.stringify(doc.body.startsWith("\n") ? doc.body : "\n" + doc.body, doc.frontmatter);
537
+ }
538
+
539
+ // src/importers/runner.ts
540
+ var SIDECAR_PATH = "collections/corpus-candidate/_candidates.yaml";
541
+ var ImporterRunner = class {
542
+ constructor(opts) {
543
+ this.opts = opts;
544
+ }
545
+ /**
546
+ * Run a single importer batch end-to-end.
547
+ *
548
+ * 1. Pre-load existing source content_hashes for dedup.
549
+ * 2. For each source the importer yields:
550
+ * a. Skip if hash already exists in the workspace.
551
+ * b. Otherwise write AIP-10 source frontmatter + body to
552
+ * `sources/<importerId>/<batchId>/<slug>.md`.
553
+ * c. Append candidate row to `_candidates.yaml`.
554
+ * 3. Emit `corpus.candidate.discovered` per archived source.
555
+ *
556
+ * Atomic per-source (each source archived + candidate written
557
+ * inside one writer.transaction).
558
+ */
559
+ async run(importer, target) {
560
+ const fs = this.opts.fs;
561
+ const writer = new CorpusWorkspaceWriter({ fs });
562
+ const reader = new CorpusWorkspaceReader({ fs });
563
+ const sidecar = new CandidatesSidecar({
564
+ fs,
565
+ path: joinPath2(this.opts.workspacePath, SIDECAR_PATH)
566
+ });
567
+ const emitter = new CorpusEventEmitter({
568
+ fs,
569
+ clock: this.opts.clock,
570
+ identity: this.opts.identity,
571
+ workspaceRoot: this.opts.workspacePath
572
+ });
573
+ const batchId = target.batchId ?? this.opts.clock.now().toISOString().slice(0, 10);
574
+ const archiveDirSegment = `sources/${target.importerId}/${batchId}`;
575
+ const snapshot = await reader.read(this.opts.workspacePath);
576
+ const existingHashes = /* @__PURE__ */ new Set();
577
+ for (const file of snapshot.sources) {
578
+ const h = file.frontmatter.content_hash;
579
+ if (typeof h === "string") existingHashes.add(h);
580
+ }
581
+ const archivedSlugs = [];
582
+ const duplicateSlugs = [];
583
+ const candidateIds = [];
584
+ const warnings = [];
585
+ for await (const source of importer.enumerate(target)) {
586
+ if (!isSourceSlug(source.slug)) {
587
+ warnings.push(`source skipped \u2014 invalid slug "${source.slug}"`);
588
+ continue;
589
+ }
590
+ if (existingHashes.has(source.contentHash)) {
591
+ duplicateSlugs.push(source.slug);
592
+ continue;
593
+ }
594
+ existingHashes.add(source.contentHash);
595
+ const sourcePath = joinPath2(
596
+ this.opts.workspacePath,
597
+ `${archiveDirSegment}/${source.slug}.md`
598
+ );
599
+ const sourceContent = serializeSource(source, archiveDirSegment, this.opts.clock);
600
+ try {
601
+ await writer.writeFile(sourcePath, sourceContent, null);
602
+ } catch (err) {
603
+ warnings.push(
604
+ `source "${source.slug}" \u2014 slug collision in ${archiveDirSegment} (${err instanceof Error ? err.message : "unknown"}); skipped`
605
+ );
606
+ continue;
607
+ }
608
+ archivedSlugs.push(source.slug);
609
+ const toCandidate = this.opts.runner?.toCandidate ?? defaultToCandidate;
610
+ const sourceId = source.slug;
611
+ const row = toCandidate(source, sourceId);
612
+ try {
613
+ await sidecar.append(row);
614
+ candidateIds.push(row.id);
615
+ } catch (err) {
616
+ warnings.push(
617
+ `candidate "${row.id}" \u2014 sidecar append failed (${err instanceof Error ? err.message : "unknown"})`
618
+ );
619
+ }
620
+ await emitter.emit("corpus.candidate.discovered", {
621
+ id: row.id,
622
+ importerId: target.importerId,
623
+ batchId,
624
+ contentHash: source.contentHash,
625
+ provenanceKind: `imported-from-${target.importerId}`
626
+ }).catch(() => void 0);
627
+ }
628
+ return Object.freeze({
629
+ importerId: target.importerId,
630
+ batchId,
631
+ archivedSlugs: Object.freeze(archivedSlugs),
632
+ duplicateSlugs: Object.freeze(duplicateSlugs),
633
+ candidateIds: Object.freeze(candidateIds),
634
+ warnings: Object.freeze(warnings)
635
+ });
636
+ }
637
+ };
638
+ function defaultToCandidate(s, sourceId) {
639
+ return {
640
+ id: sourceId,
641
+ status: "discovered",
642
+ corpusKind: "example",
643
+ sourcePath: void 0,
644
+ // filled in by archiver via path mapping
645
+ sourceUrl: s.originalUrl,
646
+ contentHash: s.contentHash,
647
+ title: s.title,
648
+ summary: void 0,
649
+ discoveredAt: (/* @__PURE__ */ new Date()).toISOString(),
650
+ discoveredBy: "ws://operators/importer-runner",
651
+ provenanceKind: "imported"
652
+ };
653
+ }
654
+ function serializeSource(source, archiveDirSegment, clock) {
655
+ const fm = {
656
+ schema: "knowledge.source/v1",
657
+ id: source.slug,
658
+ path: `${archiveDirSegment}/${source.slug}.md`,
659
+ title: source.title,
660
+ captured_at: clock.now().toISOString(),
661
+ content_hash: source.contentHash,
662
+ authority: source.authority ?? "secondary"
663
+ };
664
+ if (source.language) fm.language = source.language;
665
+ if (source.tags && source.tags.length > 0) fm.tags = source.tags;
666
+ if (source.originalUrl || source.corpusMetadata) {
667
+ fm.metadata = {
668
+ corpus: {
669
+ ...source.originalUrl ? { originalUrl: source.originalUrl } : {},
670
+ ...source.corpusMetadata ?? {}
671
+ }
672
+ };
673
+ }
674
+ return matter.stringify(
675
+ source.body.startsWith("\n") ? source.body : "\n" + source.body,
676
+ fm
677
+ );
678
+ }
679
+ function joinPath2(a, b) {
680
+ if (!a) return b;
681
+ return a.endsWith("/") ? a + b : a + "/" + b;
682
+ }
683
+ var LocalFilesImporter = class {
684
+ constructor(opts) {
685
+ this.opts = opts;
686
+ }
687
+ id = "local-files";
688
+ label = "Local Files";
689
+ async *enumerate(target) {
690
+ const config = parseConfig(target.config);
691
+ const exts = new Set(config.extensions ?? [".md"]);
692
+ const maxFiles = config.maxFiles ?? 1e3;
693
+ const paths = await this.opts.fs.walk(config.rootPath);
694
+ let yielded = 0;
695
+ for (const p of paths) {
696
+ if (yielded >= maxFiles) break;
697
+ const ext = pickExt(p);
698
+ if (!exts.has(ext)) continue;
699
+ const body = await this.opts.fs.readFile(p);
700
+ const slug = makeSlug(p, config.rootPath);
701
+ const contentHash = sha2562(body);
702
+ const title = readTitleFromBody(body, slug);
703
+ yield {
704
+ slug,
705
+ title,
706
+ contentHash,
707
+ body,
708
+ authority: "secondary",
709
+ ...config.language ? { language: config.language } : {},
710
+ ...config.tags && config.tags.length > 0 ? { tags: config.tags } : {},
711
+ corpusMetadata: {
712
+ importerSourcePath: p,
713
+ importerRoot: config.rootPath
714
+ }
715
+ };
716
+ yielded++;
717
+ }
718
+ }
719
+ };
720
+ function parseConfig(raw) {
721
+ if (typeof raw.rootPath !== "string") {
722
+ throw new Error(
723
+ `LocalFilesImporter: config.rootPath is required (string)`
724
+ );
725
+ }
726
+ const cfg = {
727
+ rootPath: raw.rootPath,
728
+ extensions: Array.isArray(raw.extensions) ? raw.extensions.filter(
729
+ (x) => typeof x === "string"
730
+ ) : void 0,
731
+ maxFiles: typeof raw.maxFiles === "number" ? raw.maxFiles : void 0,
732
+ tags: Array.isArray(raw.tags) ? raw.tags.filter(
733
+ (x) => typeof x === "string"
734
+ ) : void 0,
735
+ language: typeof raw.language === "string" ? raw.language : void 0
736
+ };
737
+ return cfg;
738
+ }
739
+ function pickExt(path) {
740
+ const i = path.lastIndexOf(".");
741
+ if (i < 0) return "";
742
+ return path.slice(i).toLowerCase();
743
+ }
744
+ function makeSlug(path, rootPath) {
745
+ const rel = path.startsWith(rootPath) ? path.slice(rootPath.length) : path;
746
+ const stripped = rel.replace(/^\/+/, "").replace(/\.[^.]+$/, "");
747
+ return slugify(stripped, { fallback: "source" });
748
+ }
749
+ function readTitleFromBody(body, fallback) {
750
+ const m = body.match(/^\s*#\s+(.+)$/m);
751
+ if (m && m[1]) return m[1].trim().slice(0, 200);
752
+ return fallback;
753
+ }
754
+ function sha2562(content) {
755
+ return "sha256:" + createHash("sha256").update(content).digest("hex");
756
+ }
757
+ var KbMigrationImporter = class {
758
+ id = "kb-migration";
759
+ label = "Migrate from existing KB";
760
+ async *enumerate(target) {
761
+ const config = parseKbMigrationConfig(target.config);
762
+ const sources = await config.provider.listSources();
763
+ const max = config.maxSources ?? sources.length;
764
+ let yielded = 0;
765
+ for (const s of sources) {
766
+ if (yielded >= max) break;
767
+ const body = config.fetchBody ? await config.fetchBody(s.id) : `(Migrated from KB ${config.sourceKbId}, source id=${s.id}. Body fetcher not configured \u2014 archive contains metadata only.)`;
768
+ const slug = slugifyKbSource(s.id, config.sourceKbId);
769
+ yield {
770
+ slug,
771
+ title: s.title ?? s.id,
772
+ contentHash: hashKbSource(s, body),
773
+ body,
774
+ originalUrl: typeof s.uri === "string" ? s.uri : void 0,
775
+ authority: "secondary",
776
+ corpusMetadata: {
777
+ provenanceKind: "imported-from-kb",
778
+ sourceKbId: config.sourceKbId,
779
+ sourceKbSourceId: s.id,
780
+ sourceKbBytes: s.bytes,
781
+ sourceKbKind: s.kind,
782
+ // Carry the original KB's metadata through verbatim so
783
+ // curators can see what was lost in translation.
784
+ sourceKbMetadata: s.metadata
785
+ }
786
+ };
787
+ yielded++;
788
+ }
789
+ }
790
+ };
791
+ function parseKbMigrationConfig(raw) {
792
+ const sourceKbId = raw.sourceKbId;
793
+ const provider = raw.provider;
794
+ if (typeof sourceKbId !== "string") {
795
+ throw new Error("KbMigrationImporter: config.sourceKbId required (string)");
796
+ }
797
+ if (!provider || typeof provider !== "object" || typeof provider.listSources !== "function") {
798
+ throw new Error(
799
+ "KbMigrationImporter: config.provider required (must implement listSources)"
800
+ );
801
+ }
802
+ return {
803
+ sourceKbId,
804
+ provider,
805
+ fetchBody: typeof raw.fetchBody === "function" ? raw.fetchBody : void 0,
806
+ maxSources: typeof raw.maxSources === "number" ? raw.maxSources : void 0
807
+ };
808
+ }
809
+ function slugifyKbSource(id, kbId) {
810
+ const combined = `${kbId}-${id}`;
811
+ const slug = combined.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, 96);
812
+ return slug || "migrated-source";
813
+ }
814
+ function hashKbSource(s, body) {
815
+ return "sha256:" + createHash("sha256").update(`${s.id}|${s.bytes}|${body}`).digest("hex");
816
+ }
817
+ var WebImporter = class {
818
+ constructor(opts) {
819
+ this.opts = opts;
820
+ }
821
+ id = "web";
822
+ label = "Web (URLs)";
823
+ async *enumerate(target) {
824
+ const config = parseConfig2(target.config);
825
+ const maxUrls = config.maxUrls ?? 1e3;
826
+ const seenSlugs = /* @__PURE__ */ new Set();
827
+ let yielded = 0;
828
+ for (const url of config.urls) {
829
+ if (yielded >= maxUrls) break;
830
+ let fetched;
831
+ try {
832
+ fetched = await this.opts.fetcher.fetch(url);
833
+ } catch {
834
+ continue;
835
+ }
836
+ if (!fetched || !fetched.text.trim()) continue;
837
+ const slug = uniqueSlug(
838
+ slugify(fetched.title, { fallback: "" }) || slugify(url, { stripScheme: true, fallback: "source" }),
839
+ seenSlugs
840
+ );
841
+ const language = normalizeLanguageTag(fetched.language ?? config.language);
842
+ yield {
843
+ slug,
844
+ title: fetched.title.slice(0, 200) || slug,
845
+ contentHash: sha2563(fetched.text),
846
+ body: fetched.text,
847
+ originalUrl: url,
848
+ authority: "secondary",
849
+ ...language ? { language } : {},
850
+ ...config.tags && config.tags.length > 0 ? { tags: config.tags } : {},
851
+ corpusMetadata: {
852
+ importerSourceUrl: url,
853
+ fetchKind: fetched.kind,
854
+ ...fetched.via ? { fetchedVia: fetched.via } : {}
855
+ }
856
+ };
857
+ yielded++;
858
+ }
859
+ }
860
+ };
861
+ function parseConfig2(raw) {
862
+ const rawUrls = raw.urls;
863
+ if (!Array.isArray(rawUrls) || rawUrls.length === 0) {
864
+ throw new Error("WebImporter: config.urls is required (non-empty string[])");
865
+ }
866
+ const urls = rawUrls.filter(
867
+ (x) => typeof x === "string" && x.length > 0
868
+ );
869
+ if (urls.length === 0) {
870
+ throw new Error("WebImporter: config.urls contained no usable strings");
871
+ }
872
+ const rawTags = raw.tags;
873
+ return {
874
+ urls,
875
+ maxUrls: typeof raw.maxUrls === "number" ? raw.maxUrls : void 0,
876
+ tags: Array.isArray(rawTags) ? rawTags.filter((x) => typeof x === "string") : void 0,
877
+ language: typeof raw.language === "string" ? raw.language : void 0
878
+ };
879
+ }
880
+ function sha2563(content) {
881
+ return "sha256:" + createHash("sha256").update(content).digest("hex");
882
+ }
883
+ var ConversationImporter = class {
884
+ constructor(opts) {
885
+ this.opts = opts;
886
+ }
887
+ id = "conversation";
888
+ label = "Conversation";
889
+ async *enumerate(target) {
890
+ const config = parseConfig3(target.config);
891
+ const maxRefs = config.maxRefs ?? 1e3;
892
+ const seenSlugs = /* @__PURE__ */ new Set();
893
+ let yielded = 0;
894
+ for (const ref of config.refs) {
895
+ if (yielded >= maxRefs) break;
896
+ let doc;
897
+ try {
898
+ doc = await this.opts.source.fetchConversation(ref);
899
+ } catch {
900
+ continue;
901
+ }
902
+ if (!doc || doc.turns.length === 0) continue;
903
+ const body = renderTranscript(doc.turns);
904
+ if (!body) continue;
905
+ const slug = uniqueSlug(
906
+ slugify(doc.id, { fallback: "" }) || slugify(doc.title ?? "conversation", { fallback: "conversation" }),
907
+ seenSlugs
908
+ );
909
+ const language = normalizeLanguageTag(doc.language ?? config.language);
910
+ const stamps = doc.turns.map((t) => t.at).filter((a) => typeof a === "string" && a.length > 0).sort();
911
+ yield {
912
+ slug,
913
+ title: (doc.title ?? `Conversation ${doc.id}`).slice(0, 200),
914
+ contentHash: sha2564(body),
915
+ body,
916
+ authority: config.authority ?? "secondary",
917
+ ...language ? { language } : {},
918
+ ...config.tags && config.tags.length > 0 ? { tags: config.tags } : {},
919
+ corpusMetadata: {
920
+ conversationRef: ref,
921
+ conversationId: doc.id,
922
+ sourceKind: "conversation",
923
+ turnCount: countTurns(doc.turns),
924
+ ...stamps.length ? { firstTurnAt: stamps[0], lastTurnAt: stamps[stamps.length - 1] } : {}
925
+ }
926
+ };
927
+ yielded++;
928
+ }
929
+ }
930
+ };
931
+ function parseConfig3(raw) {
932
+ const rawRefs = raw.refs;
933
+ if (!Array.isArray(rawRefs) || rawRefs.length === 0) {
934
+ throw new Error(
935
+ "ConversationImporter: config.refs is required (non-empty string[])"
936
+ );
937
+ }
938
+ const refs = rawRefs.filter(
939
+ (x) => typeof x === "string" && x.length > 0
940
+ );
941
+ if (refs.length === 0) {
942
+ throw new Error(
943
+ "ConversationImporter: config.refs contained no usable strings"
944
+ );
945
+ }
946
+ const rawTags = raw.tags;
947
+ return {
948
+ refs,
949
+ maxRefs: typeof raw.maxRefs === "number" ? raw.maxRefs : void 0,
950
+ tags: Array.isArray(rawTags) ? rawTags.filter((x) => typeof x === "string") : void 0,
951
+ language: typeof raw.language === "string" ? raw.language : void 0,
952
+ authority: isAuthority(raw.authority) ? raw.authority : void 0
953
+ };
954
+ }
955
+ function isAuthority(v) {
956
+ return v === "primary" || v === "secondary" || v === "rumour";
957
+ }
958
+ function renderTranscript(turns) {
959
+ const blocks = [];
960
+ for (const turn of turns) {
961
+ const text = turn.text.trim();
962
+ if (!text) continue;
963
+ blocks.push(`${labelRole(turn.role)}: ${text}`);
964
+ }
965
+ return blocks.join("\n\n");
966
+ }
967
+ function countTurns(turns) {
968
+ let n = 0;
969
+ for (const t of turns) if (t.text.trim()) n++;
970
+ return n;
971
+ }
972
+ function labelRole(role) {
973
+ const r = role.trim();
974
+ if (!r) return "Speaker";
975
+ return r.charAt(0).toUpperCase() + r.slice(1);
976
+ }
977
+ function sha2564(content) {
978
+ return "sha256:" + createHash("sha256").update(content).digest("hex");
979
+ }
980
+ var ENTRY_SLUG_OPTS = { leadingLetter: true, fallback: "entry", maxLen: 80 };
981
+ var KIND_DIR = {
982
+ principle: "principles",
983
+ pattern: "patterns",
984
+ critique: "critiques",
985
+ summary: "summaries",
986
+ example: "examples"
987
+ };
988
+ var DistillRunner = class {
989
+ constructor(opts) {
990
+ this.opts = opts;
991
+ }
992
+ async run(source) {
993
+ const items = await this.opts.distiller.distill({
994
+ title: source.title,
995
+ body: source.body,
996
+ ...source.tags ? { tags: source.tags } : {}
997
+ });
998
+ const year = this.opts.clock.now().getUTCFullYear();
999
+ const flat = this.opts.layout === "flat";
1000
+ const seen = /* @__PURE__ */ new Set();
1001
+ const entryPaths = [];
1002
+ const skipped = [];
1003
+ for (const item of items) {
1004
+ if (!item.title.trim() || !item.body.trim()) continue;
1005
+ const slug = uniqueSlug(slugify(item.title, ENTRY_SLUG_OPTS), seen, 80);
1006
+ const dir = KIND_DIR[item.kind];
1007
+ const path = flat ? `entries/${dir}/${slug}.md` : `entries/${dir}/${year}/${slug}.md`;
1008
+ if (await this.opts.fs.exists(path)) {
1009
+ skipped.push(slug);
1010
+ continue;
1011
+ }
1012
+ await this.opts.fs.writeFile(path, serializeEntry(item, slug, source, this.opts.clock));
1013
+ entryPaths.push(path);
1014
+ }
1015
+ return { sourceId: source.id, entryPaths, skipped };
1016
+ }
1017
+ };
1018
+ function serializeEntry(item, slug, source, clock) {
1019
+ const now = clock.now().toISOString();
1020
+ const fm = {
1021
+ schema: "knowledge.entry/v1",
1022
+ slug,
1023
+ kind: item.kind,
1024
+ title: item.title,
1025
+ updated_at: now,
1026
+ sources: [source.id],
1027
+ // ← derivedFrom / provenance edge
1028
+ confidence: typeof item.confidence === "number" ? item.confidence : 0.7,
1029
+ tags: dedupeTags(item.tags, source.tags),
1030
+ metadata: {
1031
+ corpus: {
1032
+ status: "active",
1033
+ ...source.domain ? { domain: source.domain } : {},
1034
+ // access inherited from the source — propagates up the chain so a
1035
+ // refined insight is never more visible than its evidence.
1036
+ ...source.access ? { access: source.access } : {},
1037
+ promotionMode: "auto-distill",
1038
+ promotedAt: now
1039
+ }
1040
+ }
1041
+ };
1042
+ const body = item.body.trim();
1043
+ return matter.stringify(body.startsWith("\n") ? body : "\n" + body, fm);
1044
+ }
1045
+ function dedupeTags(a, b) {
1046
+ const out = /* @__PURE__ */ new Set();
1047
+ for (const raw of [...a ?? [], ...b ?? []]) {
1048
+ const t = sanitizeTag(raw);
1049
+ if (t) out.add(t);
1050
+ }
1051
+ return [...out];
1052
+ }
1053
+ function sanitizeTag(raw) {
1054
+ const t = raw.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
1055
+ return /^[a-z][a-z0-9-]*$/.test(t) ? t : null;
1056
+ }
1057
+ var REFINED_KIND_SCHEMA = z.enum([
1058
+ "principle",
1059
+ "pattern",
1060
+ "critique",
1061
+ "summary",
1062
+ "example"
1063
+ ]);
1064
+ function isRefinedKind(value) {
1065
+ return REFINED_KIND_SCHEMA.safeParse(value).success;
1066
+ }
1067
+ var DISTILLED_ITEM = z.object({
1068
+ kind: REFINED_KIND_SCHEMA,
1069
+ title: z.string(),
1070
+ body: z.string(),
1071
+ confidence: z.number().optional(),
1072
+ tags: z.array(z.string()).optional()
1073
+ }).loose();
1074
+ function buildDistillPrompt(input, maxItems) {
1075
+ return `You distill a raw source (a video transcript or article) into REFINED, reusable knowledge for an AI operator. Extract the durable insights \u2014 not a summary of the video, but the transferable lessons.
1076
+
1077
+ Return a JSON array (max ${maxItems} items). Each item:
1078
+ { "kind": one of "principle" | "pattern" | "critique" | "summary" | "example",
1079
+ "title": short imperative/declarative title,
1080
+ "body": 2-5 sentences, SELF-CONTAINED (no "the speaker says"), the actual insight,
1081
+ "confidence": 0-1, "tags": [short topic tags] }
1082
+
1083
+ Guidance:
1084
+ - "principle": a durable rule of thumb. "pattern": a repeatable technique/sequence.
1085
+ "critique": a common mistake / anti-pattern. "summary": a compact overview.
1086
+ "example": a concrete worked instance worth remembering.
1087
+ - Drop filler, calls-to-action, tangents. Keep only what an operator could ACT on later.
1088
+ - Write every title and body in ENGLISH, even when the source is in another language. Translate the insight; do not copy the source language.
1089
+
1090
+ SOURCE TITLE: ${input.title}
1091
+ ${input.tags?.length ? `TAGS: ${input.tags.join(", ")}
1092
+ ` : ""}SOURCE BODY:
1093
+ ${input.body.slice(0, 24e3)}
1094
+
1095
+ Return ONLY the JSON array, no prose.`;
1096
+ }
1097
+ function parseItems(text) {
1098
+ const start = text.indexOf("[");
1099
+ const end = text.lastIndexOf("]");
1100
+ if (start < 0 || end <= start) return [];
1101
+ let raw;
1102
+ try {
1103
+ raw = JSON.parse(text.slice(start, end + 1));
1104
+ } catch {
1105
+ return [];
1106
+ }
1107
+ if (!Array.isArray(raw)) return [];
1108
+ const out = [];
1109
+ for (const r of raw) {
1110
+ const item = DISTILLED_ITEM.safeParse(r);
1111
+ if (!item.success) continue;
1112
+ const { kind, title, body, confidence, tags } = item.data;
1113
+ out.push({
1114
+ kind,
1115
+ title,
1116
+ body,
1117
+ ...typeof confidence === "number" ? { confidence } : {},
1118
+ ...tags ? { tags } : {}
1119
+ });
1120
+ }
1121
+ return out;
1122
+ }
1123
+ var ENTRY_SOURCES = z.object({ sources: z.array(z.string()).optional().catch(void 0) }).loose();
1124
+ async function scanDistilledSourceIds(fs) {
1125
+ const ids = /* @__PURE__ */ new Set();
1126
+ let rels;
1127
+ try {
1128
+ rels = await fs.walk("entries");
1129
+ } catch {
1130
+ return ids;
1131
+ }
1132
+ for (const rel of rels) {
1133
+ if (!rel.endsWith(".md")) continue;
1134
+ const path = rel.startsWith("entries/") ? rel : `entries/${rel}`;
1135
+ try {
1136
+ const fm = ENTRY_SOURCES.parse(matter(await fs.readFile(path)).data);
1137
+ if (fm.sources) for (const s of fm.sources) ids.add(s);
1138
+ } catch {
1139
+ }
1140
+ }
1141
+ return ids;
1142
+ }
1143
+
1144
+ // src/distill/registry.ts
1145
+ function createDistillRegistry() {
1146
+ const descriptors = /* @__PURE__ */ new Map();
1147
+ return {
1148
+ register(descriptor) {
1149
+ if (descriptors.has(descriptor.id)) {
1150
+ throw new Error(`Distill descriptor "${descriptor.id}" is already registered`);
1151
+ }
1152
+ descriptors.set(descriptor.id, descriptor);
1153
+ },
1154
+ has(id) {
1155
+ return descriptors.has(id);
1156
+ },
1157
+ resolve(id) {
1158
+ const descriptor = descriptors.get(id);
1159
+ if (!descriptor) {
1160
+ throw new Error(
1161
+ `Distill descriptor "${id}" is not registered. Registered: ${[...descriptors.keys()].join(", ") || "(none)"}`
1162
+ );
1163
+ }
1164
+ return descriptor;
1165
+ },
1166
+ list() {
1167
+ return [...descriptors.values()];
1168
+ }
1169
+ };
1170
+ }
1171
+
1172
+ // src/distill/run.ts
1173
+ async function runDistill(descriptor, scope) {
1174
+ const report = {
1175
+ descriptorId: descriptor.id,
1176
+ scopeId: scope.id,
1177
+ unitsConsidered: 0,
1178
+ unitsDistilled: 0,
1179
+ entriesWritten: 0,
1180
+ skipped: 0
1181
+ };
1182
+ const target = await descriptor.target(scope);
1183
+ const distilled = await scanDistilledSourceIds(target.fs);
1184
+ const binding = descriptor.bind(scope, target);
1185
+ const config = await binding.prepare(distilled);
1186
+ if (!config) return report;
1187
+ const runner = new DistillRunner({
1188
+ fs: target.fs,
1189
+ clock: target.clock,
1190
+ distiller: descriptor.distiller(scope)
1191
+ });
1192
+ for await (const imported of binding.importer.enumerate({
1193
+ importerId: descriptor.id,
1194
+ config
1195
+ })) {
1196
+ report.unitsConsidered++;
1197
+ const source = {
1198
+ id: binding.provenanceId(imported),
1199
+ title: imported.title,
1200
+ body: imported.body,
1201
+ ...imported.tags ? { tags: imported.tags } : {}
1202
+ };
1203
+ try {
1204
+ const r = await runner.run(source);
1205
+ if (r.entryPaths.length > 0) report.unitsDistilled++;
1206
+ report.entriesWritten += r.entryPaths.length;
1207
+ report.skipped += r.skipped.length;
1208
+ } catch {
1209
+ }
1210
+ }
1211
+ return report;
1212
+ }
1213
+ var ANTHROPIC_RESPONSE = z.object({
1214
+ content: z.array(
1215
+ z.object({ type: z.string(), text: z.string().optional() }).loose()
1216
+ ).optional()
1217
+ }).loose();
1218
+ var ClaudeDistiller = class {
1219
+ apiKey;
1220
+ model;
1221
+ baseUrl;
1222
+ maxItems;
1223
+ constructor(opts) {
1224
+ this.apiKey = opts.apiKey;
1225
+ this.model = opts.model ?? "claude-sonnet-4-6";
1226
+ this.baseUrl = (opts.baseUrl ?? "https://api.anthropic.com/v1").replace(
1227
+ /\/+$/,
1228
+ ""
1229
+ );
1230
+ this.maxItems = opts.maxItems ?? 8;
1231
+ }
1232
+ async distill(input) {
1233
+ const prompt = buildDistillPrompt(input, this.maxItems);
1234
+ const res = await fetch(`${this.baseUrl}/messages`, {
1235
+ method: "POST",
1236
+ headers: {
1237
+ "x-api-key": this.apiKey,
1238
+ "anthropic-version": "2023-06-01",
1239
+ "content-type": "application/json"
1240
+ },
1241
+ body: JSON.stringify({
1242
+ model: this.model,
1243
+ max_tokens: 4096,
1244
+ messages: [{ role: "user", content: prompt }]
1245
+ })
1246
+ });
1247
+ if (!res.ok) {
1248
+ throw new Error(
1249
+ `Claude distill ${res.status}: ${(await res.text()).slice(0, 200)}`
1250
+ );
1251
+ }
1252
+ const parsed = ANTHROPIC_RESPONSE.safeParse(await res.json());
1253
+ const text = parsed.success ? (parsed.data.content ?? []).find((c) => c.type === "text")?.text ?? "" : "";
1254
+ return parseItems(text);
1255
+ }
1256
+ };
1257
+
1258
+ // src/distill/windows.ts
1259
+ var WINDOW_SEP = "::";
1260
+ function windowRef(threadId, day) {
1261
+ return `${threadId}${WINDOW_SEP}${day}`;
1262
+ }
1263
+ function parseWindowRef(ref) {
1264
+ const i = ref.indexOf(WINDOW_SEP);
1265
+ if (i < 0) return null;
1266
+ const threadId = ref.slice(0, i);
1267
+ const day = ref.slice(i + WINDOW_SEP.length);
1268
+ return threadId && day ? { threadId, day } : null;
1269
+ }
1270
+ function windowSlug(threadId, day) {
1271
+ return `${threadId}-${day}`;
1272
+ }
1273
+ async function enumerateWindowRefs(source, distilled) {
1274
+ const refs = [];
1275
+ for (const thread of await source.listThreads()) {
1276
+ const threadId = thread.threadId ?? thread.id;
1277
+ if (!threadId) continue;
1278
+ for (const day of await source.listWindows(threadId)) {
1279
+ if (distilled.has(windowSlug(threadId, day))) continue;
1280
+ refs.push(windowRef(threadId, day));
1281
+ }
1282
+ }
1283
+ return refs;
1284
+ }
1285
+ var ENTRY_FRONTMATTER = z.object({
1286
+ schema: z.string().optional().catch(void 0),
1287
+ slug: z.string().optional().catch(void 0),
1288
+ kind: z.string().optional().catch(void 0),
1289
+ title: z.string().optional().catch(void 0),
1290
+ sources: z.array(z.string()).optional().catch(void 0),
1291
+ confidence: z.number().optional().catch(void 0),
1292
+ tags: z.array(z.string()).optional().catch(void 0),
1293
+ metadata: z.object({
1294
+ corpus: z.object({
1295
+ access: z.string().optional().catch(void 0),
1296
+ status: z.string().optional().catch(void 0),
1297
+ // Resolved provenance written at promote time — the origins this
1298
+ // entry was distilled from, so a recall can cite the real source.
1299
+ source_refs: z.array(
1300
+ z.object({
1301
+ id: z.string(),
1302
+ url: z.string().optional().catch(void 0),
1303
+ title: z.string().optional().catch(void 0),
1304
+ authority: z.string().optional().catch(void 0),
1305
+ language: z.string().optional().catch(void 0)
1306
+ }).loose()
1307
+ ).optional().catch(void 0)
1308
+ }).loose().optional().catch(void 0)
1309
+ }).loose().optional().catch(void 0)
1310
+ }).loose();
1311
+ async function resolveKnowledge(opts) {
1312
+ const { fs, query, allowedAccess } = opts;
1313
+ const wantTags = new Set((query.tags ?? []).map((t) => t.toLowerCase()));
1314
+ const wantKinds = query.kinds ? new Set(query.kinds) : null;
1315
+ let rels;
1316
+ try {
1317
+ rels = await fs.walk("entries");
1318
+ } catch {
1319
+ return [];
1320
+ }
1321
+ const hits = [];
1322
+ for (const rel of rels) {
1323
+ if (!rel.endsWith(".md")) continue;
1324
+ const path = `entries/${rel}`;
1325
+ let parsed;
1326
+ try {
1327
+ parsed = matter(await fs.readFile(path));
1328
+ } catch {
1329
+ continue;
1330
+ }
1331
+ const fm = ENTRY_FRONTMATTER.parse(parsed.data);
1332
+ if (fm.schema !== "knowledge.entry/v1") continue;
1333
+ if (fm.metadata?.corpus?.status === "archived") continue;
1334
+ const kind = fm.kind ?? "";
1335
+ if (wantKinds && !(isRefinedKind(kind) && wantKinds.has(kind))) continue;
1336
+ const tags = fm.tags ?? [];
1337
+ if (wantTags.size > 0 && !tags.some((t) => wantTags.has(t.toLowerCase()))) continue;
1338
+ const access = fm.metadata?.corpus?.access;
1339
+ if (allowedAccess && access && !allowedAccess.has(access)) continue;
1340
+ hits.push({
1341
+ slug: fm.slug ?? path,
1342
+ kind,
1343
+ title: fm.title ?? "",
1344
+ body: parsed.content.trim(),
1345
+ sources: fm.sources ?? [],
1346
+ ...fm.metadata?.corpus?.source_refs ? { sourceRefs: fm.metadata.corpus.source_refs } : {},
1347
+ confidence: fm.confidence ?? 0,
1348
+ tags,
1349
+ ...access ? { access } : {},
1350
+ path
1351
+ });
1352
+ }
1353
+ hits.sort((a, b) => b.confidence - a.confidence);
1354
+ return query.maxResults !== void 0 ? hits.slice(0, query.maxResults) : hits;
1355
+ }
1356
+
1357
+ // src/knowledge/overlay-fs.ts
1358
+ var WHITEOUT_SUFFIX = ".whiteout";
1359
+ var isMarker = (p) => p.endsWith(WHITEOUT_SUFFIX);
1360
+ var baseOf = (markerPath) => markerPath.slice(0, -WHITEOUT_SUFFIX.length);
1361
+ var OverlayFs = class {
1362
+ /** Layers ordered highest-precedence first. */
1363
+ layers;
1364
+ whiteout;
1365
+ writableIndex;
1366
+ constructor(layers, options = {}) {
1367
+ if (layers.length === 0) {
1368
+ throw new Error("OverlayFs requires at least one layer");
1369
+ }
1370
+ this.layers = layers;
1371
+ this.whiteout = options.whiteout ?? false;
1372
+ const w = options.writableLayer ?? 0;
1373
+ if (w < 0 || w >= layers.length) {
1374
+ throw new Error(
1375
+ `OverlayFs: writableLayer ${w} out of range [0, ${layers.length - 1}]`
1376
+ );
1377
+ }
1378
+ this.writableIndex = w;
1379
+ }
1380
+ /**
1381
+ * Top-down resolution for a single path: the first layer that declares
1382
+ * either the real entry OR its whiteout marker decides. Within one
1383
+ * layer a real entry wins over a stale marker. Returns null when no
1384
+ * layer declares the path, and `{ removed: true }` when the winning
1385
+ * layer tombstones it.
1386
+ */
1387
+ async resolvePath(path) {
1388
+ const marker = path + WHITEOUT_SUFFIX;
1389
+ for (const layer of this.layers) {
1390
+ if (await layer.exists(path)) return { layer };
1391
+ if (await layer.exists(marker)) return { removed: true };
1392
+ }
1393
+ return null;
1394
+ }
1395
+ async exists(path) {
1396
+ if (!this.whiteout) {
1397
+ for (const layer of this.layers) {
1398
+ if (await layer.exists(path)) return true;
1399
+ }
1400
+ return false;
1401
+ }
1402
+ if (isMarker(path)) return false;
1403
+ const r = await this.resolvePath(path);
1404
+ return r !== null && !("removed" in r);
1405
+ }
1406
+ async readFile(path) {
1407
+ if (this.whiteout && !isMarker(path)) {
1408
+ const r = await this.resolvePath(path);
1409
+ if (r && "layer" in r) return await r.layer.readFile(path);
1410
+ return await this.layers[0].readFile(path);
1411
+ }
1412
+ for (const layer of this.layers) {
1413
+ if (await layer.exists(path)) return await layer.readFile(path);
1414
+ }
1415
+ return await this.layers[0].readFile(path);
1416
+ }
1417
+ async stat(path) {
1418
+ if (this.whiteout && !isMarker(path)) {
1419
+ const r = await this.resolvePath(path);
1420
+ if (r && "layer" in r) return await r.layer.stat(path);
1421
+ return null;
1422
+ }
1423
+ if (this.whiteout && isMarker(path)) return null;
1424
+ for (const layer of this.layers) {
1425
+ const s = await layer.stat(path);
1426
+ if (s) return s;
1427
+ }
1428
+ return null;
1429
+ }
1430
+ async readdir(path) {
1431
+ if (!this.whiteout) {
1432
+ const seen = /* @__PURE__ */ new Set();
1433
+ for (const layer of this.layers) {
1434
+ let names;
1435
+ try {
1436
+ names = await layer.readdir(path);
1437
+ } catch {
1438
+ continue;
1439
+ }
1440
+ for (const n of names) seen.add(n);
1441
+ }
1442
+ return [...seen];
1443
+ }
1444
+ return this.unionWhiteoutAware((layer) => layer.readdir(path));
1445
+ }
1446
+ async walk(path) {
1447
+ if (!this.whiteout) {
1448
+ const seen = /* @__PURE__ */ new Set();
1449
+ for (const layer of this.layers) {
1450
+ const rels = await layer.walk(path);
1451
+ for (const rel of rels) seen.add(rel);
1452
+ }
1453
+ return [...seen];
1454
+ }
1455
+ return this.unionWhiteoutAware((layer) => layer.walk(path));
1456
+ }
1457
+ /**
1458
+ * Union the entries each layer reports for a path (basenames for
1459
+ * readdir, relative paths for walk), resolving whiteouts top-down: the
1460
+ * first layer (highest precedence) that declares an entry or its marker
1461
+ * wins. Real entries win over markers within the same layer; marker
1462
+ * files are stripped from the result.
1463
+ */
1464
+ async unionWhiteoutAware(list) {
1465
+ const decided = /* @__PURE__ */ new Map();
1466
+ for (const layer of this.layers) {
1467
+ let entries;
1468
+ try {
1469
+ entries = await list(layer);
1470
+ } catch {
1471
+ continue;
1472
+ }
1473
+ const reals = entries.filter((e) => !isMarker(e));
1474
+ const markerBases = entries.filter(isMarker).map(baseOf);
1475
+ for (const r of reals) if (!decided.has(r)) decided.set(r, "present");
1476
+ for (const b of markerBases) if (!decided.has(b)) decided.set(b, "removed");
1477
+ }
1478
+ const out = [];
1479
+ for (const [name, state] of decided) {
1480
+ if (state === "present") out.push(name);
1481
+ }
1482
+ return out;
1483
+ }
1484
+ // ── mutations target the writable layer only ────────────────────────
1485
+ async writeFile(path, content) {
1486
+ return await this.layers[this.writableIndex].writeFile(path, content);
1487
+ }
1488
+ async appendFile(path, content) {
1489
+ return await this.layers[this.writableIndex].appendFile(path, content);
1490
+ }
1491
+ async lock(path) {
1492
+ return await this.layers[this.writableIndex].lock(path);
1493
+ }
1494
+ };
1495
+ var ReadOnlyFs = class {
1496
+ constructor(inner) {
1497
+ this.inner = inner;
1498
+ }
1499
+ exists(path) {
1500
+ return this.inner.exists(path);
1501
+ }
1502
+ readFile(path) {
1503
+ return this.inner.readFile(path);
1504
+ }
1505
+ stat(path) {
1506
+ return this.inner.stat(path);
1507
+ }
1508
+ readdir(path) {
1509
+ return this.inner.readdir(path);
1510
+ }
1511
+ walk(path) {
1512
+ return this.inner.walk(path);
1513
+ }
1514
+ async writeFile(path, _content) {
1515
+ throw new Error(`ReadOnlyFs: refusing to write "${path}" \u2014 packs are immutable`);
1516
+ }
1517
+ async appendFile(path, _content) {
1518
+ throw new Error(`ReadOnlyFs: refusing to append "${path}" \u2014 packs are immutable`);
1519
+ }
1520
+ async lock(path) {
1521
+ throw new Error(`ReadOnlyFs: refusing to lock "${path}" \u2014 packs are immutable`);
1522
+ }
1523
+ };
1524
+
1525
+ // src/knowledge/mem-fs.ts
1526
+ var MemFs = class {
1527
+ files;
1528
+ constructor(files) {
1529
+ this.files = { ...files };
1530
+ }
1531
+ async exists(path) {
1532
+ return path in this.files;
1533
+ }
1534
+ async readFile(path) {
1535
+ const content = this.files[path];
1536
+ if (content === void 0) throw new Error(`MemFs: ENOENT ${path}`);
1537
+ return content;
1538
+ }
1539
+ async writeFile(path, content) {
1540
+ this.files[path] = content;
1541
+ }
1542
+ async appendFile(path, content) {
1543
+ this.files[path] = (this.files[path] ?? "") + content;
1544
+ }
1545
+ async readdir(path) {
1546
+ const prefix = path.endsWith("/") ? path : `${path}/`;
1547
+ const names = /* @__PURE__ */ new Set();
1548
+ for (const key of Object.keys(this.files)) {
1549
+ if (key.startsWith(prefix)) names.add(key.slice(prefix.length).split("/")[0]);
1550
+ }
1551
+ return [...names];
1552
+ }
1553
+ async walk(path) {
1554
+ const prefix = path.endsWith("/") ? path : `${path}/`;
1555
+ return Object.keys(this.files).filter((key) => key.startsWith(prefix)).map((key) => key.slice(prefix.length));
1556
+ }
1557
+ async stat(path) {
1558
+ const content = this.files[path];
1559
+ return content === void 0 ? null : { kind: "file", bytes: content.length };
1560
+ }
1561
+ async lock() {
1562
+ return { release: async () => {
1563
+ } };
1564
+ }
1565
+ };
1566
+ var DEFAULT_SHADOW_TRAFFIC_PCT = 0.1;
1567
+ function deterministicBucket(token, salt) {
1568
+ const h = createHash("sha256").update(`${token}|${salt}`).digest();
1569
+ return h.readUInt32BE(0) / 4294967296;
1570
+ }
1571
+ function sampleShadow(token, salt, pct = DEFAULT_SHADOW_TRAFFIC_PCT) {
1572
+ if (!token) return false;
1573
+ if (pct <= 0) return false;
1574
+ if (pct >= 1) return true;
1575
+ return deterministicBucket(token, salt) < pct;
1576
+ }
1577
+
1578
+ // src/stack/resolver.ts
1579
+ var StackResolver = class {
1580
+ constructor(registry) {
1581
+ this.registry = registry;
1582
+ }
1583
+ async resolve(ctx) {
1584
+ const providers = [...this.registry.list()].sort((a, b) => a.band - b.band);
1585
+ const entries = [];
1586
+ const skipped = [];
1587
+ for (const p of providers) {
1588
+ let shadowSampled = false;
1589
+ if (p.shadow) {
1590
+ shadowSampled = sampleShadow(ctx.conversationId, p.id, p.shadow.pct);
1591
+ if (!shadowSampled) {
1592
+ skipped.push({
1593
+ providerId: p.id,
1594
+ dimension: p.dimension,
1595
+ reason: "shadow-not-sampled"
1596
+ });
1597
+ continue;
1598
+ }
1599
+ }
1600
+ const refs = await p.resolve(ctx);
1601
+ if (refs.length === 0) {
1602
+ skipped.push({
1603
+ providerId: p.id,
1604
+ dimension: p.dimension,
1605
+ reason: "empty"
1606
+ });
1607
+ continue;
1608
+ }
1609
+ entries.push({
1610
+ providerId: p.id,
1611
+ band: p.band,
1612
+ mode: p.mode,
1613
+ dimension: p.dimension,
1614
+ refs,
1615
+ shadowSampled
1616
+ });
1617
+ }
1618
+ return Object.freeze({
1619
+ entries: Object.freeze(entries),
1620
+ skipped: Object.freeze(skipped)
1621
+ });
1622
+ }
1623
+ };
1624
+
1625
+ // src/stack/mount.ts
1626
+ function buildOverlayFromStack(opts) {
1627
+ const { guildFs, stack, loadFs } = opts;
1628
+ const constraintFs = [];
1629
+ const lensFs = [];
1630
+ for (const entry of stack.entries) {
1631
+ const target = entry.mode === "constraint" ? constraintFs : lensFs;
1632
+ for (const ref of entry.refs) {
1633
+ const fs = loadFs(ref);
1634
+ if (fs) target.push(entry.mode === "constraint" ? new ReadOnlyFs(fs) : fs);
1635
+ }
1636
+ }
1637
+ if (constraintFs.length === 0 && lensFs.length === 0) return guildFs;
1638
+ const layers = [...constraintFs, guildFs, ...lensFs];
1639
+ return new OverlayFs(layers, {
1640
+ whiteout: true,
1641
+ writableLayer: constraintFs.length
1642
+ // index of guildFs
1643
+ });
1644
+ }
1645
+ function flattenPackRefs(stack) {
1646
+ const out = [];
1647
+ for (const entry of stack.entries) {
1648
+ for (const ref of entry.refs) {
1649
+ if (ref.kind && ref.kind !== "pack") continue;
1650
+ if (!out.includes(ref.ref)) out.push(ref.ref);
1651
+ }
1652
+ }
1653
+ return out;
1654
+ }
1655
+ function partitionStackRefs(stack) {
1656
+ const lens = [];
1657
+ const constraint = [];
1658
+ for (const entry of stack.entries) {
1659
+ const target = entry.mode === "constraint" ? constraint : lens;
1660
+ for (const ref of entry.refs) {
1661
+ if (ref.kind && ref.kind !== "pack") continue;
1662
+ if (!target.includes(ref.ref)) target.push(ref.ref);
1663
+ }
1664
+ }
1665
+ return { lens, constraint };
1666
+ }
1667
+
1668
+ // src/sink/runner.ts
1669
+ var SyncRunner = class {
1670
+ constructor(opts) {
1671
+ this.opts = opts;
1672
+ }
1673
+ async run() {
1674
+ const scheme = this.opts.uriScheme ?? "corpus";
1675
+ const throttle = this.opts.throttleMs ?? 0;
1676
+ const sleep = this.opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
1677
+ const entries = await resolveKnowledge({
1678
+ fs: this.opts.fs,
1679
+ query: this.opts.select ?? {}
1680
+ });
1681
+ const results = [];
1682
+ let pushed = 0;
1683
+ let skipped = 0;
1684
+ let failed = 0;
1685
+ for (let i = 0; i < entries.length; i++) {
1686
+ const e = entries[i];
1687
+ if (i > 0 && throttle > 0) await sleep(throttle);
1688
+ const item = {
1689
+ slug: e.slug,
1690
+ kind: e.kind,
1691
+ title: e.title,
1692
+ body: e.body,
1693
+ sources: e.sources,
1694
+ tags: e.tags,
1695
+ confidence: e.confidence,
1696
+ ...e.access ? { access: e.access } : {},
1697
+ uri: `${scheme}://${e.slug}`
1698
+ };
1699
+ let res;
1700
+ try {
1701
+ res = await this.opts.sink.push(item);
1702
+ } catch (err) {
1703
+ res = { uri: item.uri, ok: false, error: err instanceof Error ? err.message : String(err) };
1704
+ }
1705
+ results.push(res);
1706
+ if (res.skipped) skipped++;
1707
+ else if (res.ok) pushed++;
1708
+ else failed++;
1709
+ }
1710
+ return { pushed, skipped, failed, results };
1711
+ }
1712
+ };
1713
+
1714
+ // src/calibration/aggregate.ts
1715
+ var DEFAULT_DISAGREEMENT_THRESHOLD = 1.5;
1716
+ function aggregateReviewerScores(reviews, opts = {}) {
1717
+ if (reviews.length === 0) {
1718
+ return Object.freeze({
1719
+ aggregate: 0,
1720
+ spread: 0,
1721
+ disagreement: false,
1722
+ sampleSize: 0,
1723
+ breakdown: Object.freeze([])
1724
+ });
1725
+ }
1726
+ const threshold = opts.disagreementThreshold ?? DEFAULT_DISAGREEMENT_THRESHOLD;
1727
+ const weighted = opts.weighted !== false;
1728
+ const breakdown = reviews.map((r) => ({
1729
+ reviewerIdentity: r.reviewerIdentity,
1730
+ score: r.score,
1731
+ weight: typeof r.weight === "number" && r.weight > 0 ? r.weight : 1
1732
+ }));
1733
+ const scores = breakdown.map((b) => b.score);
1734
+ const min = Math.min(...scores);
1735
+ const max = Math.max(...scores);
1736
+ const spread = max - min;
1737
+ const aggregate = weighted ? weightedMedian(breakdown) : plainMedian(scores);
1738
+ return Object.freeze({
1739
+ aggregate,
1740
+ spread,
1741
+ disagreement: spread > threshold,
1742
+ sampleSize: reviews.length,
1743
+ breakdown: Object.freeze(breakdown)
1744
+ });
1745
+ }
1746
+ function plainMedian(values) {
1747
+ const sorted = [...values].sort((a, b) => a - b);
1748
+ const mid = Math.floor(sorted.length / 2);
1749
+ if (sorted.length % 2 === 0) {
1750
+ return (sorted[mid - 1] + sorted[mid]) / 2;
1751
+ }
1752
+ return sorted[mid];
1753
+ }
1754
+ function weightedMedian(items) {
1755
+ const sorted = [...items].sort((a, b) => a.score - b.score);
1756
+ const totalWeight = sorted.reduce((acc2, it) => acc2 + it.weight, 0);
1757
+ if (totalWeight === 0) return plainMedian(items.map((i) => i.score));
1758
+ const half = totalWeight / 2;
1759
+ let acc = 0;
1760
+ for (let i = 0; i < sorted.length; i++) {
1761
+ acc += sorted[i].weight;
1762
+ if (acc >= half) {
1763
+ if (acc === half && i + 1 < sorted.length) {
1764
+ return (sorted[i].score + sorted[i + 1].score) / 2;
1765
+ }
1766
+ return sorted[i].score;
1767
+ }
1768
+ }
1769
+ return sorted[sorted.length - 1].score;
1770
+ }
1771
+ var DEFAULT_PATH = "_calibration/reviewer-track-record.yaml";
1772
+ var ReviewerTrackRecord = class {
1773
+ constructor(opts) {
1774
+ this.opts = opts;
1775
+ this.path = opts.path ?? DEFAULT_PATH;
1776
+ }
1777
+ path;
1778
+ /** Load the full record. Empty when the file doesn't exist yet. */
1779
+ async load() {
1780
+ if (!await this.opts.fs.exists(this.path)) return {};
1781
+ const content = await this.opts.fs.readFile(this.path);
1782
+ if (!content.trim()) return {};
1783
+ const parsed = parse(content);
1784
+ if (!parsed || typeof parsed.reviewers !== "object") return {};
1785
+ return Object.freeze(parsed.reviewers);
1786
+ }
1787
+ /** Append one entry under a reviewer's identity. */
1788
+ async append(reviewerIdentity, entry) {
1789
+ const existing = await this.load();
1790
+ const current = existing[reviewerIdentity] ?? [];
1791
+ const next = {
1792
+ reviewers: {
1793
+ ...existing,
1794
+ [reviewerIdentity]: [...current, entry]
1795
+ }
1796
+ };
1797
+ const content = stringify(next);
1798
+ await this.opts.fs.writeFile(this.path, content);
1799
+ }
1800
+ /**
1801
+ * Read all entries for a reviewer in the last N days. Used by the
1802
+ * calibration routine to compute correlation on a rolling window.
1803
+ */
1804
+ async window(reviewerIdentity, days, now) {
1805
+ const all = await this.load();
1806
+ const slice = all[reviewerIdentity] ?? [];
1807
+ const cutoffMs = now.getTime() - days * 864e5;
1808
+ return slice.filter((e) => Date.parse(e.at) >= cutoffMs);
1809
+ }
1810
+ /** Reviewer identities present in the record. */
1811
+ async listReviewers() {
1812
+ const all = await this.load();
1813
+ return Object.keys(all);
1814
+ }
1815
+ };
1816
+
1817
+ // src/calibration/correlation.ts
1818
+ var DEFAULT_MIN_SAMPLE = 10;
1819
+ var DEFAULT_CALIBRATED_THRESHOLD = 0.4;
1820
+ function pearsonCorrelation(xs, ys) {
1821
+ if (xs.length !== ys.length) {
1822
+ throw new Error(
1823
+ `pearsonCorrelation: array length mismatch (xs=${xs.length}, ys=${ys.length})`
1824
+ );
1825
+ }
1826
+ const n = xs.length;
1827
+ if (n < 2) return Number.NaN;
1828
+ let sumX = 0;
1829
+ let sumY = 0;
1830
+ for (let i = 0; i < n; i++) {
1831
+ sumX += xs[i];
1832
+ sumY += ys[i];
1833
+ }
1834
+ const meanX = sumX / n;
1835
+ const meanY = sumY / n;
1836
+ let cov = 0;
1837
+ let varX = 0;
1838
+ let varY = 0;
1839
+ for (let i = 0; i < n; i++) {
1840
+ const dx = xs[i] - meanX;
1841
+ const dy = ys[i] - meanY;
1842
+ cov += dx * dy;
1843
+ varX += dx * dx;
1844
+ varY += dy * dy;
1845
+ }
1846
+ if (varX === 0 || varY === 0) return Number.NaN;
1847
+ return cov / Math.sqrt(varX * varY);
1848
+ }
1849
+ function computeReviewerCalibration(reviewerIdentity, entries, opts = {}) {
1850
+ const minSample = opts.minSampleSize ?? DEFAULT_MIN_SAMPLE;
1851
+ const threshold = opts.calibratedThreshold ?? DEFAULT_CALIBRATED_THRESHOLD;
1852
+ const utilityRows = entries.filter(
1853
+ (e) => typeof e.observedUtility === "number"
1854
+ );
1855
+ const liftRows = entries.filter((e) => typeof e.observedLift === "number");
1856
+ if (Math.max(utilityRows.length, liftRows.length) < minSample) {
1857
+ return Object.freeze({
1858
+ reviewerIdentity,
1859
+ sampleSize: Math.max(utilityRows.length, liftRows.length),
1860
+ correlationUtility: Number.NaN,
1861
+ correlationLift: Number.NaN,
1862
+ isCalibrated: false,
1863
+ suggestedWeight: 1,
1864
+ // neutral — no penalty for insufficient data
1865
+ status: "insufficient-sample"
1866
+ });
1867
+ }
1868
+ const corrUtil = utilityRows.length >= minSample ? pearsonCorrelation(
1869
+ utilityRows.map((e) => e.qualityScore),
1870
+ utilityRows.map((e) => e.observedUtility)
1871
+ ) : Number.NaN;
1872
+ const corrLift = liftRows.length >= minSample ? pearsonCorrelation(
1873
+ liftRows.map((e) => e.qualityScore),
1874
+ liftRows.map((e) => e.observedLift)
1875
+ ) : Number.NaN;
1876
+ const available = [corrUtil, corrLift].filter((x) => !Number.isNaN(x));
1877
+ if (available.length === 0) {
1878
+ return Object.freeze({
1879
+ reviewerIdentity,
1880
+ sampleSize: entries.length,
1881
+ correlationUtility: corrUtil,
1882
+ correlationLift: corrLift,
1883
+ isCalibrated: false,
1884
+ suggestedWeight: 0.3,
1885
+ // de-weight but not zero
1886
+ status: "no-variance"
1887
+ });
1888
+ }
1889
+ const composite = available.reduce((a, b) => a + b, 0) / available.length;
1890
+ let weight;
1891
+ let isCalibrated;
1892
+ if (composite >= threshold) {
1893
+ isCalibrated = true;
1894
+ weight = 0.7 + 0.3 * ((composite - threshold) / (1 - threshold));
1895
+ } else {
1896
+ isCalibrated = false;
1897
+ weight = Math.max(
1898
+ 0,
1899
+ 0.7 * ((composite + 1) / (threshold + 1))
1900
+ );
1901
+ }
1902
+ return Object.freeze({
1903
+ reviewerIdentity,
1904
+ sampleSize: entries.length,
1905
+ correlationUtility: corrUtil,
1906
+ correlationLift: corrLift,
1907
+ isCalibrated,
1908
+ suggestedWeight: Math.max(0, Math.min(1, weight)),
1909
+ status: isCalibrated ? "calibrated" : "miscalibrated"
1910
+ });
1911
+ }
1912
+ var FILE_KIND_TO_SCHEMA = {
1913
+ "knowledge-workspace": "knowledge",
1914
+ "knowledge-source": "knowledge",
1915
+ "knowledge-entry": "knowledge",
1916
+ "collection-schema": "collection",
1917
+ "collection-item": "collection",
1918
+ "playbook": "playbook",
1919
+ "operator": "operator",
1920
+ "workflow": "workflow",
1921
+ "routine": "routine"
1922
+ };
1923
+ var CorpusValidator = class {
1924
+ validators;
1925
+ constructor(opts) {
1926
+ const ajv = new Ajv2020({
1927
+ strict: false,
1928
+ allErrors: true,
1929
+ allowUnionTypes: true
1930
+ });
1931
+ addFormats(ajv);
1932
+ for (const ext of opts.bundle.externals ?? []) {
1933
+ const id = ext.$id;
1934
+ if (id && !ajv.getSchema(id)) ajv.addSchema(ext);
1935
+ }
1936
+ const validators = {};
1937
+ for (const key of Object.keys(opts.bundle.schemas)) {
1938
+ validators[key] = ajv.compile(opts.bundle.schemas[key]);
1939
+ }
1940
+ this.validators = validators;
1941
+ }
1942
+ /**
1943
+ * Validate a single parsed file. Returns issues + a valid flag.
1944
+ * Unknown FileKind ("unknown" bucket) is reported as a single info
1945
+ * issue — the file exists but doesn't fit AIP conventions.
1946
+ */
1947
+ validateFile(file) {
1948
+ if (file.kind === "unknown") {
1949
+ return {
1950
+ valid: false,
1951
+ issues: [
1952
+ {
1953
+ path: file.path,
1954
+ instancePath: "/",
1955
+ message: "file location does not match any AIP convention; expected under sources/, entries/, collections/, playbooks/, operators/, workflows/, or routines/",
1956
+ severity: "info"
1957
+ }
1958
+ ]
1959
+ };
1960
+ }
1961
+ const schemaKey = FILE_KIND_TO_SCHEMA[file.kind];
1962
+ if (!schemaKey) {
1963
+ return {
1964
+ valid: true,
1965
+ issues: []
1966
+ };
1967
+ }
1968
+ const validator = this.validators[schemaKey];
1969
+ const ok = validator(file.frontmatter);
1970
+ if (ok) return { valid: true, issues: [] };
1971
+ const issues = (validator.errors ?? []).map((e) => ({
1972
+ path: file.path,
1973
+ instancePath: e.instancePath || "/",
1974
+ message: `${e.message}${e.params && Object.keys(e.params).length > 0 ? ` (${JSON.stringify(e.params)})` : ""}`,
1975
+ severity: "error"
1976
+ }));
1977
+ return { valid: false, issues };
1978
+ }
1979
+ /**
1980
+ * Validate every file in a workspace snapshot. Returns a flat list
1981
+ * of issues across all files plus a `valid` flag that's true only
1982
+ * if zero error-severity issues exist.
1983
+ */
1984
+ validateWorkspace(snapshot) {
1985
+ const all = [];
1986
+ const buckets = [
1987
+ snapshot.workspace ? [snapshot.workspace] : [],
1988
+ snapshot.sources,
1989
+ snapshot.entries,
1990
+ snapshot.collections,
1991
+ snapshot.collectionItems,
1992
+ snapshot.playbooks,
1993
+ snapshot.operators,
1994
+ snapshot.workflows,
1995
+ snapshot.routines,
1996
+ snapshot.unknown
1997
+ ];
1998
+ for (const bucket of buckets) {
1999
+ for (const file of bucket) {
2000
+ all.push(...this.validateFile(file).issues);
2001
+ }
2002
+ }
2003
+ return {
2004
+ valid: all.every((i) => i.severity !== "error"),
2005
+ issues: Object.freeze(all)
2006
+ };
2007
+ }
2008
+ };
2009
+
2010
+ // src/validate/linter.ts
2011
+ var CorpusLinter = class {
2012
+ constructor(opts) {
2013
+ this.opts = opts;
2014
+ }
2015
+ lint(snapshot) {
2016
+ const decls = extractLintDeclarations(snapshot);
2017
+ const issues = [];
2018
+ for (const decl of decls) {
2019
+ switch (decl.kind) {
2020
+ case "require-source":
2021
+ issues.push(...this.lintRequireSource(decl, snapshot));
2022
+ break;
2023
+ case "min-confidence":
2024
+ issues.push(...this.lintMinConfidence(decl, snapshot));
2025
+ break;
2026
+ case "max-age":
2027
+ issues.push(...this.lintMaxAge(decl, snapshot));
2028
+ break;
2029
+ case "broken-ref":
2030
+ issues.push(...this.lintBrokenRef(decl, snapshot));
2031
+ break;
2032
+ case "orphan":
2033
+ issues.push(...this.lintOrphan(decl, snapshot));
2034
+ break;
2035
+ case "custom": {
2036
+ const runner = this.opts.customRunners?.[decl.id];
2037
+ if (runner) issues.push(...runner(decl, snapshot));
2038
+ break;
2039
+ }
2040
+ }
2041
+ }
2042
+ return reportFrom(issues);
2043
+ }
2044
+ // ── Lint implementations ────────────────────────────────────────────
2045
+ lintRequireSource(decl, snapshot) {
2046
+ const out = [];
2047
+ for (const entry of snapshot.entries) {
2048
+ if (!matchesAppliesTo(entry, decl.appliesTo)) continue;
2049
+ const sources = entry.frontmatter.sources ?? [];
2050
+ if (sources.length === 0) {
2051
+ out.push({
2052
+ lintId: decl.id,
2053
+ path: entry.path,
2054
+ message: `entry has no sources[] (kind=${entry.frontmatter.kind ?? "?"})`,
2055
+ severity: decl.severity
2056
+ });
2057
+ }
2058
+ }
2059
+ return out;
2060
+ }
2061
+ lintMinConfidence(decl, snapshot) {
2062
+ const min = numberParam(decl, "min", 0.5);
2063
+ const out = [];
2064
+ for (const entry of snapshot.entries) {
2065
+ if (!matchesAppliesTo(entry, decl.appliesTo)) continue;
2066
+ const c = entry.frontmatter.confidence;
2067
+ if (typeof c === "number" && c < min) {
2068
+ out.push({
2069
+ lintId: decl.id,
2070
+ path: entry.path,
2071
+ message: `confidence ${c} < min ${min}`,
2072
+ severity: decl.severity
2073
+ });
2074
+ }
2075
+ }
2076
+ return out;
2077
+ }
2078
+ lintMaxAge(decl, snapshot) {
2079
+ const days = numberParam(decl, "days", 365);
2080
+ const cutoffMs = this.opts.clock.nowMs() - days * 864e5;
2081
+ const out = [];
2082
+ for (const entry of snapshot.entries) {
2083
+ if (!matchesAppliesTo(entry, decl.appliesTo)) continue;
2084
+ const updated = entry.frontmatter.updated_at;
2085
+ if (typeof updated !== "string") continue;
2086
+ const t = Date.parse(updated);
2087
+ if (Number.isNaN(t)) continue;
2088
+ if (t < cutoffMs) {
2089
+ out.push({
2090
+ lintId: decl.id,
2091
+ path: entry.path,
2092
+ message: `updated_at ${updated} older than ${days}d`,
2093
+ severity: decl.severity
2094
+ });
2095
+ }
2096
+ }
2097
+ return out;
2098
+ }
2099
+ lintBrokenRef(decl, snapshot) {
2100
+ const sourceIds = /* @__PURE__ */ new Set();
2101
+ for (const s of snapshot.sources) {
2102
+ const id = s.frontmatter.id;
2103
+ if (typeof id === "string") sourceIds.add(id);
2104
+ }
2105
+ const entrySlugs = /* @__PURE__ */ new Set();
2106
+ for (const e of snapshot.entries) {
2107
+ const slug = e.frontmatter.slug;
2108
+ if (typeof slug === "string") entrySlugs.add(slug);
2109
+ }
2110
+ const out = [];
2111
+ for (const entry of snapshot.entries) {
2112
+ if (!matchesAppliesTo(entry, decl.appliesTo)) continue;
2113
+ const sources = entry.frontmatter.sources ?? [];
2114
+ for (const s of sources) {
2115
+ if (typeof s === "string" && !sourceIds.has(s)) {
2116
+ out.push({
2117
+ lintId: decl.id,
2118
+ path: entry.path,
2119
+ message: `unresolved source ref: ${s}`,
2120
+ severity: decl.severity
2121
+ });
2122
+ }
2123
+ }
2124
+ for (const field of ["supersedes", "contradicts", "links"]) {
2125
+ const refs = entry.frontmatter[field] ?? [];
2126
+ for (const r of refs) {
2127
+ if (typeof r === "string" && !entrySlugs.has(r)) {
2128
+ out.push({
2129
+ lintId: decl.id,
2130
+ path: entry.path,
2131
+ message: `unresolved ${field} ref: ${r}`,
2132
+ severity: decl.severity
2133
+ });
2134
+ }
2135
+ }
2136
+ }
2137
+ }
2138
+ return out;
2139
+ }
2140
+ lintOrphan(decl, snapshot) {
2141
+ const incoming = /* @__PURE__ */ new Set();
2142
+ for (const e of snapshot.entries) {
2143
+ for (const field of ["supersedes", "contradicts", "links"]) {
2144
+ const refs = e.frontmatter[field] ?? [];
2145
+ for (const r of refs) {
2146
+ if (typeof r === "string") incoming.add(r);
2147
+ }
2148
+ }
2149
+ }
2150
+ const out = [];
2151
+ for (const entry of snapshot.entries) {
2152
+ if (!matchesAppliesTo(entry, decl.appliesTo)) continue;
2153
+ const slug = entry.frontmatter.slug;
2154
+ if (typeof slug !== "string") continue;
2155
+ if (!incoming.has(slug)) {
2156
+ out.push({
2157
+ lintId: decl.id,
2158
+ path: entry.path,
2159
+ message: `entry has no incoming references (orphan)`,
2160
+ severity: decl.severity
2161
+ });
2162
+ }
2163
+ }
2164
+ return out;
2165
+ }
2166
+ };
2167
+ function extractLintDeclarations(snapshot) {
2168
+ if (!snapshot.workspace) return [];
2169
+ const raw = snapshot.workspace.frontmatter.lints ?? [];
2170
+ const decls = [];
2171
+ for (const r of raw) {
2172
+ if (!isLintDeclaration(r)) continue;
2173
+ decls.push(r);
2174
+ }
2175
+ return decls;
2176
+ }
2177
+ function isLintDeclaration(x) {
2178
+ if (typeof x !== "object" || x === null) return false;
2179
+ const o = x;
2180
+ return typeof o.id === "string" && typeof o.kind === "string" && typeof o.appliesTo === "string" && typeof o.severity === "string";
2181
+ }
2182
+ function matchesAppliesTo(entry, appliesTo) {
2183
+ if (appliesTo === "*") return true;
2184
+ const kind = entry.frontmatter.kind;
2185
+ if (typeof kind !== "string") return false;
2186
+ return appliesTo.toLowerCase() === kind.toLowerCase();
2187
+ }
2188
+ function numberParam(decl, key, fallback) {
2189
+ const params = decl.params ?? {};
2190
+ const v = params[key];
2191
+ return typeof v === "number" ? v : fallback;
2192
+ }
2193
+ function reportFrom(issues) {
2194
+ let errorCount = 0;
2195
+ let warnCount = 0;
2196
+ let infoCount = 0;
2197
+ for (const i of issues) {
2198
+ if (i.severity === "error") errorCount++;
2199
+ else if (i.severity === "warn") warnCount++;
2200
+ else infoCount++;
2201
+ }
2202
+ return {
2203
+ issues: Object.freeze([...issues]),
2204
+ errorCount,
2205
+ warnCount,
2206
+ infoCount
2207
+ };
2208
+ }
2209
+
2210
+ // src/lifecycle/candidate.ts
2211
+ var DEFAULT_TRANSITIONS = Object.freeze({
2212
+ discovered: ["analyzed", "rejected"],
2213
+ analyzed: ["approved", "rejected", "needs-work"],
2214
+ "needs-work": ["analyzed", "rejected"],
2215
+ approved: [],
2216
+ rejected: []
2217
+ });
2218
+ function canTransition(from, to, graph = DEFAULT_TRANSITIONS) {
2219
+ if (from === to) {
2220
+ return { allowed: false, reason: "same-status (no-op)" };
2221
+ }
2222
+ const allowedTargets = graph[from] ?? [];
2223
+ if (allowedTargets.length === 0) {
2224
+ return {
2225
+ allowed: false,
2226
+ reason: `"${from}" is terminal \u2014 no outgoing transitions`
2227
+ };
2228
+ }
2229
+ if (!allowedTargets.includes(to)) {
2230
+ return {
2231
+ allowed: false,
2232
+ reason: `"${from}" cannot transition to "${to}". Allowed: ${allowedTargets.join(", ")}`
2233
+ };
2234
+ }
2235
+ return { allowed: true };
2236
+ }
2237
+ function transitionGraphFromCollection(fm) {
2238
+ const statuses = fm.statuses ?? [];
2239
+ const graph = {};
2240
+ for (const s of statuses) {
2241
+ if (typeof s !== "object" || s === null) continue;
2242
+ const o = s;
2243
+ if (typeof o.id !== "string") continue;
2244
+ if (o.terminal === true) {
2245
+ graph[o.id] = [];
2246
+ continue;
2247
+ }
2248
+ if (Array.isArray(o.transitionsTo)) {
2249
+ graph[o.id] = o.transitionsTo.filter(
2250
+ (x) => typeof x === "string"
2251
+ );
2252
+ }
2253
+ }
2254
+ return Object.keys(graph).length > 0 ? Object.freeze(graph) : DEFAULT_TRANSITIONS;
2255
+ }
2256
+ var IllegalTransitionError = class extends Error {
2257
+ constructor(from, to, reason) {
2258
+ super(
2259
+ `IllegalTransitionError: ${from} \u2192 ${to} is not allowed (${reason})`
2260
+ );
2261
+ this.from = from;
2262
+ this.to = to;
2263
+ this.reason = reason;
2264
+ this.name = "IllegalTransitionError";
2265
+ }
2266
+ };
2267
+ function assertTransition(from, to, graph) {
2268
+ const check = canTransition(from, to, graph);
2269
+ if (!check.allowed) {
2270
+ throw new IllegalTransitionError(from, to, check.reason ?? "no reason");
2271
+ }
2272
+ }
2273
+
2274
+ // src/lifecycle/gate.ts
2275
+ function extractAutoPromoteConfig(snapshot) {
2276
+ const ws = snapshot.workspace;
2277
+ if (!ws) return { enabled: false };
2278
+ const corpus = ws.frontmatter.metadata?.corpus;
2279
+ if (!corpus?.autoPromote) return { enabled: false };
2280
+ return corpus.autoPromote;
2281
+ }
2282
+ function evaluateGate(candidate, config) {
2283
+ if (!config.enabled) {
2284
+ return { passed: false, failures: [], disabled: true };
2285
+ }
2286
+ const requires = config.requires ?? {};
2287
+ const failures = [];
2288
+ const flat = candidate.frontmatter;
2289
+ const corpusMeta = flat.metadata?.corpus ?? {};
2290
+ const get = (key) => {
2291
+ if (corpusMeta[key] !== void 0) return corpusMeta[key];
2292
+ if (flat[key] !== void 0) return flat[key];
2293
+ return void 0;
2294
+ };
2295
+ if (requires.qualityScore) {
2296
+ const q = get("qualityScore");
2297
+ if (typeof q !== "number") {
2298
+ failures.push({
2299
+ rule: "qualityScore",
2300
+ message: "qualityScore is missing (required by autoPromote)"
2301
+ });
2302
+ } else {
2303
+ if (requires.qualityScore.min !== void 0 && q < requires.qualityScore.min) {
2304
+ failures.push({
2305
+ rule: "qualityScore",
2306
+ message: `qualityScore ${q} < min ${requires.qualityScore.min}`
2307
+ });
2308
+ }
2309
+ if (requires.qualityScore.max !== void 0 && q > requires.qualityScore.max) {
2310
+ failures.push({
2311
+ rule: "qualityScore",
2312
+ message: `qualityScore ${q} > max ${requires.qualityScore.max}`
2313
+ });
2314
+ }
2315
+ }
2316
+ }
2317
+ if (requires.riskScore) {
2318
+ const r = get("riskScore");
2319
+ if (typeof r !== "number") {
2320
+ failures.push({
2321
+ rule: "riskScore",
2322
+ message: "riskScore is missing (required by autoPromote)"
2323
+ });
2324
+ } else {
2325
+ if (requires.riskScore.min !== void 0 && r < requires.riskScore.min) {
2326
+ failures.push({
2327
+ rule: "riskScore",
2328
+ message: `riskScore ${r} < min ${requires.riskScore.min}`
2329
+ });
2330
+ }
2331
+ if (requires.riskScore.max !== void 0 && r > requires.riskScore.max) {
2332
+ failures.push({
2333
+ rule: "riskScore",
2334
+ message: `riskScore ${r} > max ${requires.riskScore.max}`
2335
+ });
2336
+ }
2337
+ }
2338
+ }
2339
+ if (requires.hasArchiveHash) {
2340
+ const h = get("contentHash") ?? get("content_hash");
2341
+ if (typeof h !== "string" || !/^(sha256|sha512|blake3):/.test(h)) {
2342
+ failures.push({
2343
+ rule: "hasArchiveHash",
2344
+ message: "candidate has no archive-grade content_hash"
2345
+ });
2346
+ }
2347
+ }
2348
+ if (requires.requiredFields && requires.requiredFields.length > 0) {
2349
+ const analysis = candidate.analysis ?? {};
2350
+ const body = candidate.body;
2351
+ for (const field of requires.requiredFields) {
2352
+ const inAnalysis = analysis[field];
2353
+ const inBody = body.includes(`## ${humanize(field)}`) || body.includes(`## ${field}`);
2354
+ if ((typeof inAnalysis !== "string" || inAnalysis.trim().length === 0) && !inBody) {
2355
+ failures.push({
2356
+ rule: "requiredFields",
2357
+ message: `candidate is missing required field "${field}" (not in analysis.${field} nor in body section "## ${humanize(field)}")`
2358
+ });
2359
+ }
2360
+ }
2361
+ }
2362
+ if (requires.notRestricted) {
2363
+ const access = corpusMeta.access;
2364
+ const cls = access?.classification;
2365
+ if (cls === "restricted" || cls === "secret") {
2366
+ failures.push({
2367
+ rule: "notRestricted",
2368
+ message: `candidate access.classification is "${cls}" \u2014 fails notRestricted gate`
2369
+ });
2370
+ }
2371
+ }
2372
+ return {
2373
+ passed: failures.length === 0,
2374
+ failures: Object.freeze(failures),
2375
+ disabled: false
2376
+ };
2377
+ }
2378
+ function humanize(field) {
2379
+ const spaced = field.replace(/_/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase();
2380
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
2381
+ }
2382
+
2383
+ // src/lifecycle/promote.ts
2384
+ var PromoteRejectedError = class extends Error {
2385
+ constructor(entrySlug, failures) {
2386
+ super(
2387
+ `PromoteRejectedError: ${entrySlug} failed auto-promote gate \u2014 ${failures.map((f) => `[${f.rule}] ${f.message}`).join(" / ")}`
2388
+ );
2389
+ this.entrySlug = entrySlug;
2390
+ this.failures = failures;
2391
+ this.name = "PromoteRejectedError";
2392
+ }
2393
+ };
2394
+ var CorpusPromoter = class {
2395
+ constructor(ctx) {
2396
+ this.ctx = ctx;
2397
+ }
2398
+ async promote(opts) {
2399
+ const reader = new CorpusWorkspaceReader({ fs: this.ctx.fs });
2400
+ const writer = new CorpusWorkspaceWriter({ fs: this.ctx.fs });
2401
+ const emitter = new CorpusEventEmitter({
2402
+ fs: this.ctx.fs,
2403
+ clock: this.ctx.clock,
2404
+ identity: this.ctx.identity,
2405
+ workspaceRoot: opts.workspacePath
2406
+ });
2407
+ const lockPath = joinPath3(opts.workspacePath, "_log.md");
2408
+ return await writer.transaction(lockPath, async () => {
2409
+ const snapshot = await reader.read(opts.workspacePath);
2410
+ let gatePassed = false;
2411
+ let bypassed = false;
2412
+ if (opts.bypassGate) {
2413
+ bypassed = true;
2414
+ } else {
2415
+ const config = extractAutoPromoteConfig(snapshot);
2416
+ const result = evaluateGate(
2417
+ {
2418
+ frontmatter: opts.frontmatter,
2419
+ body: opts.body
2420
+ },
2421
+ config
2422
+ );
2423
+ gatePassed = result.passed;
2424
+ if (!result.passed && !result.disabled) {
2425
+ throw new PromoteRejectedError(opts.entrySlug, result.failures);
2426
+ }
2427
+ if (result.disabled) bypassed = true;
2428
+ }
2429
+ const existing = snapshot.entries.find(
2430
+ (e) => e.frontmatter.slug === opts.entrySlug
2431
+ );
2432
+ const targetPath = existing?.path ?? opts.entryPath;
2433
+ const expectedToken = existing?.versionToken ?? null;
2434
+ const identity = await this.ctx.identity.resolve();
2435
+ const attestedFm = appendAttestation(
2436
+ opts.frontmatter,
2437
+ makeAttestation({
2438
+ kind: "promoted",
2439
+ identity: identity.principal,
2440
+ at: this.ctx.clock.now().toISOString(),
2441
+ note: bypassed ? `bypassed=true${gatePassed ? "" : " (gate not consulted)"}` : `gatePassed=${gatePassed}`
2442
+ })
2443
+ );
2444
+ const doc = {
2445
+ frontmatter: attestedFm,
2446
+ body: opts.body
2447
+ };
2448
+ const entryAbsPath = joinPath3(opts.workspacePath, targetPath);
2449
+ const entryVersionToken = await writer.writeMarkdown(
2450
+ entryAbsPath,
2451
+ doc,
2452
+ expectedToken
2453
+ );
2454
+ const freshSnapshot = await reader.read(opts.workspacePath);
2455
+ const indexResult = await this.ctx.indexer.syncEntry(
2456
+ freshSnapshot,
2457
+ opts.entrySlug
2458
+ );
2459
+ await regenIndexFile({
2460
+ fs: this.ctx.fs,
2461
+ workspacePath: opts.workspacePath,
2462
+ snapshot: freshSnapshot,
2463
+ clock: this.ctx.clock
2464
+ });
2465
+ if (opts.candidateId && opts.candidateSidecarPath) {
2466
+ const { CandidatesSidecar: CandidatesSidecar2 } = await import('./sidecar-TTWQD5R3.mjs');
2467
+ const sidecar = new CandidatesSidecar2({
2468
+ fs: this.ctx.fs,
2469
+ path: joinPath3(opts.workspacePath, opts.candidateSidecarPath)
2470
+ });
2471
+ try {
2472
+ await sidecar.take(opts.candidateId);
2473
+ } catch {
2474
+ }
2475
+ }
2476
+ await emitter.emit("corpus.entry.promoted", {
2477
+ slug: opts.entrySlug,
2478
+ kind: opts.entryKind,
2479
+ entryPath: targetPath,
2480
+ candidateId: opts.candidateId,
2481
+ bypassed,
2482
+ gatePassed,
2483
+ chunkCount: indexResult.chunkCount
2484
+ });
2485
+ return {
2486
+ entryPath: targetPath,
2487
+ entryVersionToken,
2488
+ chunkCount: indexResult.chunkCount,
2489
+ gatePassed,
2490
+ bypassed
2491
+ };
2492
+ });
2493
+ }
2494
+ };
2495
+ async function regenIndexFile(input) {
2496
+ const lines = [];
2497
+ lines.push("# Corpus index");
2498
+ lines.push("");
2499
+ lines.push(
2500
+ `Generated ${input.clock.now().toISOString()} \u2014 ${input.snapshot.entries.length} entries.`
2501
+ );
2502
+ lines.push("");
2503
+ const byKind = /* @__PURE__ */ new Map();
2504
+ for (const entry of input.snapshot.entries) {
2505
+ const k = String(entry.frontmatter.kind ?? "unknown");
2506
+ const bucket = byKind.get(k) ?? [];
2507
+ bucket.push(entry);
2508
+ byKind.set(k, bucket);
2509
+ }
2510
+ for (const [kind, entries] of [...byKind].sort(
2511
+ ([a], [b]) => a.localeCompare(b)
2512
+ )) {
2513
+ lines.push(`## ${kind} (${entries.length})`);
2514
+ lines.push("");
2515
+ for (const e of entries.sort(
2516
+ (a, b) => String(a.frontmatter.slug).localeCompare(String(b.frontmatter.slug))
2517
+ )) {
2518
+ const slug = e.frontmatter.slug;
2519
+ const title = e.frontmatter.title ?? slug;
2520
+ const corpus = e.frontmatter.metadata?.corpus;
2521
+ const q = corpus?.qualityScore;
2522
+ const s = corpus?.status ?? "active";
2523
+ const meta = [];
2524
+ if (s !== "active") meta.push(`status=${s}`);
2525
+ if (typeof q === "number") meta.push(`q=${q.toFixed(1)}`);
2526
+ const suffix = meta.length > 0 ? ` _(${meta.join(", ")})_` : "";
2527
+ lines.push(`- [[${slug}]] \u2014 ${title}${suffix}`);
2528
+ }
2529
+ lines.push("");
2530
+ }
2531
+ const content = lines.join("\n");
2532
+ await input.fs.writeFile(
2533
+ joinPath3(input.workspacePath, "_index.md"),
2534
+ content
2535
+ );
2536
+ }
2537
+ function joinPath3(a, b) {
2538
+ if (!a) return b;
2539
+ return a.endsWith("/") ? a + b : a + "/" + b;
2540
+ }
2541
+
2542
+ // src/index/chunker.ts
2543
+ var DEFAULT_TARGET = 1500;
2544
+ var DEFAULT_OVERLAP = 150;
2545
+ var DEFAULT_MAX_BYTES = 64 * 1024;
2546
+ function chunkText(text, opts = {}) {
2547
+ const target = opts.targetChars ?? DEFAULT_TARGET;
2548
+ const overlap = opts.overlapChars ?? DEFAULT_OVERLAP;
2549
+ const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
2550
+ if (overlap >= target) {
2551
+ throw new Error("chunkText: overlap must be < target");
2552
+ }
2553
+ const trimmed = text.trim();
2554
+ if (trimmed.length === 0) return [];
2555
+ if (byteLength(trimmed) <= maxBytes && trimmed.length <= target) {
2556
+ return [trimmed];
2557
+ }
2558
+ const chunks = [];
2559
+ let cursor = 0;
2560
+ while (cursor < trimmed.length) {
2561
+ let end = Math.min(cursor + target, trimmed.length);
2562
+ let slice = trimmed.slice(cursor, end);
2563
+ while (byteLength(slice) > maxBytes && slice.length > 1) {
2564
+ end = cursor + Math.floor((end - cursor) * 0.9);
2565
+ slice = trimmed.slice(cursor, end);
2566
+ }
2567
+ chunks.push(slice);
2568
+ if (end >= trimmed.length) break;
2569
+ cursor = end - overlap;
2570
+ if (cursor < 0) cursor = 0;
2571
+ }
2572
+ return chunks;
2573
+ }
2574
+ function byteLength(s) {
2575
+ return new TextEncoder().encode(s).length;
2576
+ }
2577
+
2578
+ // src/index/indexer.ts
2579
+ var CorpusIndexer = class {
2580
+ constructor(opts) {
2581
+ this.opts = opts;
2582
+ }
2583
+ /**
2584
+ * Re-sync every active entry. Used at boot (cold-install) and by
2585
+ * the admin `corpus reindex` command for drift recovery.
2586
+ */
2587
+ async reindex(snapshot) {
2588
+ let pushed = 0;
2589
+ let removed = 0;
2590
+ let skipped = 0;
2591
+ let chunkCount = 0;
2592
+ for (const entry of snapshot.entries) {
2593
+ const status = readStatus(entry);
2594
+ if (status === "active") {
2595
+ const n = await this.pushEntry(entry);
2596
+ pushed++;
2597
+ chunkCount += n;
2598
+ } else if (status === "deprecated" || status === "archived") {
2599
+ const slug = entry.frontmatter.slug;
2600
+ if (typeof slug === "string") {
2601
+ const r = await this.opts.writer.removeEntry(slug);
2602
+ removed += r.removed > 0 ? 1 : 0;
2603
+ }
2604
+ } else {
2605
+ skipped++;
2606
+ }
2607
+ }
2608
+ return { pushed, removed, skipped, chunkCount };
2609
+ }
2610
+ /**
2611
+ * Re-sync a single entry by slug. The lifecycle's `promote`
2612
+ * workflow calls this immediately after writing the entry file.
2613
+ */
2614
+ async syncEntry(snapshot, slug) {
2615
+ const entry = snapshot.entries.find((e) => e.frontmatter.slug === slug);
2616
+ if (!entry) {
2617
+ await this.opts.writer.removeEntry(slug);
2618
+ return { chunkCount: 0 };
2619
+ }
2620
+ const status = readStatus(entry);
2621
+ if (status !== "active") {
2622
+ await this.opts.writer.removeEntry(slug);
2623
+ return { chunkCount: 0 };
2624
+ }
2625
+ await this.opts.writer.removeEntry(slug);
2626
+ const n = await this.pushEntry(entry);
2627
+ return { chunkCount: n };
2628
+ }
2629
+ /** Drop one entry from the backing engine (deprecation, GDPR erase). */
2630
+ async removeEntry(slug) {
2631
+ await this.opts.writer.removeEntry(slug);
2632
+ }
2633
+ // ── Internal ─────────────────────────────────────────────────────
2634
+ async pushEntry(entry) {
2635
+ const slug = entry.frontmatter.slug;
2636
+ if (typeof slug !== "string") return 0;
2637
+ const chunks = chunkText(entry.body, this.opts.chunker).map((text) => ({
2638
+ text
2639
+ }));
2640
+ const corpus = readCorpusMetadata(entry);
2641
+ await this.opts.writer.pushChunks({
2642
+ entrySlug: slug,
2643
+ entryPath: entry.path,
2644
+ title: typeof entry.frontmatter.title === "string" ? entry.frontmatter.title : void 0,
2645
+ chunks,
2646
+ // Forward the corpus-namespaced metadata so the engine adapter
2647
+ // can hydrate hits without re-reading the file.
2648
+ entryMetadata: {
2649
+ kind: entry.frontmatter.kind,
2650
+ sources: entry.frontmatter.sources,
2651
+ confidence: entry.frontmatter.confidence,
2652
+ tags: entry.frontmatter.tags,
2653
+ ...corpus
2654
+ }
2655
+ });
2656
+ return chunks.length;
2657
+ }
2658
+ };
2659
+ function readStatus(entry) {
2660
+ const corpus = entry.frontmatter.metadata?.corpus;
2661
+ const s = corpus?.status;
2662
+ return typeof s === "string" ? s : "active";
2663
+ }
2664
+ function readCorpusMetadata(entry) {
2665
+ const corpus = entry.frontmatter.metadata?.corpus;
2666
+ return corpus ?? {};
2667
+ }
2668
+ var ANY_REF = "*";
2669
+ function prefixedRefNormalizer(singular, plural) {
2670
+ const bare = `${singular}/`;
2671
+ const ws = `ws://${plural}/`;
2672
+ return (ref) => {
2673
+ const stripped = ref.startsWith(bare) ? ref.slice(bare.length) : ref.startsWith(ws) ? ref.slice(ws.length) : ref;
2674
+ return stripped === "*" ? ANY_REF : stripped;
2675
+ };
2676
+ }
2677
+ var SLUG_RE = /^[a-z][a-z0-9-]*[a-z0-9]$/;
2678
+ function slugValidator(ref) {
2679
+ if (ref === ANY_REF) return null;
2680
+ return SLUG_RE.test(ref) ? null : `not a slug (lowercase, digits, dashes): "${ref}"`;
2681
+ }
2682
+ var identityAxis = {
2683
+ id: "identity",
2684
+ aip: 9,
2685
+ description: "The subject's own slug \u2014 targets one specific operator.",
2686
+ normalizeRef: prefixedRefNormalizer("operator", "operators"),
2687
+ validateRef: slugValidator
2688
+ };
2689
+ var roleAxis = {
2690
+ id: "role",
2691
+ aip: 47,
2692
+ description: "The AIP-47 catalog role (job) the subject fulfils \u2014 targets every operator in the role.",
2693
+ normalizeRef: prefixedRefNormalizer("role", "roles"),
2694
+ validateRef: slugValidator
2695
+ };
2696
+ var positionAxis = {
2697
+ id: "position",
2698
+ aip: 6,
2699
+ description: "The host-assigned seat label (slugified) \u2014 targets the one operator holding the seat. See AIP-47 \xA7Role vs Position vs Access role.",
2700
+ normalizeRef: prefixedRefNormalizer("position", "positions"),
2701
+ validateRef: slugValidator
2702
+ };
2703
+ var capabilityAxis = {
2704
+ id: "capability",
2705
+ aip: 9,
2706
+ description: "A capability slug the subject carries.",
2707
+ normalizeRef: prefixedRefNormalizer("capability", "capabilities"),
2708
+ validateRef: slugValidator
2709
+ };
2710
+ var WELL_KNOWN_AXES = Object.freeze([
2711
+ identityAxis,
2712
+ roleAxis,
2713
+ positionAxis,
2714
+ capabilityAxis
2715
+ ]);
2716
+ function createAxisRegistry(extra = []) {
2717
+ const registry = createRegistry({
2718
+ family: "binding-axis",
2719
+ keyBy: (axis) => axis.id
2720
+ });
2721
+ for (const axis of WELL_KNOWN_AXES) registry.register(axis);
2722
+ for (const axis of extra) registry.register(axis);
2723
+ return registry;
2724
+ }
2725
+
2726
+ // src/binding/selector.ts
2727
+ var EMPTY_SELECTOR = Object.freeze({});
2728
+ function isEmptySelector(selector) {
2729
+ return !selector.allOf?.length && !selector.anyOf?.length;
2730
+ }
2731
+ function matchesSelector(selector, dimensions, options = {}) {
2732
+ if (isEmptySelector(selector)) return false;
2733
+ const { allOf, anyOf } = selector;
2734
+ if (allOf?.length) {
2735
+ for (const term of allOf) {
2736
+ if (!termMatches(term, dimensions, options)) return false;
2737
+ }
2738
+ }
2739
+ if (anyOf?.length) {
2740
+ return anyOf.some((term) => termMatches(term, dimensions, options));
2741
+ }
2742
+ return true;
2743
+ }
2744
+ function termMatches(term, dimensions, options) {
2745
+ const raw = dimensions[term.axis];
2746
+ if (raw === void 0) return false;
2747
+ const values = typeof raw === "string" ? [raw] : raw;
2748
+ if (values.length === 0) return false;
2749
+ const axis = options.axes?.get(term.axis);
2750
+ for (const ref of term.anyOf) {
2751
+ const normalized = normalizeRef(ref, axis);
2752
+ if (normalized === ANY_REF) return true;
2753
+ if (values.includes(normalized)) return true;
2754
+ }
2755
+ return false;
2756
+ }
2757
+ function normalizeRef(ref, axis) {
2758
+ return axis?.normalizeRef ? axis.normalizeRef(ref) : ref;
2759
+ }
2760
+
2761
+ // src/binding/parse.ts
2762
+ function parseSelectorFrontmatter(raw) {
2763
+ if (!isPlainObject(raw)) return null;
2764
+ if ("allOf" in raw || "anyOf" in raw) {
2765
+ const allOf2 = readTermList(raw.allOf);
2766
+ const anyOf = readTermList(raw.anyOf);
2767
+ if (allOf2 === null || anyOf === null) return null;
2768
+ if (!allOf2.length && !anyOf.length) return null;
2769
+ return Object.freeze({
2770
+ ...allOf2.length ? { allOf: allOf2 } : {},
2771
+ ...anyOf.length ? { anyOf } : {}
2772
+ });
2773
+ }
2774
+ const allOf = [];
2775
+ for (const [axis, value] of Object.entries(raw)) {
2776
+ const refs = readRefs(value);
2777
+ if (refs === null) return null;
2778
+ allOf.push(Object.freeze({ axis, anyOf: refs }));
2779
+ }
2780
+ if (!allOf.length) return null;
2781
+ return Object.freeze({ allOf: Object.freeze(allOf) });
2782
+ }
2783
+ function readTermList(raw) {
2784
+ if (raw === void 0) return [];
2785
+ if (!Array.isArray(raw)) return null;
2786
+ const out = [];
2787
+ for (const entry of raw) {
2788
+ if (!isPlainObject(entry)) return null;
2789
+ const axis = entry.axis;
2790
+ if (typeof axis !== "string" || !axis.length) return null;
2791
+ const refs = readRefs(entry.anyOf);
2792
+ if (refs === null || !refs.length) return null;
2793
+ out.push(Object.freeze({ axis, anyOf: refs }));
2794
+ }
2795
+ return Object.freeze(out);
2796
+ }
2797
+ function readRefs(value) {
2798
+ if (typeof value === "string") {
2799
+ return value.length ? Object.freeze([value]) : null;
2800
+ }
2801
+ if (Array.isArray(value)) {
2802
+ const refs = value;
2803
+ if (!refs.length) return null;
2804
+ if (!refs.every((r) => typeof r === "string" && r.length > 0))
2805
+ return null;
2806
+ return Object.freeze([...refs]);
2807
+ }
2808
+ return null;
2809
+ }
2810
+ function isPlainObject(v) {
2811
+ return typeof v === "object" && v !== null && !Array.isArray(v);
2812
+ }
2813
+
2814
+ // src/binding/legacy.ts
2815
+ var normalizeOperatorRef = identityAxis.normalizeRef;
2816
+ var normalizeRoleRef = roleAxis.normalizeRef;
2817
+ var normalizeSkillRef = prefixedRefNormalizer("skill", "skills");
2818
+ var normalizeRuntimeRef = prefixedRefNormalizer("runtime", "runtimes");
2819
+ function compileLegacyPlaybookBinding(targets, bindsOperator) {
2820
+ const identity = /* @__PURE__ */ new Set();
2821
+ const role = /* @__PURE__ */ new Set();
2822
+ const skill = /* @__PURE__ */ new Set();
2823
+ const runtime = /* @__PURE__ */ new Set();
2824
+ if (bindsOperator) {
2825
+ identity.add(normalizeOperatorRef(bindsOperator));
2826
+ role.add(normalizeRoleRef(bindsOperator));
2827
+ }
2828
+ for (const target of targets) {
2829
+ switch (target.kind) {
2830
+ case "operator":
2831
+ identity.add(normalizeOperatorRef(target.ref));
2832
+ role.add(normalizeRoleRef(target.ref));
2833
+ break;
2834
+ case "role":
2835
+ role.add(normalizeRoleRef(target.ref));
2836
+ break;
2837
+ case "skill":
2838
+ skill.add(normalizeSkillRef(target.ref));
2839
+ break;
2840
+ case "runtime":
2841
+ runtime.add(normalizeRuntimeRef(target.ref));
2842
+ break;
2843
+ }
2844
+ }
2845
+ const anyOf = [];
2846
+ pushTerm(anyOf, identityAxis.id, identity);
2847
+ pushTerm(anyOf, roleAxis.id, role);
2848
+ pushTerm(anyOf, "skill", skill);
2849
+ pushTerm(anyOf, "runtime", runtime);
2850
+ if (!anyOf.length) return Object.freeze({});
2851
+ return Object.freeze({ anyOf: Object.freeze(anyOf) });
2852
+ }
2853
+ function pushTerm(out, axis, refs) {
2854
+ if (!refs.size) return;
2855
+ out.push(Object.freeze({ axis, anyOf: Object.freeze([...refs]) }));
2856
+ }
2857
+
2858
+ // src/binding/attachment.ts
2859
+ function matchAttachments(declarations, dimensions, options = {}) {
2860
+ return declarations.filter(
2861
+ (d) => matchesSelector(d.selector, dimensions, { axes: options.axes })
2862
+ );
2863
+ }
2864
+ function matchAttachmentRefs(declarations, kind, dimensions, options = {}) {
2865
+ const seen = /* @__PURE__ */ new Set();
2866
+ for (const d of matchAttachments(declarations, dimensions, options)) {
2867
+ if (d.asset.kind === kind) seen.add(d.asset.ref);
2868
+ }
2869
+ return [...seen];
2870
+ }
2871
+
2872
+ // src/playbooks/registry.ts
2873
+ var PlaybookRegistry = class {
2874
+ bySlug;
2875
+ all;
2876
+ constructor(opts) {
2877
+ const parsed = [];
2878
+ for (const file of opts.snapshot.playbooks) {
2879
+ const playbook = parsePlaybook(file);
2880
+ if (playbook) parsed.push(playbook);
2881
+ }
2882
+ parsed.sort((a, b) => b.priority - a.priority);
2883
+ this.all = Object.freeze(parsed);
2884
+ const map = /* @__PURE__ */ new Map();
2885
+ for (const p of parsed) map.set(p.slug, p);
2886
+ this.bySlug = map;
2887
+ }
2888
+ list() {
2889
+ return this.all;
2890
+ }
2891
+ bySlugOrNull(slug) {
2892
+ return this.bySlug.get(slug) ?? null;
2893
+ }
2894
+ listBy(query = {}) {
2895
+ return this.all.filter((p) => matches(p, query));
2896
+ }
2897
+ };
2898
+ function parsePlaybook(file) {
2899
+ const fm = file.frontmatter;
2900
+ const slug = typeof fm.slug === "string" ? fm.slug : null;
2901
+ const title = typeof fm.title === "string" ? fm.title : null;
2902
+ if (!slug || !title) return null;
2903
+ const status = readStatus2(fm);
2904
+ const kind = fm.kind === "block-replacement" ? "block-replacement" : "overlay";
2905
+ const priority = typeof fm.priority === "number" ? fm.priority : 100;
2906
+ const bindsOperator = typeof fm.binds_operator === "string" ? fm.binds_operator : void 0;
2907
+ const targets = readTargets(fm);
2908
+ const supersedes = readStringArray(fm.supersedes);
2909
+ const corpus = readCorpusMeta(fm);
2910
+ const explicitSelector = parseSelectorFrontmatter(fm.selector);
2911
+ const selector = explicitSelector ?? compileLegacyPlaybookBinding(targets, bindsOperator);
2912
+ const selectorSource = explicitSelector ? "selector" : "legacy";
2913
+ return Object.freeze({
2914
+ path: file.path,
2915
+ slug,
2916
+ title,
2917
+ status,
2918
+ kind,
2919
+ priority,
2920
+ targets,
2921
+ bindsOperator,
2922
+ selector,
2923
+ selectorSource,
2924
+ supersedes,
2925
+ body: file.body,
2926
+ corpus,
2927
+ versionToken: file.versionToken,
2928
+ file
2929
+ });
2930
+ }
2931
+ function readStatus2(fm) {
2932
+ const s = fm.status;
2933
+ if (s === "active" || s === "shadow" || s === "archived") return s;
2934
+ return "shadow";
2935
+ }
2936
+ function readTargets(fm) {
2937
+ const raw = fm.targets;
2938
+ if (!Array.isArray(raw)) return [];
2939
+ const out = [];
2940
+ for (const t of raw) {
2941
+ if (typeof t !== "object" || t === null) continue;
2942
+ const o = t;
2943
+ if (typeof o.ref !== "string" || o.kind !== "operator" && o.kind !== "role" && o.kind !== "skill" && o.kind !== "runtime")
2944
+ continue;
2945
+ out.push({ kind: o.kind, ref: o.ref });
2946
+ }
2947
+ return Object.freeze(out);
2948
+ }
2949
+ function readStringArray(v) {
2950
+ if (!Array.isArray(v)) return [];
2951
+ return Object.freeze(
2952
+ v.filter((x) => typeof x === "string")
2953
+ );
2954
+ }
2955
+ function readCorpusMeta(fm) {
2956
+ const meta = fm.metadata?.corpus;
2957
+ if (!meta || typeof meta !== "object") return {};
2958
+ return Object.freeze(meta);
2959
+ }
2960
+ var AXES = createAxisRegistry();
2961
+ function matches(p, q) {
2962
+ if (q.status !== void 0) {
2963
+ const wanted = Array.isArray(q.status) ? q.status : [q.status];
2964
+ if (!wanted.includes(p.status)) return false;
2965
+ }
2966
+ if (q.kind !== void 0 && p.kind !== q.kind) return false;
2967
+ if (q.dimensions !== void 0) {
2968
+ return matchesSelector(p.selector, q.dimensions, { axes: AXES });
2969
+ }
2970
+ if (q.forOperatorSlug !== void 0) {
2971
+ const slugs = Array.isArray(q.forOperatorSlug) ? q.forOperatorSlug : [q.forOperatorSlug];
2972
+ return matchesSelector(
2973
+ p.selector,
2974
+ { identity: slugs, role: slugs },
2975
+ { axes: AXES }
2976
+ );
2977
+ }
2978
+ if (q.operatorRef !== void 0) {
2979
+ return p.targets.some(
2980
+ (t) => t.kind === "operator" && t.ref === q.operatorRef
2981
+ );
2982
+ }
2983
+ return true;
2984
+ }
2985
+
2986
+ // src/playbooks/resolver.ts
2987
+ var OperatorOverlayResolver = class {
2988
+ constructor(registry) {
2989
+ this.registry = registry;
2990
+ }
2991
+ resolve(ctx) {
2992
+ const slugs = ctx.roleSlug ? [ctx.operatorSlug, ctx.roleSlug] : [ctx.operatorSlug];
2993
+ const candidates = this.registry.listBy({
2994
+ ...ctx.dimensions ? { dimensions: ctx.dimensions } : { forOperatorSlug: slugs },
2995
+ status: ["active", "shadow"]
2996
+ });
2997
+ const overlays = [];
2998
+ for (const p of candidates) {
2999
+ if (p.status === "archived") continue;
3000
+ const shadowSampled = p.status === "shadow" ? sampleShadow2(ctx, p) : false;
3001
+ if (p.status === "shadow" && !shadowSampled) continue;
3002
+ overlays.push({
3003
+ playbookSlug: p.slug,
3004
+ status: p.status,
3005
+ kind: p.kind,
3006
+ priority: p.priority,
3007
+ body: p.body,
3008
+ playbookPath: p.path,
3009
+ shadowSampled
3010
+ });
3011
+ }
3012
+ return Object.freeze({
3013
+ operatorSlug: ctx.operatorSlug,
3014
+ overlays: Object.freeze(overlays)
3015
+ });
3016
+ }
3017
+ };
3018
+ function sampleShadow2(ctx, p) {
3019
+ const pct = typeof p.corpus.shadowTrafficPct === "number" ? p.corpus.shadowTrafficPct : DEFAULT_SHADOW_TRAFFIC_PCT;
3020
+ return sampleShadow(ctx.conversationId, p.slug, pct);
3021
+ }
3022
+ function renderOverlays(result) {
3023
+ const overlays = result.overlays.filter((o) => o.kind === "overlay");
3024
+ const replacements = result.overlays.filter((o) => o.kind === "block-replacement").map((o) => ({ playbookSlug: o.playbookSlug, body: o.body }));
3025
+ return Object.freeze({
3026
+ appendBlock: overlays.map((o) => o.body.trim()).join("\n\n---\n\n"),
3027
+ replacements: Object.freeze(replacements)
3028
+ });
3029
+ }
3030
+ var PlaybookNotFoundError = class extends Error {
3031
+ constructor(slug) {
3032
+ super(`PlaybookNotFoundError: no playbook found with slug "${slug}"`);
3033
+ this.slug = slug;
3034
+ this.name = "PlaybookNotFoundError";
3035
+ }
3036
+ };
3037
+ var IllegalPlaybookTransitionError = class extends Error {
3038
+ constructor(slug, from, to) {
3039
+ super(
3040
+ `IllegalPlaybookTransitionError: ${slug}: ${from} \u2192 ${to} is not allowed`
3041
+ );
3042
+ this.slug = slug;
3043
+ this.from = from;
3044
+ this.to = to;
3045
+ this.name = "IllegalPlaybookTransitionError";
3046
+ }
3047
+ };
3048
+ var PlaybookLifecycle = class {
3049
+ constructor(opts) {
3050
+ this.opts = opts;
3051
+ }
3052
+ /**
3053
+ * Transition `slug` from shadow → active. Cascades through
3054
+ * `supersedes[]`: any currently-active playbook listed there is
3055
+ * archived (with `archiveReason: superseded-by-<slug>`) in the
3056
+ * same lock-guarded transaction.
3057
+ */
3058
+ async activate(slug) {
3059
+ const writer = new CorpusWorkspaceWriter({ fs: this.opts.fs });
3060
+ const lockPath = joinPath4(this.opts.workspacePath, "_log.md");
3061
+ return await writer.transaction(lockPath, async () => {
3062
+ const playbook = await this.loadOne(slug);
3063
+ assertTransition2(playbook.status, "active", slug);
3064
+ const supersededSlugs = [];
3065
+ const reg = await this.loadRegistry();
3066
+ const identity = await this.opts.identity.resolve();
3067
+ for (const supersededSlug of playbook.supersedes) {
3068
+ const prev = reg.bySlugOrNull(supersededSlug);
3069
+ if (!prev || prev.status !== "active") continue;
3070
+ const att = this.attestation(
3071
+ "archived",
3072
+ identity.principal,
3073
+ `superseded-by-${slug}`
3074
+ );
3075
+ await this.writeWith(
3076
+ prev,
3077
+ (fm) => appendAttestation(
3078
+ patchStatus(fm, "archived", {
3079
+ archiveReason: `superseded-by-${slug}`,
3080
+ archivedAt: att.at
3081
+ }),
3082
+ att
3083
+ )
3084
+ );
3085
+ await this.emitter().emit("playbook.archived", {
3086
+ slug: prev.slug,
3087
+ archiveReason: `superseded-by-${slug}`
3088
+ });
3089
+ supersededSlugs.push(prev.slug);
3090
+ }
3091
+ const activateAtt = this.attestation(
3092
+ "activated",
3093
+ identity.principal,
3094
+ `from=${playbook.status}`
3095
+ );
3096
+ const newToken = await this.writeWith(
3097
+ playbook,
3098
+ (fm) => appendAttestation(
3099
+ patchStatus(fm, "active", { activatedAt: activateAtt.at }),
3100
+ activateAtt
3101
+ )
3102
+ );
3103
+ await this.emitter().emit("playbook.activated", {
3104
+ slug,
3105
+ previousStatus: playbook.status,
3106
+ supersededSlugs
3107
+ });
3108
+ return {
3109
+ slug,
3110
+ previousStatus: playbook.status,
3111
+ versionToken: newToken,
3112
+ supersededSlugs: Object.freeze(supersededSlugs)
3113
+ };
3114
+ });
3115
+ }
3116
+ /**
3117
+ * Transition `slug` → archived. Skip-no-op if already archived.
3118
+ */
3119
+ async archive(slug, reason) {
3120
+ const writer = new CorpusWorkspaceWriter({ fs: this.opts.fs });
3121
+ const lockPath = joinPath4(this.opts.workspacePath, "_log.md");
3122
+ return await writer.transaction(lockPath, async () => {
3123
+ const playbook = await this.loadOne(slug);
3124
+ if (playbook.status === "archived") {
3125
+ return {
3126
+ slug,
3127
+ previousStatus: "archived",
3128
+ versionToken: playbook.versionToken
3129
+ };
3130
+ }
3131
+ assertTransition2(playbook.status, "archived", slug);
3132
+ const identity = await this.opts.identity.resolve();
3133
+ const att = this.attestation("archived", identity.principal, reason);
3134
+ const newToken = await this.writeWith(
3135
+ playbook,
3136
+ (fm) => appendAttestation(
3137
+ patchStatus(fm, "archived", {
3138
+ archiveReason: reason,
3139
+ archivedAt: att.at
3140
+ }),
3141
+ att
3142
+ )
3143
+ );
3144
+ await this.emitter().emit("playbook.archived", {
3145
+ slug,
3146
+ previousStatus: playbook.status,
3147
+ archiveReason: reason
3148
+ });
3149
+ return {
3150
+ slug,
3151
+ previousStatus: playbook.status,
3152
+ versionToken: newToken
3153
+ };
3154
+ });
3155
+ }
3156
+ // ── Internal ─────────────────────────────────────────────────────
3157
+ async loadOne(slug) {
3158
+ const reg = await this.loadRegistry();
3159
+ const p = reg.bySlugOrNull(slug);
3160
+ if (!p) throw new PlaybookNotFoundError(slug);
3161
+ return p;
3162
+ }
3163
+ async loadRegistry() {
3164
+ const reader = new CorpusWorkspaceReader({ fs: this.opts.fs });
3165
+ const snapshot = await reader.read(this.opts.workspacePath);
3166
+ return new PlaybookRegistry({ snapshot });
3167
+ }
3168
+ async writeWith(playbook, patch) {
3169
+ const writer = new CorpusWorkspaceWriter({ fs: this.opts.fs });
3170
+ const nextFm = patch({ ...playbook.file.frontmatter });
3171
+ const nextContent = matter.stringify(
3172
+ playbook.body.startsWith("\n") ? playbook.body : "\n" + playbook.body,
3173
+ nextFm
3174
+ );
3175
+ return await writer.writeFile(
3176
+ joinPath4(this.opts.workspacePath, playbook.path),
3177
+ nextContent,
3178
+ playbook.versionToken
3179
+ );
3180
+ }
3181
+ /** Build a transition attestation from current clock + given identity. */
3182
+ attestation(kind, identity, note) {
3183
+ return makeAttestation({
3184
+ kind,
3185
+ identity,
3186
+ at: this.opts.clock.now().toISOString(),
3187
+ ...note ? { note } : {}
3188
+ });
3189
+ }
3190
+ emitter() {
3191
+ return new CorpusEventEmitter({
3192
+ fs: this.opts.fs,
3193
+ clock: this.opts.clock,
3194
+ identity: this.opts.identity,
3195
+ workspaceRoot: this.opts.workspacePath
3196
+ });
3197
+ }
3198
+ };
3199
+ function assertTransition2(from, to, slug) {
3200
+ const allowed = {
3201
+ shadow: ["active", "archived"],
3202
+ active: ["archived"],
3203
+ archived: []
3204
+ };
3205
+ if (!allowed[from].includes(to)) {
3206
+ throw new IllegalPlaybookTransitionError(slug, from, to);
3207
+ }
3208
+ }
3209
+ function patchStatus(fm, newStatus, extraCorpusMeta) {
3210
+ const metaIn = fm.metadata ?? {};
3211
+ const corpusIn = metaIn.corpus ?? {};
3212
+ return {
3213
+ ...fm,
3214
+ status: newStatus,
3215
+ metadata: {
3216
+ ...metaIn,
3217
+ corpus: {
3218
+ ...corpusIn,
3219
+ ...extraCorpusMeta
3220
+ }
3221
+ }
3222
+ };
3223
+ }
3224
+ function joinPath4(a, b) {
3225
+ if (!a) return b;
3226
+ return a.endsWith("/") ? a + b : a + "/" + b;
3227
+ }
3228
+ var PlaybookEvaluator = class {
3229
+ constructor(opts) {
3230
+ this.opts = opts;
3231
+ }
3232
+ /**
3233
+ * Run an eval batch against a single playbook. For each case:
3234
+ * 1. Score shadow.response against rubric → shadowScore
3235
+ * 2. Score baseline.response against rubric → baselineScore
3236
+ * 3. shadow "wins" if shadowScore >= baselineScore
3237
+ * Aggregate: winRateVsBaseline = wins / n.
3238
+ *
3239
+ * On completion: writes shadowMetrics back to the PLAYBOOK.md (CAS),
3240
+ * emits a `corpus.playbook.evaluated` event (held outside the formal
3241
+ * AIP-41 taxonomy until the spec adopts it).
3242
+ */
3243
+ async runBatch(playbookSlug, batch) {
3244
+ const playbook = await this.loadPlaybook(playbookSlug);
3245
+ const perCase = [];
3246
+ let wins = 0;
3247
+ let shadowSum = 0;
3248
+ let baselineSum = 0;
3249
+ for (const c of batch.cases) {
3250
+ const shadowResult = await this.opts.evaluator.evaluate({
3251
+ rubric: batch.rubric,
3252
+ prompt: c.prompt,
3253
+ response: c.shadowResponse,
3254
+ context: {
3255
+ ...batch.contextOverrides ?? {},
3256
+ arm: "shadow",
3257
+ appliedPlaybooks: [playbookSlug]
3258
+ }
3259
+ });
3260
+ const baselineResult = await this.opts.evaluator.evaluate({
3261
+ rubric: batch.rubric,
3262
+ prompt: c.prompt,
3263
+ response: c.baselineResponse,
3264
+ context: {
3265
+ ...batch.contextOverrides ?? {},
3266
+ arm: "baseline",
3267
+ appliedPlaybooks: []
3268
+ }
3269
+ });
3270
+ const winnerArm = shadowResult.score > baselineResult.score ? "shadow" : shadowResult.score < baselineResult.score ? "baseline" : "tie";
3271
+ if (winnerArm === "shadow") wins++;
3272
+ shadowSum += shadowResult.score;
3273
+ baselineSum += baselineResult.score;
3274
+ perCase.push({
3275
+ id: c.id,
3276
+ shadowScore: shadowResult.score,
3277
+ baselineScore: baselineResult.score,
3278
+ winnerArm
3279
+ });
3280
+ }
3281
+ const sampleSize = batch.cases.length;
3282
+ const winRate = sampleSize > 0 ? wins / sampleSize : 0;
3283
+ const auto = playbook.corpus.autoPromote ?? {};
3284
+ const gteOK = typeof auto.threshold?.gte === "number" ? winRate >= auto.threshold.gte : false;
3285
+ const sizeOK = typeof auto.minSampleSize === "number" ? sampleSize >= auto.minSampleSize : sampleSize > 0;
3286
+ const readyForActivation = !!auto.enabled && gteOK && sizeOK;
3287
+ await this.persistMetrics(playbook, {
3288
+ sampleSize,
3289
+ winRateVsBaseline: winRate,
3290
+ lastEvaluatedAt: this.opts.clock.now().toISOString()
3291
+ });
3292
+ await this.appendCustomEvent("playbook.shadow.evaluated", {
3293
+ slug: playbook.slug,
3294
+ sampleSize,
3295
+ winRateVsBaseline: winRate,
3296
+ readyForActivation
3297
+ });
3298
+ return Object.freeze({
3299
+ playbookSlug: playbook.slug,
3300
+ sampleSize,
3301
+ winRateVsBaseline: winRate,
3302
+ shadowAvgScore: sampleSize > 0 ? shadowSum / sampleSize : 0,
3303
+ baselineAvgScore: sampleSize > 0 ? baselineSum / sampleSize : 0,
3304
+ readyForActivation,
3305
+ perCase: Object.freeze(perCase)
3306
+ });
3307
+ }
3308
+ // ── Internal ─────────────────────────────────────────────────────
3309
+ async loadPlaybook(slug) {
3310
+ const reader = new CorpusWorkspaceReader({ fs: this.opts.fs });
3311
+ const snapshot = await reader.read(this.opts.workspacePath);
3312
+ const reg = new PlaybookRegistry({ snapshot });
3313
+ const p = reg.bySlugOrNull(slug);
3314
+ if (!p) throw new PlaybookNotFoundError(slug);
3315
+ return p;
3316
+ }
3317
+ async persistMetrics(playbook, metrics) {
3318
+ const writer = new CorpusWorkspaceWriter({ fs: this.opts.fs });
3319
+ const identity = await this.opts.identity.resolve();
3320
+ let fm = { ...playbook.file.frontmatter };
3321
+ const metaIn = fm.metadata ?? {};
3322
+ const corpusIn = metaIn.corpus ?? {};
3323
+ fm.metadata = {
3324
+ ...metaIn,
3325
+ corpus: {
3326
+ ...corpusIn,
3327
+ shadowMetrics: metrics
3328
+ }
3329
+ };
3330
+ fm = appendAttestation(
3331
+ fm,
3332
+ makeAttestation({
3333
+ kind: "evaluated",
3334
+ identity: identity.principal,
3335
+ at: metrics.lastEvaluatedAt,
3336
+ note: `n=${metrics.sampleSize} winRate=${metrics.winRateVsBaseline.toFixed(3)}`
3337
+ })
3338
+ );
3339
+ const content = matter.stringify(
3340
+ playbook.body.startsWith("\n") ? playbook.body : "\n" + playbook.body,
3341
+ fm
3342
+ );
3343
+ await writer.writeFile(
3344
+ joinPath5(this.opts.workspacePath, playbook.path),
3345
+ content,
3346
+ playbook.versionToken
3347
+ );
3348
+ }
3349
+ async appendCustomEvent(customKind, payload) {
3350
+ const identity = await this.opts.identity.resolve();
3351
+ const line = `- ${this.opts.clock.now().toISOString()} ${customKind} by ${identity.principal} payload=${JSON.stringify(payload, [...Object.keys(payload).sort()])}
3352
+ `;
3353
+ const logPath = joinPath5(this.opts.workspacePath, "_log.md");
3354
+ if (!await this.opts.fs.exists(logPath)) {
3355
+ new CorpusEventEmitter({
3356
+ fs: this.opts.fs,
3357
+ clock: this.opts.clock,
3358
+ identity: this.opts.identity,
3359
+ workspaceRoot: this.opts.workspacePath
3360
+ });
3361
+ await this.opts.fs.writeFile(
3362
+ logPath,
3363
+ "# Corpus activity log\n\nAppend-only AIP-10 log of corpus state transitions. Each line:\n`- <iso> <kind> by <actor> payload=<json>`\n\n"
3364
+ );
3365
+ }
3366
+ await this.opts.fs.appendFile(logPath, line);
3367
+ }
3368
+ };
3369
+ function joinPath5(a, b) {
3370
+ if (!a) return b;
3371
+ return a.endsWith("/") ? a + b : a + "/" + b;
3372
+ }
3373
+
3374
+ // src/index.ts
3375
+ var SPEC_NAME = "agentcorpus/v1";
3376
+ var SPEC_VERSION = "0.1.0-alpha";
3377
+
3378
+ export { ANY_REF, ClaudeDistiller, ConversationImporter, CorpusEventEmitter, CorpusIndexer, CorpusLinter, CorpusPromoter, CorpusValidator, CorpusVersionConflictError, CorpusWorkspaceReader, CorpusWorkspaceWriter, DEFAULT_TRANSITIONS, DistillRunner, EMPTY_SELECTOR, IllegalPlaybookTransitionError, IllegalTransitionError, ImporterRunner, KbMigrationImporter, LocalFilesImporter, MemFs, OperatorOverlayResolver, OverlayFs, PlaybookEvaluator, PlaybookLifecycle, PlaybookNotFoundError, PlaybookRegistry, PromoteRejectedError, REFINED_KIND_SCHEMA, ReadOnlyFs, ReviewerTrackRecord, SPEC_NAME, SPEC_VERSION, StackResolver, SyncRunner, WELL_KNOWN_AXES, WHITEOUT_SUFFIX, WebImporter, aggregateReviewerScores, appendAttestation, assertTransition, buildDistillPrompt, buildOverlayFromStack, canTransition, capabilityAxis, chunkText, compileLegacyPlaybookBinding, computeReviewerCalibration, createAxisRegistry, createDistillRegistry, enumerateWindowRefs, evaluateAccess, evaluateCapability, evaluateGate, extractAutoPromoteConfig, flattenPackRefs, identityAxis, isEmptySelector, isEntrySlug, isRefinedKind, isSourceSlug, makeAttestation, matchAttachmentRefs, matchAttachments, matchesLanguageFilter, matchesSelector, normalizeLanguageTag, parseItems, parseSelectorFrontmatter, parseWindowRef, partitionStackRefs, pearsonCorrelation, positionAxis, prefixedRefNormalizer, readAccessModes, readAccessSpec, readAttestations, readEntryLanguage, readOperatorLocale, readWorkspaceDefaultLanguage, renderOverlays, resolveKnowledge, resolveLanguageFilter, roleAxis, runDistill, scanDistilledSourceIds, slugify, transitionGraphFromCollection, uniqueSlug, windowRef, windowSlug };
3379
+ //# sourceMappingURL=index.mjs.map
3380
+ //# sourceMappingURL=index.mjs.map