@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.
@@ -0,0 +1,2893 @@
1
+ import { FsPort, ClockPort, IdentityPort, FetcherPort, FsStat, FsLockHandle, EvalRubricPort, EvalContextPort, EvaluatorPort } from './ports/index.js';
2
+ export { CallerIdentity, EvalInputPort, EvalResultPort, FetchedSource, FetchedSourceKind, systemClock } from './ports/index.js';
3
+ import { z } from 'zod';
4
+ import { Registry } from '@agentproto/registry';
5
+
6
+ /**
7
+ * AIP-10 slug helpers — the single source of truth for turning arbitrary
8
+ * text or paths into schema-valid slugs. Replaces the three drifted
9
+ * `makeSlug` copies (web / local-files / distill importers) and the two
10
+ * `uniqueSlug` copies, so a rule change lands once instead of in four
11
+ * places.
12
+ *
13
+ * Two AIP-10 slug shapes:
14
+ * - **source** id `^[a-z0-9][a-z0-9-]*$` — a leading digit is allowed.
15
+ * - **entry / role / tag** id `^[a-z][a-z0-9-]*[a-z0-9]$` — must start
16
+ * with a letter and end alphanumeric. Pass `leadingLetter: true`.
17
+ */
18
+ interface SlugifyOptions {
19
+ /** Max length before the trailing-dash trim. Default 96. */
20
+ readonly maxLen?: number;
21
+ /** Returned when the slugified result is empty / degenerate. Default "item". */
22
+ readonly fallback?: string;
23
+ /**
24
+ * Guarantee the AIP-10 *entry* shape: a leading letter (prefix `e-` when
25
+ * the slug would otherwise start with a digit) and length ≥ 2.
26
+ */
27
+ readonly leadingLetter?: boolean;
28
+ /** Strip a leading `http(s)://` first — for slugging a URL fallback. */
29
+ readonly stripScheme?: boolean;
30
+ }
31
+ /** Slugify arbitrary text into an AIP-10-valid id. */
32
+ declare function slugify(input: string, opts?: SlugifyOptions): string;
33
+ /**
34
+ * Disambiguate a slug against a set of already-used ones by appending
35
+ * `-2`, `-3`, … The suffix is numeric, so an entry-shaped base stays
36
+ * entry-valid (ends alphanumeric).
37
+ */
38
+ declare function uniqueSlug(base: string, seen: Set<string>, maxLen?: number): string;
39
+ /** True when `s` is a valid AIP-10 source id (leading digit allowed). */
40
+ declare function isSourceSlug(s: string): boolean;
41
+ /** True when `s` is a valid AIP-10 entry/role/tag id (leading letter). */
42
+ declare function isEntrySlug(s: string): boolean;
43
+
44
+ /**
45
+ * Language-tag normalization — the single source of truth for coercing a
46
+ * raw language value into the AIP-10 source shape `^[a-z]{2}(-[A-Z]{2})?$`.
47
+ *
48
+ * Two upstreams feed it, which is why this used to be two drifted copies:
49
+ * - **STT** (Whisper verbose_json) emits a full English NAME ("english",
50
+ * "french").
51
+ * - **readability / browser** fetchers read `<html lang>` VERBATIM, in any
52
+ * case and separator ("en-us", "EN", "pt_BR", "zh-Hant-TW").
53
+ *
54
+ * One function handles both: map a known name → code, else parse a BCP-47
55
+ * tag — lowercase the 2-letter primary, uppercase a genuine 2-letter region,
56
+ * and discard scripts / extensions / 3-letter primaries (which the AIP-10
57
+ * source schema doesn't allow) rather than emit something that fails
58
+ * validation. Returns `undefined` for anything unparseable so the field is
59
+ * omitted, never written invalid.
60
+ */
61
+ declare function normalizeLanguageTag(raw: string | undefined): string | undefined;
62
+
63
+ /**
64
+ * @agentproto/corpus — public types.
65
+ *
66
+ * Frontmatter shapes here are deliberately loose (`Record<string,
67
+ * unknown>`). Strict validation lives in `validate/validator.ts` —
68
+ * keeping the reader weakly-typed lets it surface drift through the
69
+ * validator's structured errors rather than failing on parse.
70
+ */
71
+ type FileKind = "knowledge-workspace" | "knowledge-source" | "knowledge-entry" | "collection-schema" | "collection-item" | "playbook" | "operator" | "workflow" | "routine" | "unknown";
72
+ interface ParsedFile {
73
+ /** Workspace-relative path (e.g. "entries/principles/2026/foo.md"). */
74
+ readonly path: string;
75
+ /** Classification by location. */
76
+ readonly kind: FileKind;
77
+ /** Parsed YAML frontmatter. Empty object if frontmatter is missing. */
78
+ readonly frontmatter: Readonly<Record<string, unknown>>;
79
+ /** Markdown body (everything after the closing `---`). */
80
+ readonly body: string;
81
+ /** SHA-256 of the entire file contents — used as optimistic-concurrency versionToken. */
82
+ readonly versionToken: string;
83
+ }
84
+ interface CorpusWorkspaceSnapshot {
85
+ /** Root path passed to the reader (relative or absolute, host's choice). */
86
+ readonly root: string;
87
+ /** The KNOWLEDGE.md file, if found. */
88
+ readonly workspace: ParsedFile | null;
89
+ readonly sources: readonly ParsedFile[];
90
+ readonly entries: readonly ParsedFile[];
91
+ readonly collections: readonly ParsedFile[];
92
+ readonly collectionItems: readonly ParsedFile[];
93
+ readonly playbooks: readonly ParsedFile[];
94
+ readonly operators: readonly ParsedFile[];
95
+ readonly workflows: readonly ParsedFile[];
96
+ readonly routines: readonly ParsedFile[];
97
+ /** .md files we found but couldn't classify. */
98
+ readonly unknown: readonly ParsedFile[];
99
+ }
100
+ /** Validator result per file, returned by `validate/validator.ts`. */
101
+ interface ValidationIssue {
102
+ readonly path: string;
103
+ readonly instancePath: string;
104
+ readonly message: string;
105
+ readonly severity: "error" | "warn" | "info";
106
+ }
107
+ interface ValidationResult {
108
+ readonly issues: readonly ValidationIssue[];
109
+ readonly valid: boolean;
110
+ }
111
+ /** Lint result, returned by `validate/linter.ts`. */
112
+ interface LintIssue {
113
+ readonly lintId: string;
114
+ readonly path: string;
115
+ readonly message: string;
116
+ readonly severity: "error" | "warn" | "info";
117
+ }
118
+ interface LintReport {
119
+ readonly issues: readonly LintIssue[];
120
+ readonly errorCount: number;
121
+ readonly warnCount: number;
122
+ readonly infoCount: number;
123
+ }
124
+ /** Event taxonomy emitted by the corpus to `_log.md`. */
125
+ type CorpusEventKind = "corpus.candidate.discovered" | "corpus.candidate.analyzed" | "corpus.candidate.approved" | "corpus.candidate.rejected" | "corpus.candidate.needs_work" | "corpus.entry.promoted" | "corpus.entry.deprecated" | "corpus.entry.archived" | "corpus.gap.opened" | "corpus.gap.resolved" | "playbook.shadow.registered" | "playbook.activated" | "playbook.archived";
126
+ interface CorpusEvent {
127
+ readonly kind: CorpusEventKind;
128
+ readonly at: string;
129
+ readonly actor: string;
130
+ readonly payload: Readonly<Record<string, unknown>>;
131
+ }
132
+ /**
133
+ * Per-state-transition attestation stored under
134
+ * `metadata.corpus.attestations[]` on entries, candidates, and
135
+ * playbooks. The audit chain that `_log.md` rolls up.
136
+ *
137
+ * `model` + `promptHash` are recorded when an AI agent produced the
138
+ * transition (analyzed by analyst-bot, scored by reviewer-bot, …).
139
+ * Human-driven transitions omit them. `signature` is reserved for
140
+ * AIP-23 signing once the identity infra is wired — until then it
141
+ * stays optional and the chain relies on principal + at + hash.
142
+ */
143
+ type AttestationKind = "created" | "analyzed" | "reviewed" | "promoted" | "deprecated" | "archived" | "activated" | "evaluated" | "imported" | "tombstoned";
144
+ interface Attestation {
145
+ readonly kind: AttestationKind;
146
+ /** AIP-23-style identity ref. e.g. "ws://operators/corpus-curator". */
147
+ readonly identity: string;
148
+ readonly at: string;
149
+ /** Optional model id for AI-produced transitions. */
150
+ readonly model?: string;
151
+ /** Optional sha256 of the prompt + system prompt used. */
152
+ readonly promptHash?: string;
153
+ /** Optional signature (AIP-23). Reserved until identity infra lands. */
154
+ readonly signature?: string;
155
+ /** Free-form note — appears in curator UI tooltips. */
156
+ readonly note?: string;
157
+ }
158
+ /**
159
+ * A reusable corpus starter — pure TS data, no filesystem dependency.
160
+ *
161
+ * Follows the @agentproto/role-catalog pattern: a preset is a
162
+ * declarative bundle of file paths → file contents that a host writes
163
+ * to a target workspace via the FsPort. The kit defines the
164
+ * interface; individual packages under `@agentproto/corpus-presets/*`
165
+ * ship per-vertical content.
166
+ *
167
+ * `files` keys are workspace-relative paths (no leading slash). The
168
+ * host (e.g. corpus-cli's `init` command, or any embedding runtime)
169
+ * is responsible for iterating + writing via FsPort.
170
+ */
171
+ interface CorpusPreset {
172
+ readonly slug: string;
173
+ readonly title: string;
174
+ readonly description?: string;
175
+ readonly files: Readonly<Record<string, string>>;
176
+ /**
177
+ * Optional bootstrap hook. Hosts that need to do more than write
178
+ * files (e.g. seed _candidates.yaml sidecar, register a KB row)
179
+ * implement this. The default `init` flow just writes `files`.
180
+ */
181
+ readonly bootstrap?: (ctx: CorpusPresetBootstrapContext) => Promise<void>;
182
+ }
183
+ interface CorpusPresetBootstrapContext {
184
+ /** Workspace root within whatever fs the host has handed us. */
185
+ readonly workspacePath: string;
186
+ /**
187
+ * Write a single file relative to workspacePath. Implementations
188
+ * typically just call FsPort.writeFile. Exposed as a callback so
189
+ * the preset doesn't need to import FsPort directly (keeps presets
190
+ * minimally typed).
191
+ */
192
+ readonly write: (relativePath: string, content: string) => Promise<void>;
193
+ }
194
+
195
+ /**
196
+ * Attestation helpers — append + read the per-transition audit chain
197
+ * stored under `metadata.corpus.attestations[]`.
198
+ *
199
+ * Pure. The lifecycle workflows (promote, playbook activate/archive,
200
+ * playbook evaluator) call `appendAttestation` immediately before
201
+ * writing the file. Tooling that displays the chain (curator UI,
202
+ * `corpus events:trace <slug>`) calls `readAttestations`.
203
+ */
204
+
205
+ /**
206
+ * Append an attestation to a frontmatter object's
207
+ * `metadata.corpus.attestations[]`. Returns a NEW frontmatter
208
+ * object — mutation-free so callers can chain transformations.
209
+ *
210
+ * If `metadata.corpus.attestations` doesn't exist, it's created. If
211
+ * the new attestation is byte-identical to the last entry (same kind
212
+ * + identity + at), it's a no-op (idempotent against
213
+ * lifecycle workflow retries).
214
+ */
215
+ declare function appendAttestation(frontmatter: Readonly<Record<string, unknown>>, attestation: Attestation): Record<string, unknown>;
216
+ /**
217
+ * Read the attestation chain off a frontmatter object. Returns `[]`
218
+ * if the entry has none (e.g. legacy entries written before the
219
+ * attestation chain shipped).
220
+ */
221
+ declare function readAttestations(frontmatter: Readonly<Record<string, unknown>>): readonly Attestation[];
222
+ /**
223
+ * Convenience: build an attestation from current clock + identity.
224
+ * Lifecycle code typically calls this then immediately
225
+ * `appendAttestation`.
226
+ */
227
+ declare function makeAttestation(input: {
228
+ readonly kind: Attestation["kind"];
229
+ readonly identity: string;
230
+ readonly at: string;
231
+ readonly model?: string;
232
+ readonly promptHash?: string;
233
+ readonly note?: string;
234
+ }): Attestation;
235
+
236
+ /**
237
+ * Access policy — pure logic that decides whether a caller can see a
238
+ * piece of corpus content based on its `metadata.corpus.access` spec.
239
+ *
240
+ * Inputs:
241
+ * spec — what the entry/source declares (classification + allowed refs)
242
+ * caller — identityTree (resolved by IdentityPort)
243
+ *
244
+ * Output:
245
+ * permitted: boolean
246
+ * reason: human-readable
247
+ * redact: bytes-redaction hint for getSource() (some classifications
248
+ * let the caller see metadata but not body bytes)
249
+ *
250
+ * Default policy by classification:
251
+ *
252
+ * public — anyone can see (and read bytes)
253
+ * internal — same-guild only (default for unmarked entries)
254
+ * restricted — explicit allowedRoles / allowedOperators / allowedUsers
255
+ * ONLY; classification fallback is deny
256
+ * secret — same as restricted PLUS bytes are redacted for
257
+ * callers below the bar (they see frontmatter, not body)
258
+ *
259
+ * Engines/UIs filter silently — no "you have N results but can't see
260
+ * them" leakage to the caller below.
261
+ */
262
+ type AccessClassification = "public" | "internal" | "restricted" | "secret";
263
+ interface CorpusAccessSpec {
264
+ readonly classification?: AccessClassification;
265
+ readonly allowedRoles?: readonly string[];
266
+ readonly allowedOperators?: readonly string[];
267
+ readonly allowedUsers?: readonly string[];
268
+ readonly allowedGuilds?: readonly string[];
269
+ readonly allowedOrgs?: readonly string[];
270
+ /** Free-form host extension. */
271
+ readonly metadata?: Readonly<Record<string, unknown>>;
272
+ }
273
+ interface AccessCaller {
274
+ /** identityTree most-specific → least-specific. */
275
+ readonly identityTree: readonly string[];
276
+ }
277
+ /**
278
+ * Per-evaluation context the adapter passes alongside the caller.
279
+ * Distinct from the spec (entry-side) and the caller (subject-side);
280
+ * this carries the *workspace-side* facts that classification rules
281
+ * need but that the entry doesn't repeat per-row.
282
+ */
283
+ interface AccessContext {
284
+ /**
285
+ * Guild slug that owns the workspace this entry lives in (e.g.
286
+ * "acme-marketing"). When set, the `internal` classification
287
+ * requires the caller's identityTree to include
288
+ * `ws://guilds/<homeGuild>` — i.e. an entry tagged internal is
289
+ * visible only to that guild, not to anyone-in-any-guild.
290
+ *
291
+ * When omitted, `internal` falls back to "fail-closed" rather than
292
+ * the old "any-guild" semantics, on the principle that an
293
+ * unidentifiable workspace shouldn't leak its content.
294
+ */
295
+ readonly homeGuild?: string;
296
+ }
297
+ interface AccessDecision {
298
+ readonly permitted: boolean;
299
+ readonly reason: string;
300
+ /** For getSource — true means body bytes MUST be redacted. */
301
+ readonly redactBytes: boolean;
302
+ }
303
+ /**
304
+ * Evaluate access for a caller against an access spec. Returns a
305
+ * decision the caller (engine adapter, tool) uses to filter / redact.
306
+ *
307
+ * Missing `access` block in frontmatter = treat as `classification:
308
+ * internal`, no explicit allow lists. Same-guild matches via the
309
+ * identityTree.
310
+ */
311
+ declare function evaluateAccess(spec: CorpusAccessSpec | undefined, caller: AccessCaller, context?: AccessContext): AccessDecision;
312
+ /**
313
+ * Extract the access spec from a frontmatter object's
314
+ * `metadata.corpus.access`. Returns undefined when the entry has
315
+ * none — caller treats it as `internal` (the default).
316
+ */
317
+ declare function readAccessSpec(frontmatter: Readonly<Record<string, unknown>>): CorpusAccessSpec | undefined;
318
+
319
+ /**
320
+ * Capability — uniform tool-boundary enforcement.
321
+ *
322
+ * Every corpus tool declares a required `Capability`. The host
323
+ * (Guilde corpus.bundle / corpus-cli) calls `evaluateCapability` at
324
+ * dispatch time. Capabilities are configured per-workspace under
325
+ * `KNOWLEDGE.md.metadata.corpus.accessModes`.
326
+ *
327
+ * Pure logic. Audit + actual rejection happen in the host.
328
+ */
329
+ type Capability = "read" | "cite" | "flag-learning" | "curate" | "promote" | "activate-playbook" | "admin-reindex" | "bypass-default-filters";
330
+ interface CapabilityRule {
331
+ readonly allowedRoles: readonly string[];
332
+ /** Optional rate-limit hint — enforcement lives in the host. */
333
+ readonly rateLimit?: {
334
+ readonly perOperator?: number;
335
+ readonly window?: string;
336
+ };
337
+ /** Requires curator approval before the action commits. */
338
+ readonly requireApproval?: boolean;
339
+ /** Audit-log the call (default false). */
340
+ readonly audit?: boolean;
341
+ }
342
+ type AccessModesMap = Readonly<Partial<Record<Capability, CapabilityRule>>>;
343
+ interface CapabilityCaller {
344
+ /** identityTree most-specific → least-specific. */
345
+ readonly identityTree: readonly string[];
346
+ }
347
+ interface CapabilityDecision {
348
+ readonly permitted: boolean;
349
+ readonly reason: string;
350
+ /** Rate-limit hint to enforce, if defined. */
351
+ readonly rateLimit?: CapabilityRule["rateLimit"];
352
+ /** True if the tool MUST collect approval before committing. */
353
+ readonly requireApproval: boolean;
354
+ /** True if the host should write an audit entry on completion. */
355
+ readonly audit: boolean;
356
+ }
357
+ declare function evaluateCapability(capability: Capability, accessModes: AccessModesMap | undefined, caller: CapabilityCaller): CapabilityDecision;
358
+ /**
359
+ * Read accessModes from a parsed KNOWLEDGE.md workspace
360
+ * frontmatter. Returns undefined if not declared — caller uses
361
+ * DEFAULT_RULES.
362
+ */
363
+ declare function readAccessModes(workspaceFrontmatter: Readonly<Record<string, unknown>>): AccessModesMap | undefined;
364
+
365
+ /**
366
+ * Language filtering — pure helpers for BCP-47 locale matching.
367
+ *
368
+ * Where language lives:
369
+ * - AIP-10 source: native top-level `language` field (already in spec).
370
+ * - AIP-10 entry: under `metadata.corpus.language` until the AIP-10
371
+ * spec hoists `language` to first-class on entries.
372
+ * - Workspace default: `KNOWLEDGE.md.metadata.corpus.languages.default`.
373
+ * - Operator locale: `OPERATOR.md.metadata.corpus.locale`.
374
+ *
375
+ * Default filter: an entry surfaces to an operator if its language
376
+ * matches the operator's locale OR matches the workspace default
377
+ * (typically en-US so generic principles aren't locale-gated). Empty
378
+ * `language` on the entry = treat as the workspace default (it's
379
+ * almost always the corpus's home language).
380
+ *
381
+ * Locale matching is loose (`en` matches `en-US` / `en-GB`) so we
382
+ * don't drown the user in misses on regional variants.
383
+ */
384
+ interface ResolveLanguageFilterInput {
385
+ /** BCP-47 locale the caller is operating in. */
386
+ readonly callerLocale?: string;
387
+ /**
388
+ * Default language declared in `KNOWLEDGE.md.metadata.corpus.languages.default`.
389
+ * Used as the implicit language for entries that don't declare one.
390
+ */
391
+ readonly workspaceDefaultLanguage?: string;
392
+ }
393
+ interface LanguageFilter {
394
+ /**
395
+ * The set of language codes that surface. Used by middleware to
396
+ * filter hits silently. Empty set = no language filter (everyone
397
+ * sees everything; happens when neither caller nor workspace
398
+ * declares a language).
399
+ */
400
+ readonly allowedLanguages: ReadonlySet<string>;
401
+ /**
402
+ * Whether entries with no declared language should pass through.
403
+ * True when the workspace declares a default — those entries
404
+ * inherit it.
405
+ */
406
+ readonly allowUnspecified: boolean;
407
+ }
408
+ declare function resolveLanguageFilter(input: ResolveLanguageFilterInput): LanguageFilter;
409
+ declare function matchesLanguageFilter(entryLanguage: string | undefined, filter: LanguageFilter): boolean;
410
+ /**
411
+ * Convenience: read the operator's locale from frontmatter.
412
+ * Falls back to undefined when not declared.
413
+ */
414
+ declare function readOperatorLocale(operatorFrontmatter: Readonly<Record<string, unknown>>): string | undefined;
415
+ /**
416
+ * Read the workspace's default language from KNOWLEDGE.md.
417
+ */
418
+ declare function readWorkspaceDefaultLanguage(workspaceFrontmatter: Readonly<Record<string, unknown>>): string | undefined;
419
+ /**
420
+ * Read an entry / source's language. Sources expose it at top level
421
+ * (AIP-10 spec); entries put it under `metadata.corpus.language`
422
+ * until the corresponding AIP-10 amendment lands.
423
+ */
424
+ declare function readEntryLanguage(frontmatter: Readonly<Record<string, unknown>>): string | undefined;
425
+
426
+ /**
427
+ * CandidatesSidecar — accessor for `collections/<name>/_candidates.yaml`.
428
+ *
429
+ * The plan calls out lazy materialization (so-so #6): the
430
+ * "discovered" state has high volume and short lifespan; AIP-18 ITEM
431
+ * files only materialize when a candidate transitions to `analyzed`.
432
+ * Until then, candidates live in a YAML sidecar keyed by id.
433
+ *
434
+ * Sidecar shape:
435
+ * candidates:
436
+ * - id: <slug>
437
+ * status: discovered # may also be transitioning (rare)
438
+ * sourceUrl: ... # collection-specific fields are free-form
439
+ * contentHash: ...
440
+ * discoveredAt: ISO
441
+ * discoveredBy: ws://operators/<slug>
442
+ *
443
+ * The accessor enforces:
444
+ * - id uniqueness within the file
445
+ * - atomic full-file writes (no partial sidecar)
446
+ * - status transition discipline (a row stays here only while
447
+ * `status === "discovered"`; transitions out remove the row and
448
+ * return the data so the caller can materialize ITEM.md)
449
+ */
450
+
451
+ interface CandidateRow {
452
+ readonly id: string;
453
+ readonly status: "discovered" | string;
454
+ /** Open-ended — collection schema's `fields` defines what's allowed. */
455
+ readonly [key: string]: unknown;
456
+ }
457
+ interface CandidatesSidecarOptions {
458
+ readonly fs: FsPort;
459
+ /** Workspace-relative path of the sidecar file. */
460
+ readonly path: string;
461
+ }
462
+ declare class CandidatesSidecar {
463
+ private readonly opts;
464
+ constructor(opts: CandidatesSidecarOptions);
465
+ /**
466
+ * Load every row from disk. Returns an empty list if the file
467
+ * doesn't exist (a fresh corpus has no candidates yet).
468
+ */
469
+ load(): Promise<readonly CandidateRow[]>;
470
+ /**
471
+ * Append a new candidate. Refuses if `id` already exists — sidecar
472
+ * keys are unique. Returns the full updated list.
473
+ */
474
+ append(row: CandidateRow): Promise<readonly CandidateRow[]>;
475
+ /**
476
+ * Mutate one row in-place. Throws if not found.
477
+ */
478
+ update(id: string, patch: Readonly<Record<string, unknown>>): Promise<CandidateRow>;
479
+ /**
480
+ * Remove a row by id. Returns the removed row so the caller can
481
+ * materialize an ITEM.md from its fields. Throws if not found.
482
+ *
483
+ * Use this when a candidate transitions out of `discovered`: pull
484
+ * it from the sidecar, write the ITEM.md, append an event.
485
+ */
486
+ take(id: string): Promise<CandidateRow>;
487
+ /**
488
+ * Whole-file replace. Used in tests and by the indexer for
489
+ * bulk migrations. Prefer append/update/take for normal flow.
490
+ */
491
+ write(candidates: readonly CandidateRow[]): Promise<void>;
492
+ }
493
+ declare class SidecarDuplicateError extends Error {
494
+ readonly path: string;
495
+ readonly id: string;
496
+ constructor(path: string, id: string);
497
+ }
498
+ declare class SidecarNotFoundError extends Error {
499
+ readonly path: string;
500
+ readonly id: string;
501
+ constructor(path: string, id: string);
502
+ }
503
+
504
+ /**
505
+ * CorpusImporter — pluggable bootstrap contract.
506
+ *
507
+ * Real corpora are rarely born from blank fixtures — they come from
508
+ * Notion workspaces, Drive folders, web crawls, PDFs, internal docs,
509
+ * existing knowledge bases. Importers are how a corpus gets seeded
510
+ * from reality without hand-authoring every file.
511
+ *
512
+ * The pattern is symmetric with IKnowledgeProvider / IEvaluator:
513
+ * 1. Kit (this package) ships the contract + a reference impl
514
+ * (`LocalFilesImporter`).
515
+ * 2. Adapter packages (Notion, Drive, web-crawl, …) implement the
516
+ * contract via their own SDKs/HTTP layer.
517
+ * 3. The host (Guilde corpus-host) wires a registry + dispatch.
518
+ *
519
+ * Importers write to two places, atomically per source:
520
+ * - `sources/<importer-id>/<batch>/<slug>.md` — archived AIP-10 source
521
+ * - `_candidates.yaml` — discovered candidate row
522
+ *
523
+ * Both writes go through CorpusWorkspaceWriter so versionToken /
524
+ * transaction-lock semantics hold the same way as the rest of the
525
+ * corpus lifecycle.
526
+ */
527
+
528
+ interface ImporterTarget {
529
+ /**
530
+ * Stable importer instance id. Allows the same importer adapter to
531
+ * run multiple times against different sources (e.g. two distinct
532
+ * Notion workspaces). Reflected in the archive path:
533
+ * `sources/<importerId>/<batch>/`.
534
+ */
535
+ readonly importerId: string;
536
+ /** Optional batch id — defaults to ISO timestamp on first archive. */
537
+ readonly batchId?: string;
538
+ /** Importer-specific configuration (auth tokens, source spec, filters). */
539
+ readonly config: Readonly<Record<string, unknown>>;
540
+ }
541
+ /**
542
+ * One source unit the importer produces. The host writes this to
543
+ * `sources/<importerId>/<batchId>/<slug>.md` as AIP-10 frontmatter +
544
+ * body, and appends a row to `_candidates.yaml`.
545
+ */
546
+ interface ImportedSource {
547
+ /** Stable kebab-case slug for the source. */
548
+ readonly slug: string;
549
+ /** Human-readable title. */
550
+ readonly title: string;
551
+ /** sha256-prefixed content hash (importer responsibility — dedup gate). */
552
+ readonly contentHash: string;
553
+ /** Source body (markdown). */
554
+ readonly body: string;
555
+ /** Optional original URL. */
556
+ readonly originalUrl?: string;
557
+ /** AIP-10 authority class. */
558
+ readonly authority?: "primary" | "secondary" | "rumour";
559
+ /** BCP-47 language code — drives the locale-matching retrieval filter. */
560
+ readonly language?: string;
561
+ readonly tags?: readonly string[];
562
+ /** Vendor extension fields go under metadata.corpus.* on the source frontmatter. */
563
+ readonly corpusMetadata?: Readonly<Record<string, unknown>>;
564
+ }
565
+ interface BatchReport {
566
+ /** Importer that produced the batch. */
567
+ readonly importerId: string;
568
+ readonly batchId: string;
569
+ /** Sources archived this run. */
570
+ readonly archivedSlugs: readonly string[];
571
+ /** Sources skipped because contentHash already exists. */
572
+ readonly duplicateSlugs: readonly string[];
573
+ /** Candidate ids written to _candidates.yaml. */
574
+ readonly candidateIds: readonly string[];
575
+ /** Diagnostic messages (non-fatal warnings, dedup reasons, etc.). */
576
+ readonly warnings: readonly string[];
577
+ }
578
+ /**
579
+ * The contract concrete importers implement. Two methods:
580
+ * - `enumerate(target)` — yields ImportedSource objects.
581
+ * Caller iterates + decides what to do with each (run dedup,
582
+ * archive, append).
583
+ *
584
+ * The kit ships the "what to do with each" logic in
585
+ * `ImporterRunner` — concrete importers focus ONLY on producing
586
+ * `ImportedSource` from their data source.
587
+ */
588
+ interface CorpusImporter {
589
+ /** Stable id, e.g. "local-files", "notion", "drive", "web-crawl". */
590
+ readonly id: string;
591
+ /** Brief human-readable label (UI metadata). */
592
+ readonly label: string;
593
+ /**
594
+ * Yield sources for the configured target. Async iterator so the
595
+ * host can stream + back-pressure on large imports. Each source
596
+ * carries everything the runner needs to archive + materialize a
597
+ * candidate.
598
+ */
599
+ enumerate(target: ImporterTarget): AsyncIterable<ImportedSource>;
600
+ }
601
+ interface ImporterRunnerOptions {
602
+ /**
603
+ * Build the candidate row from an ImportedSource. Lets callers
604
+ * customize the corpusKind heuristic + add app-specific fields.
605
+ * Default: maps to `corpusKind: "example"`.
606
+ */
607
+ readonly toCandidate?: (s: ImportedSource, sourceId: string) => CandidateRow;
608
+ }
609
+
610
+ /**
611
+ * ImporterRunner — drives a CorpusImporter to actually land files
612
+ * + candidate rows in the workspace.
613
+ *
614
+ * Importers themselves are pure source-of-data — they yield
615
+ * `ImportedSource`. The runner is where I/O lives: archives sources
616
+ * to `sources/<importerId>/<batchId>/`, appends `_candidates.yaml`
617
+ * rows, dedups against existing sources by `content_hash`, emits a
618
+ * `corpus.candidate.discovered` event per imported source.
619
+ *
620
+ * Same fs+writer+emitter pattern as the rest of the corpus lifecycle.
621
+ */
622
+
623
+ declare class ImporterRunner {
624
+ private readonly opts;
625
+ constructor(opts: {
626
+ readonly fs: FsPort;
627
+ readonly clock: ClockPort;
628
+ readonly identity: IdentityPort;
629
+ readonly workspacePath: string;
630
+ readonly runner?: ImporterRunnerOptions;
631
+ });
632
+ /**
633
+ * Run a single importer batch end-to-end.
634
+ *
635
+ * 1. Pre-load existing source content_hashes for dedup.
636
+ * 2. For each source the importer yields:
637
+ * a. Skip if hash already exists in the workspace.
638
+ * b. Otherwise write AIP-10 source frontmatter + body to
639
+ * `sources/<importerId>/<batchId>/<slug>.md`.
640
+ * c. Append candidate row to `_candidates.yaml`.
641
+ * 3. Emit `corpus.candidate.discovered` per archived source.
642
+ *
643
+ * Atomic per-source (each source archived + candidate written
644
+ * inside one writer.transaction).
645
+ */
646
+ run(importer: CorpusImporter, target: ImporterTarget): Promise<BatchReport>;
647
+ }
648
+
649
+ /**
650
+ * LocalFilesImporter — reads .md files from a configured FsPort path
651
+ * and yields them as ImportedSource. Reference implementation that
652
+ * proves the importer surface end-to-end without external SDKs.
653
+ *
654
+ * Config (target.config):
655
+ * - rootPath: string — workspace-relative path to scan
656
+ * - extensions?: string[] — defaults to [".md"]
657
+ * - maxFiles?: number — defaults to 1000
658
+ * - tags?: string[] — applied to every imported source
659
+ * - language?: string — applied to every source (BCP-47)
660
+ *
661
+ * Pure kit code — consumes FsPort, no node:fs / no network.
662
+ *
663
+ * Slug derivation: filename without extension, lowercased + slug-safed.
664
+ * Content hash: sha256 of UTF-8 body bytes.
665
+ */
666
+
667
+ interface LocalFilesImporterOptions {
668
+ readonly fs: FsPort;
669
+ }
670
+ declare class LocalFilesImporter implements CorpusImporter {
671
+ private readonly opts;
672
+ readonly id = "local-files";
673
+ readonly label = "Local Files";
674
+ constructor(opts: LocalFilesImporterOptions);
675
+ enumerate(target: ImporterTarget): AsyncIterable<ImportedSource>;
676
+ }
677
+
678
+ /**
679
+ * KbMigrationImporter — turn an existing knowledge base into a
680
+ * stream of `ImportedSource` for the runner.
681
+ *
682
+ * Use case: a guild already has a populated qdrant/gbrain KB row
683
+ * with N documents. Day-1 corpus onboarding shouldn't be "abandon
684
+ * your knowledge and start over" — this importer reads the source
685
+ * KB via its IKnowledgeProvider, materializes each source as an
686
+ * importable, and the runner archives them into `sources/` +
687
+ * appends candidates. The source KB stays intact (read-only).
688
+ *
689
+ * The minimal IKnowledgeProvider-like shape we consume — keeps the
690
+ * kit decoupled from @agstudio/integration-knowledge.
691
+ *
692
+ * Each migrated source carries `provenanceKind: imported-from-kb`
693
+ * and `sourceKbId` in `metadata.corpus` so curators can trace which
694
+ * KB row each item came from.
695
+ */
696
+
697
+ /**
698
+ * Structural shape — any IKnowledgeProvider satisfies it. We don't
699
+ * import IKnowledgeProvider directly to keep the kit dep-free.
700
+ */
701
+ interface KbListLike {
702
+ listSources(): Promise<readonly KbSourceLike[]>;
703
+ }
704
+ interface KbSourceLike {
705
+ readonly id: string;
706
+ readonly kind: string;
707
+ readonly uri: string;
708
+ readonly title?: string;
709
+ readonly bytes: number;
710
+ readonly metadata: Readonly<Record<string, unknown>>;
711
+ }
712
+ interface KbMigrationConfig {
713
+ /** Stable id of the source KB (used in provenance + archive path). */
714
+ readonly sourceKbId: string;
715
+ /** Live provider instance from the source KB. */
716
+ readonly provider: KbListLike;
717
+ /**
718
+ * Optional fetcher for the body of each source. The migration
719
+ * importer enumerates source METADATA from listSources(); to
720
+ * archive the actual content, the host must supply a way to fetch
721
+ * the body. If omitted, sources are imported with an empty body +
722
+ * a note pointing back to the source KB.
723
+ */
724
+ readonly fetchBody?: (sourceId: string) => Promise<string>;
725
+ /** Optional max sources to import this batch. */
726
+ readonly maxSources?: number;
727
+ }
728
+ declare class KbMigrationImporter implements CorpusImporter {
729
+ readonly id = "kb-migration";
730
+ readonly label = "Migrate from existing KB";
731
+ enumerate(target: ImporterTarget): AsyncIterable<ImportedSource>;
732
+ }
733
+
734
+ /**
735
+ * WebImporter — turns a list of URLs into corpus sources by reducing
736
+ * each to text through an injected FetcherPort. The importer is pure:
737
+ * it slugs, hashes, and shapes ImportedSource; the FetcherPort supplies
738
+ * the environment-bound "URL → { title, text }" capability (YouTube
739
+ * captions, article readability, or — at a fatter tier — transcription).
740
+ *
741
+ * Config (target.config):
742
+ * - urls: string[] — required. The source URLs to import.
743
+ * - maxUrls?: number — defaults to 1000.
744
+ * - tags?: string[] — applied to every imported source.
745
+ * - language?: string — fallback BCP-47 when the fetcher omits one.
746
+ *
747
+ * A URL the fetcher returns `null` for (e.g. a caption-less video on a
748
+ * non-transcribing tier) is skipped — the runner records it as a
749
+ * warning rather than aborting the batch.
750
+ *
751
+ * Pure kit code — consumes FetcherPort, no node:fs / no network of its
752
+ * own. Slug: derived from title, else the URL. Hash: sha256 of the text.
753
+ */
754
+
755
+ interface WebImporterOptions {
756
+ readonly fetcher: FetcherPort;
757
+ }
758
+ declare class WebImporter implements CorpusImporter {
759
+ private readonly opts;
760
+ readonly id = "web";
761
+ readonly label = "Web (URLs)";
762
+ constructor(opts: WebImporterOptions);
763
+ enumerate(target: ImporterTarget): AsyncIterable<ImportedSource>;
764
+ }
765
+
766
+ /**
767
+ * ConversationSourcePort — the "conversation ref → turns" boundary the
768
+ * ConversationImporter consumes. Symmetric with FetcherPort (URL → text):
769
+ * it keeps the importer PURE and chat-store-agnostic. The importer decides
770
+ * *what* to import (windowing, transcript shape, dedup hash); the port supplies
771
+ * the one capability that is environment-bound — resolving a conversation
772
+ * reference to its turns, wherever they live.
773
+ *
774
+ * Structural interface, NOT nominal — any object of this shape satisfies it.
775
+ * Concrete implementations live where the messages live:
776
+ *
777
+ * - a companion app over its Mastra thread store (per-user threads),
778
+ * - a guild runtime over its conversation log,
779
+ * - a fake (tests): canned turns per ref.
780
+ *
781
+ * Returning `null` lets the importer skip-with-warning rather than abort the
782
+ * batch; throwing is reserved for hard failures (auth, store unreachable).
783
+ */
784
+ /** One turn in a conversation. */
785
+ interface ConversationTurn {
786
+ /** Free-form speaker role — "user" | "assistant" | "system" | a name. */
787
+ readonly role: string;
788
+ /** The turn's text, already flattened to plain text by the port. */
789
+ readonly text: string;
790
+ /** ISO-8601 timestamp, when known — for provenance + window ordering. */
791
+ readonly at?: string;
792
+ }
793
+ /**
794
+ * One resolved slice of conversation — the raw material a ConversationImporter
795
+ * turns into a `knowledge.source`.
796
+ */
797
+ interface ConversationDoc {
798
+ /**
799
+ * Stable id for THIS slice — becomes the source slug base and the `sources:`
800
+ * provenance ref. Chat is append-only, so for incremental distill the host
801
+ * encodes the window here (e.g. `thread-123-2026-06-06`): a growing
802
+ * conversation then yields a distinct source per window instead of colliding
803
+ * on one immutable slug.
804
+ */
805
+ readonly id: string;
806
+ /** Human-readable title, when the store has one. */
807
+ readonly title?: string;
808
+ /** The turns in chronological order. */
809
+ readonly turns: readonly ConversationTurn[];
810
+ /** BCP-47 language hint, when the store knows it. */
811
+ readonly language?: string;
812
+ }
813
+ interface ConversationSourcePort {
814
+ /** Resolve a conversation reference to its turns, or null to skip it. */
815
+ fetchConversation(ref: string): Promise<ConversationDoc | null>;
816
+ }
817
+
818
+ /**
819
+ * ConversationImporter — turns conversations into corpus sources by rendering
820
+ * each to a transcript through an injected ConversationSourcePort. The importer
821
+ * is pure: it slugs, hashes, and shapes ImportedSource; the port supplies the
822
+ * environment-bound "ref → turns" capability (a Mastra thread store, a chat DB).
823
+ *
824
+ * This is the seam for conversation→knowledge distill — the resulting sources
825
+ * feed DistillRunner exactly like web sources do (the pipeline is provenance-
826
+ * agnostic). Chat is append-only, so a single immutable slug per conversation
827
+ * would collide once new turns arrive; the host handles this by WINDOWING —
828
+ * giving each ConversationDoc a window-unique id (e.g. `<thread>-<date>`), so
829
+ * each window distills once and a daily run only sees fresh windows.
830
+ *
831
+ * Config (target.config):
832
+ * - refs: string[] — required. Refs to resolve.
833
+ * - tags?: string[] — applied to every source.
834
+ * - language?: string — fallback BCP-47.
835
+ * - authority?: "primary"|"secondary"|"rumour" — default "secondary".
836
+ * - maxRefs?: number — defaults to 1000.
837
+ *
838
+ * Pure kit code — consumes ConversationSourcePort, no chat-store dependency.
839
+ * Slug: derived from the doc id. Hash: sha256 of the rendered transcript, so
840
+ * an unchanged window re-import dedups in ImporterRunner.
841
+ */
842
+
843
+ interface ConversationImporterOptions {
844
+ readonly source: ConversationSourcePort;
845
+ }
846
+ declare class ConversationImporter implements CorpusImporter {
847
+ private readonly opts;
848
+ readonly id = "conversation";
849
+ readonly label = "Conversation";
850
+ constructor(opts: ConversationImporterOptions);
851
+ enumerate(target: ImporterTarget): AsyncIterable<ImportedSource>;
852
+ }
853
+
854
+ /**
855
+ * Distill — raw source → refined knowledge entries (the KNOWLEDGE layer).
856
+ *
857
+ * The "simple mode": no graph engine. A refined entry is just an AIP-10
858
+ * `knowledge.entry/v1` markdown file whose `sources: [<id>]` frontmatter
859
+ * IS the `derivedFrom` provenance edge. The filesystem refs ARE the graph;
860
+ * a graph engine (mind-graph / gbrain) is an optional index added later.
861
+ *
862
+ * The kit owns the contract + the runner (what to write, where, with what
863
+ * provenance). The LLM that actually extracts insights is an injected
864
+ * `DistillPort` — host-supplied (Claude/OpenAI), so the kit stays pure.
865
+ */
866
+
867
+ /** Generic AIP-10 refined kinds — NOT domain-specific. */
868
+ declare const REFINED_KIND_SCHEMA: z.ZodEnum<{
869
+ example: "example";
870
+ summary: "summary";
871
+ principle: "principle";
872
+ pattern: "pattern";
873
+ critique: "critique";
874
+ }>;
875
+ type RefinedKind = z.infer<typeof REFINED_KIND_SCHEMA>;
876
+ /** Runtime guard — narrows an untyped value to RefinedKind without a cast. */
877
+ declare function isRefinedKind(value: unknown): value is RefinedKind;
878
+ interface DistilledItem {
879
+ readonly kind: RefinedKind;
880
+ readonly title: string;
881
+ /** The refined insight, markdown. Self-contained, not a quote dump. */
882
+ readonly body: string;
883
+ /** 0–1 model confidence. */
884
+ readonly confidence?: number;
885
+ readonly tags?: readonly string[];
886
+ }
887
+ interface DistillInput {
888
+ readonly title: string;
889
+ readonly body: string;
890
+ readonly tags?: readonly string[];
891
+ /** Hint for the kinds worth extracting (e.g. only principles). */
892
+ readonly kinds?: readonly RefinedKind[];
893
+ }
894
+ /**
895
+ * The LLM boundary. Given a raw source, return refined items. Injected by
896
+ * the host (corpus-cli `AnthropicDistiller`, a Guilde operator, …) so the
897
+ * kit takes no model dependency.
898
+ */
899
+ interface DistillPort {
900
+ distill(input: DistillInput): Promise<readonly DistilledItem[]>;
901
+ }
902
+
903
+ /**
904
+ * DistillRunner — turn one raw source into refined AIP-10 entries.
905
+ *
906
+ * Reads nothing itself beyond what it's handed; writes each distilled item
907
+ * as `entries/<kind-plural>/<year>/<slug>.md` (layout: "dated", default) or
908
+ * `entries/<kind-plural>/<slug>.md` (layout: "flat") with `sources: [<sourceId>]`
909
+ * (the provenance edge) and inherited `access`. Pure: consumes FsPort +
910
+ * ClockPort + an injected DistillPort. The filesystem refs are the graph.
911
+ */
912
+
913
+ interface DistillSource {
914
+ /** The raw source's AIP-10 id (becomes the `sources:` provenance ref). */
915
+ readonly id: string;
916
+ readonly title: string;
917
+ readonly body: string;
918
+ readonly tags?: readonly string[];
919
+ /** Access scope inherited by every entry distilled from this source. */
920
+ readonly access?: string;
921
+ readonly domain?: string;
922
+ }
923
+ interface DistillRunReport {
924
+ readonly sourceId: string;
925
+ readonly entryPaths: readonly string[];
926
+ readonly skipped: readonly string[];
927
+ }
928
+ /** Where distilled entries land under `entries/<kind>/`. */
929
+ type EntryLayout = "dated" | "flat";
930
+ interface DistillRunnerOptions {
931
+ readonly fs: FsPort;
932
+ readonly clock: ClockPort;
933
+ readonly distiller: DistillPort;
934
+ /**
935
+ * Entry path layout. "dated" (default) → `entries/<kind>/<year>/<slug>.md`
936
+ * (scales to large corpora over time); "flat" → `entries/<kind>/<slug>.md`.
937
+ */
938
+ readonly layout?: EntryLayout;
939
+ }
940
+ declare class DistillRunner {
941
+ private readonly opts;
942
+ constructor(opts: DistillRunnerOptions);
943
+ run(source: DistillSource): Promise<DistillRunReport>;
944
+ }
945
+
946
+ /**
947
+ * Shared distill prompt + parse — the model-agnostic core of distillation.
948
+ *
949
+ * Every DistillPort backend (an HTTP Messages-API distiller, a CLI-agent
950
+ * distiller, an in-app model adapter) asks the same thing and reads the same
951
+ * shape back, so the prompt and the tolerant JSON parse live here once. A
952
+ * backend owns only the transport (HTTP vs. child process vs. SDK) and how it
953
+ * extracts the model's text.
954
+ */
955
+
956
+ /** Build the distillation prompt for one source, capped at `maxItems`. */
957
+ declare function buildDistillPrompt(input: DistillInput, maxItems: number): string;
958
+ /** Tolerant parse: grab the first `[...]` block (allows ```json fences / prose). */
959
+ declare function parseItems(text: string): DistilledItem[];
960
+
961
+ /**
962
+ * scanDistilledSourceIds — which raw source ids already have ≥1 refined entry
963
+ * derived from them, found by reading the `sources:` provenance backlink on
964
+ * every `entries/**​/*.md`. Lets an incremental distill skip sources already
965
+ * done, so a daily re-run is idempotent (no duplicate entries, no wasted spend).
966
+ *
967
+ * Tolerant of both `walk` conventions a host's FsPort may use — paths relative
968
+ * to the walked dir (MemFs) or anchored at the workspace root (a workspace
969
+ * adapter) — by normalizing each back under `entries/` before reading.
970
+ */
971
+
972
+ declare function scanDistilledSourceIds(fs: FsPort): Promise<Set<string>>;
973
+
974
+ /**
975
+ * Distill registry — the catalog of distill kinds.
976
+ *
977
+ * Distill is four swappable slots: SOURCE (raw material) → IMPORTER (how to
978
+ * render it) → DISTILLER (the LLM) → TARGET (where entries land). A
979
+ * `DistillDescriptor` binds all four for one kind ("conversation", "web", …);
980
+ * the registry holds them by id so the cron/worker can dispatch polymorphically
981
+ * — adding a kind is registering a descriptor, with zero pipeline changes.
982
+ *
983
+ * Lightweight on purpose (mirrors the AIP-35 filesystem-provider registry):
984
+ * descriptors are code-registered, so there is no `resolveConfig` / secrets /
985
+ * UI-fields surface the way user-connected engine descriptors carry. Vendor-
986
+ * neutral — the kit owns the contract + the generic runner; the per-kind
987
+ * bindings live in each app's core package.
988
+ */
989
+
990
+ /**
991
+ * Who a distill runs for. `id` is the durable handle carried on the queued job
992
+ * (`resolveScope` rehydrates it in the worker); `userId` feeds the job's owner.
993
+ * Kinds extend this with their own scope fields (a guild id, a workspace id, …).
994
+ */
995
+ interface DistillScope {
996
+ readonly id: string;
997
+ readonly userId: string;
998
+ readonly [key: string]: unknown;
999
+ }
1000
+ /** Where distilled entries land — the corpus filesystem for a scope. */
1001
+ interface DistillTarget {
1002
+ readonly fs: FsPort;
1003
+ readonly clock: ClockPort;
1004
+ /** Optional corpus root within the fs (defaults to the fs root, ""). */
1005
+ readonly root?: string;
1006
+ }
1007
+ /**
1008
+ * SOURCE + IMPORTER bound to one scope. Co-constructed so both share a single
1009
+ * (cache-bearing) source instance where relevant (e.g. a windowed conversation
1010
+ * source loads each thread once). The binding owns the importer-native config
1011
+ * AND the provenance mapping, so `runDistill` stays importer-agnostic — a
1012
+ * conversation kind keys on `{refs}`/`conversationId`, a web kind on
1013
+ * `{urls}`/`originalUrl`, with no per-kind code in the runner.
1014
+ */
1015
+ interface DistillBinding {
1016
+ /** The kit importer for this kind (ConversationImporter, WebImporter, …). */
1017
+ readonly importer: CorpusImporter;
1018
+ /**
1019
+ * Build the importer's NATIVE config for the fresh (not-yet-distilled)
1020
+ * material, or `null` when nothing new is left to import. The config is
1021
+ * passed straight to `importer.enumerate({ config })`, so its shape is the
1022
+ * importer's own (`{refs}`, `{urls}`, …). Filter against `distilled` (the set
1023
+ * of provenance ids already backing ≥1 entry) so a re-run only sees fresh work.
1024
+ */
1025
+ prepare(distilled: ReadonlySet<string>): Promise<Readonly<Record<string, unknown>> | null>;
1026
+ /**
1027
+ * Stable provenance id for an imported source — becomes the entry's
1028
+ * `sources:` backlink and the dedup key matched against `distilled`. MUST
1029
+ * agree with the ids `prepare` filters on (e.g. the window slug, the URL).
1030
+ */
1031
+ provenanceId(imported: ImportedSource): string;
1032
+ }
1033
+ /**
1034
+ * One catalog entry: a distill kind, with its four slots bound to a scope.
1035
+ * Generic over the scope shape so each kind keeps its own typed scope while the
1036
+ * registry stores them uniformly.
1037
+ */
1038
+ interface DistillDescriptor<S extends DistillScope = DistillScope> {
1039
+ /** Stable kind id — "conversation" | "web" | … */
1040
+ readonly id: string;
1041
+ /** Queue type the cron submits and the worker handles, e.g. "distill:conversation". */
1042
+ readonly jobType: string;
1043
+ /** Human-readable label (diagnostics / surfaces). */
1044
+ readonly label: string;
1045
+ /** Tags applied to every source distilled by this kind (e.g. ["personal"]). */
1046
+ readonly tags?: readonly string[];
1047
+ /**
1048
+ * SOURCE + IMPORTER slots, bound to the scope. Receives the resolved TARGET
1049
+ * so an importer that reads the corpus filesystem (e.g. refine-its-own-sources)
1050
+ * can bind against it without resolving the target a second time.
1051
+ */
1052
+ bind(scope: S, target: DistillTarget): DistillBinding;
1053
+ /** DISTILLER slot: the LLM boundary for this scope. */
1054
+ distiller(scope: S): DistillPort;
1055
+ /** TARGET slot: where entries land for this scope. */
1056
+ target(scope: S): Promise<DistillTarget>;
1057
+ /**
1058
+ * Consent gate. Checked at fan-out (bulk filter) AND re-checked in the worker
1059
+ * (authoritative — it can flip between enqueue and execution). Absent ⇒ no gate.
1060
+ */
1061
+ consent?(scope: S): Promise<boolean>;
1062
+ /** Who to run for. Implementations should stream/paginate at scale. */
1063
+ scopes(): Promise<readonly S[]>;
1064
+ /** Rehydrate a scope from its `id` — the worker holds only the id post-enqueue. */
1065
+ resolveScope(scopeId: string): Promise<S>;
1066
+ }
1067
+ interface DistillRegistry {
1068
+ /** Register a kind. Throws on duplicate id. */
1069
+ register<S extends DistillScope>(descriptor: DistillDescriptor<S>): void;
1070
+ has(id: string): boolean;
1071
+ /** Resolve by id. Throws when the id was never registered. */
1072
+ resolve(id: string): DistillDescriptor;
1073
+ /** All registered descriptors (for the cron fan-out + worker handler map). */
1074
+ list(): DistillDescriptor[];
1075
+ }
1076
+ declare function createDistillRegistry(): DistillRegistry;
1077
+
1078
+ /**
1079
+ * runDistill — the generic distill pass for one (descriptor × scope).
1080
+ *
1081
+ * The kind-agnostic generalization of the conversation pipeline: resolve the
1082
+ * scope's TARGET, scan which provenance ids are already distilled, ask the
1083
+ * binding (SOURCE + IMPORTER) for the fresh refs, import each, and run the
1084
+ * DISTILLER over it — writing refined AIP-10 entries via `DistillRunner`.
1085
+ * No per-kind branches: every variation rides on the descriptor's slots.
1086
+ *
1087
+ * Idempotent: a unit whose provenance id already backs an entry is skipped by
1088
+ * the binding's `prepare`, and `DistillRunner` additionally skips any entry
1089
+ * slug that already exists — so a daily re-run only adds fresh material.
1090
+ */
1091
+
1092
+ interface DistillReport {
1093
+ readonly descriptorId: string;
1094
+ readonly scopeId: string;
1095
+ /** Sources the importer yielded this run. */
1096
+ unitsConsidered: number;
1097
+ /** Units that produced ≥1 new entry. */
1098
+ unitsDistilled: number;
1099
+ entriesWritten: number;
1100
+ /** Entry slugs skipped because an identical title already existed. */
1101
+ skipped: number;
1102
+ }
1103
+ declare function runDistill<S extends DistillScope>(descriptor: DistillDescriptor<S>, scope: S): Promise<DistillReport>;
1104
+
1105
+ /**
1106
+ * ClaudeDistiller — a `DistillPort` backed by Claude over the Messages API.
1107
+ * Reuses the kit's shared prompt + tolerant parse (`buildDistillPrompt` /
1108
+ * `parseItems`), owning only the HTTP transport. SDK-free (raw fetch) so the
1109
+ * job takes no client-lib dependency.
1110
+ *
1111
+ * Vendor-neutral: the only host coupling is an injected `apiKey` / `model` /
1112
+ * `baseUrl` — no app names, no env-name assumptions. Distillation is bulk +
1113
+ * background, so a mid-tier model is the sensible default; the caller gates
1114
+ * the spend (consent + incremental windows).
1115
+ */
1116
+
1117
+ interface ClaudeDistillerOptions {
1118
+ readonly apiKey: string;
1119
+ /** Defaults to a mid-tier model — distillation is bulk + background. */
1120
+ readonly model?: string;
1121
+ readonly baseUrl?: string;
1122
+ /** Max refined items to extract per source. */
1123
+ readonly maxItems?: number;
1124
+ }
1125
+ declare class ClaudeDistiller implements DistillPort {
1126
+ private readonly apiKey;
1127
+ private readonly model;
1128
+ private readonly baseUrl;
1129
+ private readonly maxItems;
1130
+ constructor(opts: ClaudeDistillerOptions);
1131
+ distill(input: DistillInput): Promise<readonly DistilledItem[]>;
1132
+ }
1133
+
1134
+ /**
1135
+ * Conversation windowing — pure helpers + the windowed-source contract.
1136
+ *
1137
+ * Chat is append-only, so a single immutable slug per conversation would
1138
+ * collide once new turns arrive. The fix is WINDOWING: each (thread,
1139
+ * completed-UTC-day) slice is the unit that distils exactly once (a closed day
1140
+ * is immutable). These helpers compose / parse the window ref the importer
1141
+ * resolves and the provenance slug the distilled-scan dedups on.
1142
+ *
1143
+ * No transport, no chat-store, no model dependency — just string algebra over
1144
+ * `(threadId, day)` plus a structural source contract. A fake satisfying
1145
+ * `ConversationWindowSource` is all a test needs.
1146
+ */
1147
+
1148
+ /** Compose the importer ref for one (thread, day) window. */
1149
+ declare function windowRef(threadId: string, day: string): string;
1150
+ /** Parse a window ref back to its parts, or null when malformed. */
1151
+ declare function parseWindowRef(ref: string): {
1152
+ threadId: string;
1153
+ day: string;
1154
+ } | null;
1155
+ /** Slug base a window resolves to — also the `sources:` provenance id, so the
1156
+ * distilled-scan can dedup windows across runs with no slug drift. */
1157
+ declare function windowSlug(threadId: string, day: string): string;
1158
+ /** A thread the source owns — minimal shape the enumeration needs. */
1159
+ interface ConversationThreadRef {
1160
+ readonly id: string;
1161
+ readonly threadId?: string;
1162
+ }
1163
+ /**
1164
+ * The windowed source the distill pipeline drives: a `ConversationSourcePort`
1165
+ * that can also enumerate its threads and each thread's completed-day windows.
1166
+ * A fake satisfying this is all a test needs (no DB, no chat store).
1167
+ */
1168
+ interface ConversationWindowSource extends ConversationSourcePort {
1169
+ listThreads(): Promise<ConversationThreadRef[]>;
1170
+ listWindows(threadId: string): Promise<string[]>;
1171
+ }
1172
+ /**
1173
+ * Enumerate the window refs worth distilling for a windowed source: every
1174
+ * (thread, completed-day) window whose provenance slug isn't already distilled.
1175
+ * Generic over any `ConversationWindowSource`, so a binding never re-implements
1176
+ * the thread→window→skip loop.
1177
+ */
1178
+ declare function enumerateWindowRefs(source: ConversationWindowSource, distilled: ReadonlySet<string>): Promise<string[]>;
1179
+
1180
+ /**
1181
+ * resolveKnowledge — the `knowledge:` binding resolver (KNOWLEDGE→SKILL link).
1182
+ *
1183
+ * Filesystem-first: a skill's binding (`{ tags, kinds }`) is resolved against
1184
+ * the refined `entries/**​/*.md` on disk — no graph engine. Returns the
1185
+ * matching refined entries (with their `sources:` provenance), filtered by the
1186
+ * caller's access scope so an operator never sees knowledge above its clearance.
1187
+ *
1188
+ * A graph engine (mind-graph / gbrain) can later replace this scan with
1189
+ * traversal/activation behind the SAME signature — the binding contract is stable.
1190
+ */
1191
+
1192
+ interface CorpusEntryQuery {
1193
+ /** Match entries sharing ANY of these tags. Empty/absent = no tag filter. */
1194
+ readonly tags?: readonly string[];
1195
+ /** Restrict to these refined kinds. Empty/absent = all kinds. */
1196
+ readonly kinds?: readonly RefinedKind[];
1197
+ /** Cap results (highest-confidence first). */
1198
+ readonly maxResults?: number;
1199
+ }
1200
+ /** A resolved origin an entry was distilled from (id + where to find it). */
1201
+ interface SourceRef {
1202
+ readonly id: string;
1203
+ readonly url?: string;
1204
+ readonly title?: string;
1205
+ readonly authority?: string;
1206
+ readonly language?: string;
1207
+ }
1208
+ interface ResolvedEntry {
1209
+ readonly slug: string;
1210
+ readonly kind: string;
1211
+ readonly title: string;
1212
+ readonly body: string;
1213
+ /** Provenance — the raw source ids this entry was derived from. */
1214
+ readonly sources: readonly string[];
1215
+ /** Resolved provenance — id + url + title for each source, when available. */
1216
+ readonly sourceRefs?: readonly SourceRef[];
1217
+ readonly confidence: number;
1218
+ readonly tags: readonly string[];
1219
+ readonly access?: string;
1220
+ readonly path: string;
1221
+ }
1222
+ interface ResolveKnowledgeOptions {
1223
+ readonly fs: FsPort;
1224
+ readonly query: CorpusEntryQuery;
1225
+ /**
1226
+ * Access scopes the caller (operator) is cleared for. An entry passes if it
1227
+ * has no access (public) or its access ∈ this set. Omit = no access filter.
1228
+ */
1229
+ readonly allowedAccess?: ReadonlySet<string>;
1230
+ }
1231
+ declare function resolveKnowledge(opts: ResolveKnowledgeOptions): Promise<readonly ResolvedEntry[]>;
1232
+
1233
+ /**
1234
+ * OverlayFs — stacks FsPorts so a writable layer shadows read-only ones.
1235
+ *
1236
+ * This is the customization engine for the SKILL→KNOWLEDGE→SOURCES spine:
1237
+ * a guild's editable corpus (its system workspace) sits ON TOP of one or
1238
+ * more read-only knowledge packs (shared, shipped). Layers are ordered
1239
+ * highest-precedence first.
1240
+ *
1241
+ * overlay = new OverlayFs([guildFs, packFs]) // guild wins
1242
+ *
1243
+ * Resolution is by RELATIVE PATH:
1244
+ * - reads (readFile/stat/exists) return the first layer that has the path
1245
+ * - walk/readdir UNION across layers, deduped — so an entry the guild
1246
+ * re-authors at the same path appears ONCE, with the guild's content
1247
+ * - a path only in the pack passes through unchanged (the floor)
1248
+ * - a path only in the guild is additive (extend)
1249
+ * - writes/appends/locks always target the top (writable) layer; the
1250
+ * packs stay pristine for every other guild
1251
+ *
1252
+ * This keeps `resolveKnowledge` untouched: it walks `entries/**` over the
1253
+ * overlay and never sees a pack entry that the guild has shadowed.
1254
+ */
1255
+
1256
+ /**
1257
+ * Sibling-path marker that removes (whiteouts) a lower layer's entry. To
1258
+ * drop `entries/foo.md`, a higher layer authors `entries/foo.md.whiteout`.
1259
+ * A marker file (not a frontmatter flag) keeps overlay resolution a pure
1260
+ * path operation — listing a directory never reads entry bodies.
1261
+ */
1262
+ declare const WHITEOUT_SUFFIX = ".whiteout";
1263
+ interface OverlayFsOptions {
1264
+ /**
1265
+ * Honor `.whiteout` markers so a higher layer can REMOVE (not just
1266
+ * shadow) a lower entry. OFF by default: with whiteout disabled the
1267
+ * overlay is pure union + shadow, byte-for-byte the original behavior.
1268
+ */
1269
+ readonly whiteout?: boolean;
1270
+ /**
1271
+ * Index of the writable layer (writes / appends / locks target it).
1272
+ * Defaults to 0. Set this when a read-only CONSTRAINT floor is pinned
1273
+ * ABOVE the writable layer: read precedence (layer order) and the write
1274
+ * target are then independent — the floor wins reads, the guild layer
1275
+ * still takes writes.
1276
+ */
1277
+ readonly writableLayer?: number;
1278
+ }
1279
+ declare class OverlayFs implements FsPort {
1280
+ /** Layers ordered highest-precedence first. */
1281
+ private readonly layers;
1282
+ private readonly whiteout;
1283
+ private readonly writableIndex;
1284
+ constructor(layers: readonly FsPort[], options?: OverlayFsOptions);
1285
+ /**
1286
+ * Top-down resolution for a single path: the first layer that declares
1287
+ * either the real entry OR its whiteout marker decides. Within one
1288
+ * layer a real entry wins over a stale marker. Returns null when no
1289
+ * layer declares the path, and `{ removed: true }` when the winning
1290
+ * layer tombstones it.
1291
+ */
1292
+ private resolvePath;
1293
+ exists(path: string): Promise<boolean>;
1294
+ readFile(path: string): Promise<string>;
1295
+ stat(path: string): Promise<FsStat | null>;
1296
+ readdir(path: string): Promise<readonly string[]>;
1297
+ walk(path: string): Promise<readonly string[]>;
1298
+ /**
1299
+ * Union the entries each layer reports for a path (basenames for
1300
+ * readdir, relative paths for walk), resolving whiteouts top-down: the
1301
+ * first layer (highest precedence) that declares an entry or its marker
1302
+ * wins. Real entries win over markers within the same layer; marker
1303
+ * files are stripped from the result.
1304
+ */
1305
+ private unionWhiteoutAware;
1306
+ writeFile(path: string, content: string): Promise<void>;
1307
+ appendFile(path: string, content: string): Promise<void>;
1308
+ lock(path: string): Promise<FsLockHandle>;
1309
+ }
1310
+ /**
1311
+ * ReadOnlyFs — wraps an FsPort and rejects every mutation. Use to seal a
1312
+ * knowledge pack so a misconfigured host can never write into the shared,
1313
+ * shipped corpus (the guild's edits belong in its own overlay layer).
1314
+ */
1315
+ declare class ReadOnlyFs implements FsPort {
1316
+ private readonly inner;
1317
+ constructor(inner: FsPort);
1318
+ exists(path: string): Promise<boolean>;
1319
+ readFile(path: string): Promise<string>;
1320
+ stat(path: string): Promise<FsStat | null>;
1321
+ readdir(path: string): Promise<readonly string[]>;
1322
+ walk(path: string): Promise<readonly string[]>;
1323
+ writeFile(path: string, _content: string): Promise<void>;
1324
+ appendFile(path: string, _content: string): Promise<void>;
1325
+ lock(path: string): Promise<FsLockHandle>;
1326
+ }
1327
+
1328
+ /**
1329
+ * MemFs — an in-memory FsPort over a flat `path → content` map.
1330
+ *
1331
+ * Paths are workspace-relative (no leading slash), exactly like the disk
1332
+ * adapters; `walk(dir)` returns paths relative to the walked directory, so it
1333
+ * composes with `resolveKnowledge` (which re-prefixes) and with `OverlayFs`
1334
+ * unchanged.
1335
+ *
1336
+ * Primary use: a knowledge PACK whose entries are inlined into the JS bundle
1337
+ * at build time (see `@guilde/knowledge-packs`). Reading from an in-memory map
1338
+ * means packs ride inside `dist` — no runtime `fs`, no path resolution, no
1339
+ * loose data files for the deploy to drop. Wrap in `ReadOnlyFs` for packs.
1340
+ */
1341
+
1342
+ declare class MemFs implements FsPort {
1343
+ private readonly files;
1344
+ constructor(files: Record<string, string>);
1345
+ exists(path: string): Promise<boolean>;
1346
+ readFile(path: string): Promise<string>;
1347
+ writeFile(path: string, content: string): Promise<void>;
1348
+ appendFile(path: string, content: string): Promise<void>;
1349
+ readdir(path: string): Promise<readonly string[]>;
1350
+ walk(path: string): Promise<readonly string[]>;
1351
+ stat(path: string): Promise<FsStat | null>;
1352
+ lock(): Promise<FsLockHandle>;
1353
+ }
1354
+
1355
+ /**
1356
+ * Knowledge-stack layer model (AIP-10 §Composition).
1357
+ *
1358
+ * A knowledge stack is the ordered set of layers an operator's recall is
1359
+ * resolved against: its own packs, its role's packs, an org house-style
1360
+ * view, a regional compliance floor, and so on. Each layer is produced by
1361
+ * a registered `LayerProvider` — adding a dimension (region, compliance,
1362
+ * tier, …) is registering one provider, never editing the resolver.
1363
+ *
1364
+ * The model has two knobs:
1365
+ * - `band` — precedence among LENS layers (lower = higher precedence).
1366
+ * - `mode` — `lens` (overridable) vs `constraint` (a non-shadowable,
1367
+ * non-tombstoneable floor; see AIP-10).
1368
+ *
1369
+ * Vendor-neutral: a provider reads a generic `ResolutionContext` and emits
1370
+ * `LayerRef`s the host knows how to mount. The context carries dimension
1371
+ * values plus an opaque, host-typed `subject` (an operator row, a role
1372
+ * handle, …) so per-app providers stay type-safe without leaking app
1373
+ * types into the kit.
1374
+ */
1375
+ /** How a layer composes against the others. */
1376
+ type LayerMode = "lens" | "constraint";
1377
+ /** A pointer to a mountable knowledge source — a pack id or a view ref. */
1378
+ interface LayerRef {
1379
+ /** Pack id (`screen-cv`) or KNOWLEDGE.md view ref (`region/fr`). */
1380
+ readonly ref: string;
1381
+ /** How the host should resolve `ref`. Defaults to `pack`. */
1382
+ readonly kind?: "pack" | "view";
1383
+ }
1384
+ /**
1385
+ * What the resolver is given. `subject` is opaque to the kit; providers
1386
+ * narrow it. `dimensions` is the open facet bag (`region`, `compliance`,
1387
+ * `org`, `tier`, `locale`, …).
1388
+ */
1389
+ interface ResolutionContext<TSubject = unknown> {
1390
+ /** Stable bucketing token for shadow rollout (e.g. conversation id). */
1391
+ readonly conversationId?: string;
1392
+ /** Dimension values keyed by dimension name. Missing/undefined = absent.
1393
+ * List-valued dimensions (e.g. `capability`) carry every value at once —
1394
+ * providers that expect a scalar must guard. */
1395
+ readonly dimensions?: Readonly<Record<string, string | readonly string[] | undefined>>;
1396
+ /** Host payload providers read to compute their refs. */
1397
+ readonly subject?: TSubject;
1398
+ }
1399
+ /** Optional shadow-rollout config for a layer. */
1400
+ interface LayerShadow {
1401
+ /** Fraction of conversations the layer fires on (0..1). */
1402
+ readonly pct?: number;
1403
+ }
1404
+ /**
1405
+ * A pluggable layer. Behavior lives on the descriptor — the resolver never
1406
+ * branches on `id` or `dimension`. Register via
1407
+ * `createRegistry<LayerProvider>({ family, keyBy: p => p.id })`.
1408
+ */
1409
+ interface LayerProvider<TSubject = unknown> {
1410
+ /** Registry key. */
1411
+ readonly id: string;
1412
+ /** Precedence among lenses; lower wins. Constraints are hoisted regardless. */
1413
+ readonly band: number;
1414
+ /** `lens` (overridable) or `constraint` (floor). */
1415
+ readonly mode: LayerMode;
1416
+ /** Dimension this layer adapts (`operator`, `role`, `region`, …) for audit. */
1417
+ readonly dimension?: string;
1418
+ /** When set, the layer is a shadow arm sampled deterministically. */
1419
+ readonly shadow?: LayerShadow;
1420
+ /** Compute the layer's refs for a context. Empty → the layer is inert. */
1421
+ resolve(ctx: ResolutionContext<TSubject>): readonly LayerRef[] | Promise<readonly LayerRef[]>;
1422
+ }
1423
+ /** One resolved layer in the stack, carrying audit attribution. */
1424
+ interface StackEntry {
1425
+ readonly providerId: string;
1426
+ readonly band: number;
1427
+ readonly mode: LayerMode;
1428
+ readonly dimension?: string;
1429
+ readonly refs: readonly LayerRef[];
1430
+ /** True iff this layer was a shadow arm that sampled in for this context. */
1431
+ readonly shadowSampled: boolean;
1432
+ }
1433
+ /** The resolved stack + the audit trail of layers that did NOT contribute. */
1434
+ interface StackResolution {
1435
+ /** Contributing layers, band-ordered (constraints keep their band here; the
1436
+ * mount step hoists them). */
1437
+ readonly entries: readonly StackEntry[];
1438
+ /** Providers that resolved to nothing or sampled out, for "why not" audits. */
1439
+ readonly skipped: readonly StackSkip[];
1440
+ }
1441
+ interface StackSkip {
1442
+ readonly providerId: string;
1443
+ readonly dimension?: string;
1444
+ readonly reason: "empty" | "shadow-not-sampled";
1445
+ }
1446
+
1447
+ /**
1448
+ * StackResolver — given a `ResolutionContext` and a registry of
1449
+ * `LayerProvider`s, produce the band-ordered list of layers an operator's
1450
+ * recall is resolved against.
1451
+ *
1452
+ * The generalization of `OperatorOverlayResolver` (playbooks): same
1453
+ * ctx → registry → priority-sort → ordered-list shape, same deterministic
1454
+ * shadow sampling for % rollout. Pure — no I/O. The host turns the emitted
1455
+ * refs into mounted FsPorts (see `buildOverlayFromStack`).
1456
+ */
1457
+
1458
+ declare class StackResolver<TSubject = unknown> {
1459
+ private readonly registry;
1460
+ constructor(registry: Registry<LayerProvider<TSubject>>);
1461
+ resolve(ctx: ResolutionContext<TSubject>): Promise<StackResolution>;
1462
+ }
1463
+
1464
+ /**
1465
+ * Turn a resolved knowledge stack into a mounted `OverlayFs`, and the
1466
+ * helpers that flatten it.
1467
+ *
1468
+ * Mount order encodes the AIP-10 plane-precedence invariant:
1469
+ *
1470
+ * [ ...constraint layers (read-only floor, band-ordered) ] ← top
1471
+ * [ guild writable layer ] ← writes here
1472
+ * [ ...lens layers (band-ordered) ] ← floor packs
1473
+ *
1474
+ * Constraints are hoisted ABOVE the guild layer so neither a guild edit
1475
+ * nor a `.whiteout` can shadow or remove them (they sit higher, and
1476
+ * top-down resolution + whiteout-only-suppresses-lower make them a true
1477
+ * floor). Writes still target the guild layer via `writableLayer`, so the
1478
+ * read floor and the write target stay independent.
1479
+ */
1480
+
1481
+ interface BuildOverlayOptions {
1482
+ /** The guild's own editable corpus — the single writable layer. */
1483
+ readonly guildFs: FsPort;
1484
+ /** The resolved stack (band-ordered entries). */
1485
+ readonly stack: StackResolution;
1486
+ /** Resolve a layer ref to an FsPort, or null if it has no mountable source. */
1487
+ readonly loadFs: (ref: LayerRef) => FsPort | null;
1488
+ }
1489
+ /**
1490
+ * Build the mounted overlay for a resolved stack. Returns `guildFs`
1491
+ * unchanged when no layer resolves to a mountable source (so the
1492
+ * single-layer fast path is preserved).
1493
+ */
1494
+ declare function buildOverlayFromStack(opts: BuildOverlayOptions): FsPort;
1495
+ /**
1496
+ * Flatten a resolved stack to a de-duplicated, band-ordered list of pack
1497
+ * refs (kind `pack` or unspecified). This reproduces the legacy
1498
+ * `resolveOperatorPacks` output shape (operator packs before role packs,
1499
+ * each id once) so the pack-id contract stays stable.
1500
+ */
1501
+ declare function flattenPackRefs(stack: StackResolution): string[];
1502
+ /** Pack refs split by composition mode, each list deduped + band-ordered. */
1503
+ interface StackRefPartition {
1504
+ /** Lens packs — mount UNDER the writable layer (the guild shadows them). */
1505
+ readonly lens: readonly string[];
1506
+ /** Constraint packs — mount ABOVE the writable layer (a non-shadowable floor). */
1507
+ readonly constraint: readonly string[];
1508
+ }
1509
+ /**
1510
+ * Partition a resolved stack's pack refs by mode. The host mounts `lens`
1511
+ * below the guild layer and `constraint` above it (read-only), so a
1512
+ * compliance/legal floor cannot be shadowed or tombstoned. With no
1513
+ * constraint layers resolved, `lens` equals `flattenPackRefs` — the legacy
1514
+ * pack-id contract.
1515
+ */
1516
+ declare function partitionStackRefs(stack: StackResolution): StackRefPartition;
1517
+
1518
+ /**
1519
+ * Sink — the agnostic outbound boundary: push refined corpus entries to an
1520
+ * external store. The kit knows NOTHING about any host (Guilde, etc.) — it
1521
+ * just hands `SinkItem`s to a `SinkPort`. Concrete sinks (a config-driven MCP
1522
+ * caller, an HTTP endpoint, a local mirror) live in host/adapter packages and
1523
+ * are selected by a sink manifest, not by code in the kit.
1524
+ *
1525
+ * This is how a vendor-neutral corpus links to a specific host: via a
1526
+ * declarative sink config + MCP, never a host-named command.
1527
+ */
1528
+ interface SinkItem {
1529
+ readonly slug: string;
1530
+ readonly kind: string;
1531
+ readonly title: string;
1532
+ readonly body: string;
1533
+ /** Provenance — raw source ids this entry derived from. */
1534
+ readonly sources: readonly string[];
1535
+ readonly tags: readonly string[];
1536
+ readonly confidence: number;
1537
+ readonly access?: string;
1538
+ /** Stable address for idempotent upsert, e.g. `corpus://<slug>`. */
1539
+ readonly uri: string;
1540
+ }
1541
+ interface SinkPushResult {
1542
+ readonly uri: string;
1543
+ readonly ok: boolean;
1544
+ readonly skipped?: boolean;
1545
+ readonly error?: string;
1546
+ }
1547
+ interface SinkPort {
1548
+ /** Push one item. Idempotent on `uri` is the sink's responsibility. */
1549
+ push(item: SinkItem): Promise<SinkPushResult>;
1550
+ }
1551
+
1552
+ /**
1553
+ * SyncRunner — push refined corpus entries to a SinkPort. Agnostic: it scans
1554
+ * `entries/` (via resolveKnowledge), shapes each as a SinkItem, and pushes
1555
+ * through the injected sink. The sink (and its host binding) is config, not
1556
+ * code here. Sources stay local (raw archive); only refined knowledge syncs.
1557
+ */
1558
+
1559
+ interface SyncRunnerOptions {
1560
+ readonly fs: FsPort;
1561
+ readonly sink: SinkPort;
1562
+ /** Which entries to sync (tags/kinds). Empty = all refined entries. */
1563
+ readonly select?: CorpusEntryQuery;
1564
+ /** URI scheme for the item address. Default `corpus`. */
1565
+ readonly uriScheme?: string;
1566
+ /** Pace between pushes (ms). Default 0. */
1567
+ readonly throttleMs?: number;
1568
+ readonly sleep?: (ms: number) => Promise<void>;
1569
+ }
1570
+ interface SyncReport {
1571
+ readonly pushed: number;
1572
+ readonly skipped: number;
1573
+ readonly failed: number;
1574
+ readonly results: readonly SinkPushResult[];
1575
+ }
1576
+ declare class SyncRunner {
1577
+ private readonly opts;
1578
+ constructor(opts: SyncRunnerOptions);
1579
+ run(): Promise<SyncReport>;
1580
+ }
1581
+
1582
+ /**
1583
+ * Multi-reviewer score aggregation.
1584
+ *
1585
+ * High-stakes corpus entries (those flagged by curator policy as
1586
+ * needing N-reviewer review) get scored by multiple reviewers — the
1587
+ * single-reviewer path is fine for the bulk of the queue, but the
1588
+ * top quality tier deserves redundancy.
1589
+ *
1590
+ * Aggregation policy:
1591
+ * - **Median** is the default — robust to a single outlier reviewer.
1592
+ * - **Weighted-median** uses reviewer track-record weights so a
1593
+ * well-calibrated reviewer's score counts for more than a fresh /
1594
+ * under-calibrated one.
1595
+ *
1596
+ * Disagreement detection: when the spread between the highest and
1597
+ * lowest review exceeds a threshold, the aggregate carries a
1598
+ * `disagreement: true` flag — the curator's queue can route those
1599
+ * entries to human adjudication rather than auto-trust the median.
1600
+ *
1601
+ * Pure — no I/O. Inputs come from the lifecycle workflow; outputs
1602
+ * feed the gate evaluator.
1603
+ */
1604
+ interface ReviewerScore {
1605
+ /** AIP-23-style identity. */
1606
+ readonly reviewerIdentity: string;
1607
+ /** 0..5 (matches metadata.corpus.qualityScore convention). */
1608
+ readonly score: number;
1609
+ /**
1610
+ * Optional confidence weight from the reviewer's track record
1611
+ * (correlation with downstream utility/lift). Defaults to 1.0
1612
+ * for unknown reviewers. See `correlation.ts`.
1613
+ */
1614
+ readonly weight?: number;
1615
+ readonly at: string;
1616
+ readonly note?: string;
1617
+ }
1618
+ interface AggregateOptions {
1619
+ /** Spread threshold above which `disagreement` is flagged. Default: 1.5. */
1620
+ readonly disagreementThreshold?: number;
1621
+ /** When true, use weighted-median; else plain median. */
1622
+ readonly weighted?: boolean;
1623
+ }
1624
+ interface AggregateResult {
1625
+ /** Aggregate qualityScore (0..5). */
1626
+ readonly aggregate: number;
1627
+ /** Highest vs lowest review difference. */
1628
+ readonly spread: number;
1629
+ /** True when spread > disagreementThreshold. */
1630
+ readonly disagreement: boolean;
1631
+ /** Number of reviewers that contributed. */
1632
+ readonly sampleSize: number;
1633
+ /** Per-reviewer breakdown for the curator UI. */
1634
+ readonly breakdown: readonly {
1635
+ readonly reviewerIdentity: string;
1636
+ readonly score: number;
1637
+ readonly weight: number;
1638
+ }[];
1639
+ }
1640
+ /**
1641
+ * Aggregate N reviewer scores into one final score + a disagreement
1642
+ * flag. Empty input → aggregate=0, disagreement=false (caller decides
1643
+ * whether that's an error).
1644
+ */
1645
+ declare function aggregateReviewerScores(reviews: readonly ReviewerScore[], opts?: AggregateOptions): AggregateResult;
1646
+
1647
+ /**
1648
+ * Reviewer track record — per-reviewer history of (score → observed
1649
+ * outcome) used to compute calibration weights.
1650
+ *
1651
+ * Stored at `corpus/_calibration/reviewer-track-record.yaml`:
1652
+ *
1653
+ * reviewers:
1654
+ * "ws://operators/quality-reviewer":
1655
+ * - { entrySlug, qualityScore, observedUtility?, observedLift?, at }
1656
+ * "ws://operators/junior-reviewer":
1657
+ * - ...
1658
+ *
1659
+ * Append-only — every review pass adds rows. The calibration routine
1660
+ * reads the configured window (typically 90 days), correlates the
1661
+ * reviewer's scores against observed retrieval-quality outcomes, and
1662
+ * derives a weight in [0, 1] that feeds the multi-reviewer aggregator
1663
+ * (see `aggregate.ts`).
1664
+ */
1665
+
1666
+ interface TrackRecordEntry {
1667
+ readonly entrySlug: string;
1668
+ /** Reviewer-assigned 0..5 quality score at review time. */
1669
+ readonly qualityScore: number;
1670
+ /** Retrieval utility (0..1 — fraction of hits actually cited). */
1671
+ readonly observedUtility?: number;
1672
+ /** Retrieval lift (>1 = boost vs baseline, <1 = drag). */
1673
+ readonly observedLift?: number;
1674
+ readonly at: string;
1675
+ }
1676
+ interface ReviewerTrackRecordOptions {
1677
+ readonly fs: FsPort;
1678
+ /** Workspace-relative path. Defaults to `_calibration/reviewer-track-record.yaml`. */
1679
+ readonly path?: string;
1680
+ }
1681
+ declare class ReviewerTrackRecord {
1682
+ private readonly opts;
1683
+ private readonly path;
1684
+ constructor(opts: ReviewerTrackRecordOptions);
1685
+ /** Load the full record. Empty when the file doesn't exist yet. */
1686
+ load(): Promise<Readonly<Record<string, readonly TrackRecordEntry[]>>>;
1687
+ /** Append one entry under a reviewer's identity. */
1688
+ append(reviewerIdentity: string, entry: TrackRecordEntry): Promise<void>;
1689
+ /**
1690
+ * Read all entries for a reviewer in the last N days. Used by the
1691
+ * calibration routine to compute correlation on a rolling window.
1692
+ */
1693
+ window(reviewerIdentity: string, days: number, now: Date): Promise<readonly TrackRecordEntry[]>;
1694
+ /** Reviewer identities present in the record. */
1695
+ listReviewers(): Promise<readonly string[]>;
1696
+ }
1697
+
1698
+ /**
1699
+ * Calibration — correlate a reviewer's qualityScore against the
1700
+ * downstream retrieval-quality outcomes (utility + lift). Reviewers
1701
+ * whose scores don't predict outcomes are miscalibrated; their
1702
+ * weight drops in the multi-reviewer aggregator until they re-train.
1703
+ *
1704
+ * Math: Pearson correlation coefficient on two parallel arrays.
1705
+ * Returns NaN-safe r in [-1, 1]. Threshold for "calibrated" is host
1706
+ * policy; the helper just reports.
1707
+ *
1708
+ * Pure — no I/O. Inputs come from `ReviewerTrackRecord.window`,
1709
+ * outputs feed the calibration routine + multi-reviewer aggregator.
1710
+ */
1711
+
1712
+ interface ReviewerCalibration {
1713
+ readonly reviewerIdentity: string;
1714
+ readonly sampleSize: number;
1715
+ /**
1716
+ * Pearson r in [-1, 1]. Higher = reviewer's qualityScore predicts
1717
+ * downstream outcomes better. Negative = reviewer is inversely
1718
+ * correlated (worse than random). NaN when sampleSize < 2 or all
1719
+ * scores are identical (no variance).
1720
+ */
1721
+ readonly correlationUtility: number;
1722
+ readonly correlationLift: number;
1723
+ /** Whether this reviewer meets the calibrated threshold. */
1724
+ readonly isCalibrated: boolean;
1725
+ /** Suggested weight for the multi-reviewer aggregator (0..1). */
1726
+ readonly suggestedWeight: number;
1727
+ /** Reason flag — useful for curator UI tooltips. */
1728
+ readonly status: "calibrated" | "miscalibrated" | "insufficient-sample" | "no-variance";
1729
+ }
1730
+ interface CalibrationOptions {
1731
+ /** Minimum sample size to compute calibration. Defaults to 10. */
1732
+ readonly minSampleSize?: number;
1733
+ /** Pearson r threshold above which a reviewer is "calibrated". Defaults to 0.4. */
1734
+ readonly calibratedThreshold?: number;
1735
+ }
1736
+ /**
1737
+ * Pearson correlation coefficient on two parallel numeric arrays.
1738
+ *
1739
+ * r = cov(X, Y) / (σ_X · σ_Y)
1740
+ *
1741
+ * Returns NaN when:
1742
+ * - either array is < 2 elements
1743
+ * - either array has zero variance (all values identical)
1744
+ *
1745
+ * NaN is the explicit "I can't compute this" signal — callers
1746
+ * decide whether NaN means "default to neutral weight" or "flag for
1747
+ * human review".
1748
+ */
1749
+ declare function pearsonCorrelation(xs: readonly number[], ys: readonly number[]): number;
1750
+ /**
1751
+ * Compute a single reviewer's calibration from their track-record
1752
+ * window. Returns a ReviewerCalibration summary.
1753
+ *
1754
+ * Strategy: average the utility-correlation and lift-correlation
1755
+ * (when both are available). If only one outcome was recorded
1756
+ * across the window (rare), use that one alone.
1757
+ */
1758
+ declare function computeReviewerCalibration(reviewerIdentity: string, entries: readonly TrackRecordEntry[], opts?: CalibrationOptions): ReviewerCalibration;
1759
+
1760
+ /**
1761
+ * CorpusWorkspaceReader — scan + parse a corpus workspace via FsPort.
1762
+ *
1763
+ * Pure path-discipline + YAML parsing — no schema validation here. The
1764
+ * reader's job is to surface every .md file in the workspace and
1765
+ * classify it by location. Schema validation happens in
1766
+ * `validate/validator.ts` so callers can run both in parallel and get
1767
+ * crisp error categorization.
1768
+ *
1769
+ * Pure: no `node:fs`, no `node:path`. All paths are joined manually
1770
+ * with "/" so the kit runs on Node, Bun, Deno, and browser-side
1771
+ * editors (e.g. curator UI lint preview).
1772
+ */
1773
+
1774
+ interface CorpusWorkspaceReaderOptions {
1775
+ readonly fs: FsPort;
1776
+ }
1777
+ declare class CorpusWorkspaceReader {
1778
+ private readonly opts;
1779
+ constructor(opts: CorpusWorkspaceReaderOptions);
1780
+ /**
1781
+ * Scan the workspace rooted at `root` and return a typed snapshot.
1782
+ * `root` is workspace-relative (the host resolves to a backing
1783
+ * storage); commonly "" for the workspace root itself.
1784
+ */
1785
+ read(root: string): Promise<CorpusWorkspaceSnapshot>;
1786
+ }
1787
+
1788
+ /**
1789
+ * CorpusWorkspaceWriter — atomic file writes with versionToken
1790
+ * optimistic-concurrency guard.
1791
+ *
1792
+ * Every write goes through `writeFile(path, content, expected?)`:
1793
+ * - If `expected` is provided, the writer reads the current file's
1794
+ * content hash and refuses to write if it doesn't match → throws
1795
+ * CorpusVersionConflictError. Caller retries with the fresh
1796
+ * versionToken.
1797
+ * - If `expected` is null, the writer requires the file to NOT
1798
+ * exist (create-only).
1799
+ * - If `expected` is undefined, the writer overwrites unconditionally
1800
+ * (use sparingly — bypasses the concurrency-correctness story).
1801
+ *
1802
+ * Multi-file transactions (e.g. promote entry + update _index.md +
1803
+ * append _log.md) use FsPort.lock on a sentinel path so concurrent
1804
+ * promoters serialize cleanly.
1805
+ *
1806
+ * The writer is the ONLY component allowed to call FsPort.writeFile
1807
+ * for workspace content. The reader/validator/linter never write.
1808
+ * The event emitter writes only _log.md via append.
1809
+ */
1810
+
1811
+ declare class CorpusVersionConflictError extends Error {
1812
+ readonly path: string;
1813
+ readonly expected: string;
1814
+ readonly actual: string;
1815
+ constructor(path: string, expected: string, actual: string);
1816
+ }
1817
+ interface CorpusWorkspaceWriterOptions {
1818
+ readonly fs: FsPort;
1819
+ }
1820
+ /**
1821
+ * Frontmatter + body source pair, used by helpers like `writeEntry`
1822
+ * that need to serialize the .md back from structured data.
1823
+ */
1824
+ interface MarkdownDoc {
1825
+ readonly frontmatter: Readonly<Record<string, unknown>>;
1826
+ readonly body: string;
1827
+ }
1828
+ declare class CorpusWorkspaceWriter {
1829
+ private readonly opts;
1830
+ constructor(opts: CorpusWorkspaceWriterOptions);
1831
+ /**
1832
+ * Compute the versionToken for raw file content. Exposed so the
1833
+ * caller can stage a write and pass the same hash function used
1834
+ * inside the writer.
1835
+ */
1836
+ static versionTokenOf(content: string): string;
1837
+ /**
1838
+ * Atomic write with optimistic concurrency check.
1839
+ *
1840
+ * - expected === undefined → unconditional overwrite (use sparingly)
1841
+ * - expected === null → create-only (refuses if exists)
1842
+ * - expected === string → CAS check (refuses if current hash differs)
1843
+ *
1844
+ * Returns the new versionToken on success.
1845
+ */
1846
+ writeFile(path: string, content: string, expected?: string | null): Promise<string>;
1847
+ /**
1848
+ * Serialize a markdown doc (frontmatter + body) and write atomically.
1849
+ * Frontmatter keys are stringified in a stable order so version
1850
+ * tokens are deterministic across emitter rebuilds.
1851
+ */
1852
+ writeMarkdown(path: string, doc: MarkdownDoc, expected?: string | null): Promise<string>;
1853
+ /**
1854
+ * Run `fn` while holding a workspace-scoped lock. Used for
1855
+ * multi-file transactions (promote entry + update _index.md + log
1856
+ * append). The lock path is host-specific; we default to
1857
+ * `_log.md` because it's the file every transaction touches.
1858
+ *
1859
+ * `lockPath` is workspace-relative.
1860
+ */
1861
+ transaction<T>(lockPath: string, fn: () => Promise<T>): Promise<T>;
1862
+ }
1863
+
1864
+ /**
1865
+ * CorpusValidator — validate a workspace snapshot against the AIP JSON
1866
+ * Schemas (the spec source-of-truth at
1867
+ * projects/agentproto/agentproto/specs/resources/aip-XX/draft/).
1868
+ *
1869
+ * The validator is constructed once with an AJV instance pre-loaded
1870
+ * with every referenced schema, then run against a `ParsedFile` or a
1871
+ * whole `CorpusWorkspaceSnapshot`. Schema loading is delegated to a
1872
+ * factory function so the kit doesn't carry the spec JSON inside — the
1873
+ * host (cloud adapter or local CLI) supplies the schemas at boot.
1874
+ *
1875
+ * Why AJV not zod: the AgentProto zod schemas at @agentproto/(aip)/schema
1876
+ * are partially generated stubs today (z.any() oneOf branches in
1877
+ * knowledge/collection/playbook). AJV against the canonical JSON
1878
+ * Schemas is the authoritative path until the zod stubs are
1879
+ * regenerated from the JSON Schemas. The corpus kit is the place that
1880
+ * exercises both, so this discipline matters.
1881
+ */
1882
+
1883
+ /**
1884
+ * Map from FileKind → JSON Schema key. The schema bundle the host
1885
+ * supplies must include these keys.
1886
+ */
1887
+ type AipSchemaKey = "knowledge" | "collection" | "playbook" | "operator" | "workflow" | "routine";
1888
+ interface AipSchemaBundle {
1889
+ /** Schemas keyed by AIP doctype. Values are JSON Schema documents. */
1890
+ readonly schemas: Readonly<Record<AipSchemaKey, unknown>>;
1891
+ /**
1892
+ * External schemas to register with AJV before compilation (so
1893
+ * `$ref` cross-document references resolve). The known cross-refs
1894
+ * today are AIP-16 IO and AIP-17 RUNNER.
1895
+ */
1896
+ readonly externals?: ReadonlyArray<unknown>;
1897
+ }
1898
+ interface CorpusValidatorOptions {
1899
+ readonly bundle: AipSchemaBundle;
1900
+ }
1901
+ declare class CorpusValidator {
1902
+ private readonly validators;
1903
+ constructor(opts: CorpusValidatorOptions);
1904
+ /**
1905
+ * Validate a single parsed file. Returns issues + a valid flag.
1906
+ * Unknown FileKind ("unknown" bucket) is reported as a single info
1907
+ * issue — the file exists but doesn't fit AIP conventions.
1908
+ */
1909
+ validateFile(file: ParsedFile): ValidationResult;
1910
+ /**
1911
+ * Validate every file in a workspace snapshot. Returns a flat list
1912
+ * of issues across all files plus a `valid` flag that's true only
1913
+ * if zero error-severity issues exist.
1914
+ */
1915
+ validateWorkspace(snapshot: CorpusWorkspaceSnapshot): ValidationResult;
1916
+ }
1917
+
1918
+ /**
1919
+ * CorpusLinter — implements the AIP-10 lint kinds declared in
1920
+ * KNOWLEDGE.md.lints[]. Five canonical kinds + `custom` delegation
1921
+ * hook for hosts that ship their own checks.
1922
+ *
1923
+ * Lint kinds (per AIP-10 spec):
1924
+ * - require-source — entries of kinds X MUST have ≥1 source ref
1925
+ * - min-confidence — entries of kinds X SHOULD have confidence ≥ N
1926
+ * - max-age — entries of kinds X with updated_at older than N days
1927
+ * - broken-ref — sources/links/supersedes references that don't resolve
1928
+ * - orphan — entries that no other entry references (incoming = 0)
1929
+ * - custom — delegated to host-supplied callbacks keyed by lint id
1930
+ *
1931
+ * The linter assumes the workspace has already passed schema validation.
1932
+ * Running lint on invalid workspace yields garbage. Callers should run
1933
+ * the validator first; the writer pipeline enforces this.
1934
+ */
1935
+
1936
+ interface LintDeclaration {
1937
+ readonly id: string;
1938
+ readonly kind: "require-source" | "min-confidence" | "max-age" | "broken-ref" | "orphan" | "custom";
1939
+ readonly appliesTo: string;
1940
+ readonly severity: "error" | "warn" | "info";
1941
+ readonly params?: Readonly<Record<string, unknown>>;
1942
+ }
1943
+ type CustomLintRunner = (decl: LintDeclaration, snapshot: CorpusWorkspaceSnapshot) => readonly LintIssue[];
1944
+ interface CorpusLinterOptions {
1945
+ readonly clock: ClockPort;
1946
+ /** Per-id implementations for `kind: custom` lints. */
1947
+ readonly customRunners?: Readonly<Record<string, CustomLintRunner>>;
1948
+ }
1949
+ declare class CorpusLinter {
1950
+ private readonly opts;
1951
+ constructor(opts: CorpusLinterOptions);
1952
+ lint(snapshot: CorpusWorkspaceSnapshot): LintReport;
1953
+ private lintRequireSource;
1954
+ private lintMinConfidence;
1955
+ private lintMaxAge;
1956
+ private lintBrokenRef;
1957
+ private lintOrphan;
1958
+ }
1959
+
1960
+ /**
1961
+ * CorpusEventEmitter — append-only writer for `_log.md`.
1962
+ *
1963
+ * The corpus' audit trail. Every state transition (candidate
1964
+ * discovered/analyzed/approved/rejected, entry promoted/deprecated,
1965
+ * playbook activated/archived, gap opened/resolved) appends one line
1966
+ * here.
1967
+ *
1968
+ * Line format (NDJSON-ish, but inside a markdown file for AIP-10
1969
+ * conformance — _log.md is the canonical AIP-10 log doctype):
1970
+ *
1971
+ * - {ISO} {kind} by {actor} payload={JSON}
1972
+ *
1973
+ * Concrete:
1974
+ *
1975
+ * - 2026-05-22T14:30:00.000Z corpus.entry.promoted by ws://operators/corpus-curator payload={"slug":"contrarian-short-form-hooks","kind":"pattern"}
1976
+ *
1977
+ * Why line-per-event (not blocks): every host needs to grep / tail /
1978
+ * stream the log without a markdown parser. Inside an .md file =
1979
+ * AIP-10 conformant; line-format = ops-friendly.
1980
+ *
1981
+ * Atomicity: relies on FsPort.appendFile being atomic against
1982
+ * concurrent appends. Hosts MUST honor that — cloud topology uses
1983
+ * append APIs (S3 Express, GCS), local topology uses POSIX append
1984
+ * (atomic up to PIPE_BUF).
1985
+ */
1986
+
1987
+ interface CorpusEventEmitterOptions {
1988
+ readonly fs: FsPort;
1989
+ readonly clock: ClockPort;
1990
+ readonly identity: IdentityPort;
1991
+ /**
1992
+ * Workspace root the emitter targets — `_log.md` is written
1993
+ * relative to this. Hosts that mount multiple corpora pass one
1994
+ * emitter per workspace root.
1995
+ */
1996
+ readonly workspaceRoot: string;
1997
+ }
1998
+ declare class CorpusEventEmitter {
1999
+ private readonly opts;
2000
+ constructor(opts: CorpusEventEmitterOptions);
2001
+ /**
2002
+ * Emit one event. Returns the event as written (including the
2003
+ * ISO timestamp and actor resolved at emit time). The .md log file
2004
+ * is created on first append if missing.
2005
+ */
2006
+ emit(kind: CorpusEventKind, payload: Readonly<Record<string, unknown>>): Promise<CorpusEvent>;
2007
+ }
2008
+
2009
+ /**
2010
+ * Candidate state machine.
2011
+ *
2012
+ * AIP-18 collection COLLECTION.md declares the `statuses[]` array
2013
+ * with explicit `transitionsTo`. This module enforces those
2014
+ * transitions at the corpus-kit boundary so callers can never
2015
+ * silently land an item in an illegal state.
2016
+ *
2017
+ * The corpus' canonical candidate collection (per the plan) has:
2018
+ *
2019
+ * discovered → analyzed | rejected
2020
+ * analyzed → approved | rejected | needs-work
2021
+ * needs-work → analyzed | rejected
2022
+ * approved = terminal
2023
+ * rejected = terminal
2024
+ *
2025
+ * This module is pure (no I/O). The lifecycle workflow caller pairs
2026
+ * it with the writer + emitter to durably enact each transition.
2027
+ */
2028
+ type CandidateStatus = "discovered" | "analyzed" | "needs-work" | "approved" | "rejected";
2029
+ /**
2030
+ * Default transition graph mirroring the corpus-candidate AIP-18
2031
+ * collection shape shipped in the marketing preset. Hosts that load a
2032
+ * custom collection schema may override via
2033
+ * `transitionGraphFromCollection()`.
2034
+ */
2035
+ declare const DEFAULT_TRANSITIONS: Readonly<Record<CandidateStatus, readonly CandidateStatus[]>>;
2036
+ interface TransitionCheck {
2037
+ readonly allowed: boolean;
2038
+ readonly reason?: string;
2039
+ }
2040
+ /**
2041
+ * Check whether a candidate can transition from `from` to `to`.
2042
+ */
2043
+ declare function canTransition(from: CandidateStatus, to: CandidateStatus, graph?: Readonly<Record<CandidateStatus, readonly CandidateStatus[]>>): TransitionCheck;
2044
+ /**
2045
+ * Build a transition graph from a parsed COLLECTION.md frontmatter.
2046
+ * Hosts call this once at boot to align the candidate state machine
2047
+ * with whatever collection schema the workspace ships.
2048
+ *
2049
+ * Falls back to DEFAULT_TRANSITIONS if the frontmatter doesn't carry
2050
+ * a recognizable `statuses[]` array — the caller is encouraged to
2051
+ * pass the parsed frontmatter directly rather than re-flattening.
2052
+ */
2053
+ declare function transitionGraphFromCollection(fm: Readonly<Record<string, unknown>>): Readonly<Record<string, readonly string[]>>;
2054
+ declare class IllegalTransitionError extends Error {
2055
+ readonly from: CandidateStatus;
2056
+ readonly to: CandidateStatus;
2057
+ readonly reason: string;
2058
+ constructor(from: CandidateStatus, to: CandidateStatus, reason: string);
2059
+ }
2060
+ /**
2061
+ * Validate a transition and throw if disallowed. Convenience wrapper
2062
+ * around canTransition for callers that want fail-fast semantics.
2063
+ */
2064
+ declare function assertTransition(from: CandidateStatus, to: CandidateStatus, graph?: Readonly<Record<CandidateStatus, readonly CandidateStatus[]>>): void;
2065
+
2066
+ /**
2067
+ * Auto-promote gate — evaluates a candidate against
2068
+ * `KNOWLEDGE.md.metadata.corpus.autoPromote.requires`.
2069
+ *
2070
+ * The plan's auto-promote requirements (from the workspace manifest
2071
+ * example) typically include:
2072
+ *
2073
+ * qualityScore: { min: 4.2 }
2074
+ * riskScore: { max: 1.5 }
2075
+ * hasArchiveHash: true
2076
+ * requiredFields: [why_it_works, transferable_pattern, use_when, avoid_when]
2077
+ * notRestricted: true
2078
+ *
2079
+ * This gate returns a structured GateResult with pass/fail + the
2080
+ * specific rules that failed, so the caller can either auto-promote
2081
+ * (all pass) or route to human review (corpus-review collection)
2082
+ * with the failure reasons attached.
2083
+ *
2084
+ * Pure — no I/O.
2085
+ */
2086
+
2087
+ interface AutoPromoteRequirements {
2088
+ readonly qualityScore?: {
2089
+ min?: number;
2090
+ max?: number;
2091
+ };
2092
+ readonly riskScore?: {
2093
+ min?: number;
2094
+ max?: number;
2095
+ };
2096
+ readonly hasArchiveHash?: boolean;
2097
+ readonly requiredFields?: readonly string[];
2098
+ readonly notRestricted?: boolean;
2099
+ }
2100
+ interface AutoPromoteConfig {
2101
+ readonly enabled: boolean;
2102
+ readonly requires?: AutoPromoteRequirements;
2103
+ }
2104
+ interface CandidateForGate {
2105
+ /** Parsed candidate frontmatter (analyzed state) + body. */
2106
+ readonly frontmatter: Readonly<Record<string, unknown>>;
2107
+ readonly body: string;
2108
+ /** Optional analysis output — fields the analyst produced. */
2109
+ readonly analysis?: Readonly<Record<string, unknown>>;
2110
+ }
2111
+ interface GateFailure {
2112
+ readonly rule: string;
2113
+ readonly message: string;
2114
+ }
2115
+ interface GateResult {
2116
+ readonly passed: boolean;
2117
+ readonly failures: readonly GateFailure[];
2118
+ /** True when the workspace itself has auto-promote disabled. */
2119
+ readonly disabled: boolean;
2120
+ }
2121
+ /**
2122
+ * Read the auto-promote config from a workspace snapshot. Returns
2123
+ * a "disabled" config if the workspace doesn't declare one — the
2124
+ * gate will short-circuit to "always require human review".
2125
+ */
2126
+ declare function extractAutoPromoteConfig(snapshot: CorpusWorkspaceSnapshot): AutoPromoteConfig;
2127
+ /**
2128
+ * Evaluate a candidate against the workspace's auto-promote config.
2129
+ * Returns `passed: true` only when every declared requirement holds.
2130
+ */
2131
+ declare function evaluateGate(candidate: CandidateForGate, config: AutoPromoteConfig): GateResult;
2132
+
2133
+ /**
2134
+ * Chunking strategy for corpus entries.
2135
+ *
2136
+ * Split an entry body into chunks that fit the backing engine's
2137
+ * `maxChunkBytes` capability. Naive but predictable — overlap window
2138
+ * keeps semantic continuity across boundaries; smarter sentence /
2139
+ * semantic chunking can land later behind the same call.
2140
+ *
2141
+ * Matches the qdrant adapter's CHUNK_TARGET_CHARS / CHUNK_OVERLAP_CHARS
2142
+ * numerology so corpus + qdrant agree on chunk shape end-to-end.
2143
+ */
2144
+ interface ChunkerOptions {
2145
+ /** Soft target per chunk (characters). Default 1500 ≈ 375 tokens. */
2146
+ readonly targetChars?: number;
2147
+ /** Overlap window between adjacent chunks. Default 150. */
2148
+ readonly overlapChars?: number;
2149
+ /** Hard ceiling — backing engine's maxChunkBytes (Buffer-byte). */
2150
+ readonly maxBytes?: number;
2151
+ }
2152
+ declare function chunkText(text: string, opts?: ChunkerOptions): string[];
2153
+
2154
+ /**
2155
+ * WriterPort — the corpus kit's path into a backing knowledge engine.
2156
+ *
2157
+ * The kit never imports IKnowledgeProvider — that's an agstudio type.
2158
+ * Instead it consumes this minimal port; the host (agstudio
2159
+ * CorpusInternalWriter in cloud, future local-CLI bridge) supplies
2160
+ * the implementation.
2161
+ *
2162
+ * Matches the shape of `CorpusInternalWriter` in
2163
+ * `packages/integration/knowledge/src/providers/corpus/internal-writer.ts`
2164
+ * so the cloud-topology adapter is a one-to-one satisfying type.
2165
+ */
2166
+ interface WriterChunk {
2167
+ readonly text: string;
2168
+ readonly metadata?: Readonly<Record<string, unknown>>;
2169
+ }
2170
+ interface PushChunksInput {
2171
+ readonly entrySlug: string;
2172
+ readonly entryPath: string;
2173
+ readonly title?: string;
2174
+ readonly uri?: string;
2175
+ readonly chunks: readonly WriterChunk[];
2176
+ readonly entryMetadata?: Readonly<Record<string, unknown>>;
2177
+ }
2178
+ interface WriterPort {
2179
+ pushChunks(input: PushChunksInput): Promise<readonly string[]>;
2180
+ removeEntry(entrySlug: string): Promise<{
2181
+ removed: number;
2182
+ }>;
2183
+ }
2184
+
2185
+ /**
2186
+ * CorpusIndexer — coordinator that ships active entries into the
2187
+ * backing engine via WriterPort.
2188
+ *
2189
+ * The kit doesn't know about IKnowledgeProvider — agstudio's
2190
+ * CorpusInternalWriter satisfies WriterPort and bridges to the
2191
+ * actual engine (qdrant / gbrain). Local topology supplies its own
2192
+ * WriterPort (e.g. dockered qdrant).
2193
+ *
2194
+ * Three public methods:
2195
+ * - reindex(snapshot) full sync: pushes every active entry
2196
+ * - syncEntry(snapshot, slug) delta: re-push a single entry
2197
+ * - removeEntry(slug) delta: drop a deprecated/archived entry
2198
+ *
2199
+ * Policy: only entries with `metadata.corpus.status === "active"`
2200
+ * (or unset, treated as active) are shipped. Deprecated/archived
2201
+ * entries are removed from the backing engine.
2202
+ */
2203
+
2204
+ interface CorpusIndexerOptions {
2205
+ readonly writer: WriterPort;
2206
+ /** Optional chunker tuning. Defaults match the backing engine's caps. */
2207
+ readonly chunker?: ChunkerOptions;
2208
+ }
2209
+ interface IndexReport {
2210
+ readonly pushed: number;
2211
+ readonly removed: number;
2212
+ readonly skipped: number;
2213
+ readonly chunkCount: number;
2214
+ }
2215
+ declare class CorpusIndexer {
2216
+ private readonly opts;
2217
+ constructor(opts: CorpusIndexerOptions);
2218
+ /**
2219
+ * Re-sync every active entry. Used at boot (cold-install) and by
2220
+ * the admin `corpus reindex` command for drift recovery.
2221
+ */
2222
+ reindex(snapshot: CorpusWorkspaceSnapshot): Promise<IndexReport>;
2223
+ /**
2224
+ * Re-sync a single entry by slug. The lifecycle's `promote`
2225
+ * workflow calls this immediately after writing the entry file.
2226
+ */
2227
+ syncEntry(snapshot: CorpusWorkspaceSnapshot, slug: string): Promise<{
2228
+ chunkCount: number;
2229
+ }>;
2230
+ /** Drop one entry from the backing engine (deprecation, GDPR erase). */
2231
+ removeEntry(slug: string): Promise<void>;
2232
+ private pushEntry;
2233
+ }
2234
+
2235
+ /**
2236
+ * Promote workflow — turn an analyzed candidate into a published
2237
+ * AIP-10 entry. The fan-out the plan describes:
2238
+ *
2239
+ * 1. Validate the promotion request (gate check, slug uniqueness)
2240
+ * 2. Acquire workspace lock (atomicity across the multi-file write)
2241
+ * 3. Write the entry file (AIP-10 conformant frontmatter + body)
2242
+ * 4. Update _index.md (faceted catalog regen)
2243
+ * 5. Sync to backing engine via CorpusIndexer
2244
+ * 6. Append corpus.entry.promoted event to _log.md
2245
+ * 7. Take the candidate row out of the sidecar (or update the
2246
+ * AIP-18 ITEM.md if it was materialized already)
2247
+ *
2248
+ * Idempotent on candidate.id — re-running promote for the same id
2249
+ * reuses the existing entry path rather than duplicating. The
2250
+ * workspace lock guards against concurrent promoters.
2251
+ *
2252
+ * Pure-ish: the workflow itself is logic + orchestration; every I/O
2253
+ * effect goes through the injected reader/writer/emitter/indexer.
2254
+ */
2255
+
2256
+ interface PromoteOptions {
2257
+ /** Workspace root inside FsPort (typically ""). */
2258
+ readonly workspacePath: string;
2259
+ /** Slug of the entry to create (or update, if it already exists). */
2260
+ readonly entrySlug: string;
2261
+ /** AIP-10 entry kind (principle | pattern | …). */
2262
+ readonly entryKind: string;
2263
+ /** Path of the entry within the workspace, e.g. entries/patterns/2026/foo.md */
2264
+ readonly entryPath: string;
2265
+ /** AIP-10 entry frontmatter to publish. */
2266
+ readonly frontmatter: Readonly<Record<string, unknown>>;
2267
+ /** Entry body (markdown after frontmatter). */
2268
+ readonly body: string;
2269
+ /**
2270
+ * Optional candidate id to consume from a sidecar. When provided,
2271
+ * the workflow removes the row from `collections/<name>/_candidates.yaml`
2272
+ * on success (caller specifies the collection via `candidateSidecar`).
2273
+ */
2274
+ readonly candidateId?: string;
2275
+ readonly candidateSidecarPath?: string;
2276
+ /**
2277
+ * Bypass the auto-promote gate. Used when a curator manually
2278
+ * approves a candidate that failed the gate (corpus-review path).
2279
+ * The emitted event still records the bypass.
2280
+ */
2281
+ readonly bypassGate?: boolean;
2282
+ }
2283
+ interface PromoteResult {
2284
+ readonly entryPath: string;
2285
+ readonly entryVersionToken: string;
2286
+ readonly chunkCount: number;
2287
+ readonly gatePassed: boolean;
2288
+ readonly bypassed: boolean;
2289
+ }
2290
+ interface PromoteContext {
2291
+ readonly fs: FsPort;
2292
+ readonly clock: ClockPort;
2293
+ readonly identity: IdentityPort;
2294
+ readonly indexer: CorpusIndexer;
2295
+ }
2296
+ declare class PromoteRejectedError extends Error {
2297
+ readonly entrySlug: string;
2298
+ readonly failures: ReadonlyArray<{
2299
+ rule: string;
2300
+ message: string;
2301
+ }>;
2302
+ constructor(entrySlug: string, failures: ReadonlyArray<{
2303
+ rule: string;
2304
+ message: string;
2305
+ }>);
2306
+ }
2307
+ /**
2308
+ * Workspace-scoped promoter. Construct once per workspace + per
2309
+ * actor (identity is captured by the injected IdentityPort).
2310
+ */
2311
+ declare class CorpusPromoter {
2312
+ private readonly ctx;
2313
+ constructor(ctx: PromoteContext);
2314
+ promote(opts: PromoteOptions): Promise<PromoteResult>;
2315
+ }
2316
+
2317
+ /**
2318
+ * Attachment-binding axes — the dimensions a selector can target.
2319
+ *
2320
+ * An axis is a named dimension of the consuming subject (an operator,
2321
+ * usually): its identity slug, the AIP-47 role it fulfils, the
2322
+ * host-assigned position label, the capabilities it carries. Assets
2323
+ * (playbook overlays, knowledge packs, skills) declare a `Selector`
2324
+ * over these axes; the host supplies `Dimensions` for the subject and
2325
+ * one matcher evaluates `selector ⊨ dimensions`.
2326
+ *
2327
+ * This package ships only the AIP-concept axes. Hosts register extra
2328
+ * axes (org units, regions, compliance regimes, …) on their own
2329
+ * registry — nothing here knows about any host's org model.
2330
+ */
2331
+
2332
+ interface AxisDefinition {
2333
+ /** Axis id — the key used in `selector:` frontmatter and in Dimensions. */
2334
+ readonly id: string;
2335
+ /** AIP that owns the axis's value vocabulary, when one does. */
2336
+ readonly aip?: number;
2337
+ /** Doc line for catalog/UI surfaces. */
2338
+ readonly description?: string;
2339
+ /**
2340
+ * Authoring-time ref check. Returns an error message, or null when
2341
+ * the ref is acceptable. Hosts inject catalog-aware validators here
2342
+ * (e.g. "role ref must be a known catalog slug").
2343
+ */
2344
+ readonly validateRef?: (ref: string) => string | null;
2345
+ /**
2346
+ * Canonicalize a ref before matching — absorbs the AIP-12 ref shapes
2347
+ * (`operator/<slug>`, `ws://operators/<slug>`, globs → `"*"`).
2348
+ */
2349
+ readonly normalizeRef?: (ref: string) => string;
2350
+ }
2351
+ /** Matches any present value on the axis. */
2352
+ declare const ANY_REF = "*";
2353
+ /**
2354
+ * Ref-shape normalizer for an axis addressed as `<singular>/<slug>` or
2355
+ * `ws://<plural>/<slug>` — the shapes AIP-12 targets historically used.
2356
+ * Globs (`<singular>/*`, `ws://<plural>/*`) collapse to `ANY_REF`.
2357
+ */
2358
+ declare function prefixedRefNormalizer(singular: string, plural: string): (ref: string) => string;
2359
+ /** The operator's own slug — AIP-9 identity. */
2360
+ declare const identityAxis: AxisDefinition;
2361
+ /** The AIP-47 catalog role (job) the operator fulfils. */
2362
+ declare const roleAxis: AxisDefinition;
2363
+ /** The host-assigned, user-settable seat label (slugified). */
2364
+ declare const positionAxis: AxisDefinition;
2365
+ /** A capability slug the subject carries. */
2366
+ declare const capabilityAxis: AxisDefinition;
2367
+ declare const WELL_KNOWN_AXES: readonly AxisDefinition[];
2368
+ type AxisRegistry = Registry<AxisDefinition>;
2369
+ /**
2370
+ * Axis registry seeded with the well-known AIP axes. Hosts pass their
2371
+ * extra axes; ids must not collide with the well-known set.
2372
+ */
2373
+ declare function createAxisRegistry(extra?: readonly AxisDefinition[]): AxisRegistry;
2374
+
2375
+ /**
2376
+ * Selector ⊨ Dimensions — the one matcher every attachment kind uses.
2377
+ *
2378
+ * A `Selector` is a small boolean expression over axis terms; the host
2379
+ * supplies the subject's `Dimensions` (axis → value(s)) and the matcher
2380
+ * decides whether the asset attaches. Scope (org / guild / workspace
2381
+ * reach) is NOT expressed here — reach is decided by which corpus
2382
+ * workspaces the host aggregates before matching ever runs.
2383
+ */
2384
+
2385
+ interface SelectorTerm {
2386
+ readonly axis: string;
2387
+ /** Refs the term accepts — OR within the list. `"*"` = any present value. */
2388
+ readonly anyOf: readonly string[];
2389
+ }
2390
+ interface Selector {
2391
+ /** Every term must match (AND). */
2392
+ readonly allOf?: readonly SelectorTerm[];
2393
+ /** At least one term must match (OR). */
2394
+ readonly anyOf?: readonly SelectorTerm[];
2395
+ }
2396
+ /**
2397
+ * The subject's axis values. A missing key means the subject has no
2398
+ * value on that axis — terms against it fail (never match-all).
2399
+ */
2400
+ type Dimensions = Readonly<Record<string, string | readonly string[] | undefined>>;
2401
+ interface MatchOptions {
2402
+ /** Axis registry supplying per-axis `normalizeRef`. Optional — without it refs match verbatim. */
2403
+ readonly axes?: AxisRegistry;
2404
+ }
2405
+ declare const EMPTY_SELECTOR: Selector;
2406
+ /** True when the selector has no terms at all. */
2407
+ declare function isEmptySelector(selector: Selector): boolean;
2408
+ /**
2409
+ * Evaluate `selector ⊨ dimensions`.
2410
+ *
2411
+ * - An EMPTY selector matches NOTHING — an asset that wants everyone
2412
+ * in scope says so with an explicit `{ axis, anyOf: ["*"] }` term.
2413
+ * (Parity with the legacy empty-`targets` behavior.)
2414
+ * - A term fails when the dimension is absent.
2415
+ * - A term matches when its normalized refs intersect the dimension's
2416
+ * values, or when it carries `"*"` and the dimension is present.
2417
+ */
2418
+ declare function matchesSelector(selector: Selector, dimensions: Dimensions, options?: MatchOptions): boolean;
2419
+
2420
+ /**
2421
+ * `selector:` frontmatter → Selector.
2422
+ *
2423
+ * Authoring shape (AIP-12 §selector):
2424
+ *
2425
+ * selector:
2426
+ * role: sales-rep # one ref
2427
+ * capability: [demo, forecast] # OR within the list
2428
+ *
2429
+ * Keys are axis ids; multiple keys AND together. The advanced
2430
+ * `allOf:`/`anyOf:` long form is accepted verbatim for selectors that
2431
+ * need OR across axes.
2432
+ *
2433
+ * Parsing never throws — malformed input returns null and the caller
2434
+ * falls back to the legacy `targets` compile. A selector that silently
2435
+ * dropped a malformed term could attach an overlay to the wrong
2436
+ * operators; null keeps the file on its legacy binding instead.
2437
+ */
2438
+
2439
+ declare function parseSelectorFrontmatter(raw: unknown): Selector | null;
2440
+
2441
+ /**
2442
+ * Legacy `targets[]` / `binds_operator` → Selector compile.
2443
+ *
2444
+ * Permanent, not transitional: PLAYBOOK.md files authored before
2445
+ * `selector:` live in real corpus workspaces and keep working forever.
2446
+ *
2447
+ * The legacy format is axis-AMBIGUOUS: a `kind: "operator"` ref (and
2448
+ * `binds_operator`) historically held either an operator slug OR a
2449
+ * role slug — the old matcher tried both. The compile preserves that
2450
+ * exactly by putting each such ref in BOTH the identity and the role
2451
+ * bucket of an `anyOf` selector. `kind: "role" | "skill" | "runtime"`
2452
+ * refs compile to their own axes (a host that supplies no `skill`
2453
+ * dimension keeps today's no-match behavior for skill targets).
2454
+ */
2455
+
2456
+ interface LegacyTarget {
2457
+ readonly kind: string;
2458
+ readonly ref: string;
2459
+ }
2460
+ declare function compileLegacyPlaybookBinding(targets: readonly LegacyTarget[], bindsOperator?: string): Selector;
2461
+
2462
+ /**
2463
+ * Attachment declarations — "this asset attaches to subjects matching
2464
+ * this selector", evaluated by the one matcher.
2465
+ *
2466
+ * A host keeps a flat list of declarations (a knowledge pack attaches to
2467
+ * a set of roles, a skill attaches to one position, …) and asks which
2468
+ * ones a given subject's `Dimensions` satisfy. This replaces the older
2469
+ * shape where bindings were grafted onto the subject (e.g. mutating a
2470
+ * role handle at boot): the declaration is data, matching is a pure
2471
+ * function, nothing is mutated.
2472
+ *
2473
+ * Asset-kind-agnostic and vendor-neutral — the `kind` is the host's
2474
+ * (`"pack" | "skill" | …`); the selector axes are AIP concepts. `band` /
2475
+ * `mode` are optional layering hints a host's mount step may honor; the
2476
+ * matcher itself ignores them.
2477
+ */
2478
+
2479
+ /** What a declaration attaches — an opaque, host-typed ref under a kind. */
2480
+ interface AttachmentAsset<Kind extends string = string> {
2481
+ readonly kind: Kind;
2482
+ readonly ref: string;
2483
+ }
2484
+ /** An asset + the selector deciding which subjects it attaches to. */
2485
+ interface AttachmentDeclaration<Kind extends string = string> {
2486
+ readonly asset: AttachmentAsset<Kind>;
2487
+ /** Subjects whose `Dimensions` satisfy this attach the asset. */
2488
+ readonly selector: Selector;
2489
+ /** Optional precedence hint for the host's mount step (see AIP-10 bands). */
2490
+ readonly band?: number;
2491
+ /** Optional composition hint — `lens` vs `constraint`. */
2492
+ readonly mode?: LayerMode;
2493
+ }
2494
+ interface MatchAttachmentsOptions {
2495
+ /** Axis registry supplying per-axis `normalizeRef` for the matcher. */
2496
+ readonly axes?: AxisRegistry;
2497
+ }
2498
+ /**
2499
+ * The subset of `declarations` whose selector matches `dimensions`. Order
2500
+ * is preserved; de-duplication of resulting refs is the caller's call
2501
+ * (the same asset may attach via several declarations).
2502
+ */
2503
+ declare function matchAttachments<Kind extends string>(declarations: readonly AttachmentDeclaration<Kind>[], dimensions: Dimensions, options?: MatchAttachmentsOptions): AttachmentDeclaration<Kind>[];
2504
+ /**
2505
+ * Matched asset refs for one kind, de-duplicated, first-seen order — the
2506
+ * flat shape most mount steps want.
2507
+ */
2508
+ declare function matchAttachmentRefs<Kind extends string>(declarations: readonly AttachmentDeclaration<Kind>[], kind: Kind, dimensions: Dimensions, options?: MatchAttachmentsOptions): string[];
2509
+
2510
+ /**
2511
+ * Playbook surface types — shared by registry, resolver, lifecycle.
2512
+ *
2513
+ * AIP-12 PLAYBOOK frontmatter is rich (targets, kind, lock_check,
2514
+ * evidence, status, supersedes, history, …). The registry parses
2515
+ * the full shape; the public types here cover the fields the
2516
+ * resolver and the lifecycle need.
2517
+ *
2518
+ * Corpus-specific extension fields (shadowTrafficPct, autoPromote,
2519
+ * shadowMetrics, archiveReason, execution) live under
2520
+ * `metadata.corpus.*` until the AIP-12 spec hoists them to first-class
2521
+ * fields; we surface them as a typed namespace on the parsed playbook
2522
+ * so callers don't dig into untyped frontmatter.
2523
+ */
2524
+
2525
+ type PlaybookStatus = "shadow" | "active" | "archived";
2526
+ type PlaybookKind = "overlay" | "block-replacement";
2527
+ type PlaybookTargetKind = "operator" | "role" | "skill" | "runtime";
2528
+ interface PlaybookTarget {
2529
+ readonly kind: PlaybookTargetKind;
2530
+ readonly ref: string;
2531
+ }
2532
+ interface PlaybookCorpusMeta {
2533
+ /** 0..1 — fraction of traffic this shadow plays on. */
2534
+ readonly shadowTrafficPct?: number;
2535
+ /** Records the win-rate vs baseline + sample size. */
2536
+ readonly shadowMetrics?: {
2537
+ readonly sampleSize?: number;
2538
+ readonly winRateVsBaseline?: number | null;
2539
+ readonly lastEvaluatedAt?: string | null;
2540
+ };
2541
+ readonly autoPromote?: {
2542
+ readonly enabled: boolean;
2543
+ readonly metric: string;
2544
+ readonly threshold: {
2545
+ gte?: number;
2546
+ lte?: number;
2547
+ };
2548
+ readonly minSampleSize: number;
2549
+ readonly cooldown?: string;
2550
+ };
2551
+ readonly archiveReason?: string | null;
2552
+ readonly execution?: "sandboxed" | "inProcess";
2553
+ readonly authoredBy?: string;
2554
+ readonly derivedFromGap?: string;
2555
+ /** Free-form forward-compat. */
2556
+ readonly [key: string]: unknown;
2557
+ }
2558
+ interface Playbook {
2559
+ /** Workspace-relative path of the .md file. */
2560
+ readonly path: string;
2561
+ readonly slug: string;
2562
+ readonly title: string;
2563
+ readonly status: PlaybookStatus;
2564
+ readonly kind: PlaybookKind;
2565
+ /** Priority for overlay ordering — higher first. Default 100. */
2566
+ readonly priority: number;
2567
+ readonly targets: readonly PlaybookTarget[];
2568
+ /** Optional convenience — narrower than targets[]. */
2569
+ readonly bindsOperator?: string;
2570
+ /**
2571
+ * The binding, always populated: parsed from `selector:` frontmatter
2572
+ * when present, otherwise compiled from the legacy `targets[]` /
2573
+ * `binds_operator` fields (see binding/legacy.ts). Matching goes
2574
+ * through this — never through `targets` directly.
2575
+ */
2576
+ readonly selector: Selector;
2577
+ /** Where `selector` came from — explicit frontmatter or legacy compile. */
2578
+ readonly selectorSource: "selector" | "legacy";
2579
+ readonly supersedes: readonly string[];
2580
+ /** Markdown body — the overlay text. */
2581
+ readonly body: string;
2582
+ /** Corpus-namespaced extension fields (metadata.corpus.*). */
2583
+ readonly corpus: PlaybookCorpusMeta;
2584
+ /** Hash of file content for optimistic-concurrency CAS writes. */
2585
+ readonly versionToken: string;
2586
+ /** The raw parsed file — exposed for the lifecycle CAS path. */
2587
+ readonly file: ParsedFile;
2588
+ }
2589
+ /** Selector for `listBy` queries. */
2590
+ interface PlaybookQuery {
2591
+ readonly status?: PlaybookStatus | readonly PlaybookStatus[];
2592
+ readonly operatorRef?: string;
2593
+ /** Match on `kind` (overlay | block-replacement). */
2594
+ readonly kind?: PlaybookKind;
2595
+ /**
2596
+ * Match playbooks bound to this slug (binds_operator OR targets[]).
2597
+ * Accepts several slugs so a caller can match an operator by more than
2598
+ * one handle it answers to — e.g. its identity slug AND the role slug it
2599
+ * fulfils. A playbook matches if it binds/targets ANY of them.
2600
+ *
2601
+ * Sugar over `dimensions: { identity: slugs, role: slugs }` — prefer
2602
+ * `dimensions` when the caller knows which axis each value belongs to.
2603
+ */
2604
+ readonly forOperatorSlug?: string | readonly string[];
2605
+ /**
2606
+ * Typed subject dimensions (axis → value(s)) evaluated against each
2607
+ * playbook's `selector`. Wins over `forOperatorSlug` when both are set.
2608
+ */
2609
+ readonly dimensions?: Dimensions;
2610
+ }
2611
+
2612
+ /**
2613
+ * PlaybookRegistry — load + index every AIP-12 PLAYBOOK.md in the
2614
+ * workspace's `playbooks/` directory.
2615
+ *
2616
+ * The registry is a thin read layer over a `CorpusWorkspaceSnapshot`:
2617
+ * - parses raw frontmatter into the typed `Playbook` shape,
2618
+ * - exposes O(1) lookups by slug,
2619
+ * - filters by status / kind / target operator on listBy().
2620
+ *
2621
+ * No FsPort dependency — the snapshot already carries the parsed
2622
+ * files. The lifecycle layer is the one that writes; this registry
2623
+ * is pure read.
2624
+ */
2625
+
2626
+ interface PlaybookRegistryOptions {
2627
+ readonly snapshot: CorpusWorkspaceSnapshot;
2628
+ }
2629
+ declare class PlaybookRegistry {
2630
+ private readonly bySlug;
2631
+ private readonly all;
2632
+ constructor(opts: PlaybookRegistryOptions);
2633
+ list(): readonly Playbook[];
2634
+ bySlugOrNull(slug: string): Playbook | null;
2635
+ listBy(query?: PlaybookQuery): readonly Playbook[];
2636
+ }
2637
+
2638
+ /**
2639
+ * OperatorOverlayResolver — given an operator + conversation context,
2640
+ * returns the playbook overlays to splice into the operator's prompt.
2641
+ *
2642
+ * Decision rules:
2643
+ * 1. `active` playbooks targeting the operator are ALWAYS included.
2644
+ * 2. `shadow` playbooks fire on a deterministic-hash subset of
2645
+ * conversations (`shadowTrafficPct`, default 0.10) — same
2646
+ * conversation always lands the same arm so an eval batch is
2647
+ * reproducible.
2648
+ * 3. `archived` playbooks are never included (per the AIP-12
2649
+ * lifecycle terminal state).
2650
+ * 4. Multiple active playbooks for the same operator stack by
2651
+ * `priority` (higher first). `kind: block-replacement` overrides
2652
+ * the operator's default block for the named trait;
2653
+ * `kind: overlay` appends to the prompt.
2654
+ *
2655
+ * Pure — no I/O. Consumes a `PlaybookRegistry` (read-only) + the
2656
+ * resolution context. Lifecycle writes go through the lifecycle
2657
+ * module.
2658
+ */
2659
+
2660
+ interface ResolveContext {
2661
+ /**
2662
+ * Slug of the operator the overlays apply to. Matches against
2663
+ * `binds_operator` AND `targets[].ref` (with operator/* glob).
2664
+ */
2665
+ readonly operatorSlug: string;
2666
+ /**
2667
+ * The role (job) slug the operator fulfils, when distinct from its
2668
+ * identity slug. Overlays bind by role — "the recruiting SOP for any
2669
+ * recruiter" — so a playbook targeting this role matches every operator
2670
+ * fulfilling it, not just one. Matched in addition to `operatorSlug`.
2671
+ *
2672
+ * @deprecated Supply `dimensions` instead — it carries role, position,
2673
+ * capability, and any host-registered axis in one typed map.
2674
+ */
2675
+ readonly roleSlug?: string;
2676
+ /**
2677
+ * Typed subject dimensions (axis → value(s)) evaluated against each
2678
+ * playbook's selector. When present, wins over the
2679
+ * `operatorSlug`/`roleSlug` slug-pair matching.
2680
+ */
2681
+ readonly dimensions?: Dimensions;
2682
+ /**
2683
+ * Conversation id (or another stable bucketing token). Used to
2684
+ * deterministically place a conversation into shadow-traffic
2685
+ * buckets, so the same conversation consistently sees (or doesn't
2686
+ * see) a given shadow overlay.
2687
+ */
2688
+ readonly conversationId?: string;
2689
+ }
2690
+ interface ResolvedOverlay {
2691
+ readonly playbookSlug: string;
2692
+ readonly status: PlaybookStatus;
2693
+ readonly kind: Playbook["kind"];
2694
+ readonly priority: number;
2695
+ /** The overlay body. */
2696
+ readonly body: string;
2697
+ /** Audit attribution. */
2698
+ readonly playbookPath: string;
2699
+ /** True iff this overlay came from a shadow playbook + was sampled in. */
2700
+ readonly shadowSampled: boolean;
2701
+ }
2702
+ interface ResolveResult {
2703
+ readonly operatorSlug: string;
2704
+ readonly overlays: readonly ResolvedOverlay[];
2705
+ }
2706
+ declare class OperatorOverlayResolver {
2707
+ private readonly registry;
2708
+ constructor(registry: PlaybookRegistry);
2709
+ resolve(ctx: ResolveContext): ResolveResult;
2710
+ }
2711
+ /**
2712
+ * Render-ready combination of overlays for a prompt. Returns the
2713
+ * concatenated overlay bodies (priority-ordered, separated by
2714
+ * `\n\n---\n\n`). `block-replacement` overlays are listed in the
2715
+ * `replacements` field separately — the host's prompt assembler
2716
+ * decides how to merge those into the operator's named blocks.
2717
+ */
2718
+ interface RenderedOverlays {
2719
+ readonly appendBlock: string;
2720
+ readonly replacements: ReadonlyArray<{
2721
+ readonly playbookSlug: string;
2722
+ readonly body: string;
2723
+ }>;
2724
+ }
2725
+ declare function renderOverlays(result: ResolveResult): RenderedOverlays;
2726
+
2727
+ /**
2728
+ * PlaybookLifecycle — activate / archive / supersede AIP-12
2729
+ * playbooks, atomically.
2730
+ *
2731
+ * State machine (per the AIP-12 spec):
2732
+ * shadow → active | archived
2733
+ * active → archived
2734
+ * archived = terminal
2735
+ *
2736
+ * activate(slug):
2737
+ * 1. Read current playbook (CAS via versionToken).
2738
+ * 2. For each slug in playbook.supersedes that's currently active:
2739
+ * transition that slug to `archived` first.
2740
+ * 3. Update the target playbook's status → "active", clear
2741
+ * shadowMetrics.lastEvaluatedAt to mark the activation moment.
2742
+ * 4. Atomic file write (CAS).
2743
+ * 5. Emit `playbook.activated` event.
2744
+ *
2745
+ * archive(slug, reason):
2746
+ * 1. CAS read.
2747
+ * 2. Set status → "archived", metadata.corpus.archiveReason = reason.
2748
+ * 3. Atomic write.
2749
+ * 4. Emit `playbook.archived`.
2750
+ *
2751
+ * Multi-file transaction guarded by FsPort.lock so a concurrent
2752
+ * activate can't half-supersede.
2753
+ */
2754
+
2755
+ declare class PlaybookNotFoundError extends Error {
2756
+ readonly slug: string;
2757
+ constructor(slug: string);
2758
+ }
2759
+ declare class IllegalPlaybookTransitionError extends Error {
2760
+ readonly slug: string;
2761
+ readonly from: PlaybookStatus;
2762
+ readonly to: PlaybookStatus;
2763
+ constructor(slug: string, from: PlaybookStatus, to: PlaybookStatus);
2764
+ }
2765
+ interface PlaybookLifecycleOptions {
2766
+ readonly fs: FsPort;
2767
+ readonly clock: ClockPort;
2768
+ readonly identity: IdentityPort;
2769
+ readonly workspacePath: string;
2770
+ }
2771
+ interface ActivateResult {
2772
+ readonly slug: string;
2773
+ readonly previousStatus: PlaybookStatus;
2774
+ readonly versionToken: string;
2775
+ readonly supersededSlugs: readonly string[];
2776
+ }
2777
+ interface ArchiveResult {
2778
+ readonly slug: string;
2779
+ readonly previousStatus: PlaybookStatus;
2780
+ readonly versionToken: string;
2781
+ }
2782
+ declare class PlaybookLifecycle {
2783
+ private readonly opts;
2784
+ constructor(opts: PlaybookLifecycleOptions);
2785
+ /**
2786
+ * Transition `slug` from shadow → active. Cascades through
2787
+ * `supersedes[]`: any currently-active playbook listed there is
2788
+ * archived (with `archiveReason: superseded-by-<slug>`) in the
2789
+ * same lock-guarded transaction.
2790
+ */
2791
+ activate(slug: string): Promise<ActivateResult>;
2792
+ /**
2793
+ * Transition `slug` → archived. Skip-no-op if already archived.
2794
+ */
2795
+ archive(slug: string, reason: string): Promise<ArchiveResult>;
2796
+ private loadOne;
2797
+ private loadRegistry;
2798
+ private writeWith;
2799
+ /** Build a transition attestation from current clock + given identity. */
2800
+ private attestation;
2801
+ private emitter;
2802
+ }
2803
+
2804
+ /**
2805
+ * PlaybookEvaluator — runs shadow vs baseline eval batches on AIP-12
2806
+ * playbooks, records winRateVsBaseline + sample size, decides
2807
+ * promotion readiness against `metadata.corpus.autoPromote`.
2808
+ *
2809
+ * Inputs (per eval-case the host hands us):
2810
+ * - prompt: the test prompt
2811
+ * - shadow.response: agent output WITH the playbook overlay applied
2812
+ * - baseline.response: agent output WITHOUT the overlay
2813
+ * - rubric: how to score both arms
2814
+ *
2815
+ * Output:
2816
+ * - winRateVsBaseline = fraction of cases where shadow.score >= baseline.score
2817
+ * - sampleSize = n
2818
+ * - readyForActivation = winRate >= threshold && n >= minSampleSize
2819
+ *
2820
+ * The evaluator does NOT itself activate the playbook — it writes
2821
+ * shadowMetrics back to the PLAYBOOK.md (via the writer) and lets
2822
+ * the curator's `activate` workflow decide. That keeps the
2823
+ * audit chain clean: evaluator measures, curator decides.
2824
+ *
2825
+ * Pure logic. Backing IEvaluator + writer/emitter come in as ports.
2826
+ */
2827
+
2828
+ interface EvalCase {
2829
+ /** Optional stable id — used in event payloads + per-case audit. */
2830
+ readonly id?: string;
2831
+ readonly prompt: string;
2832
+ /** Agent output WITH the playbook overlay applied. */
2833
+ readonly shadowResponse: string;
2834
+ /** Agent output WITHOUT the overlay (baseline). */
2835
+ readonly baselineResponse: string;
2836
+ }
2837
+ interface PlaybookBatchOptions {
2838
+ readonly rubric: EvalRubricPort;
2839
+ readonly cases: readonly EvalCase[];
2840
+ /** Forwarded to evaluator + recorded in event payloads. */
2841
+ readonly contextOverrides?: Partial<EvalContextPort>;
2842
+ }
2843
+ interface PlaybookBatchResult {
2844
+ readonly playbookSlug: string;
2845
+ readonly sampleSize: number;
2846
+ readonly winRateVsBaseline: number;
2847
+ readonly shadowAvgScore: number;
2848
+ readonly baselineAvgScore: number;
2849
+ readonly readyForActivation: boolean;
2850
+ /** Per-case details — useful for the curator UI. */
2851
+ readonly perCase: readonly {
2852
+ readonly id?: string;
2853
+ readonly shadowScore: number;
2854
+ readonly baselineScore: number;
2855
+ readonly winnerArm: "shadow" | "baseline" | "tie";
2856
+ }[];
2857
+ }
2858
+ interface PlaybookEvaluatorOptions {
2859
+ readonly fs: FsPort;
2860
+ readonly clock: ClockPort;
2861
+ readonly identity: IdentityPort;
2862
+ readonly workspacePath: string;
2863
+ readonly evaluator: EvaluatorPort;
2864
+ }
2865
+ declare class PlaybookEvaluator {
2866
+ private readonly opts;
2867
+ constructor(opts: PlaybookEvaluatorOptions);
2868
+ /**
2869
+ * Run an eval batch against a single playbook. For each case:
2870
+ * 1. Score shadow.response against rubric → shadowScore
2871
+ * 2. Score baseline.response against rubric → baselineScore
2872
+ * 3. shadow "wins" if shadowScore >= baselineScore
2873
+ * Aggregate: winRateVsBaseline = wins / n.
2874
+ *
2875
+ * On completion: writes shadowMetrics back to the PLAYBOOK.md (CAS),
2876
+ * emits a `corpus.playbook.evaluated` event (held outside the formal
2877
+ * AIP-41 taxonomy until the spec adopts it).
2878
+ */
2879
+ runBatch(playbookSlug: string, batch: PlaybookBatchOptions): Promise<PlaybookBatchResult>;
2880
+ private loadPlaybook;
2881
+ private persistMetrics;
2882
+ private appendCustomEvent;
2883
+ }
2884
+
2885
+ /**
2886
+ * @agentproto/corpus — composition of AIP-10/12/18/9/15/41.
2887
+ *
2888
+ * Spec: https://agentproto.sh/docs/corpus
2889
+ */
2890
+ declare const SPEC_NAME: "agentcorpus/v1";
2891
+ declare const SPEC_VERSION: "0.1.0-alpha";
2892
+
2893
+ export { ANY_REF, type AccessCaller, type AccessClassification, type AccessContext, type AccessDecision, type AccessModesMap, type ActivateResult, type AggregateOptions, type AggregateResult, type AipSchemaBundle, type AipSchemaKey, type ArchiveResult, type AttachmentAsset, type AttachmentDeclaration, type Attestation, type AttestationKind, type AutoPromoteConfig, type AutoPromoteRequirements, type AxisDefinition, type AxisRegistry, type BatchReport, type BuildOverlayOptions, type CalibrationOptions, type CandidateForGate, type CandidateRow, type CandidateStatus, CandidatesSidecar, type CandidatesSidecarOptions, type Capability, type CapabilityCaller, type CapabilityDecision, type CapabilityRule, type ChunkerOptions, ClaudeDistiller, type ClaudeDistillerOptions, ClockPort, type ConversationDoc, ConversationImporter, type ConversationImporterOptions, type ConversationSourcePort, type ConversationThreadRef, type ConversationTurn, type ConversationWindowSource, type CorpusAccessSpec, type CorpusEntryQuery, type CorpusEvent, CorpusEventEmitter, type CorpusEventEmitterOptions, type CorpusEventKind, type CorpusImporter, CorpusIndexer, type CorpusIndexerOptions, CorpusLinter, type CorpusLinterOptions, type CorpusPreset, type CorpusPresetBootstrapContext, CorpusPromoter, CorpusValidator, type CorpusValidatorOptions, CorpusVersionConflictError, CorpusWorkspaceReader, type CorpusWorkspaceReaderOptions, type CorpusWorkspaceSnapshot, CorpusWorkspaceWriter, type CorpusWorkspaceWriterOptions, type CustomLintRunner, DEFAULT_TRANSITIONS, type Dimensions, type DistillBinding, type DistillDescriptor, type DistillInput, type DistillPort, type DistillRegistry, type DistillReport, type DistillRunReport, DistillRunner, type DistillRunnerOptions, type DistillScope, type DistillSource, type DistillTarget, type DistilledItem, EMPTY_SELECTOR, type EntryLayout, type EvalCase, EvalContextPort, EvalRubricPort, EvaluatorPort, FetcherPort, type FileKind, FsLockHandle, FsPort, FsStat, type GateFailure, type GateResult, IdentityPort, IllegalPlaybookTransitionError, IllegalTransitionError, type ImportedSource, ImporterRunner, type ImporterRunnerOptions, type ImporterTarget, type IndexReport, type KbListLike, type KbMigrationConfig, KbMigrationImporter, type KbSourceLike, type LanguageFilter, type LayerMode, type LayerProvider, type LayerRef, type LayerShadow, type LintIssue, type LintReport, LocalFilesImporter, type LocalFilesImporterOptions, type MarkdownDoc, type MatchAttachmentsOptions, type MatchOptions, MemFs, OperatorOverlayResolver, OverlayFs, type OverlayFsOptions, type ParsedFile, type Playbook, type PlaybookBatchOptions, type PlaybookBatchResult, type PlaybookCorpusMeta, PlaybookEvaluator, type PlaybookEvaluatorOptions, type PlaybookKind, PlaybookLifecycle, type PlaybookLifecycleOptions, PlaybookNotFoundError, type PlaybookQuery, PlaybookRegistry, type PlaybookRegistryOptions, type PlaybookStatus, type PlaybookTarget, type PlaybookTargetKind, type PromoteContext, type PromoteOptions, PromoteRejectedError, type PromoteResult, type PushChunksInput, REFINED_KIND_SCHEMA, ReadOnlyFs, type RefinedKind, type RenderedOverlays, type ResolutionContext, type ResolveContext, type ResolveKnowledgeOptions, type ResolveLanguageFilterInput, type ResolveResult, type ResolvedEntry, type ResolvedOverlay, type ReviewerCalibration, type ReviewerScore, ReviewerTrackRecord, type ReviewerTrackRecordOptions, SPEC_NAME, SPEC_VERSION, type Selector, type SelectorTerm, SidecarDuplicateError, SidecarNotFoundError, type SinkItem, type SinkPort, type SinkPushResult, type SlugifyOptions, type SourceRef, type StackEntry, type StackRefPartition, type StackResolution, StackResolver, type StackSkip, type SyncReport, SyncRunner, type SyncRunnerOptions, type TrackRecordEntry, type TransitionCheck, type ValidationIssue, type ValidationResult, WELL_KNOWN_AXES, WHITEOUT_SUFFIX, WebImporter, type WebImporterOptions, type WriterChunk, type WriterPort, 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 };