@agentproto/corpus 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/chunk-KYX2DTEH.mjs +14 -0
- package/dist/chunk-KYX2DTEH.mjs.map +1 -0
- package/dist/chunk-UPX26MYH.mjs +104 -0
- package/dist/chunk-UPX26MYH.mjs.map +1 -0
- package/dist/index.d.ts +2893 -0
- package/dist/index.mjs +3380 -0
- package/dist/index.mjs.map +1 -0
- package/dist/ports/index.d.ts +205 -0
- package/dist/ports/index.mjs +3 -0
- package/dist/ports/index.mjs.map +1 -0
- package/dist/sidecar-TTWQD5R3.mjs +3 -0
- package/dist/sidecar-TTWQD5R3.mjs.map +1 -0
- package/package.json +80 -0
package/dist/index.d.ts
ADDED
|
@@ -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 };
|