@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/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/chunk-KYX2DTEH.mjs +14 -0
- package/dist/chunk-KYX2DTEH.mjs.map +1 -0
- package/dist/chunk-UPX26MYH.mjs +104 -0
- package/dist/chunk-UPX26MYH.mjs.map +1 -0
- package/dist/index.d.ts +2893 -0
- package/dist/index.mjs +3380 -0
- package/dist/index.mjs.map +1 -0
- package/dist/ports/index.d.ts +205 -0
- package/dist/ports/index.mjs +3 -0
- package/dist/ports/index.mjs.map +1 -0
- package/dist/sidecar-TTWQD5R3.mjs +3 -0
- package/dist/sidecar-TTWQD5R3.mjs.map +1 -0
- package/package.json +80 -0
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
|