@tangle-network/agent-knowledge 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +256 -74
  2. package/dist/benchmarks/index.d.ts +5 -0
  3. package/dist/benchmarks/index.js +52 -0
  4. package/dist/chunk-DQ3PDMDP.js +115 -0
  5. package/dist/chunk-DQ3PDMDP.js.map +1 -0
  6. package/dist/chunk-ULXZI235.js +1855 -0
  7. package/dist/chunk-ULXZI235.js.map +1 -0
  8. package/dist/{chunk-U6KQ4FS3.js → chunk-VN2OGUUP.js} +306 -40
  9. package/dist/chunk-VN2OGUUP.js.map +1 -0
  10. package/dist/{chunk-WWY5FTKQ.js → chunk-W6VWYTNH.js} +10 -115
  11. package/dist/chunk-W6VWYTNH.js.map +1 -0
  12. package/dist/chunk-XVU5FFQA.js +49 -0
  13. package/dist/chunk-XVU5FFQA.js.map +1 -0
  14. package/dist/cli.js +4 -2
  15. package/dist/cli.js.map +1 -1
  16. package/dist/index-Cj5jRXOz.d.ts +426 -0
  17. package/dist/index.d.ts +2225 -1058
  18. package/dist/index.js +4463 -1275
  19. package/dist/index.js.map +1 -1
  20. package/dist/memory/index.d.ts +5 -70
  21. package/dist/memory/index.js +20 -4
  22. package/dist/{types-BEDGlXB-.d.ts → types-C17sAROL.d.ts} +1 -1
  23. package/dist/types-CjqPcTTK.d.ts +319 -0
  24. package/dist/viz/index.d.ts +1 -1
  25. package/docs/architecture.md +7 -5
  26. package/docs/eval/investment-material-facts.md +252 -0
  27. package/docs/eval/rag-eval-roadmap.md +119 -0
  28. package/docs/results/adaptive.md +120 -0
  29. package/docs/results/claim-grounding.md +97 -0
  30. package/docs/results/cost-quality.md +27 -0
  31. package/docs/results/investment-thesis.md +306 -0
  32. package/docs/results/research-driving.md +265 -0
  33. package/docs/two-agent-research-ab.md +539 -0
  34. package/package.json +8 -9
  35. package/dist/chunk-MU5CEOGE.js +0 -387
  36. package/dist/chunk-MU5CEOGE.js.map +0 -1
  37. package/dist/chunk-U6KQ4FS3.js.map +0 -1
  38. package/dist/chunk-WWY5FTKQ.js.map +0 -1
  39. package/dist/profiles/index.d.ts +0 -194
  40. package/dist/profiles/index.js +0 -11
  41. package/docs/recursive-research-leaf.md +0 -86
  42. /package/dist/{profiles → benchmarks}/index.js.map +0 -0
package/dist/index.d.ts CHANGED
@@ -1,11 +1,15 @@
1
- import { S as SourceRecord, c as KnowledgeIndex, d as KnowledgeSearchResult, e as KnowledgeEventType, f as KnowledgeEvent, g as KnowledgePage, b as KnowledgeGraph, h as KnowledgeLintFinding, i as KnowledgeWriteBlock, j as KnowledgeClaim, k as KnowledgeRelease, l as SourceRegistry, m as KnowledgeWriteParseResult } from './types-BEDGlXB-.js';
2
- export { C as ClaimRef, n as KnowledgeBaseCandidate, K as KnowledgeGraphEdge, a as KnowledgeGraphNode, o as KnowledgeId, p as KnowledgePolicy, q as KnowledgeRelation, r as KnowledgeUnit, s as SourceAnchor } from './types-BEDGlXB-.js';
1
+ import { S as SourceRecord, c as KnowledgeIndex, d as KnowledgeSearchResult, e as SourceRegistry, f as KnowledgeEvent, g as KnowledgeEventType, h as KnowledgePage, b as KnowledgeGraph, i as KnowledgeLintFinding, j as KnowledgeWriteBlock, k as KnowledgeClaim, l as KnowledgeRelease, m as KnowledgeWriteParseResult } from './types-C17sAROL.js';
2
+ export { C as ClaimRef, n as KnowledgeBaseCandidate, K as KnowledgeGraphEdge, a as KnowledgeGraphNode, o as KnowledgeId, p as KnowledgePolicy, q as KnowledgeRelation, r as KnowledgeUnit, s as SourceAnchor } from './types-C17sAROL.js';
3
+ import { KnowledgeRequirementCategory, KnowledgeAcquisitionMode, KnowledgeImportance, KnowledgeFreshness, KnowledgeSensitivity, KnowledgeRequirement, KnowledgeBundle, KnowledgeReadinessReport, UserQuestion, DataAcquisitionPlan, ControlRuntimeConfig, ControlEvalResult, AnalystSeverity, AnalystFinding, RunRecord, ReleaseTraceEvidence, GateDecision, ReleaseConfidenceScorecard } from '@tangle-network/agent-eval';
4
+ import { R as RunRetrievalImprovementLoopResult, a as RunRetrievalImprovementLoopOptions, b as RetrievalConfig } from './index-Cj5jRXOz.js';
5
+ export { B as BuildRetrievalBenchmarkCasesFromQrelsOptions, c as BuildRetrievalEvalDispatchOptions, d as BuildRetrievalParameterCandidatesOptions, I as INDUSTRY_MEMORY_BENCHMARKS, e as INDUSTRY_RAG_BENCHMARKS, K as KnowledgeAnswerBenchmarkCase, f as KnowledgeAnswerBenchmarkTaskKind, g as KnowledgeBenchmarkArtifact, h as KnowledgeBenchmarkCase, i as KnowledgeBenchmarkCaseBase, j as KnowledgeBenchmarkDistribution, k as KnowledgeBenchmarkEvaluation, l as KnowledgeBenchmarkFamily, m as KnowledgeBenchmarkReport, n as KnowledgeBenchmarkResponder, o as KnowledgeBenchmarkScenario, p as KnowledgeBenchmarkSliceSummary, q as KnowledgeBenchmarkSource, r as KnowledgeBenchmarkSpec, s as KnowledgeBenchmarkSplit, t as KnowledgeBenchmarkTaskKind, u as KnowledgeClaimMatcher, v as KnowledgeMemoryBenchmarkCase, w as KnowledgeMemoryBenchmarkTaskKind, x as KnowledgeMemoryEvent, y as KnowledgeMemoryFactMatcher, z as KnowledgeRetrievalBenchmarkCase, A as KnowledgeRetrievalBenchmarkQrel, C as KnowledgeRetrievalBenchmarkQuery, M as MemoryAdapterBenchmarkCandidate, D as MemoryAdapterBenchmarkRankingRow, E as RetrievalEvalArtifact, F as RetrievalEvalRetriever, G as RetrievalEvalRetrieverInput, H as RetrievalEvalRetrieverResult, J as RetrievalEvalScenario, L as RetrievalGoldTarget, N as RetrievalMetricSummary, O as RetrievalMetricWeights, P as RetrievalParameterSearchSpace, Q as RetrievalParameterSweepProposerOptions, S as RetrievalRecallJudgeOptions, T as RetrievedKnowledgeHit, U as RetrievedSourceSpan, V as RunKnowledgeBenchmarkSuiteOptions, W as RunKnowledgeBenchmarkSuiteResult, X as RunMemoryAdapterBenchmarkOptions, Y as RunMemoryAdapterBenchmarkResult, Z as buildFirstPartyMemoryLifecycleBenchmarkCases, _ as buildIndustryMemoryBenchmarkSmokeCases, $ as buildIndustryRagBenchmarkSmokeCases, a0 as buildKnowledgeBenchmarkScenarios, a1 as buildRetrievalBenchmarkCasesFromQrels, a2 as buildRetrievalEvalDispatch, a3 as buildRetrievalParameterCandidates, a4 as createInMemoryBenchmarkAdapter, a5 as createMemoryAdapterBenchmarkResponder, a6 as createNoopMemoryBenchmarkAdapter, a7 as isKnowledgeMemoryBenchmarkCase, a8 as knowledgeBenchmarkJudge, a9 as parseKnowledgeBenchmarkJsonl, aa as parseKnowledgeBenchmarkQrels, ab as renderKnowledgeBenchmarkReportMarkdown, ac as respondToIndustryMemoryBenchmarkSmokeCase, ad as respondToIndustryRagBenchmarkSmokeCase, ae as retrievalConfigFromSurface, af as retrievalConfigSurface, ag as retrievalParameterSweepProposer, ah as retrievalRecallJudge, ai as runKnowledgeBenchmarkSuite, aj as runMemoryAdapterBenchmark, ak as runRetrievalImprovementLoop, al as scoreKnowledgeBenchmarkArtifact, am as scoreMemoryBenchmarkArtifact, an as scoreRetrievalArtifact, ao as summarizeKnowledgeBenchmarkCampaign } from './index-Cj5jRXOz.js';
3
6
  import { KnowledgeFragment } from './sources/index.js';
4
7
  export { CornellLiiSelector, CornellLiiSourceOptions, FetchOpts, FragmentProvenance, IRS_DIMENSION_HINTS, IrsPublicationsSourceOptions, KnowledgeSource, MAX_RESPONSE_BYTES, MIN_REQUEST_GAP_MS, POLITE_USER_AGENT, PoliteFetchOptions, PoliteFetchResult, StateSosEntity, StateSosSourceConfig, __resetHttpThrottle, createCornellLiiSource, createIrsPublicationsSource, createStateSosSource, extractLinks, firstMatch, htmlToText, innerHtmlById, looksLikeBlockPage, politeFetch } from './sources/index.js';
5
- import { KnowledgeRequirementCategory, KnowledgeAcquisitionMode, KnowledgeImportance, KnowledgeFreshness, KnowledgeSensitivity, KnowledgeRequirement, KnowledgeBundle, KnowledgeReadinessReport, UserQuestion, DataAcquisitionPlan, AnalystSeverity, AnalystFinding, RunRecord, ReleaseTraceEvidence, GateDecision, ReleaseConfidenceScorecard, ControlRuntimeConfig, ControlEvalResult } from '@tangle-network/agent-eval';
6
- export { AgentMemoryAdapter, AgentMemoryContext, AgentMemoryHit, AgentMemoryHitSchema, AgentMemoryKind, AgentMemoryKindSchema, AgentMemoryScope, AgentMemoryScopeSchema, AgentMemorySearchOptions, AgentMemoryWriteInput, AgentMemoryWriteInputSchema, AgentMemoryWriteResult, Neo4jAgentMemoryAdapterOptions, createNeo4jAgentMemoryAdapter, defaultGetMemoryContext, memoryHitToSourceRecord, memoryWriteResultToSourceRecord, renderMemoryContext } from './memory/index.js';
7
- import { Budget, ExecutorConfig, SuperviseOptions, DeliverableSpec, SupervisedResult } from '@tangle-network/agent-runtime/loops';
8
+ import { JsonValue, Scenario, JudgeConfig } from '@tangle-network/agent-eval/campaign';
9
+ export { AgentMemoryHitSchema, AgentMemoryKindSchema, AgentMemoryScopeSchema, AgentMemoryWriteInputSchema, Neo4jAgentMemoryAdapterOptions, createNeo4jAgentMemoryAdapter, defaultGetMemoryContext, memoryHitToSourceRecord, memoryWriteResultToSourceRecord, renderMemoryContext } from './memory/index.js';
10
+ export { A as AgentMemoryAdapter, b as AgentMemoryContext, c as AgentMemoryHit, d as AgentMemoryKind, a as AgentMemoryScope, e as AgentMemorySearchOptions, f as AgentMemoryWriteInput, g as AgentMemoryWriteResult, R as RetrievalHoldoutBypassReason, h as RetrievalHoldoutCallContext, i as RetrievalHoldoutConfig, j as RetrievalHoldoutEligibleItem, k as RetrievalHoldoutEvent, l as RetrievalHoldoutOffPolicyOptions, m as RetrievalHoldoutOffPolicyResult, n as RetrievalHoldoutResult, o as RetrievalHoldoutSessionState, p as RetrievalHoldoutSessionSummary, q as applyRetrievalHoldout, r as applySessionStickyRetrievalHoldout, s as deterministicRng, t as emitRetrievalHoldoutBypass, u as resetRetrievalHoldoutRegistry, v as retrievalHoldoutConfigHash, w as toOffPolicyTrajectory } from './types-CjqPcTTK.js';
8
11
  import { z } from 'zod';
12
+ import '@tangle-network/agent-eval/rl';
9
13
 
10
14
  interface SourceAdapterInput {
11
15
  uri: string;
@@ -28,140 +32,6 @@ interface SourceAdapter {
28
32
  declare const textSourceAdapter: SourceAdapter;
29
33
  declare function mediaTypeFor(uri: string): string;
30
34
 
31
- /**
32
- * Change detection across snapshots of one source's fragments.
33
- *
34
- * The output drives the continuous-ingestion loop: each `KnowledgeChange`
35
- * carries the eval dimensions affected (`affectedDimensions`), which an
36
- * agent-eval campaign scheduler consumes to decide which campaigns to
37
- * re-run. Three change kinds:
38
- *
39
- * - `added` — fragment id appears in `next` but not `prev`.
40
- * - `removed` — fragment id appears in `prev` but not `next`. Typical
41
- * trigger: an authority retires a Wex slug, or a state SOS reorganises
42
- * its forms catalogue.
43
- * - `modified` — fragment id appears in both, body hash differs. This
44
- * is the dominant change kind in practice — the Ryan-LLC v. FTC
45
- * vacatur case manifests as a `modified` on the Wex non-compete
46
- * fragment, NOT as a removed one.
47
- *
48
- * Unverifiable fragments are filtered out before diffing — comparing a
49
- * captcha-blocked snapshot against a real one would falsely fire every
50
- * fragment as removed. The caller can inspect the raw lists if they want
51
- * to surface block-page failures.
52
- *
53
- * Within-snapshot duplicate ids are an upstream bug; this function picks
54
- * the LAST one and emits a diagnostic via the `warnings` field.
55
- *
56
- * @stable
57
- */
58
- type KnowledgeChangeKind = 'added' | 'removed' | 'modified';
59
- interface KnowledgeChange {
60
- /** Source-scoped id (matches `KnowledgeFragment.id`). */
61
- fragmentId: string;
62
- kind: KnowledgeChangeKind;
63
- /**
64
- * For `added`: full body of the new fragment.
65
- * For `removed`: full body of the prior fragment.
66
- * For `modified`: unified-diff-style payload `{ before, after }` body strings.
67
- */
68
- diff?: {
69
- before?: string;
70
- after?: string;
71
- };
72
- /**
73
- * Eval dimensions to re-score. Computed as the union of both fragments'
74
- * `dimensionHints`. The eval cron treats this as a set of campaign tags.
75
- */
76
- affectedDimensions: string[];
77
- /** URL of the affected authority page (from whichever side has it). */
78
- url?: string;
79
- /**
80
- * Source-attested change time. For `modified`, takes the NEXT fragment's
81
- * `sourceUpdatedAt`. For `removed`, takes the PRIOR fragment's
82
- * `sourceUpdatedAt`. For `added`, takes the NEXT fragment's
83
- * `sourceUpdatedAt`. Consumers index changes by this date.
84
- */
85
- detectedAt: string;
86
- }
87
- interface DetectChangesResult {
88
- changes: KnowledgeChange[];
89
- /** Counts by kind — handy for dashboards. */
90
- summary: {
91
- added: number;
92
- removed: number;
93
- modified: number;
94
- };
95
- /** Non-fatal diagnostics (duplicate ids, dropped unverifiable fragments). */
96
- warnings: string[];
97
- }
98
- interface DetectChangesOptions {
99
- /**
100
- * When true (default), unverifiable fragments are dropped from both
101
- * sides before comparison. Set false ONLY when debugging block-page
102
- * issues — comparing against unverifiable content emits false
103
- * `removed`/`modified` changes.
104
- */
105
- skipUnverifiable?: boolean;
106
- /**
107
- * When provided, only changes whose `affectedDimensions` intersect this
108
- * set are returned. Useful for cron loops that schedule per-dimension
109
- * eval campaigns and only care about a subset.
110
- */
111
- filterDimensions?: string[];
112
- }
113
- declare function detectChanges(prev: KnowledgeFragment[], next: KnowledgeFragment[], options?: DetectChangesOptions): DetectChangesResult;
114
-
115
- interface ChunkingOptions {
116
- targetChars: number;
117
- maxChars: number;
118
- minChars: number;
119
- overlapChars: number;
120
- }
121
- interface KnowledgeChunk {
122
- index: number;
123
- text: string;
124
- headingPath: string;
125
- charStart: number;
126
- charEnd: number;
127
- oversized: boolean;
128
- }
129
- declare function chunkMarkdown(content: string, options?: Partial<ChunkingOptions>): KnowledgeChunk[];
130
- declare function stripFrontmatter(content: string): {
131
- body: string;
132
- bodyOffset: number;
133
- };
134
-
135
- interface DiscoveryTask {
136
- id: string;
137
- goal: string;
138
- query?: string;
139
- sourceHints?: string[];
140
- metadata?: Record<string, unknown>;
141
- }
142
- interface DiscoveryResult {
143
- taskId: string;
144
- summary: string;
145
- sourceUris?: string[];
146
- claims?: Array<{
147
- text: string;
148
- sourceUri?: string;
149
- confidence?: number;
150
- }>;
151
- followUpTasks?: DiscoveryTask[];
152
- metadata?: Record<string, unknown>;
153
- }
154
- interface KnowledgeDiscoveryWorker {
155
- run(task: DiscoveryTask, signal?: AbortSignal): Promise<DiscoveryResult> | DiscoveryResult;
156
- }
157
- interface KnowledgeDiscoveryDispatcher {
158
- dispatch(tasks: DiscoveryTask[], options?: {
159
- concurrency?: number;
160
- signal?: AbortSignal;
161
- }): Promise<DiscoveryResult[]>;
162
- }
163
- declare function createLocalDiscoveryDispatcher(worker: KnowledgeDiscoveryWorker): KnowledgeDiscoveryDispatcher;
164
-
165
35
  interface KnowledgeReadinessSpec {
166
36
  id: string;
167
37
  description: string;
@@ -252,1059 +122,2356 @@ interface EvalKnowledgeBundleBuildResult {
252
122
  }
253
123
  declare function buildEvalKnowledgeBundle(options: BuildEvalKnowledgeBundleOptions): EvalKnowledgeBundleBuildResult;
254
124
 
255
- interface KnowledgeEventQuery {
256
- type?: KnowledgeEventType;
257
- target?: string;
258
- limit?: number;
125
+ interface AddSourceOptions {
126
+ copyIntoRaw?: boolean;
127
+ adapters?: SourceAdapter[];
128
+ now?: () => Date;
259
129
  }
260
- declare function createKnowledgeEvent(input: {
261
- type: KnowledgeEventType;
262
- actor?: string;
263
- target?: string;
130
+ interface AddSourceTextInput {
131
+ uri: string;
132
+ text: string;
133
+ title?: string;
134
+ mediaType?: string;
135
+ validUntil?: string;
136
+ lastVerifiedAt?: string;
264
137
  metadata?: Record<string, unknown>;
265
- now?: () => Date;
266
- }): KnowledgeEvent;
138
+ }
139
+ declare function loadSourceRegistry(root: string): Promise<SourceRegistry>;
140
+ declare function writeSourceRegistry(root: string, registry: SourceRegistry): Promise<void>;
141
+ declare function addSourcePath(root: string, sourcePath: string, options?: AddSourceOptions): Promise<SourceRecord[]>;
142
+ declare function addSourceText(root: string, input: AddSourceTextInput, options?: Pick<AddSourceOptions, 'adapters' | 'now'>): Promise<SourceRecord>;
143
+ declare function sourceRegistryPath(root: string): string;
267
144
 
268
145
  /**
269
- * Knowledge freshness store: tracks when each `(workspaceId, sourceId)` pair
270
- * was last successfully refreshed, and reports staleness against a TTL.
271
- *
272
- * The contract is intentionally minimal — just enough to drive a cron loop:
273
- *
274
- * ```ts
275
- * const store = createFileSystemFreshnessStore({ root: '.agent-knowledge' })
276
- * for (const source of sources) {
277
- * if (await store.stale({ workspaceId, sourceId: source.id, ttlMs: DAY })) {
278
- * const fragments = await source.fetch({ cacheDir })
279
- * await persistFragments(fragments)
280
- * await store.mark({ workspaceId, sourceId: source.id, when: new Date() })
281
- * }
282
- * }
283
- * ```
284
- *
285
- * Per-tenant isolation is enforced by `workspaceId` keying — there is no
286
- * global mutable state across workspaces.
287
- *
288
- * Two adapters ship in-package:
289
- *
290
- * - `createFileSystemFreshnessStore` — JSON file under the knowledge root,
291
- * mirrors the layout convention already used by `sources.json`.
292
- * - `createD1FreshnessStoreStub` — adapter scaffold for Cloudflare D1 /
293
- * Postgres. Production consumers should implement the `D1Adapter`
294
- * interface inside their own app; this stub exists to anchor the shape.
295
- *
296
- * @stable contract — interface is frozen at 0.x within this major.
297
- * @stable filesystem adapter
298
- * @experimental D1 stub — interface will evolve as real consumers wire it.
146
+ * A knowledge gap the loop surfaces from `scoreKnowledgeReadiness`. The worker
147
+ * targets these; the driver folds the unfilled remainder into the worker's next
148
+ * prompt and runs its own gap-fill pass over them.
299
149
  */
300
- /** Identity for one freshness record. */
301
- interface FreshnessKey {
302
- workspaceId: string;
303
- sourceId: string;
304
- }
305
- /** TTL bound for staleness checks. */
306
- interface FreshnessTtl extends FreshnessKey {
307
- /** Milliseconds `Date.now() - last() > ttlMs` stale. */
308
- ttlMs: number;
309
- /** Injected clock for deterministic tests; defaults to system time. */
310
- now?: Date;
311
- }
312
- /** Mark argument. */
313
- interface FreshnessMark extends FreshnessKey {
314
- when: Date;
315
- /** Optional content hash captured at refresh time; aids debugging. */
316
- contentHash?: string;
317
- }
318
- interface KnowledgeFreshnessStore {
319
- /** Last refresh time, or null if never refreshed. */
320
- last(key: FreshnessKey): Promise<Date | null>;
321
- /** Record a successful refresh. */
322
- mark(input: FreshnessMark): Promise<void>;
323
- /** True iff `last(key)` is null or older than `ttlMs`. */
324
- stale(input: FreshnessTtl): Promise<boolean>;
325
- /** All records for a workspace — useful for dashboards / debugging. */
326
- list(workspaceId: string): Promise<FreshnessRecord[]>;
327
- }
328
- interface FreshnessRecord {
329
- workspaceId: string;
330
- sourceId: string;
331
- lastRefreshedAt: string;
332
- contentHash?: string;
333
- }
334
- interface FileSystemFreshnessStoreOptions {
335
- /**
336
- * Knowledge root. The store writes to `<root>/.agent-knowledge/freshness.json`,
337
- * mirroring the convention used by `sources.json`.
338
- */
339
- root: string;
150
+ interface KnowledgeGap {
151
+ /** Readiness-spec id this gap belongs to. */
152
+ id: string;
153
+ /** Human-readable description of what's missing. */
154
+ description: string;
155
+ /** The search query the readiness check ran for this requirement. */
156
+ query: string;
157
+ /** True when the gap blocks readiness (vs. a soft, non-blocking gap). */
158
+ blocking: boolean;
340
159
  }
160
+ /** A new source the worker (or driver) discovered and wants to add to the KB. */
161
+ type ResearchSourceProposal = AddSourceTextInput;
341
162
  /**
342
- * Filesystem-backed implementation. Single JSON file per knowledge root,
343
- * indexed by `${workspaceId}::${sourceId}`. Reads parse on every call
344
- * cron tick rate is well below the cost of one JSON parse.
163
+ * What a research agent contributes in one round. Both the worker and (when
164
+ * `driverResearches` is on) the driver produce this shape the worker ADDS
165
+ * primary findings, the driver gap-FILLS the ones the worker missed.
345
166
  *
346
- * Concurrent writes from a single process serialize through `writeQueue`.
347
- * Cross-process concurrency is undefined; the consuming app should run the
348
- * cron in a single worker.
349
- */
350
- declare function createFileSystemFreshnessStore(options: FileSystemFreshnessStoreOptions): KnowledgeFreshnessStore;
167
+ * `proposalText` is the safe write-protocol text (`---FILE: knowledge/...---`
168
+ * blocks). The loop only applies it AFTER the driver has verified the round's
169
+ * sources, so a rejected source never reaches the curated pages.
170
+ */
171
+ interface ResearchContribution {
172
+ /** Immutable sources to register (the raw evidence). */
173
+ sources?: ResearchSourceProposal[];
174
+ /** Safe write-protocol text producing curated `knowledge/*.md` pages. */
175
+ proposalText?: string;
176
+ /**
177
+ * Build the page write-protocol text FROM the sources the driver accepted —
178
+ * the curated, citing pages the readiness gate searches. Receives the
179
+ * registered `SourceRecord`s (with their assigned ids, so a page's frontmatter
180
+ * `sources:` can cite them). Returns `---FILE: knowledge/...---` block text or
181
+ * `undefined`. Runs after verification, so a page never cites a rejected
182
+ * source. Concatenated after any static `proposalText`.
183
+ */
184
+ buildPages?: (acceptedSources: SourceRecord[]) => string | undefined;
185
+ /** Free-form research transcript — products can persist this. */
186
+ notes?: string;
187
+ metadata?: Record<string, unknown>;
188
+ }
189
+ /** Context handed to the worker each round. */
190
+ interface WorkerResearchContext {
191
+ root: string;
192
+ goal: string;
193
+ round: number;
194
+ index: KnowledgeIndex;
195
+ /** Gaps the readiness gate currently reports — what the worker should close. */
196
+ gaps: KnowledgeGap[];
197
+ /** Steer text the driver folded in from the previous round's remaining gaps. */
198
+ steer?: string;
199
+ readiness: EvalKnowledgeBundleBuildResult;
200
+ signal?: AbortSignal;
201
+ }
202
+ /** Context handed to the driver's verifier for one candidate source. */
203
+ interface SourceVerificationContext {
204
+ root: string;
205
+ goal: string;
206
+ round: number;
207
+ index: KnowledgeIndex;
208
+ gaps: KnowledgeGap[];
209
+ /** Sources already accepted earlier THIS round (in-round dedup). */
210
+ acceptedThisRound: ResearchSourceProposal[];
211
+ signal?: AbortSignal;
212
+ }
213
+ /** A single rejected source plus the reason the driver gave. */
214
+ interface RejectedSource {
215
+ source: ResearchSourceProposal;
216
+ reason: string;
217
+ }
218
+ /** Context handed to the driver's gap-fill pass (only when `driverResearches`). */
219
+ interface DriverResearchContext {
220
+ root: string;
221
+ goal: string;
222
+ round: number;
223
+ index: KnowledgeIndex;
224
+ /** Gaps STILL open after the worker's accepted contribution applied. */
225
+ remainingGaps: KnowledgeGap[];
226
+ readiness: EvalKnowledgeBundleBuildResult;
227
+ signal?: AbortSignal;
228
+ }
351
229
  /**
352
- * D1 / Postgres adapter scaffold. Production consumers implement
353
- * `D1Adapter` against their own driver (better-sqlite3, postgres,
354
- * Cloudflare D1 binding, ...). This factory wires the adapter to the
355
- * `KnowledgeFreshnessStore` interface.
230
+ * The differentiated driver role.
356
231
  *
357
- * The expected schema:
232
+ * - `verifySource` — the gate the worker's additions pass before they commit.
233
+ * Return `{ accept: true }` to keep a source or `{ accept: false, reason }`
234
+ * to reject it (not real / not relevant / duplicate). The loop dedups exact
235
+ * duplicates (same `uri` already in the KB or accepted this round) BEFORE
236
+ * calling this, so the verifier only sees genuinely-new candidates.
237
+ * - `research` — the driver's OWN gap-fill pass over the gaps the worker left
238
+ * open. Only invoked when `driverResearches` is true.
239
+ * - `foldGaps` — turn the remaining gaps into a steer string for the worker's
240
+ * next prompt. Defaults to a compact bulleted list when omitted.
241
+ */
242
+ interface ResearchDriver {
243
+ verifySource(source: ResearchSourceProposal, ctx: SourceVerificationContext): Promise<SourceVerdict> | SourceVerdict;
244
+ research?(ctx: DriverResearchContext): Promise<ResearchContribution> | ResearchContribution;
245
+ foldGaps?(gaps: KnowledgeGap[]): string;
246
+ }
247
+ type SourceVerdict = {
248
+ accept: true;
249
+ } | {
250
+ accept: false;
251
+ reason: string;
252
+ };
253
+ /** The worker: primary research targeting the round's gaps. */
254
+ type ResearchWorker = (ctx: WorkerResearchContext) => Promise<ResearchContribution> | ResearchContribution;
255
+ interface VerifiedResearchLoopOptions {
256
+ root: string;
257
+ goal: string;
258
+ worker: ResearchWorker;
259
+ driver: ResearchDriver;
260
+ /**
261
+ * When false (default), the driver ONLY verifies + gates — a pure coordinator
262
+ * that contributes no research of its own (the "doesn't participate in the
263
+ * work" mode). When true, the driver also runs its `research` gap-fill pass
264
+ * each round over the gaps the worker left open.
265
+ */
266
+ driverResearches?: boolean;
267
+ maxRounds?: number;
268
+ actor?: string;
269
+ /** Readiness specs define the gate; an empty list means the loop never gates. */
270
+ readinessSpecs?: KnowledgeReadinessSpec[];
271
+ readinessTaskId?: string;
272
+ readiness?: Omit<BuildEvalKnowledgeBundleOptions, 'taskId' | 'index' | 'specs'>;
273
+ sourceOptions?: Pick<AddSourceOptions, 'adapters' | 'now'>;
274
+ signal?: AbortSignal;
275
+ onRound?: (round: VerifiedResearchRound) => Promise<void> | void;
276
+ }
277
+ interface VerifiedResearchRound {
278
+ round: number;
279
+ /** Gaps reported at the START of the round (what the worker targeted). */
280
+ gaps: KnowledgeGap[];
281
+ /** Worker sources accepted by the driver and written to the KB. */
282
+ acceptedWorkerSources: SourceRecord[];
283
+ /** Worker sources the driver rejected (with reasons) — never written. */
284
+ rejectedWorkerSources: RejectedSource[];
285
+ /** Sources the driver itself added in its gap-fill pass. */
286
+ driverSources: SourceRecord[];
287
+ /** Curated pages written this round (worker proposal + driver proposal). */
288
+ writtenPages: string[];
289
+ readiness?: EvalKnowledgeBundleBuildResult;
290
+ /** True once the readiness gate reports no blocking gaps. */
291
+ ready: boolean;
292
+ event: KnowledgeEvent;
293
+ notes: {
294
+ worker?: string;
295
+ driver?: string;
296
+ };
297
+ }
298
+ interface VerifiedResearchLoopResult {
299
+ root: string;
300
+ goal: string;
301
+ rounds: number;
302
+ ready: boolean;
303
+ index: KnowledgeIndex;
304
+ readiness?: EvalKnowledgeBundleBuildResult;
305
+ steps: VerifiedResearchRound[];
306
+ }
307
+ /**
308
+ * Two-agent (driver + worker) sibling of `runKnowledgeResearchLoop`.
358
309
  *
359
- * ```sql
360
- * CREATE TABLE knowledge_freshness (
361
- * workspace_id TEXT NOT NULL,
362
- * source_id TEXT NOT NULL,
363
- * last_refreshed_at TEXT NOT NULL,
364
- * content_hash TEXT,
365
- * PRIMARY KEY (workspace_id, source_id)
366
- * );
367
- * ```
310
+ * Both agents research to grow ONE knowledge base. The roles are differentiated:
311
+ *
312
+ * - **WORKER** = primary research. Each round it reads the open gaps, discovers
313
+ * new sources, and proposes additions (`sources` + `proposalText`). It ADDS.
314
+ * - **DRIVER** = the verifier / gap-filler / gate. It (1) VERIFIES the worker's
315
+ * sources before they commit — dedup against the KB, then `verifySource`
316
+ * rejects ones that aren't real/relevant; (2) GAP-FILLS the gaps the worker
317
+ * missed with its own research pass (when `driverResearches`); (3) folds the
318
+ * remaining gaps into the worker's next prompt; and (4) GATES on
319
+ * `scoreKnowledgeReadiness` — the loop stops as soon as there are no blocking
320
+ * gaps.
321
+ *
322
+ * Set `driverResearches: false` (default) for the pure-coordinator mode: the
323
+ * driver only verifies + gates and contributes no research itself.
324
+ *
325
+ * Composes existing atoms — `initKnowledgeBase`, `addSourceText`,
326
+ * `applyKnowledgeWriteBlocks`, `buildEvalKnowledgeBundle` (the readiness gate),
327
+ * and `searchKnowledge` — and reinvents none of them.
368
328
  */
369
- interface D1Adapter {
370
- get(workspaceId: string, sourceId: string): Promise<FreshnessRecord | null>;
371
- upsert(record: FreshnessRecord): Promise<void>;
372
- listByWorkspace(workspaceId: string): Promise<FreshnessRecord[]>;
329
+ declare function runVerifiedResearchLoop(options: VerifiedResearchLoopOptions): Promise<VerifiedResearchLoopResult>;
330
+ /**
331
+ * Helper for verifiers: does the candidate source's text/title overlap any page
332
+ * the readiness search returns for a gap query? A cheap relevance heuristic the
333
+ * driver can compose into `verifySource` (real verifiers can do more).
334
+ */
335
+ declare function sourceMatchesGaps(source: ResearchSourceProposal, index: KnowledgeIndex, gaps: KnowledgeGap[]): KnowledgeSearchResult[];
336
+ /** @deprecated Renamed to {@link runVerifiedResearchLoop}. */
337
+ declare const runTwoAgentResearchLoop: typeof runVerifiedResearchLoop;
338
+ /** @deprecated Renamed to {@link VerifiedResearchLoopOptions}. */
339
+ type TwoAgentResearchLoopOptions = VerifiedResearchLoopOptions;
340
+ /** @deprecated Renamed to {@link VerifiedResearchLoopResult}. */
341
+ type TwoAgentResearchLoopResult = VerifiedResearchLoopResult;
342
+ /** @deprecated Renamed to {@link VerifiedResearchRound}. */
343
+ type TwoAgentResearchRound = VerifiedResearchRound;
344
+
345
+ /**
346
+ * Real web-research worker + verifying driver for `runVerifiedResearchLoop`.
347
+ *
348
+ * This is the GENERAL, any-topic implementation behind the two-agent research
349
+ * loop's live arm. Given the open knowledge gaps the readiness gate surfaces,
350
+ * the worker:
351
+ *
352
+ * 1. asks an LLM (glm-5.2 by default) to turn each gap into focused web
353
+ * search queries,
354
+ * 2. runs a REAL web search over the Tangle router (`POST /v1/search` — the
355
+ * same endpoint `tcloud mcp`'s `web_search` tool forwards to), so there is
356
+ * no hardcoded corpus,
357
+ * 3. fetches the top results with the repo's polite, cached `politeFetch` and
358
+ * reduces each page to text with `htmlToText`,
359
+ * 4. proposes the readable, verifiable pages as `ResearchSourceProposal`s plus
360
+ * a `buildPages` that writes citing `knowledge/*.md` pages from the sources
361
+ * the driver accepts.
362
+ *
363
+ * The verifying DRIVER is the differentiated role from the two-agent loop: a
364
+ * second LLM pass that judges each fetched source's on-topic relevance to the
365
+ * goal + open gaps and rejects off-topic / spam / already-covered material. The
366
+ * worker ADDS; the driver GATES. Together they build a cleaner knowledge base
367
+ * than a single agent at the same compute budget.
368
+ *
369
+ * Dependency-free on purpose: it talks to the router over `fetch` directly with
370
+ * the published OpenAI-compatible chat shape and the `/v1/search` shape, so it
371
+ * works whether or not the `tcloud` CLI is installed. Point it at any router by
372
+ * passing `baseUrl`; supply the key via `apiKey` or `TANGLE_API_KEY`.
373
+ */
374
+
375
+ /** One live web result, as the router's `/v1/search` returns it. */
376
+ interface WebSearchHit {
377
+ title: string;
378
+ url: string;
379
+ snippet?: string;
373
380
  }
374
- declare function createD1FreshnessStoreStub(adapter: D1Adapter): KnowledgeFreshnessStore;
381
+ /**
382
+ * The two router capabilities the worker/driver need. Injectable so tests can
383
+ * stub the network; the default talks to the live Tangle router over `fetch`.
384
+ */
385
+ interface RouterClient {
386
+ /** Live web search — returns title/url/snippet hits. */
387
+ search(query: string, opts?: {
388
+ maxResults?: number;
389
+ }): Promise<WebSearchHit[]>;
390
+ /** Chat completion — returns the assistant message's visible text. */
391
+ chat(messages: {
392
+ role: 'system' | 'user';
393
+ content: string;
394
+ }[], maxTokens?: number): Promise<string>;
395
+ /** Cumulative cost (chat + search) since this client was created. */
396
+ usage(): RouterUsage;
397
+ }
398
+ /**
399
+ * Cumulative router cost — the per-arm signal the A/B reports ALONGSIDE quality,
400
+ * so "2.3 fewer sources" can be read against the token/$/latency it cost. A
401
+ * two-agent round is one worker pass plus N `verifySource` LLM calls; counting
402
+ * each call here is what surfaces that the two-agent loop spends more inference
403
+ * than its "equal passes" budget implies.
404
+ */
405
+ interface RouterUsage {
406
+ chatCalls: number;
407
+ searchCalls: number;
408
+ promptTokens: number;
409
+ completionTokens: number;
410
+ usd: number;
411
+ wallMs: number;
412
+ }
413
+ interface TangleRouterOptions {
414
+ /** Router base URL. Defaults to `https://router.tangle.tools/v1`. */
415
+ baseUrl?: string;
416
+ /** Bearer key. Defaults to `process.env.TANGLE_API_KEY`. */
417
+ apiKey?: string;
418
+ /** Chat model id. Defaults to `glm-5.2`. */
419
+ model?: string;
420
+ /** Optional preferred search provider (exa | you | perplexity | …). */
421
+ searchProvider?: string;
422
+ /**
423
+ * Retries on a TRANSIENT upstream status (502/503/504/429) with exponential
424
+ * backoff. Default 4. A 4xx that isn't 429, and a 401, are NOT retried — those
425
+ * are not transient. After the budget is exhausted the call still fails loud
426
+ * with the original `RouterError`, so the fail-closed contract holds; this only
427
+ * stops a single upstream-capacity blip from voiding a whole multi-topic run.
428
+ */
429
+ maxRetries?: number;
430
+ /** Base backoff in ms (doubled each retry, ±25% jitter). Default 1500. */
431
+ retryBaseMs?: number;
432
+ signal?: AbortSignal;
433
+ }
434
+ /** A small error so a failed router call fails loud rather than returning junk. */
435
+ declare class RouterError extends Error {
436
+ readonly status: number;
437
+ constructor(status: number, message: string);
438
+ }
439
+ /**
440
+ * Build a dependency-free Tangle router client over `fetch`. This is the same
441
+ * wire surface the `tcloud` SDK + `tcloud mcp` use (`/v1/search` for web search,
442
+ * `/v1/chat/completions` for chat) so it needs no CLI installed.
443
+ */
444
+ declare function createTangleRouterClient(options?: TangleRouterOptions): RouterClient;
445
+ interface WebResearchWorkerOptions {
446
+ /** Router client. Defaults to a live Tangle router client from env creds. */
447
+ router?: RouterClient;
448
+ router_options?: TangleRouterOptions;
449
+ /** Max search queries the LLM may form per gap. Default 2. */
450
+ queriesPerGap?: number;
451
+ /** Max web results fetched per query. Default 3. */
452
+ resultsPerQuery?: number;
453
+ /** Hard cap on sources proposed per round (across all gaps). Default 6. */
454
+ maxSourcesPerRound?: number;
455
+ /** Disk cache dir for `politeFetch`. Optional; speeds repeat runs. */
456
+ cacheDir?: string;
457
+ /** Minimum readable text length to keep a fetched page. Default 200. */
458
+ minTextChars?: number;
459
+ /** Max chars of page text stored per source (keeps pages bounded). Default 4000. */
460
+ maxTextChars?: number;
461
+ }
462
+ /**
463
+ * The real web-research worker. Conforms to the loop's `ResearchWorker`
464
+ * contract: given the open gaps, it returns the sources it found plus a
465
+ * `buildPages` that emits citing pages from the sources the driver accepted.
466
+ */
467
+ declare function createWebResearchWorker(options?: WebResearchWorkerOptions): ResearchWorker;
468
+ interface VerifyingDriverOptions {
469
+ router?: RouterClient;
470
+ router_options?: TangleRouterOptions;
471
+ /**
472
+ * When the LLM verdict can't be parsed, default to REJECT (fail-closed) so a
473
+ * model hiccup never poisons the KB with an unverified source. Set `true` to
474
+ * accept-on-parse-failure only if you have a reason to. Default false.
475
+ */
476
+ acceptOnParseFailure?: boolean;
477
+ }
478
+ /**
479
+ * The verifying driver: a real LLM pass that judges each candidate source's
480
+ * on-topic relevance to the goal + open gaps and whether it duplicates material
481
+ * already accepted this round. This is the differentiated coordinator role — it
482
+ * GATES the worker's additions; it adds nothing itself.
483
+ *
484
+ * The loop already dedups exact-uri duplicates and only calls this on genuinely
485
+ * new candidates, so the verifier focuses on relevance + near-duplicate
486
+ * judgement, not bookkeeping.
487
+ */
488
+ declare function createVerifyingResearchDriver(options?: VerifyingDriverOptions): ResearchDriver;
375
489
 
376
- interface ParsedFrontmatter {
377
- frontmatter: Record<string, unknown>;
490
+ /**
491
+ * Adaptive verifier mode for `runVerifiedResearchLoop`.
492
+ *
493
+ * The cost/quality A/B (`docs/results/cost-quality.md`) found the LLM relevance
494
+ * verifier's cleanliness win is dominated by DE-DUPLICATION — which a
495
+ * deterministic content-hash / canonical-URL check captures at ~none of the LLM
496
+ * premium — and that an LLM check only earns its dollar on the off-scope tail.
497
+ * The honest production move it names is: do the cheap deterministic work first,
498
+ * spend the LLM only where it pays. This module is that driver.
499
+ *
500
+ * Per candidate source the adaptive driver runs THREE stages, cheapest first,
501
+ * and stops at the first that decides:
502
+ *
503
+ * 1. DEDUP ($0, no LLM). Reject a source whose CONTENT (normalized-text hash)
504
+ * or whose CANONICAL URL matches one already accepted this round or already
505
+ * in the knowledge base. This is the de-dup the relevance judge was being
506
+ * paid to do; doing it deterministically is free and exact.
507
+ *
508
+ * 2. HEURISTIC TRIAGE ($0, no LLM). For a unique survivor, a cheap host /
509
+ * title / length signal classifies it as clearly-keep, clearly-drop, or
510
+ * AMBIGUOUS. Clear cases are resolved without a model: an authoritative host
511
+ * (arxiv, *.edu, *.gov, official docs) with a substantial readable body is
512
+ * kept; an obvious spam/listicle/marketing title or a too-thin body is
513
+ * dropped. Only genuinely ambiguous survivors fall through.
514
+ *
515
+ * 3. LLM ESCALATION ($, one call). ONLY the ambiguous survivors reach the LLM
516
+ * `verifySource` — the shipped `createVerifyingResearchDriver` relevance
517
+ * judge. This is where the verifier earns its premium: the off-scope tail a
518
+ * cheap rule can't adjudicate.
519
+ *
520
+ * The result is the cost/quality frontier point the doc predicted: most of the
521
+ * cleanliness (dedup + clear drops) at a fraction of the LLM $/calls (only the
522
+ * ambiguous tail pays). It is a real `ResearchDriver` — same contract the
523
+ * two-agent loop already gates on — and reuses `sha256`, the relevance verifier,
524
+ * and the index; it reinvents none of them.
525
+ */
526
+
527
+ /**
528
+ * Canonicalize a URL for duplicate detection: lowercase host, strip a leading
529
+ * `www.`, drop the scheme, the fragment, a trailing slash, and tracking query
530
+ * params (`utm_*`, `ref`, `fbclid`, `gclid`, …). Two URLs that differ only by
531
+ * those decorations canonicalize to the same key, so the dedup stage treats them
532
+ * as the same source. Falls back to the lowercased raw string when the input is
533
+ * not a parseable absolute URL (so non-http identifiers still dedup by equality).
534
+ */
535
+ declare function canonicalizeUrl(uri: string): string;
536
+ /**
537
+ * A stable content key for a fetched page: the sha256 of its normalized text
538
+ * (lowercased, punctuation/whitespace collapsed). Two pages whose readable body
539
+ * is the same modulo formatting collide here, so the dedup stage rejects a
540
+ * mirror/syndication of an already-accepted source even when the URL differs.
541
+ */
542
+ declare function contentKey(text: string): string;
543
+ /** Why the deterministic dedup stage rejected a candidate (for audit/notes). */
544
+ type DedupReason = 'duplicate-url' | 'duplicate-content';
545
+ /** The cheap-triage classification of a unique survivor. */
546
+ type TriageClass = 'keep' | 'drop' | 'ambiguous';
547
+ /** One source's adaptive routing decision, for instrumentation and the doc. */
548
+ interface AdaptiveDecision {
549
+ uri: string;
550
+ /** The stage that decided this source: dedup | heuristic | llm. */
551
+ stage: 'dedup' | 'heuristic' | 'llm';
552
+ accepted: boolean;
553
+ /** The triage class assigned (set once past dedup). */
554
+ triage?: TriageClass;
555
+ reason?: string;
556
+ }
557
+ /** Running tally of where the adaptive driver spent its decisions. */
558
+ interface AdaptiveStats {
559
+ total: number;
560
+ /** Rejected by deterministic dedup (URL or content). $0. */
561
+ dedupRejected: number;
562
+ /** Kept by the cheap heuristic without an LLM call. $0. */
563
+ heuristicKept: number;
564
+ /** Dropped by the cheap heuristic without an LLM call. $0. */
565
+ heuristicDropped: number;
566
+ /** Escalated to the LLM relevance verifier ($ — the only paid stage). */
567
+ llmCalls: number;
568
+ /** Of the escalations, how many the LLM accepted. */
569
+ llmAccepted: number;
570
+ decisions: AdaptiveDecision[];
571
+ }
572
+ interface AdaptiveDriverOptions {
573
+ /** Router client for the LLM escalation. Defaults to a live client from env. */
574
+ router?: RouterClient;
575
+ router_options?: TangleRouterOptions;
576
+ /** Passed through to the escalation relevance verifier. */
577
+ verifying?: Pick<VerifyingDriverOptions, 'acceptOnParseFailure'>;
578
+ /**
579
+ * Hosts an authoritative source lives on. A unique survivor on one of these,
580
+ * with a substantial body, is KEPT deterministically (no LLM). Suffix-matched
581
+ * against the canonical host, so `arxiv.org` matches `export.arxiv.org`. The
582
+ * defaults cover papers, official docs, and standards bodies.
583
+ */
584
+ authoritativeHosts?: string[];
585
+ /**
586
+ * Title/snippet patterns that mark obvious spam / listicle / marketing — a
587
+ * unique survivor matching one is DROPPED deterministically (no LLM).
588
+ */
589
+ spamPatterns?: RegExp[];
590
+ /**
591
+ * Below this many readable chars a survivor is too thin to be a real reference
592
+ * and is dropped deterministically. Default 400.
593
+ */
594
+ minBodyChars?: number;
595
+ /**
596
+ * A survivor whose body is at or above this many chars AND on an authoritative
597
+ * host is kept without an LLM call. Default 600.
598
+ */
599
+ substantialBodyChars?: number;
600
+ /** Receives each routing decision as it is made (for live instrumentation). */
601
+ onDecision?: (decision: AdaptiveDecision) => void;
602
+ }
603
+ /**
604
+ * Classify a UNIQUE survivor (already past dedup) with cheap host/title/length
605
+ * signals only — no LLM. Returns `keep`, `drop`, or `ambiguous`. `ambiguous` is
606
+ * the residue the LLM is reserved for: on-topic-looking pages on unknown hosts
607
+ * with a plausible body, which a host/title rule cannot adjudicate.
608
+ */
609
+ declare function triageSource(source: ResearchSourceProposal, options: {
610
+ authoritativeHosts: string[];
611
+ spamPatterns: RegExp[];
612
+ minBodyChars: number;
613
+ substantialBodyChars: number;
614
+ }): {
615
+ triage: TriageClass;
616
+ reason: string;
617
+ };
618
+ interface AdaptiveResearchDriver {
619
+ verifySource(source: ResearchSourceProposal, ctx: SourceVerificationContext): Promise<SourceVerdict>;
620
+ /** Live tally of where decisions were spent — the cost/quality instrumentation. */
621
+ stats(): AdaptiveStats;
622
+ }
623
+ /**
624
+ * Build the adaptive verifier. The deterministic stages (dedup + heuristic
625
+ * triage) cost $0; only AMBIGUOUS survivors escalate to the LLM relevance
626
+ * verifier. `stats()` exposes where every decision was spent so the A/B can read
627
+ * the LLM $/calls the adaptive driver saved against the cleanliness it kept.
628
+ *
629
+ * Dedup state is kept on the driver instance: it tracks the canonical URLs and
630
+ * content hashes it has ACCEPTED, plus those it sees in the verification
631
+ * context's `acceptedThisRound` and the KB index. Use one driver per loop run.
632
+ */
633
+ declare function createAdaptiveResearchDriver(options?: AdaptiveDriverOptions): AdaptiveResearchDriver;
634
+
635
+ /**
636
+ * Change detection across snapshots of one source's fragments.
637
+ *
638
+ * The output drives the continuous-ingestion loop: each `KnowledgeChange`
639
+ * carries the eval dimensions affected (`affectedDimensions`), which an
640
+ * agent-eval campaign scheduler consumes to decide which campaigns to
641
+ * re-run. Three change kinds:
642
+ *
643
+ * - `added` — fragment id appears in `next` but not `prev`.
644
+ * - `removed` — fragment id appears in `prev` but not `next`. Typical
645
+ * trigger: an authority retires a Wex slug, or a state SOS reorganises
646
+ * its forms catalogue.
647
+ * - `modified` — fragment id appears in both, body hash differs. This
648
+ * is the dominant change kind in practice — the Ryan-LLC v. FTC
649
+ * vacatur case manifests as a `modified` on the Wex non-compete
650
+ * fragment, NOT as a removed one.
651
+ *
652
+ * Unverifiable fragments are filtered out before diffing — comparing a
653
+ * captcha-blocked snapshot against a real one would falsely fire every
654
+ * fragment as removed. The caller can inspect the raw lists if they want
655
+ * to surface block-page failures.
656
+ *
657
+ * Within-snapshot duplicate ids are an upstream bug; this function picks
658
+ * the LAST one and emits a diagnostic via the `warnings` field.
659
+ *
660
+ * @stable
661
+ */
662
+ type KnowledgeChangeKind = 'added' | 'removed' | 'modified';
663
+ interface KnowledgeChange {
664
+ /** Source-scoped id (matches `KnowledgeFragment.id`). */
665
+ fragmentId: string;
666
+ kind: KnowledgeChangeKind;
667
+ /**
668
+ * For `added`: full body of the new fragment.
669
+ * For `removed`: full body of the prior fragment.
670
+ * For `modified`: unified-diff-style payload `{ before, after }` body strings.
671
+ */
672
+ diff?: {
673
+ before?: string;
674
+ after?: string;
675
+ };
676
+ /**
677
+ * Eval dimensions to re-score. Computed as the union of both fragments'
678
+ * `dimensionHints`. The eval cron treats this as a set of campaign tags.
679
+ */
680
+ affectedDimensions: string[];
681
+ /** URL of the affected authority page (from whichever side has it). */
682
+ url?: string;
683
+ /**
684
+ * Source-attested change time. For `modified`, takes the NEXT fragment's
685
+ * `sourceUpdatedAt`. For `removed`, takes the PRIOR fragment's
686
+ * `sourceUpdatedAt`. For `added`, takes the NEXT fragment's
687
+ * `sourceUpdatedAt`. Consumers index changes by this date.
688
+ */
689
+ detectedAt: string;
690
+ }
691
+ interface DetectChangesResult {
692
+ changes: KnowledgeChange[];
693
+ /** Counts by kind — handy for dashboards. */
694
+ summary: {
695
+ added: number;
696
+ removed: number;
697
+ modified: number;
698
+ };
699
+ /** Non-fatal diagnostics (duplicate ids, dropped unverifiable fragments). */
700
+ warnings: string[];
701
+ }
702
+ interface DetectChangesOptions {
703
+ /**
704
+ * When true (default), unverifiable fragments are dropped from both
705
+ * sides before comparison. Set false ONLY when debugging block-page
706
+ * issues — comparing against unverifiable content emits false
707
+ * `removed`/`modified` changes.
708
+ */
709
+ skipUnverifiable?: boolean;
710
+ /**
711
+ * When provided, only changes whose `affectedDimensions` intersect this
712
+ * set are returned. Useful for cron loops that schedule per-dimension
713
+ * eval campaigns and only care about a subset.
714
+ */
715
+ filterDimensions?: string[];
716
+ }
717
+ declare function detectChanges(prev: KnowledgeFragment[], next: KnowledgeFragment[], options?: DetectChangesOptions): DetectChangesResult;
718
+
719
+ interface ChunkingOptions {
720
+ targetChars: number;
721
+ maxChars: number;
722
+ minChars: number;
723
+ overlapChars: number;
724
+ }
725
+ interface KnowledgeChunk {
726
+ index: number;
727
+ text: string;
728
+ headingPath: string;
729
+ charStart: number;
730
+ charEnd: number;
731
+ oversized: boolean;
732
+ }
733
+ declare function chunkMarkdown(content: string, options?: Partial<ChunkingOptions>): KnowledgeChunk[];
734
+ declare function stripFrontmatter(content: string): {
378
735
  body: string;
736
+ bodyOffset: number;
737
+ };
738
+
739
+ /**
740
+ * Claim-grounding mode for `runVerifiedResearchLoop`.
741
+ *
742
+ * The two-agent loop's existing verifier judges a source's on-topic RELEVANCE
743
+ * (is this page about the goal?). On the topic sets we have measured, its
744
+ * cleanliness win is dominated by DE-DUPLICATION — which a deterministic
745
+ * content-hash / canonical-URL check captures at ~none of the LLM premium (see
746
+ * `docs/results/cost-quality.md`). That makes the LLM verifier look expensive
747
+ * for what a cheap rule already does.
748
+ *
749
+ * Claim-grounding targets a DIFFERENT, harder error band: a citation that is
750
+ * relevant and unique but **misattributed** — the page is on-topic, the URL is
751
+ * real, yet the specific CLAIM the source is cited for does NOT actually appear
752
+ * in the page. This is the citation-fabrication failure mode of LLM research:
753
+ * the model writes a plausible sentence and hangs a real URL off it that never
754
+ * says any such thing. Neither de-dup nor a relevance judge catches it (both can
755
+ * pass a misattributed-but-on-topic page); only checking the claim against the
756
+ * fetched text does.
757
+ *
758
+ * The check is EXECUTABLE GROUND TRUTH, not another LLM opinion: the worker
759
+ * attaches the specific claim it is citing the source for, and the verifier
760
+ * tests whether that claim is PRESENT (verbatim, normalized, or as a sufficient
761
+ * content-word overlap / close paraphrase) in the `htmlToText` output of the
762
+ * page the worker actually fetched. A claim that is not grounded is rejected as
763
+ * misattributed. Because the oracle is deterministic text presence — not a model
764
+ * call — it is a deployable, non-oracle verifier: it can run in production with
765
+ * zero inference cost, OR be composed with the LLM relevance verifier so the
766
+ * loop rejects BOTH off-topic AND misattributed sources.
767
+ *
768
+ * This module is content-free and any-topic: it adds (1) a way for a proposal to
769
+ * carry the claim it is cited for, (2) the `groundClaimInText` oracle, and (3) a
770
+ * `ResearchDriver` that gates on grounding. It composes the existing
771
+ * `ResearchDriver` / `ResearchSourceProposal` contracts and the shipped
772
+ * `htmlToText`; it reinvents none of them.
773
+ */
774
+
775
+ /**
776
+ * Metadata key under which a proposal carries the specific claim it is cited
777
+ * for. The worker sets `metadata[citedClaimKey] = '<the claim>'`; the
778
+ * claim-grounding driver reads it and checks it against the fetched page text.
779
+ */
780
+ declare const citedClaimKey = "citedClaim";
781
+ /** Read the cited claim a proposal carries, if any. */
782
+ declare function citedClaimOf(source: ResearchSourceProposal): string | undefined;
783
+ /** Attach a cited claim to a proposal (immutably returns a new proposal). */
784
+ declare function withCitedClaim(source: ResearchSourceProposal, claim: string): ResearchSourceProposal;
785
+ interface GroundingResult {
786
+ /** True when the claim is sufficiently present in the page text. */
787
+ grounded: boolean;
788
+ /** How the claim matched (or why it didn't). For audit/notes. */
789
+ mode: 'verbatim' | 'normalized' | 'overlap' | 'absent' | 'empty-claim' | 'empty-text';
790
+ /**
791
+ * Fraction of the claim's content words found in the page text. 1 for a
792
+ * verbatim/normalized hit; the measured overlap otherwise.
793
+ */
794
+ overlap: number;
795
+ /** Content words present in the claim but NOT in the page text. */
796
+ missingWords: string[];
797
+ }
798
+ interface GroundClaimOptions {
799
+ /**
800
+ * Minimum fraction of the claim's content words that must appear in the page
801
+ * text to count as a close paraphrase when there is no verbatim/normalized
802
+ * hit. Default 0.7 — a high bar, because a misattribution is exactly a claim
803
+ * whose specific words the page does not contain.
804
+ */
805
+ minOverlap?: number;
806
+ /**
807
+ * Content words shorter than this are ignored (drops "the", "of", "is", …)
808
+ * and never count toward overlap. Default 3.
809
+ */
810
+ minWordLength?: number;
811
+ }
812
+ /**
813
+ * THE ORACLE. Is `claim` grounded in `pageText` (the `htmlToText` output of the
814
+ * page the worker fetched)? Deterministic, no model call:
815
+ *
816
+ * 1. verbatim — the claim string appears as-is (case-insensitive).
817
+ * 2. normalized — the claim appears after collapsing punctuation/whitespace
818
+ * on both sides (so "5.4x" vs "5.4 x", smart quotes, etc. still match).
819
+ * 3. overlap — a close paraphrase: at least `minOverlap` of the claim's
820
+ * substantive content words appear in the page text. A misattributed page
821
+ * fails here because the SPECIFIC words the claim asserts are absent.
822
+ *
823
+ * Returns the match mode, the measured overlap, and the missing content words —
824
+ * enough for the driver to give a precise rejection reason and for a test/doc to
825
+ * audit WHY a claim grounded or didn't.
826
+ */
827
+ declare function groundClaimInText(claim: string, pageText: string, options?: GroundClaimOptions): GroundingResult;
828
+ interface ClaimGroundingDriverOptions extends GroundClaimOptions {
829
+ /**
830
+ * Optional second verifier to compose AFTER grounding passes. When set, a
831
+ * source must BOTH ground its claim AND pass this verifier (e.g. the LLM
832
+ * relevance driver's `verifySource`). Lets the loop reject off-topic AND
833
+ * misattributed sources in one driver. Omit for the pure, zero-inference
834
+ * grounding gate.
835
+ */
836
+ relevanceVerifier?: (source: ResearchSourceProposal, ctx: SourceVerificationContext) => Promise<SourceVerdict> | SourceVerdict;
837
+ /**
838
+ * What to do when a proposal carries NO cited claim. `'reject'` (default) is
839
+ * fail-closed: in claim-grounding mode every source must declare what it is
840
+ * cited for, so an un-annotated source is treated as ungrounded. `'accept'`
841
+ * lets un-annotated sources through to the relevance verifier (if any) —
842
+ * useful when mixing annotated and legacy proposals.
843
+ */
844
+ onMissingClaim?: 'reject' | 'accept';
845
+ }
846
+ /**
847
+ * A `ResearchDriver`-shaped verifier (just the `verifySource` arm) that gates on
848
+ * CLAIM GROUNDING: it rejects a source whose cited claim is not present in its
849
+ * fetched page text — a misattributed / fabricated citation — and (optionally)
850
+ * composes a relevance verifier after grounding passes.
851
+ *
852
+ * The returned function matches `ResearchDriver['verifySource']`, so it drops
853
+ * straight into `runVerifiedResearchLoop` as `{ verifySource: createClaimGroundingVerifier(...) }`.
854
+ */
855
+ declare function createClaimGroundingVerifier(options?: ClaimGroundingDriverOptions): (source: ResearchSourceProposal, ctx: SourceVerificationContext) => Promise<SourceVerdict>;
856
+ interface WorkerClaimDecorationOptions {
857
+ router?: RouterClient;
858
+ router_options?: TangleRouterOptions;
859
+ /** Max output tokens for the claim-extraction call. Default 1200 (glm floor). */
860
+ maxTokens?: number;
379
861
  }
380
- declare function parseFrontmatter(content: string): ParsedFrontmatter;
381
- declare function formatFrontmatter(frontmatter: Record<string, unknown>, body: string): string;
382
-
383
- declare function buildKnowledgeGraph(pages: KnowledgePage[]): KnowledgeGraph;
862
+ /**
863
+ * Ask an LLM to state, for one source, the single specific factual claim a
864
+ * researcher would cite THIS page for toward the goal. Used to DECORATE the
865
+ * sources a relevance-only worker produced with the claim a citation would make,
866
+ * so the claim-grounding verifier has something executable to check. The model
867
+ * is told to ground the claim in the provided excerpt; the verifier then checks
868
+ * it against the FULL page text independently — the model does not get to mark
869
+ * its own homework.
870
+ *
871
+ * Returns the proposal annotated via `withCitedClaim`, or the original proposal
872
+ * unchanged if the model returns nothing parseable (the verifier's
873
+ * `onMissingClaim` policy then decides).
874
+ */
875
+ declare function createClaimDecorator(options?: WorkerClaimDecorationOptions): (source: ResearchSourceProposal, goal: string) => Promise<ResearchSourceProposal>;
384
876
 
385
- declare function sha256(text: string): string;
386
- declare function slugify(input: string): string;
387
- declare function stableId(prefix: string, content: string): string;
877
+ /**
878
+ * The SINGLE-AGENT COLLECTION driver — the blind-collection baseline (Arm A).
879
+ *
880
+ * This is the honest null the depth A/B is measured against. The other drivers
881
+ * spend extra inference to do something differentiated:
882
+ * - `createVerifyingResearchDriver` runs an LLM gate per source (Arm B),
883
+ * - `createResearchDrivingDriver` extracts claims, tracks corroboration, and
884
+ * synthesizes deep follow-up questions to drive depth (Arm C).
885
+ *
886
+ * This driver does NONE of that. It is a pass-through: it accepts every source
887
+ * the worker proposes and contributes no research, no gating, and no steering of
888
+ * its own. The loop still dedups exact-uri duplicates before calling
889
+ * `verifySource` (that is the loop's job, not the driver's), and the default
890
+ * `foldGaps` (a plain bulleted list of the still-open readiness gaps) still folds
891
+ * the gaps into the worker's next prompt — so the worker keeps researching, but
892
+ * NOTHING intelligent sits between the worker and the knowledge base.
893
+ *
894
+ * In other words: ONE agent (the worker) collects sources round after round, and
895
+ * the "driver" is an inert rubber stamp. That is exactly what "single-agent
896
+ * collection" means — the topology with zero coordinator intelligence — so its
897
+ * material-facts score is the floor every other arm must beat to justify its
898
+ * extra inference cost.
899
+ *
900
+ * It adds NO router calls of its own: `verifySource` is a synchronous accept and
901
+ * `foldGaps` is omitted so the loop uses its built-in gap list. So Arm A's cost
902
+ * is the worker's cost alone — the cleanest possible blind-collection baseline.
903
+ */
388
904
 
389
- declare function buildKnowledgeIndex(root: string): Promise<KnowledgeIndex>;
390
- declare function writeKnowledgeIndex(root: string): Promise<KnowledgeIndex>;
905
+ /**
906
+ * Build the single-agent collection driver. Accepts every source; never gates,
907
+ * never researches, never steers beyond the loop's default open-gap list. The
908
+ * worker is the only agent that thinks.
909
+ */
910
+ declare function createCollectionResearchDriver(): ResearchDriver;
391
911
 
392
- interface KnowledgeInspection {
393
- pageCount: number;
394
- sourceCount: number;
395
- expiredSourceCount: number;
396
- staleSourceCount: number;
397
- edgeCount: number;
398
- findingCount: number;
399
- blockingFindingCount: number;
400
- topPages: Array<{
401
- path: string;
402
- title: string;
403
- degree: number;
404
- sources: number;
405
- }>;
406
- sourceFreshness: SourceFreshnessInspection[];
407
- findings: KnowledgeLintFinding[];
408
- }
409
- interface SourceFreshnessInspection {
912
+ interface DiscoveryTask {
410
913
  id: string;
411
- title?: string;
412
- uri: string;
413
- status: 'fresh' | 'expired' | 'unknown';
414
- validUntil?: string;
415
- lastVerifiedAt?: string;
914
+ goal: string;
915
+ query?: string;
916
+ sourceHints?: string[];
917
+ metadata?: Record<string, unknown>;
416
918
  }
417
- declare function inspectKnowledgeIndex(index: KnowledgeIndex, options?: {
418
- now?: Date;
419
- }): KnowledgeInspection;
420
- interface KnowledgeExplanation {
421
- target: string;
422
- page?: KnowledgePage;
423
- sources: Array<{
424
- id: string;
425
- title?: string;
426
- uri: string;
427
- }>;
428
- links: string[];
429
- inbound: string[];
430
- related: Array<{
431
- path: string;
432
- title: string;
433
- score: number;
919
+ interface DiscoveryResult {
920
+ taskId: string;
921
+ summary: string;
922
+ sourceUris?: string[];
923
+ claims?: Array<{
924
+ text: string;
925
+ sourceUri?: string;
926
+ confidence?: number;
434
927
  }>;
928
+ followUpTasks?: DiscoveryTask[];
929
+ metadata?: Record<string, unknown>;
435
930
  }
436
- declare function explainKnowledgeTarget(index: KnowledgeIndex, target: string): KnowledgeExplanation;
437
-
438
- interface KbStore {
439
- putSource(source: SourceRecord): Promise<void>;
440
- getSource(id: string): Promise<SourceRecord | null>;
441
- listSources(): Promise<SourceRecord[]>;
442
- putPage(page: KnowledgePage): Promise<void>;
443
- getPage(idOrPath: string): Promise<KnowledgePage | null>;
444
- listPages(): Promise<KnowledgePage[]>;
445
- putIndex(index: KnowledgeIndex): Promise<void>;
446
- getIndex(): Promise<KnowledgeIndex | null>;
447
- putEvent(event: KnowledgeEvent): Promise<void>;
448
- listEvents(query?: KnowledgeEventQuery): Promise<KnowledgeEvent[]>;
449
- }
450
- declare class MemoryKbStore implements KbStore {
451
- private readonly sources;
452
- private readonly pages;
453
- private readonly events;
454
- private index;
455
- putSource(source: SourceRecord): Promise<void>;
456
- getSource(id: string): Promise<SourceRecord | null>;
457
- listSources(): Promise<SourceRecord[]>;
458
- putPage(page: KnowledgePage): Promise<void>;
459
- getPage(idOrPath: string): Promise<KnowledgePage | null>;
460
- listPages(): Promise<KnowledgePage[]>;
461
- putIndex(index: KnowledgeIndex): Promise<void>;
462
- getIndex(): Promise<KnowledgeIndex | null>;
463
- putEvent(event: KnowledgeEvent): Promise<void>;
464
- listEvents(query?: KnowledgeEventQuery): Promise<KnowledgeEvent[]>;
931
+ interface KnowledgeDiscoveryWorker {
932
+ run(task: DiscoveryTask, signal?: AbortSignal): Promise<DiscoveryResult> | DiscoveryResult;
465
933
  }
466
- declare class FileSystemKbStore extends MemoryKbStore {
467
- private readonly dir;
468
- constructor(dir: string);
469
- putIndex(index: KnowledgeIndex): Promise<void>;
470
- getIndex(): Promise<KnowledgeIndex | null>;
471
- putEvent(event: KnowledgeEvent): Promise<void>;
934
+ interface KnowledgeDiscoveryDispatcher {
935
+ dispatch(tasks: DiscoveryTask[], options?: {
936
+ concurrency?: number;
937
+ signal?: AbortSignal;
938
+ }): Promise<DiscoveryResult[]>;
472
939
  }
940
+ declare function createLocalDiscoveryDispatcher(worker: KnowledgeDiscoveryWorker): KnowledgeDiscoveryDispatcher;
473
941
 
474
- declare function lintKnowledgeIndex(index: KnowledgeIndex): KnowledgeLintFinding[];
475
-
476
- interface ApplyWriteBlocksResult {
477
- written: string[];
478
- warnings: string[];
942
+ interface KnowledgeEventQuery {
943
+ type?: KnowledgeEventType;
944
+ target?: string;
945
+ limit?: number;
479
946
  }
480
- declare function applyKnowledgeWriteBlocks(root: string, proposalText: string): Promise<ApplyWriteBlocksResult>;
481
- declare function applyKnowledgeWriteBlocksFile(root: string, proposalPath: string): Promise<ApplyWriteBlocksResult>;
947
+ declare function createKnowledgeEvent(input: {
948
+ type: KnowledgeEventType;
949
+ actor?: string;
950
+ target?: string;
951
+ metadata?: Record<string, unknown>;
952
+ now?: () => Date;
953
+ }): KnowledgeEvent;
482
954
 
483
955
  /**
484
- * Bridge from `AnalystFinding` (agent-eval) to knowledge proposals.
956
+ * Knowledge freshness store: tracks when each `(workspaceId, sourceId)` pair
957
+ * was last successfully refreshed, and reports staleness against a TTL.
485
958
  *
486
- * Closes the failure wiki side of the recursive-self-improvement
487
- * loop: a knowledge-gap or knowledge-poisoning finding produced by an
488
- * analyst becomes a concrete proposal an operator (or auto-merge bot)
489
- * can review and apply. The bridge is intentionally lossless on the
490
- * fail-loud side — a finding the parser can't classify returns a
491
- * `KnowledgeProposalParseError` rather than a silent skip, so the
492
- * loop never accepts an underspecified edit.
959
+ * The contract is intentionally minimal just enough to drive a cron loop:
493
960
  *
494
- * Subject grammar this bridge understands (analyst-side convention,
495
- * stamped in the kind prompts):
961
+ * ```ts
962
+ * const store = createFileSystemFreshnessStore({ root: '.agent-knowledge' })
963
+ * for (const source of sources) {
964
+ * if (await store.stale({ workspaceId, sourceId: source.id, ttlMs: DAY })) {
965
+ * const fragments = await source.fetch({ cacheDir })
966
+ * await persistFragments(fragments)
967
+ * await store.mark({ workspaceId, sourceId: source.id, when: new Date() })
968
+ * }
969
+ * }
970
+ * ```
496
971
  *
497
- * agent-knowledge:wiki:<page-slug> create / update page
498
- * agent-knowledge:wiki:<page-slug>#<heading> insert section under page
499
- * agent-knowledge:claim:<topic> draft claim row
500
- * agent-knowledge:raw:<source-id> lift raw → curated
501
- * agent-knowledge:stale:<page-slug> mark page superseded
972
+ * Per-tenant isolation is enforced by `workspaceId` keying — there is no
973
+ * global mutable state across workspaces.
502
974
  *
503
- * Anything else (websearch:outdated:*, tool-doc:*, system-prompt:*,
504
- * memory:*) is NOT a knowledge-base concern and returns `null` so the
505
- * loop's improvement-applier handles it.
975
+ * Two adapters ship in-package:
976
+ *
977
+ * - `createFileSystemFreshnessStore` JSON file under the knowledge root,
978
+ * mirrors the layout convention already used by `sources.json`.
979
+ * - `createD1FreshnessStoreStub` — adapter scaffold for Cloudflare D1 /
980
+ * Postgres. Production consumers should implement the `D1Adapter`
981
+ * interface inside their own app; this stub exists to anchor the shape.
982
+ *
983
+ * @stable contract — interface is frozen at 0.x within this major.
984
+ * @stable filesystem adapter
985
+ * @experimental D1 stub — interface will evolve as real consumers wire it.
506
986
  */
507
-
508
- interface KnowledgeProposal {
509
- /**
510
- * Stable id derived from the finding so cross-run diffs share an
511
- * identity. Re-proposing the same finding produces the same id.
512
- */
513
- id: string;
514
- /** The finding that generated this proposal — useful for audit + revert. */
515
- sourceFindingId: string;
516
- /** What the proposal does. */
517
- kind: 'create-page' | 'update-page' | 'append-section' | 'create-claim' | 'lift-raw' | 'mark-stale';
518
- /** Locus on disk (page slug or claim topic). */
519
- locus: string;
520
- /**
521
- * Page write blocks the standard `applyKnowledgeWriteBlocks` consumer
522
- * accepts. Empty for proposals that don't change page text (e.g.
523
- * `create-claim` produces a `claim` field instead).
524
- */
525
- writeBlocks: KnowledgeWriteBlock[];
526
- /**
527
- * Granular claim draft for proposals whose unit-of-change is a claim
528
- * row rather than a whole page. `status: 'draft'` until reviewed.
529
- */
530
- claim?: KnowledgeClaim;
531
- /** Per-proposal metadata: severity, confidence, source span. */
532
- metadata: {
533
- severity: AnalystSeverity;
534
- confidence: number;
535
- evidence_uri?: string;
536
- analyst_id: string;
537
- };
987
+ /** Identity for one freshness record. */
988
+ interface FreshnessKey {
989
+ workspaceId: string;
990
+ sourceId: string;
538
991
  }
539
- declare class KnowledgeProposalParseError extends Error {
540
- readonly findingId: string;
541
- readonly subject: string;
542
- constructor(findingId: string, subject: string, message: string);
992
+ /** TTL bound for staleness checks. */
993
+ interface FreshnessTtl extends FreshnessKey {
994
+ /** Milliseconds — `Date.now() - last() > ttlMs` ⇒ stale. */
995
+ ttlMs: number;
996
+ /** Injected clock for deterministic tests; defaults to system time. */
997
+ now?: Date;
998
+ }
999
+ /** Mark argument. */
1000
+ interface FreshnessMark extends FreshnessKey {
1001
+ when: Date;
1002
+ /** Optional content hash captured at refresh time; aids debugging. */
1003
+ contentHash?: string;
1004
+ }
1005
+ interface KnowledgeFreshnessStore {
1006
+ /** Last refresh time, or null if never refreshed. */
1007
+ last(key: FreshnessKey): Promise<Date | null>;
1008
+ /** Record a successful refresh. */
1009
+ mark(input: FreshnessMark): Promise<void>;
1010
+ /** True iff `last(key)` is null or older than `ttlMs`. */
1011
+ stale(input: FreshnessTtl): Promise<boolean>;
1012
+ /** All records for a workspace — useful for dashboards / debugging. */
1013
+ list(workspaceId: string): Promise<FreshnessRecord[]>;
1014
+ }
1015
+ interface FreshnessRecord {
1016
+ workspaceId: string;
1017
+ sourceId: string;
1018
+ lastRefreshedAt: string;
1019
+ contentHash?: string;
1020
+ }
1021
+ interface FileSystemFreshnessStoreOptions {
1022
+ /**
1023
+ * Knowledge root. The store writes to `<root>/.agent-knowledge/freshness.json`,
1024
+ * mirroring the convention used by `sources.json`.
1025
+ */
1026
+ root: string;
543
1027
  }
544
1028
  /**
545
- * Convert one `AnalystFinding` into a knowledge proposal. Returns
546
- * `null` when the finding's locus isn't a knowledge-base concern
547
- * (`websearch:outdated:*`, `tool-doc:*`, `system-prompt:*`,
548
- * `memory:*`, missing subject). Throws when the locus IS a
549
- * knowledge-base concern but is malformed — that's a bug in the
550
- * analyst prompt and should fail loud.
1029
+ * Filesystem-backed implementation. Single JSON file per knowledge root,
1030
+ * indexed by `${workspaceId}::${sourceId}`. Reads parse on every call
1031
+ * cron tick rate is well below the cost of one JSON parse.
551
1032
  *
552
- * Caller convention: feed the function the analyst's full findings
553
- * list and filter out the `null`s; the orchestrator passes the
554
- * remaining proposals to the existing review / apply pipeline.
1033
+ * Concurrent writes from a single process serialize through `writeQueue`.
1034
+ * Cross-process concurrency is undefined; the consuming app should run the
1035
+ * cron in a single worker.
555
1036
  */
556
- declare function proposeFromFinding(finding: AnalystFinding): KnowledgeProposal | null;
1037
+ declare function createFileSystemFreshnessStore(options: FileSystemFreshnessStoreOptions): KnowledgeFreshnessStore;
557
1038
  /**
558
- * Plural convenience: filter + map across an entire findings batch
559
- * with one call. Parse errors collect into `errors[]`; the loop
560
- * decides per-error whether to abort or continue.
1039
+ * D1 / Postgres adapter scaffold. Production consumers implement
1040
+ * `D1Adapter` against their own driver (better-sqlite3, postgres,
1041
+ * Cloudflare D1 binding, ...). This factory wires the adapter to the
1042
+ * `KnowledgeFreshnessStore` interface.
1043
+ *
1044
+ * The expected schema:
1045
+ *
1046
+ * ```sql
1047
+ * CREATE TABLE knowledge_freshness (
1048
+ * workspace_id TEXT NOT NULL,
1049
+ * source_id TEXT NOT NULL,
1050
+ * last_refreshed_at TEXT NOT NULL,
1051
+ * content_hash TEXT,
1052
+ * PRIMARY KEY (workspace_id, source_id)
1053
+ * );
1054
+ * ```
561
1055
  */
562
- interface ProposeFromFindingsResult {
563
- proposals: KnowledgeProposal[];
564
- skipped: number;
565
- errors: KnowledgeProposalParseError[];
1056
+ interface D1Adapter {
1057
+ get(workspaceId: string, sourceId: string): Promise<FreshnessRecord | null>;
1058
+ upsert(record: FreshnessRecord): Promise<void>;
1059
+ listByWorkspace(workspaceId: string): Promise<FreshnessRecord[]>;
566
1060
  }
567
- declare function proposeFromFindings(findings: ReadonlyArray<AnalystFinding>): ProposeFromFindingsResult;
1061
+ declare function createD1FreshnessStoreStub(adapter: D1Adapter): KnowledgeFreshnessStore;
568
1062
 
569
- interface KnowledgeReleaseReport {
570
- release: KnowledgeRelease;
571
- scorecard: ReleaseConfidenceScorecard;
572
- candidateRuns: RunRecord[];
573
- baselineRuns: RunRecord[];
574
- }
575
- /**
576
- * Campaign-native release report. The caller (a consumer's KB self-improvement
577
- * loop) supplies the candidate/baseline `RunRecord[]` (e.g. via
578
- * `campaignToRunRecords`) + optional per-instance `ReleaseTraceEvidence` + the
579
- * gate decision; this folds them into a `ReleaseConfidenceScorecard` + a
580
- * `KnowledgeRelease`. Release confidence is computed from run records + traces,
581
- * independent of any optimizer result shape.
582
- */
583
- interface KnowledgeReleaseInput {
584
- candidateId: string;
585
- baselineId?: string;
586
- candidateRuns: RunRecord[];
587
- baselineRuns?: RunRecord[];
588
- traces?: ReleaseTraceEvidence[];
589
- gateDecision?: GateDecision | null;
590
- /**
591
- * True when a held-out split was evaluated (drives the holdout threshold).
592
- *
593
- * Constraint: the substrate gate keys the holdout requirement off a scenario
594
- * corpus, which this run-only report does not carry. With `hasHoldout: true`
595
- * the gate fails closed (`missing_holdout_split`) even when holdout RunRecords
596
- * are supplied. Callers that gate on a real held-out split should drive
597
- * `evaluateReleaseConfidence` directly with a dataset/scenarios.
598
- */
599
- hasHoldout?: boolean;
600
- /** Candidate is the search-best variant — a promotion precondition. Default true. */
601
- promotedIsBest?: boolean;
602
- createdAt?: string;
603
- minScore?: number;
1063
+ interface ParsedFrontmatter {
1064
+ frontmatter: Record<string, unknown>;
1065
+ body: string;
604
1066
  }
605
- declare function knowledgeReleaseReport(input: KnowledgeReleaseInput): KnowledgeReleaseReport;
1067
+ declare function parseFrontmatter(content: string): ParsedFrontmatter;
1068
+ declare function formatFrontmatter(frontmatter: Record<string, unknown>, body: string): string;
606
1069
 
607
- interface AddSourceOptions {
608
- copyIntoRaw?: boolean;
609
- adapters?: SourceAdapter[];
610
- now?: () => Date;
1070
+ declare function buildKnowledgeGraph(pages: KnowledgePage[]): KnowledgeGraph;
1071
+
1072
+ declare function sha256(text: string): string;
1073
+ declare function slugify(input: string): string;
1074
+ declare function stableId(prefix: string, content: string): string;
1075
+
1076
+ declare function buildKnowledgeIndex(root: string): Promise<KnowledgeIndex>;
1077
+ declare function writeKnowledgeIndex(root: string): Promise<KnowledgeIndex>;
1078
+
1079
+ interface KnowledgeInspection {
1080
+ pageCount: number;
1081
+ sourceCount: number;
1082
+ expiredSourceCount: number;
1083
+ staleSourceCount: number;
1084
+ edgeCount: number;
1085
+ findingCount: number;
1086
+ blockingFindingCount: number;
1087
+ topPages: Array<{
1088
+ path: string;
1089
+ title: string;
1090
+ degree: number;
1091
+ sources: number;
1092
+ }>;
1093
+ sourceFreshness: SourceFreshnessInspection[];
1094
+ findings: KnowledgeLintFinding[];
611
1095
  }
612
- interface AddSourceTextInput {
613
- uri: string;
614
- text: string;
1096
+ interface SourceFreshnessInspection {
1097
+ id: string;
615
1098
  title?: string;
616
- mediaType?: string;
1099
+ uri: string;
1100
+ status: 'fresh' | 'expired' | 'unknown';
617
1101
  validUntil?: string;
618
1102
  lastVerifiedAt?: string;
619
- metadata?: Record<string, unknown>;
620
- }
621
- declare function loadSourceRegistry(root: string): Promise<SourceRegistry>;
622
- declare function writeSourceRegistry(root: string, registry: SourceRegistry): Promise<void>;
623
- declare function addSourcePath(root: string, sourcePath: string, options?: AddSourceOptions): Promise<SourceRecord[]>;
624
- declare function addSourceText(root: string, input: AddSourceTextInput, options?: Pick<AddSourceOptions, 'adapters' | 'now'>): Promise<SourceRecord>;
625
- declare function sourceRegistryPath(root: string): string;
626
-
627
- interface ValidateKnowledgeOptions {
628
- strict?: boolean;
629
- }
630
- interface ValidateKnowledgeResult {
631
- ok: boolean;
632
- findings: KnowledgeLintFinding[];
633
- }
634
- declare function validateKnowledgeIndex(index: KnowledgeIndex, options?: ValidateKnowledgeOptions): ValidateKnowledgeResult;
635
-
636
- interface KnowledgeResearchLoopContext {
637
- root: string;
638
- goal: string;
639
- iteration: number;
640
- index: KnowledgeIndex;
641
- lintFindings: KnowledgeLintFinding[];
642
- validation: ValidateKnowledgeResult;
643
- readiness?: EvalKnowledgeBundleBuildResult;
644
- previousSteps: KnowledgeResearchLoopStep[];
645
- signal?: AbortSignal;
646
- }
647
- interface KnowledgeResearchLoopDecision {
648
- /**
649
- * Free-form notes from the researcher. Keep this human-readable; products can
650
- * store it as the research transcript.
651
- */
652
- notes?: string;
653
- /**
654
- * Local files to register as immutable sources before applying proposals.
655
- */
656
- sourcePaths?: string[];
657
- /**
658
- * Textual source artifacts discovered by an agent, browser worker, connector,
659
- * or deep-research process.
660
- */
661
- sourceTexts?: AddSourceTextInput[];
662
- /**
663
- * Safe write protocol text. The loop parses and applies only accepted
664
- * `---FILE: knowledge/...---` blocks.
665
- */
666
- proposalText?: string;
667
- /**
668
- * The researcher decides when the wiki is good enough. The loop deliberately
669
- * does not encode a domain-specific definition of "done".
670
- */
671
- done?: boolean;
672
- metadata?: Record<string, unknown>;
673
- }
674
- interface KnowledgeResearchLoopStep {
675
- iteration: number;
676
- notes?: string;
677
- addedSources: SourceRecord[];
678
- applied?: ApplyWriteBlocksResult;
679
- lintFindings: KnowledgeLintFinding[];
680
- validation: ValidateKnowledgeResult;
681
- readiness?: EvalKnowledgeBundleBuildResult;
682
- event: KnowledgeEvent;
683
- done: boolean;
684
- metadata?: Record<string, unknown>;
685
1103
  }
686
- interface RunKnowledgeResearchLoopOptions {
687
- root: string;
688
- goal: string;
689
- maxIterations?: number;
690
- actor?: string;
691
- strict?: ValidateKnowledgeOptions['strict'];
692
- readinessSpecs?: KnowledgeReadinessSpec[];
693
- readinessTaskId?: string;
694
- readiness?: Omit<BuildEvalKnowledgeBundleOptions, 'taskId' | 'index' | 'specs'>;
695
- sourceOptions?: Pick<AddSourceOptions, 'adapters' | 'now'>;
696
- signal?: AbortSignal;
697
- step(context: KnowledgeResearchLoopContext): Promise<KnowledgeResearchLoopDecision> | KnowledgeResearchLoopDecision;
698
- onStep?: (step: KnowledgeResearchLoopStep) => Promise<void> | void;
699
- }
700
- interface KnowledgeResearchLoopResult {
701
- root: string;
702
- goal: string;
703
- iterations: number;
704
- done: boolean;
705
- index: KnowledgeIndex;
706
- lintFindings: KnowledgeLintFinding[];
707
- validation: ValidateKnowledgeResult;
708
- readiness?: EvalKnowledgeBundleBuildResult;
709
- steps: KnowledgeResearchLoopStep[];
710
- }
711
- type KnowledgeControlLoopState = KnowledgeResearchLoopContext;
712
- type KnowledgeControlLoopAction = KnowledgeResearchLoopDecision;
713
- type KnowledgeControlLoopActionResult = KnowledgeResearchLoopStep;
714
- interface KnowledgeControlLoopAdapterOptions {
715
- root: string;
716
- goal: string;
717
- actor?: string;
718
- strict?: ValidateKnowledgeOptions['strict'];
719
- readinessSpecs?: KnowledgeReadinessSpec[];
720
- readinessTaskId?: string;
721
- readiness?: Omit<BuildEvalKnowledgeBundleOptions, 'taskId' | 'index' | 'specs'>;
722
- sourceOptions?: Pick<AddSourceOptions, 'adapters' | 'now'>;
723
- }
724
- type KnowledgeControlLoopAdapter = Pick<ControlRuntimeConfig<KnowledgeControlLoopState, KnowledgeControlLoopAction, KnowledgeControlLoopActionResult, ControlEvalResult>, 'intent' | 'observe' | 'validate' | 'act' | 'shouldStop'>;
725
- /**
726
- * Adapter for running knowledge growth through `agent-eval`'s generic control
727
- * runtime. The caller still owns `decide`: that can be a proposer agent,
728
- * reviewer agent, deterministic policy, or a composition of all three.
729
- */
730
- declare function createKnowledgeControlLoopAdapter(options: KnowledgeControlLoopAdapterOptions): KnowledgeControlLoopAdapter;
731
- declare function runKnowledgeResearchLoop(options: RunKnowledgeResearchLoopOptions): Promise<KnowledgeResearchLoopResult>;
1104
+ declare function inspectKnowledgeIndex(index: KnowledgeIndex, options?: {
1105
+ now?: Date;
1106
+ }): KnowledgeInspection;
1107
+ interface KnowledgeExplanation {
1108
+ target: string;
1109
+ page?: KnowledgePage;
1110
+ sources: Array<{
1111
+ id: string;
1112
+ title?: string;
1113
+ uri: string;
1114
+ }>;
1115
+ links: string[];
1116
+ inbound: string[];
1117
+ related: Array<{
1118
+ path: string;
1119
+ title: string;
1120
+ score: number;
1121
+ }>;
1122
+ }
1123
+ declare function explainKnowledgeTarget(index: KnowledgeIndex, target: string): KnowledgeExplanation;
732
1124
 
733
1125
  /**
734
- * Default standing instructions for a research supervisor. It does NOT solve the
735
- * research itself — it DECOMPOSES the goal into sub-topics and spawns one
736
- * researcher per sub-topic over the live `Scope`, widening/narrowing until the
737
- * readiness gate (the deliverable check) reports the knowledge base is ready.
1126
+ * HELD-OUT INVESTMENT-RESEARCH EVAL SET.
1127
+ *
1128
+ * The point of this file is the same FIREWALL the deep-question exam uses
1129
+ * (`tests/loops/held-out-exam.ts`): the material facts and their checkable
1130
+ * fragments are NEVER shown to a research loop. A loop is told only the company
1131
+ * + ticker + a research-as-of CUTOFF date, and asked to write an investment
1132
+ * thesis. AFTER it finishes, we grade the thesis it produced against THESE
1133
+ * facts — facts it never saw — so a high score is thesis QUALITY (it surfaced
1134
+ * the buried, material, non-obvious drivers) and not teaching-to-the-test.
1135
+ *
1136
+ * Each fact is a DEPTH fact by construction: a single ticker / company-name web
1137
+ * search does NOT surface it. They are the things buried in the filings or
1138
+ * knowable from then-available primary sources that a thorough analyst flags and
1139
+ * a one-shot search misses — customer/revenue/deposit concentration, a debt
1140
+ * maturity wall, a margin-trend reversal, a governance / related-party item, a
1141
+ * specific competitive or regulatory risk, an off-balance-sheet loss.
1142
+ *
1143
+ * THREE HARD RULES, enforced by how the data was gathered (see
1144
+ * docs/eval/investment-material-facts.md for the per-item provenance + the
1145
+ * curation-bias disclosure):
1146
+ *
1147
+ * 1. SPECIFIC + CHECKABLE. Each fact carries `expected` keyword groups — the
1148
+ * specific number / name / phrase — so a deterministic, model-free
1149
+ * substring grader (`gradeFactAgainstText`) can score "did the thesis
1150
+ * surface it". $0, reproducible, and it cannot leak the answer key into a
1151
+ * model the loop could observe.
1152
+ *
1153
+ * 2. DERIVED FROM REAL FETCHED EVIDENCE. Every fact records the primary source
1154
+ * it came from (`sourceUrl`, an SEC EDGAR 10-K) and the literal `evidence`
1155
+ * value read out of that document. Nothing here is invented; an item that
1156
+ * could not be independently sourced was DROPPED, not guessed (the drop log
1157
+ * is in the doc).
1158
+ *
1159
+ * 3. KNOWABLE AT THE CUTOFF. Every fact was disclosed in, or computable from, a
1160
+ * document available on or before the company's `cutoff` date. Post-cutoff
1161
+ * hindsight (the eventual bankruptcy / collapse) is NOT a checklist item —
1162
+ * it is recorded separately as `knownOutcome`, purely for the reader, and is
1163
+ * never graded.
1164
+ *
1165
+ * Grading mirrors the deep-question exam exactly: a fact is SURFACED when the
1166
+ * thesis text contains at least `minGroups` of its expected groups; a group is
1167
+ * satisfied when ANY of its `anyOf` fragments appears (case-insensitive
1168
+ * substring), so a faithful thesis phrased in its own words still grades as a
1169
+ * hit. `anyOf` groups model synonyms; the load-bearing tokens are the specific
1170
+ * numbers / names.
738
1171
  */
739
- declare const RESEARCH_SUPERVISOR_SYSTEM_PROMPT: string;
740
- interface ResearchSupervisorOptions {
741
- /** Where the knowledge base lives. The deliverable check reads readiness here. */
742
- root: string;
743
- /** The research goal handed to the supervisor as its task. */
744
- goal: string;
1172
+ /** A required answer component: satisfied when any synonym fragment is present. */
1173
+ interface ExpectedGroup {
1174
+ /** Human label for the component (for the doc / audit). */
1175
+ label: string;
1176
+ /** Case-insensitive substring fragments; any one present satisfies the group. */
1177
+ anyOf: string[];
1178
+ }
1179
+ /** Lens the fact belongs to — so a set can be checked for category coverage. */
1180
+ type MaterialFactLens = 'concentration' | 'leverage' | 'margin-trend' | 'liquidity' | 'capital-return' | 'governance' | 'off-balance-sheet' | 'regulatory';
1181
+ /** One held-out material fact with a checkable expected answer + its provenance. */
1182
+ interface MaterialFact {
1183
+ /** Stable id, `ticker/fN`. */
1184
+ id: string;
1185
+ /** Which analyst lens this fact exercises. For coverage + the doc. */
1186
+ lens: MaterialFactLens;
745
1187
  /**
746
- * Readiness specs define DONE: the supervisor's deliverable check passes once
747
- * `scoreKnowledgeReadiness` reports no blocking gaps over the live KB. Required
748
- * — without a gate the supervisor would never know when to stop.
1188
+ * The material fact, in plain words — for the doc/audit. NEVER shown to a loop.
1189
+ * This is the thing a thorough analyst would flag and a ticker search misses.
749
1190
  */
750
- readinessSpecs: KnowledgeReadinessSpec[];
751
- readinessTaskId?: string;
752
- readiness?: Omit<BuildEvalKnowledgeBundleOptions, 'taskId' | 'index' | 'specs'>;
753
- /** The conserved compute pool for the whole supervised run. */
754
- budget: Budget;
1191
+ fact: string;
1192
+ /**
1193
+ * The checkable answer as required keyword GROUPS. The thesis text must contain
1194
+ * at least `minGroups` of these groups (default: all). A group is satisfied
1195
+ * when ANY of its `anyOf` fragments appears (case-insensitive substring).
1196
+ */
1197
+ expected: ExpectedGroup[];
755
1198
  /**
756
- * WHERE researcher workers run the caller's real backend seam (sandbox with
757
- * a `sandboxClient`, or router/cli/bridge). The supervisor spawns researchers
758
- * on this backend; assembling the seam (creds, sandbox SDK) is the caller's,
759
- * so there is no fabricated default.
1199
+ * Minimum number of `expected` groups the thesis must contain to count the
1200
+ * fact SURFACED. Default = all groups (the strict bar). Lowered (and documented
1201
+ * inline) only when the fact is genuinely satisfiable by a subset.
760
1202
  */
761
- backend: ExecutorConfig;
762
- /** Harness for the researcher worker profile (default `researcherProfile`'s). */
763
- harness?: string;
764
- /** The supervisor brain's router model. */
765
- supervisorModel?: string;
766
- /** Custom supervisor instructions. Defaults to {@link RESEARCH_SUPERVISOR_SYSTEM_PROMPT}. */
767
- supervisorSystemPrompt?: string;
768
- /** Extra `supervise()` knobs (perWorker, maxLiveWorkers, router, allowedModels, …). */
769
- superviseOptions?: Partial<Omit<SuperviseOptions, 'budget' | 'backend' | 'deliverable'>>;
1203
+ minGroups?: number;
1204
+ /**
1205
+ * PROVENANCE. The primary source URL this fact was read from — an SEC EDGAR
1206
+ * 10-K primary document, fetched live during curation.
1207
+ */
1208
+ sourceUrl: string;
1209
+ /**
1210
+ * The literal value / phrase read out of `sourceUrl` that grounds the fact.
1211
+ * This is the "cite the actual filing + the value" requirement — verbatim or
1212
+ * near-verbatim from the filing, with the figure.
1213
+ */
1214
+ evidence: string;
1215
+ }
1216
+ /** A company + the cutoff a loop researches as-of + its held-out material facts. */
1217
+ interface CompanyEvalCase {
1218
+ /** Ticker as of the cutoff. */
1219
+ ticker: string;
1220
+ /** Legal name as of the cutoff (what the loop is told to research). */
1221
+ company: string;
1222
+ /** SEC Central Index Key (CIK), zero-stripped — the EDGAR filer id. */
1223
+ cik: string;
1224
+ /**
1225
+ * Research-as-of date (ISO). The loop must reason as if it is this date; every
1226
+ * `evidence` value was knowable on or before it. >= 18 months before this set
1227
+ * was curated, so the outcome is known but is NOT a checklist item.
1228
+ */
1229
+ cutoff: string;
1230
+ /** Sector, for coverage / the curation-bias disclosure. */
1231
+ sector: string;
1232
+ /**
1233
+ * The known POST-cutoff outcome — recorded for the reader ONLY, never graded.
1234
+ * Keeping it out of `facts` is what makes the set hindsight-free.
1235
+ */
1236
+ knownOutcome: string;
1237
+ /** The held-out material facts for this company. */
1238
+ facts: MaterialFact[];
770
1239
  }
771
1240
  /**
772
- * Build the deliverable (completion oracle) for a research supervisor: "settled
773
- * the knowledge base is ready". It re-reads the KB from disk and runs the
774
- * readiness gate, so the supervisor stops on the REAL grounded state — not on a
775
- * worker's self-report.
1241
+ * The eval set. 5 public companies, 5-8 held-out material facts each, every fact
1242
+ * grounded in a primary SEC EDGAR 10-K filed on or before the cutoff.
1243
+ *
1244
+ * CURATION-BIAS DISCLOSURE (full version in the doc): all five are companies
1245
+ * whose buried risks later materialized, because that is where the material-vs-
1246
+ * surface distinction is sharpest AND where the figures are easy to verify after
1247
+ * the fact. This biases the set toward downside risks (two of the eight lenses,
1248
+ * concentration + leverage, dominate) and toward distressed names. A production
1249
+ * eval would balance these with companies whose buried facts were POSITIVE
1250
+ * drivers and with survivors. This set is honest about that and reports the lens
1251
+ * distribution so the bias is measurable, not hidden.
776
1252
  */
777
- declare function knowledgeReadinessDeliverable(options: Pick<ResearchSupervisorOptions, 'root' | 'goal' | 'readinessSpecs' | 'readinessTaskId' | 'readiness'>): DeliverableSpec<unknown>;
1253
+ declare const investmentThesisSet: CompanyEvalCase[];
778
1254
  /**
779
- * A research supervisor: a SUPERVISOR brain creates the topology dynamically
780
- * how many researchers, split by sub-topic over the `Scope`, with the
781
- * knowledge-readiness gate as its stop condition.
782
- *
783
- * This is a thin wrapper over `supervise(profile, goal, opts)`. It composes the
784
- * existing pieces and builds nothing new: the researcher `AgentProfile` (from
785
- * `researcherProfile`) is the worker shape the supervisor spawns, and
786
- * `knowledgeReadinessDeliverable` (over `scoreKnowledgeReadiness`) is the
787
- * completion oracle that decides when the KB is ready.
788
- *
789
- * Needs creds (a real supervisor router brain + a worker backend), so it is the
790
- * LIVE path. The offline two-agent loop is `runTwoAgentResearchLoop`.
1255
+ * Grade ONE material fact against an investment thesis's full text. Returns
1256
+ * whether the thesis SURFACED it plus which expected groups were found. The
1257
+ * check is a deterministic case-insensitive substring scan $0, model-free,
1258
+ * reproducible — so the eval never leaks into a model the loop could observe.
791
1259
  */
792
- declare function runResearchSupervisor(options: ResearchSupervisorOptions): Promise<SupervisedResult<unknown>>;
793
-
794
- declare const SourceAnchorSchema: z.ZodObject<{
795
- id: z.ZodString;
796
- sourceId: z.ZodString;
797
- label: z.ZodOptional<z.ZodString>;
798
- page: z.ZodOptional<z.ZodNumber>;
799
- lineStart: z.ZodOptional<z.ZodNumber>;
800
- lineEnd: z.ZodOptional<z.ZodNumber>;
801
- charStart: z.ZodOptional<z.ZodNumber>;
802
- charEnd: z.ZodOptional<z.ZodNumber>;
803
- timestampMs: z.ZodOptional<z.ZodNumber>;
804
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
805
- }, z.core.$strip>;
806
- declare const SourceRecordSchema: z.ZodObject<{
807
- id: z.ZodString;
808
- uri: z.ZodString;
809
- title: z.ZodOptional<z.ZodString>;
810
- mediaType: z.ZodOptional<z.ZodString>;
811
- contentHash: z.ZodString;
812
- text: z.ZodOptional<z.ZodString>;
813
- anchors: z.ZodOptional<z.ZodArray<z.ZodObject<{
814
- id: z.ZodString;
815
- sourceId: z.ZodString;
816
- label: z.ZodOptional<z.ZodString>;
817
- page: z.ZodOptional<z.ZodNumber>;
818
- lineStart: z.ZodOptional<z.ZodNumber>;
819
- lineEnd: z.ZodOptional<z.ZodNumber>;
820
- charStart: z.ZodOptional<z.ZodNumber>;
821
- charEnd: z.ZodOptional<z.ZodNumber>;
822
- timestampMs: z.ZodOptional<z.ZodNumber>;
823
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
824
- }, z.core.$strip>>>;
825
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
826
- createdAt: z.ZodString;
827
- }, z.core.$strip>;
828
- declare const KnowledgePageSchema: z.ZodObject<{
829
- id: z.ZodString;
830
- path: z.ZodString;
831
- title: z.ZodString;
832
- text: z.ZodString;
833
- frontmatter: z.ZodRecord<z.ZodString, z.ZodUnknown>;
834
- sourceIds: z.ZodArray<z.ZodString>;
835
- tags: z.ZodArray<z.ZodString>;
836
- outLinks: z.ZodArray<z.ZodString>;
837
- }, z.core.$strip>;
838
- declare const KnowledgeGraphNodeSchema: z.ZodObject<{
839
- id: z.ZodString;
840
- title: z.ZodString;
841
- path: z.ZodString;
842
- tags: z.ZodArray<z.ZodString>;
843
- sourceIds: z.ZodArray<z.ZodString>;
844
- outDegree: z.ZodNumber;
845
- inDegree: z.ZodNumber;
846
- }, z.core.$strip>;
847
- declare const KnowledgeGraphEdgeSchema: z.ZodObject<{
848
- source: z.ZodString;
849
- target: z.ZodString;
850
- weight: z.ZodNumber;
851
- reasons: z.ZodArray<z.ZodString>;
852
- }, z.core.$strip>;
853
- declare const KnowledgeIndexSchema: z.ZodObject<{
854
- root: z.ZodString;
855
- generatedAt: z.ZodString;
856
- sources: z.ZodArray<z.ZodObject<{
857
- id: z.ZodString;
858
- uri: z.ZodString;
859
- title: z.ZodOptional<z.ZodString>;
860
- mediaType: z.ZodOptional<z.ZodString>;
861
- contentHash: z.ZodString;
862
- text: z.ZodOptional<z.ZodString>;
863
- anchors: z.ZodOptional<z.ZodArray<z.ZodObject<{
864
- id: z.ZodString;
865
- sourceId: z.ZodString;
866
- label: z.ZodOptional<z.ZodString>;
867
- page: z.ZodOptional<z.ZodNumber>;
868
- lineStart: z.ZodOptional<z.ZodNumber>;
869
- lineEnd: z.ZodOptional<z.ZodNumber>;
870
- charStart: z.ZodOptional<z.ZodNumber>;
871
- charEnd: z.ZodOptional<z.ZodNumber>;
872
- timestampMs: z.ZodOptional<z.ZodNumber>;
873
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
874
- }, z.core.$strip>>>;
875
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
876
- createdAt: z.ZodString;
877
- }, z.core.$strip>>;
878
- pages: z.ZodArray<z.ZodObject<{
879
- id: z.ZodString;
880
- path: z.ZodString;
881
- title: z.ZodString;
882
- text: z.ZodString;
883
- frontmatter: z.ZodRecord<z.ZodString, z.ZodUnknown>;
884
- sourceIds: z.ZodArray<z.ZodString>;
885
- tags: z.ZodArray<z.ZodString>;
886
- outLinks: z.ZodArray<z.ZodString>;
887
- }, z.core.$strip>>;
888
- graph: z.ZodObject<{
889
- nodes: z.ZodArray<z.ZodObject<{
890
- id: z.ZodString;
891
- title: z.ZodString;
892
- path: z.ZodString;
893
- tags: z.ZodArray<z.ZodString>;
894
- sourceIds: z.ZodArray<z.ZodString>;
895
- outDegree: z.ZodNumber;
896
- inDegree: z.ZodNumber;
897
- }, z.core.$strip>>;
898
- edges: z.ZodArray<z.ZodObject<{
899
- source: z.ZodString;
900
- target: z.ZodString;
901
- weight: z.ZodNumber;
902
- reasons: z.ZodArray<z.ZodString>;
903
- }, z.core.$strip>>;
904
- }, z.core.$strip>;
905
- }, z.core.$strip>;
906
- declare const KnowledgeEventSchema: z.ZodObject<{
907
- id: z.ZodString;
908
- type: z.ZodEnum<{
909
- "source.added": "source.added";
910
- "proposal.applied": "proposal.applied";
911
- "index.built": "index.built";
912
- "lint.run": "lint.run";
913
- "optimization.run": "optimization.run";
914
- "release.promoted": "release.promoted";
915
- "release.rejected": "release.rejected";
916
- }>;
917
- createdAt: z.ZodString;
918
- actor: z.ZodOptional<z.ZodString>;
919
- target: z.ZodOptional<z.ZodString>;
920
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
921
- }, z.core.$strip>;
922
- declare const KnowledgeBaseCandidateSchema: z.ZodObject<{
923
- id: z.ZodString;
924
- units: z.ZodArray<z.ZodObject<{
925
- id: z.ZodString;
926
- title: z.ZodString;
927
- text: z.ZodString;
928
- claims: z.ZodOptional<z.ZodArray<z.ZodObject<{
929
- id: z.ZodString;
930
- text: z.ZodString;
931
- refs: z.ZodArray<z.ZodObject<{
932
- sourceId: z.ZodString;
933
- anchorId: z.ZodOptional<z.ZodString>;
934
- quote: z.ZodOptional<z.ZodString>;
935
- }, z.core.$strip>>;
936
- confidence: z.ZodOptional<z.ZodNumber>;
937
- status: z.ZodOptional<z.ZodEnum<{
938
- draft: "draft";
939
- active: "active";
940
- superseded: "superseded";
941
- rejected: "rejected";
942
- }>>;
943
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
944
- }, z.core.$strip>>>;
945
- relations: z.ZodOptional<z.ZodArray<z.ZodObject<{
946
- sourceId: z.ZodString;
947
- targetId: z.ZodString;
948
- predicate: z.ZodString;
949
- weight: z.ZodOptional<z.ZodNumber>;
950
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
951
- }, z.core.$strip>>>;
952
- sourceIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
953
- tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
954
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
955
- updatedAt: z.ZodOptional<z.ZodString>;
956
- }, z.core.$strip>>;
957
- retrievalPolicy: z.ZodOptional<z.ZodString>;
958
- synthesisPolicy: z.ZodOptional<z.ZodString>;
959
- questionPolicy: z.ZodOptional<z.ZodString>;
960
- updatePolicy: z.ZodOptional<z.ZodString>;
961
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
962
- }, z.core.$strip>;
963
-
964
- declare function searchKnowledge(index: KnowledgeIndex, query: string, limit?: number): KnowledgeSearchResult[];
965
- declare function tokenizeQuery(query: string): string[];
966
- declare function reciprocalRankFusion(rankLists: string[][], k?: number): Map<string, number>;
1260
+ declare function gradeFactAgainstText(fact: MaterialFact, thesisText: string): {
1261
+ surfaced: boolean;
1262
+ groupsFound: number;
1263
+ groupsTotal: number;
1264
+ foundLabels: string[];
1265
+ };
1266
+ /** Grade a whole company's thesis text: how many of its held-out facts it surfaces. */
1267
+ declare function gradeCompanyAgainstText(company: CompanyEvalCase, thesisText: string): {
1268
+ surfaced: number;
1269
+ total: number;
1270
+ perFact: ReturnType<typeof gradeFactAgainstText>[];
1271
+ };
1272
+ /** Total held-out facts across the set (the denominator the doc reports). */
1273
+ declare function totalMaterialFacts(set?: CompanyEvalCase[]): number;
1274
+ /** Count facts per lens across the set — used to report (and bound) curation bias. */
1275
+ declare function lensDistribution(set?: CompanyEvalCase[]): Record<MaterialFactLens, number>;
967
1276
 
968
- interface KnowledgeLayout {
969
- root: string;
970
- knowledgeDir: string;
971
- rawSourcesDir: string;
972
- sourceRegistryPath: string;
973
- indexPath: string;
974
- logPath: string;
975
- cacheDir: string;
976
- }
977
- declare function layoutFor(root: string): KnowledgeLayout;
978
1277
  /**
979
- * Filenames that `initKnowledgeBase` writes as human-navigation scaffolding.
980
- * These are excluded from the page index — they exist on disk so authors can
981
- * curate their vault, but they are not searchable content.
1278
+ * The INVESTMENT-THESIS research task.
982
1279
  *
983
- * Add new scaffold filenames here (and only here) to keep lint, validate, viz,
984
- * and the indexer consistent.
985
- */
986
- declare const SCAFFOLD_PAGE_BASENAMES: readonly string[];
987
- /**
988
- * True when a knowledge-relative path points at a scaffold file rather than
989
- * authored content. Accepts both repo-relative paths (`knowledge/index.md`)
990
- * and any nested `<dir>/index.md` or `<dir>/log.md` (for example
991
- * `knowledge/concepts/index.md`) so subdirectory README-style scaffolds are
992
- * also excluded.
1280
+ * Given `{ company, ticker, cik, cutoff }`, drive the SAME two-agent research
1281
+ * loop the ML deep-question A/B uses (`runVerifiedResearchLoop` + the real web
1282
+ * worker) to research the company AS OF the cutoff — web + SEC EDGAR, both public
1283
+ * — and produce an investment-thesis PAGE in the knowledge base: a judgment, the
1284
+ * drivers, and the risks, grounded in what it fetched.
1285
+ *
1286
+ * This file builds NOTHING new for the loop: it composes the existing worker +
1287
+ * driver + loop, supplies the readiness specs that steer the worker toward the
1288
+ * filing-level evidence (the analyst lenses), then writes a synthesis thesis page
1289
+ * the metric (`materialFactsSurfaced`) grades against the HELD-OUT checklist.
1290
+ *
1291
+ * THE FIREWALL: the task is told ONLY company + ticker + cutoff (+ the generic
1292
+ * analyst-lens readiness specs every company gets). It is NEVER shown the
1293
+ * checklist. The checklist is read only afterward, by the metric. So a high score
1294
+ * is research depth, not teaching-to-the-test.
993
1295
  */
994
- declare function isScaffoldPath(path: string): boolean;
995
- declare function initKnowledgeBase(root: string): Promise<KnowledgeLayout>;
996
- declare function loadKnowledgePages(root: string): Promise<KnowledgePage[]>;
997
- declare function writeJson(path: string, value: unknown): Promise<void>;
998
1296
 
999
- /**
1000
- * A knowledge gap the loop surfaces from `scoreKnowledgeReadiness`. The worker
1001
- * targets these; the driver folds the unfilled remainder into the worker's next
1002
- * prompt and runs its own gap-fill pass over them.
1003
- */
1004
- interface KnowledgeGap {
1005
- /** Readiness-spec id this gap belongs to. */
1006
- id: string;
1007
- /** Human-readable description of what's missing. */
1008
- description: string;
1009
- /** The search query the readiness check ran for this requirement. */
1010
- query: string;
1011
- /** True when the gap blocks readiness (vs. a soft, non-blocking gap). */
1012
- blocking: boolean;
1297
+ /** The minimal brief a thesis run is given — the firewall boundary. */
1298
+ interface ThesisTaskInput {
1299
+ /** Legal name as of the cutoff what the loop researches. */
1300
+ company: string;
1301
+ /** Ticker as of the cutoff. */
1302
+ ticker: string;
1303
+ /** SEC Central Index Key (CIK), zero-stripped the EDGAR filer id. */
1304
+ cik: string;
1305
+ /** Research-as-of date (ISO). The loop must reason as if it is this date. */
1306
+ cutoff: string;
1307
+ /** Sector, for the readiness query context (NOT a checklist hint). */
1308
+ sector?: string;
1013
1309
  }
1014
- /** A new source the worker (or driver) discovered and wants to add to the KB. */
1015
- type ResearchSourceProposal = AddSourceTextInput;
1016
1310
  /**
1017
- * What a research agent contributes in one round. Both the worker and (when
1018
- * `driverResearches` is on) the driver produce this shape the worker ADDS
1019
- * primary findings, the driver gap-FILLS the ones the worker missed.
1311
+ * The generic analyst-lens readiness specs every company gets. They are the ONLY
1312
+ * thing the loop is told about WHAT to look for, and they name the LENSES a
1313
+ * thorough analyst checks (balance-sheet risk, concentration, leverage, margins,
1314
+ * liquidity, governance, regulatory) and where they live (the latest SEC 10-K) —
1315
+ * NOT the answers. They steer the worker's web/EDGAR search toward the filing,
1316
+ * not toward the held-out facts (which the loop never sees).
1020
1317
  *
1021
- * `proposalText` is the safe write-protocol text (`---FILE: knowledge/...---`
1022
- * blocks). The loop only applies it AFTER the driver has verified the round's
1023
- * sources, so a rejected source never reaches the curated pages.
1318
+ * `minSources` is set above 1 so the readiness gate stays UNMET after a single
1319
+ * fetch and the loop runs multiple rounds the depth-driving driver needs >1
1320
+ * round to steer, exactly as the ML-exam multi-round probe established.
1321
+ */
1322
+ declare function thesisReadinessSpecs(input: ThesisTaskInput): KnowledgeReadinessSpec[];
1323
+ interface ThesisRunOptions {
1324
+ /** The KB root the loop writes into. */
1325
+ root: string;
1326
+ /** Shared router client (web search + chat). Defaults to env creds. */
1327
+ router: RouterClient;
1328
+ /** The driver — verify/dedup or research-driving. The loop's coordinator. */
1329
+ driver: ResearchDriver;
1330
+ /** Round budget. Default 3 (the depth-driving driver needs >1). */
1331
+ maxRounds?: number;
1332
+ /** Worker tuning forwarded to `createWebResearchWorker`. */
1333
+ workerOptions?: Omit<WebResearchWorkerOptions, 'router'>;
1334
+ /** Max tokens for the synthesis pass. Default 1600 (above glm-5.2's reasoning floor). */
1335
+ synthesisMaxTokens?: number;
1336
+ signal?: AbortSignal;
1337
+ }
1338
+ interface ThesisRunResult {
1339
+ loop: VerifiedResearchLoopResult;
1340
+ /** The synthesized thesis text. */
1341
+ thesis: string;
1342
+ /** Path of the thesis page written into the KB. */
1343
+ thesisPath: string;
1344
+ }
1345
+ /**
1346
+ * Run the full thesis task: drive the two-agent loop to research the company AS
1347
+ * OF the cutoff, then synthesize + write the thesis page. Returns the loop result
1348
+ * + the thesis text + the page path. The caller grades the KB with
1349
+ * `materialFactsSurfaced(root, checklist)` — the checklist is never passed here.
1024
1350
  */
1025
- interface ResearchContribution {
1026
- /** Immutable sources to register (the raw evidence). */
1027
- sources?: ResearchSourceProposal[];
1028
- /** Safe write-protocol text producing curated `knowledge/*.md` pages. */
1351
+ declare function runInvestmentThesisTask(input: ThesisTaskInput, options: ThesisRunOptions): Promise<ThesisRunResult>;
1352
+
1353
+ interface ApplyWriteBlocksResult {
1354
+ written: string[];
1355
+ warnings: string[];
1356
+ }
1357
+ declare function applyKnowledgeWriteBlocks(root: string, proposalText: string): Promise<ApplyWriteBlocksResult>;
1358
+ declare function applyKnowledgeWriteBlocksFile(root: string, proposalPath: string): Promise<ApplyWriteBlocksResult>;
1359
+
1360
+ interface ValidateKnowledgeOptions {
1361
+ strict?: boolean;
1362
+ }
1363
+ interface ValidateKnowledgeResult {
1364
+ ok: boolean;
1365
+ findings: KnowledgeLintFinding[];
1366
+ }
1367
+ declare function validateKnowledgeIndex(index: KnowledgeIndex, options?: ValidateKnowledgeOptions): ValidateKnowledgeResult;
1368
+
1369
+ interface KnowledgeResearchLoopContext {
1370
+ root: string;
1371
+ goal: string;
1372
+ iteration: number;
1373
+ index: KnowledgeIndex;
1374
+ lintFindings: KnowledgeLintFinding[];
1375
+ validation: ValidateKnowledgeResult;
1376
+ readiness?: EvalKnowledgeBundleBuildResult;
1377
+ previousSteps: KnowledgeResearchLoopStep[];
1378
+ signal?: AbortSignal;
1379
+ }
1380
+ interface KnowledgeResearchLoopDecision {
1381
+ /**
1382
+ * Free-form notes from the researcher. Keep this human-readable; products can
1383
+ * store it as the research transcript.
1384
+ */
1385
+ notes?: string;
1386
+ /**
1387
+ * Local files to register as immutable sources before applying proposals.
1388
+ */
1389
+ sourcePaths?: string[];
1390
+ /**
1391
+ * Textual source artifacts discovered by an agent, browser worker, connector,
1392
+ * or deep-research process.
1393
+ */
1394
+ sourceTexts?: AddSourceTextInput[];
1395
+ /**
1396
+ * Safe write protocol text. The loop parses and applies only accepted
1397
+ * `---FILE: knowledge/...---` blocks.
1398
+ */
1029
1399
  proposalText?: string;
1030
1400
  /**
1031
- * Build the page write-protocol text FROM the sources the driver accepted
1032
- * the curated, citing pages the readiness gate searches. Receives the
1033
- * registered `SourceRecord`s (with their assigned ids, so a page's frontmatter
1034
- * `sources:` can cite them). Returns `---FILE: knowledge/...---` block text or
1035
- * `undefined`. Runs after verification, so a page never cites a rejected
1036
- * source. Concatenated after any static `proposalText`.
1401
+ * The researcher decides when the wiki is good enough. The loop deliberately
1402
+ * does not encode a domain-specific definition of "done".
1037
1403
  */
1038
- buildPages?: (acceptedSources: SourceRecord[]) => string | undefined;
1039
- /** Free-form research transcript — products can persist this. */
1404
+ done?: boolean;
1405
+ metadata?: Record<string, unknown>;
1406
+ }
1407
+ interface KnowledgeResearchLoopStep {
1408
+ iteration: number;
1040
1409
  notes?: string;
1410
+ addedSources: SourceRecord[];
1411
+ applied?: ApplyWriteBlocksResult;
1412
+ lintFindings: KnowledgeLintFinding[];
1413
+ validation: ValidateKnowledgeResult;
1414
+ readiness?: EvalKnowledgeBundleBuildResult;
1415
+ event: KnowledgeEvent;
1416
+ done: boolean;
1041
1417
  metadata?: Record<string, unknown>;
1042
1418
  }
1043
- /** Context handed to the worker each round. */
1044
- interface WorkerResearchContext {
1419
+ interface RunKnowledgeResearchLoopOptions {
1045
1420
  root: string;
1046
1421
  goal: string;
1047
- round: number;
1048
- index: KnowledgeIndex;
1049
- /** Gaps the readiness gate currently reports — what the worker should close. */
1050
- gaps: KnowledgeGap[];
1051
- /** Steer text the driver folded in from the previous round's remaining gaps. */
1052
- steer?: string;
1053
- readiness: EvalKnowledgeBundleBuildResult;
1422
+ maxIterations?: number;
1423
+ actor?: string;
1424
+ strict?: ValidateKnowledgeOptions['strict'];
1425
+ readinessSpecs?: KnowledgeReadinessSpec[];
1426
+ readinessTaskId?: string;
1427
+ readiness?: Omit<BuildEvalKnowledgeBundleOptions, 'taskId' | 'index' | 'specs'>;
1428
+ sourceOptions?: Pick<AddSourceOptions, 'adapters' | 'now'>;
1054
1429
  signal?: AbortSignal;
1430
+ step(context: KnowledgeResearchLoopContext): Promise<KnowledgeResearchLoopDecision> | KnowledgeResearchLoopDecision;
1431
+ onStep?: (step: KnowledgeResearchLoopStep) => Promise<void> | void;
1055
1432
  }
1056
- /** Context handed to the driver's verifier for one candidate source. */
1057
- interface SourceVerificationContext {
1433
+ interface KnowledgeResearchLoopResult {
1058
1434
  root: string;
1059
1435
  goal: string;
1060
- round: number;
1436
+ iterations: number;
1437
+ done: boolean;
1061
1438
  index: KnowledgeIndex;
1062
- gaps: KnowledgeGap[];
1063
- /** Sources already accepted earlier THIS round (in-round dedup). */
1064
- acceptedThisRound: ResearchSourceProposal[];
1065
- signal?: AbortSignal;
1066
- }
1067
- /** A single rejected source plus the reason the driver gave. */
1068
- interface RejectedSource {
1069
- source: ResearchSourceProposal;
1070
- reason: string;
1439
+ lintFindings: KnowledgeLintFinding[];
1440
+ validation: ValidateKnowledgeResult;
1441
+ readiness?: EvalKnowledgeBundleBuildResult;
1442
+ steps: KnowledgeResearchLoopStep[];
1071
1443
  }
1072
- /** Context handed to the driver's gap-fill pass (only when `driverResearches`). */
1073
- interface DriverResearchContext {
1444
+ type KnowledgeControlLoopState = KnowledgeResearchLoopContext;
1445
+ type KnowledgeControlLoopAction = KnowledgeResearchLoopDecision;
1446
+ type KnowledgeControlLoopActionResult = KnowledgeResearchLoopStep;
1447
+ interface KnowledgeControlLoopAdapterOptions {
1074
1448
  root: string;
1075
1449
  goal: string;
1076
- round: number;
1077
- index: KnowledgeIndex;
1078
- /** Gaps STILL open after the worker's accepted contribution applied. */
1079
- remainingGaps: KnowledgeGap[];
1080
- readiness: EvalKnowledgeBundleBuildResult;
1081
- signal?: AbortSignal;
1450
+ actor?: string;
1451
+ strict?: ValidateKnowledgeOptions['strict'];
1452
+ readinessSpecs?: KnowledgeReadinessSpec[];
1453
+ readinessTaskId?: string;
1454
+ readiness?: Omit<BuildEvalKnowledgeBundleOptions, 'taskId' | 'index' | 'specs'>;
1455
+ sourceOptions?: Pick<AddSourceOptions, 'adapters' | 'now'>;
1082
1456
  }
1457
+ type KnowledgeControlLoopAdapter = Pick<ControlRuntimeConfig<KnowledgeControlLoopState, KnowledgeControlLoopAction, KnowledgeControlLoopActionResult, ControlEvalResult>, 'intent' | 'observe' | 'validate' | 'act' | 'shouldStop'>;
1083
1458
  /**
1084
- * The differentiated driver role.
1085
- *
1086
- * - `verifySource` the gate the worker's additions pass before they commit.
1087
- * Return `{ accept: true }` to keep a source or `{ accept: false, reason }`
1088
- * to reject it (not real / not relevant / duplicate). The loop dedups exact
1089
- * duplicates (same `uri` already in the KB or accepted this round) BEFORE
1090
- * calling this, so the verifier only sees genuinely-new candidates.
1091
- * - `research` — the driver's OWN gap-fill pass over the gaps the worker left
1092
- * open. Only invoked when `driverResearches` is true.
1093
- * - `foldGaps` — turn the remaining gaps into a steer string for the worker's
1094
- * next prompt. Defaults to a compact bulleted list when omitted.
1459
+ * Adapter for running knowledge growth through `agent-eval`'s generic control
1460
+ * runtime. The caller still owns `decide`: that can be a proposer agent,
1461
+ * reviewer agent, deterministic policy, or a composition of all three.
1095
1462
  */
1096
- interface ResearchDriver {
1097
- verifySource(source: ResearchSourceProposal, ctx: SourceVerificationContext): Promise<SourceVerdict> | SourceVerdict;
1098
- research?(ctx: DriverResearchContext): Promise<ResearchContribution> | ResearchContribution;
1099
- foldGaps?(gaps: KnowledgeGap[]): string;
1463
+ declare function createKnowledgeControlLoopAdapter(options: KnowledgeControlLoopAdapterOptions): KnowledgeControlLoopAdapter;
1464
+ declare function runKnowledgeResearchLoop(options: RunKnowledgeResearchLoopOptions): Promise<KnowledgeResearchLoopResult>;
1465
+
1466
+ type RagKnowledgeImprovementPhase = 'retrieval-tuning' | 'gap-diagnosis' | 'knowledge-acquisition' | 'knowledge-update' | 'answer-quality' | 'promotion';
1467
+ type RagKnowledgeImprovementPhaseStatus = 'completed' | 'skipped' | 'failed';
1468
+ type RagGapKind = 'missing-source' | 'stale-source' | 'retrieval-miss' | 'retrieval-noise' | 'chunking-mismatch' | 'missing-multihop-evidence' | 'generator-unsupported-claim' | 'citation-mismatch' | 'incorrect-abstention' | 'unknown';
1469
+ type RagGapSeverity = 'info' | 'warning' | 'error' | 'critical';
1470
+ interface RagGapFinding {
1471
+ id: string;
1472
+ kind: RagGapKind;
1473
+ severity: RagGapSeverity;
1474
+ message: string;
1475
+ scenarioId?: string;
1476
+ evidence?: Record<string, JsonValue>;
1477
+ }
1478
+ interface RagKnowledgeImprovementPhaseResult {
1479
+ phase: RagKnowledgeImprovementPhase;
1480
+ status: RagKnowledgeImprovementPhaseStatus;
1481
+ summary: string;
1482
+ startedAt: string;
1483
+ finishedAt: string;
1484
+ metadata?: Record<string, JsonValue>;
1100
1485
  }
1101
- type SourceVerdict = {
1102
- accept: true;
1103
- } | {
1104
- accept: false;
1486
+ interface RagPhaseInputBase {
1487
+ goal: string;
1488
+ phases: readonly RagKnowledgeImprovementPhaseResult[];
1489
+ signal?: AbortSignal;
1490
+ }
1491
+ interface RagDiagnosisInput extends RagPhaseInputBase {
1492
+ retrieval?: RunRetrievalImprovementLoopResult;
1493
+ }
1494
+ interface RagKnowledgeAcquisitionInput extends RagPhaseInputBase {
1495
+ retrieval?: RunRetrievalImprovementLoopResult;
1496
+ findings: readonly RagGapFinding[];
1497
+ }
1498
+ interface RagKnowledgeUpdateInput extends RagPhaseInputBase {
1499
+ retrieval?: RunRetrievalImprovementLoopResult;
1500
+ findings: readonly RagGapFinding[];
1501
+ acquisition?: KnowledgeResearchLoopDecision;
1502
+ }
1503
+ interface RagKnowledgeUpdateResult {
1504
+ applied: boolean;
1505
+ summary: string;
1506
+ research?: KnowledgeResearchLoopResult;
1507
+ metadata?: Record<string, JsonValue>;
1508
+ }
1509
+ interface RagAnswerQualityInput extends RagPhaseInputBase {
1510
+ retrieval?: RunRetrievalImprovementLoopResult;
1511
+ findings: readonly RagGapFinding[];
1512
+ acquisition?: KnowledgeResearchLoopDecision;
1513
+ knowledgeUpdate?: RagKnowledgeUpdateResult;
1514
+ }
1515
+ interface RagAnswerQualityResult {
1516
+ passed: boolean;
1517
+ metrics: Record<string, number>;
1518
+ findings?: readonly RagGapFinding[];
1519
+ metadata?: Record<string, JsonValue>;
1520
+ }
1521
+ interface RagPromotionInput extends RagPhaseInputBase {
1522
+ retrieval?: RunRetrievalImprovementLoopResult;
1523
+ findings: readonly RagGapFinding[];
1524
+ acquisition?: KnowledgeResearchLoopDecision;
1525
+ knowledgeUpdate?: RagKnowledgeUpdateResult;
1526
+ answerQuality?: RagAnswerQualityResult;
1527
+ }
1528
+ interface RagPromotionResult {
1529
+ promoted: boolean;
1105
1530
  reason: string;
1106
- };
1107
- /** The worker: primary research targeting the round's gaps. */
1108
- type ResearchWorker = (ctx: WorkerResearchContext) => Promise<ResearchContribution> | ResearchContribution;
1109
- interface TwoAgentResearchLoopOptions {
1531
+ metadata?: Record<string, JsonValue>;
1532
+ }
1533
+ interface RagKnowledgeResearchOptions extends Omit<RunKnowledgeResearchLoopOptions, 'goal' | 'signal' | 'step'> {
1534
+ goal?: string;
1535
+ step?: RunKnowledgeResearchLoopOptions['step'];
1536
+ }
1537
+ interface RunRagKnowledgeImprovementLoopOptions {
1538
+ goal: string;
1539
+ retrieval?: RunRetrievalImprovementLoopOptions;
1540
+ diagnose?: (input: RagDiagnosisInput) => MaybePromise$1<readonly RagGapFinding[]>;
1541
+ acquireKnowledge?: (input: RagKnowledgeAcquisitionInput) => MaybePromise$1<KnowledgeResearchLoopDecision>;
1542
+ knowledgeResearch?: RagKnowledgeResearchOptions;
1543
+ updateKnowledge?: (input: RagKnowledgeUpdateInput) => MaybePromise$1<RagKnowledgeUpdateResult>;
1544
+ evaluateAnswers?: (input: RagAnswerQualityInput) => MaybePromise$1<RagAnswerQualityResult>;
1545
+ promote?: (input: RagPromotionInput) => MaybePromise$1<RagPromotionResult>;
1546
+ enabledPhases?: readonly RagKnowledgeImprovementPhase[];
1547
+ requiredPhases?: readonly RagKnowledgeImprovementPhase[];
1548
+ signal?: AbortSignal;
1549
+ now?: () => Date;
1550
+ }
1551
+ interface RunRagKnowledgeImprovementLoopResult {
1552
+ goal: string;
1553
+ phases: readonly RagKnowledgeImprovementPhaseResult[];
1554
+ retrieval?: RunRetrievalImprovementLoopResult;
1555
+ findings: readonly RagGapFinding[];
1556
+ acquisition?: KnowledgeResearchLoopDecision;
1557
+ knowledgeUpdate?: RagKnowledgeUpdateResult;
1558
+ answerQuality?: RagAnswerQualityResult;
1559
+ promotion?: RagPromotionResult;
1560
+ }
1561
+ type MaybePromise$1<T> = T | Promise<T>;
1562
+ declare function runRagKnowledgeImprovementLoop(options: RunRagKnowledgeImprovementLoopOptions): Promise<RunRagKnowledgeImprovementLoopResult>;
1563
+
1564
+ type RagEvalProvider = 'agent-knowledge' | 'ragas' | 'deepeval' | 'trulens' | 'ragchecker' | 'custom';
1565
+ type RagEvalMetricKey = 'context_precision' | 'context_recall' | 'context_relevance' | 'context_sufficiency' | 'faithfulness' | 'groundedness' | 'answer_relevance' | 'answer_correctness' | 'citation_support' | 'abstention' | 'unsupported_answer_rate';
1566
+ type RagEvalSlice = 'known-answer' | 'paraphrase' | 'distractor' | 'freshness' | 'multi-source' | 'unanswerable' | 'long-tail' | 'custom';
1567
+ interface RagEvalContext {
1568
+ id: string;
1569
+ text: string;
1570
+ rank?: number;
1571
+ pageId?: string;
1572
+ sourceId?: string;
1573
+ anchorId?: string;
1574
+ stale?: boolean;
1575
+ metadata?: Record<string, JsonValue>;
1576
+ }
1577
+ interface RagEvalCitation {
1578
+ id: string;
1579
+ claimId?: string;
1580
+ contextId?: string;
1581
+ pageId?: string;
1582
+ sourceId?: string;
1583
+ anchorId?: string;
1584
+ quote?: string;
1585
+ metadata?: Record<string, JsonValue>;
1586
+ }
1587
+ interface RagEvalClaim {
1588
+ id: string;
1589
+ text: string;
1590
+ citationIds?: readonly string[];
1591
+ metadata?: Record<string, JsonValue>;
1592
+ }
1593
+ interface RagRequiredContext {
1594
+ id?: string;
1595
+ text?: string;
1596
+ pageId?: string;
1597
+ sourceId?: string;
1598
+ anchorId?: string;
1599
+ }
1600
+ interface RagAnswerEvalScenario extends Scenario {
1601
+ kind: 'rag-answer-eval';
1602
+ query: string;
1603
+ referenceAnswer?: string;
1604
+ expectedClaims?: readonly string[];
1605
+ forbiddenClaims?: readonly string[];
1606
+ requiredContext?: readonly RagRequiredContext[];
1607
+ unanswerable?: boolean;
1608
+ requireCitations?: boolean;
1609
+ slices?: readonly RagEvalSlice[];
1610
+ thresholds?: Partial<Record<RagEvalMetricKey, number>>;
1611
+ }
1612
+ interface ExternalRagEvalScore {
1613
+ provider: RagEvalProvider | string;
1614
+ scores: Record<string, number>;
1615
+ reasons?: Record<string, string>;
1616
+ metadata?: Record<string, JsonValue>;
1617
+ }
1618
+ interface RagAnswerEvalArtifact {
1619
+ query: string;
1620
+ answer: string;
1621
+ contexts: readonly RagEvalContext[];
1622
+ claims?: readonly RagEvalClaim[];
1623
+ citations?: readonly RagEvalCitation[];
1624
+ abstained?: boolean;
1625
+ durationMs?: number;
1626
+ costUsd?: number;
1627
+ externalScores?: readonly ExternalRagEvalScore[];
1628
+ metadata?: Record<string, JsonValue>;
1629
+ }
1630
+ interface RagAnswerMetricSummary {
1631
+ metrics: Record<RagEvalMetricKey, number>;
1632
+ composite: number;
1633
+ passed: boolean;
1634
+ findings: readonly RagGapFinding[];
1635
+ claimCount: number;
1636
+ supportedClaimCount: number;
1637
+ citedClaimCount: number;
1638
+ supportedCitationCount: number;
1639
+ matchedRequiredContextCount: number;
1640
+ requiredContextCount: number;
1641
+ providerScores: Record<string, Record<RagEvalMetricKey, number>>;
1642
+ }
1643
+ interface RagAnswerQualityJudgeOptions {
1644
+ name?: string;
1645
+ thresholds?: Partial<Record<RagEvalMetricKey, number>>;
1646
+ weights?: Partial<Record<RagEvalMetricKey, number>>;
1647
+ externalScorePolicy?: 'prefer-external' | 'deterministic-first';
1648
+ minClaimSupport?: number;
1649
+ }
1650
+ interface RagAnswerEvalCase {
1651
+ scenario: RagAnswerEvalScenario;
1652
+ artifact: RagAnswerEvalArtifact;
1653
+ }
1654
+ interface RagAnswerQualityHookOptions {
1655
+ scenarios: readonly RagAnswerEvalScenario[];
1656
+ run: (scenario: RagAnswerEvalScenario) => MaybePromise<RagAnswerEvalArtifact>;
1657
+ externalEvaluator?: (item: RagAnswerEvalCase) => MaybePromise<ExternalRagEvalScore | readonly ExternalRagEvalScore[] | undefined>;
1658
+ thresholds?: Partial<Record<RagEvalMetricKey, number>>;
1659
+ weights?: Partial<Record<RagEvalMetricKey, number>>;
1660
+ }
1661
+ interface RagCalibrationOptions {
1662
+ scenario: RagAnswerEvalScenario;
1663
+ strong: RagAnswerEvalArtifact;
1664
+ weak: RagAnswerEvalArtifact;
1665
+ judge?: JudgeConfig<RagAnswerEvalArtifact, RagAnswerEvalScenario>;
1666
+ minStrongScore?: number;
1667
+ maxWeakScore?: number;
1668
+ signal?: AbortSignal;
1669
+ }
1670
+ interface RagCalibrationResult {
1671
+ passed: boolean;
1672
+ strongScore: number;
1673
+ weakScore: number;
1674
+ gap: number;
1675
+ }
1676
+ interface KnowledgeBaseQualityOptions {
1677
+ now?: Date;
1678
+ strict?: boolean;
1679
+ minCitationRate?: number;
1680
+ maxStaleSourceRate?: number;
1681
+ }
1682
+ interface KnowledgeBaseQualityReport {
1683
+ ok: boolean;
1684
+ metrics: {
1685
+ page_count: number;
1686
+ source_count: number;
1687
+ citation_rate: number;
1688
+ source_backed_page_rate: number;
1689
+ stale_source_rate: number;
1690
+ duplicate_source_hash_rate: number;
1691
+ lint_error_count: number;
1692
+ lint_warning_count: number;
1693
+ };
1694
+ findings: readonly RagGapFinding[];
1695
+ }
1696
+ type MaybePromise<T> = T | Promise<T>;
1697
+ declare function ragAnswerQualityJudge(options?: RagAnswerQualityJudgeOptions): JudgeConfig<RagAnswerEvalArtifact, RagAnswerEvalScenario>;
1698
+ declare function scoreRagAnswerArtifact(artifact: RagAnswerEvalArtifact, scenario: RagAnswerEvalScenario, options?: RagAnswerQualityJudgeOptions): RagAnswerMetricSummary;
1699
+ declare function diagnoseRagAnswerFailure(metrics: Record<RagEvalMetricKey, number>, scenario: RagAnswerEvalScenario, thresholds?: Partial<Record<RagEvalMetricKey, number>>): RagGapFinding[];
1700
+ declare function createRagAnswerQualityHook(options: RagAnswerQualityHookOptions): () => Promise<RagAnswerQualityResult>;
1701
+ declare function calibrateRagAnswerJudge(options: RagCalibrationOptions): Promise<RagCalibrationResult>;
1702
+ declare function normalizeExternalRagScores(scores: readonly ExternalRagEvalScore[]): Record<string, Record<RagEvalMetricKey, number>>;
1703
+ declare function toRagasEvaluationRows(cases: readonly RagAnswerEvalCase[]): {
1704
+ user_input: string;
1705
+ response: string;
1706
+ retrieved_contexts: string[];
1707
+ reference: string | undefined;
1708
+ reference_contexts: string[];
1709
+ }[];
1710
+ declare function toDeepEvalTestCases(cases: readonly RagAnswerEvalCase[]): {
1711
+ input: string;
1712
+ actual_output: string;
1713
+ expected_output: string | undefined;
1714
+ retrieval_context: string[];
1715
+ context: string[];
1716
+ }[];
1717
+ declare function toTruLensRecords(cases: readonly RagAnswerEvalCase[]): {
1718
+ input: string;
1719
+ output: string;
1720
+ context: string;
1721
+ }[];
1722
+ declare function toRagCheckerRecords(cases: readonly RagAnswerEvalCase[]): {
1723
+ query_id: string;
1724
+ query: string;
1725
+ gt_answer: string | undefined;
1726
+ response: string;
1727
+ retrieved_context: {
1728
+ doc_id: string;
1729
+ text: string;
1730
+ }[];
1731
+ claims: string[];
1732
+ }[];
1733
+ declare function scoreKnowledgeBaseIndex(index: KnowledgeIndex, options?: KnowledgeBaseQualityOptions): KnowledgeBaseQualityReport;
1734
+
1735
+ type KnowledgeImprovementStatus = 'running' | 'candidate-ready' | 'promoted' | 'rejected' | 'blocked';
1736
+ interface KnowledgeImprovementMetric {
1737
+ score: number;
1738
+ passed: boolean;
1739
+ dimensions?: Record<string, number>;
1740
+ notes?: string;
1741
+ }
1742
+ interface KnowledgeImprovementEvaluationInput {
1743
+ runId: string;
1744
+ iteration: number;
1745
+ root: string;
1746
+ baselineRoot: string;
1747
+ candidateRoot: string;
1748
+ baselineIndex: KnowledgeIndex;
1749
+ candidateIndex: KnowledgeIndex;
1750
+ baseHash: string;
1751
+ candidateHash: string;
1752
+ validation: ValidateKnowledgeResult;
1753
+ readiness?: EvalKnowledgeBundleBuildResult;
1754
+ kbQuality: KnowledgeBaseQualityReport;
1755
+ lifecycle?: RunRagKnowledgeImprovementLoopResult;
1756
+ signal?: AbortSignal;
1757
+ }
1758
+ type KnowledgeImprovementEvaluator = (input: KnowledgeImprovementEvaluationInput) => Promise<KnowledgeImprovementMetric> | KnowledgeImprovementMetric;
1759
+ interface KnowledgeImprovementLifecycleRecord {
1760
+ stage: 'candidate-update' | 'candidate-evaluation';
1761
+ phases: readonly RagKnowledgeImprovementPhaseResult[];
1762
+ findingCount: number;
1763
+ retrievalWinnerConfig?: RetrievalConfig;
1764
+ answerQuality?: RagAnswerQualityResult;
1765
+ promotionDecision?: RagPromotionResult;
1766
+ }
1767
+ interface KnowledgeImprovementCandidateRecord {
1768
+ iteration: number;
1769
+ candidateId: string;
1770
+ candidateRoot: string;
1771
+ baseHash: string;
1772
+ candidateHash?: string;
1773
+ status: KnowledgeImprovementStatus;
1774
+ createdAt: string;
1775
+ updatedAt: string;
1776
+ validation?: ValidateKnowledgeResult;
1777
+ kbQuality?: KnowledgeBaseQualityReport;
1778
+ readinessBlockingMissing?: number;
1779
+ evaluation?: KnowledgeImprovementMetric;
1780
+ lifecycle?: readonly KnowledgeImprovementLifecycleRecord[];
1781
+ retrievalWinnerConfig?: RetrievalConfig;
1782
+ answerQuality?: RagAnswerQualityResult;
1783
+ promotionDecision?: RagPromotionResult;
1784
+ notes?: string;
1785
+ }
1786
+ interface KnowledgeImprovementRunState {
1787
+ version: 1;
1788
+ runId: string;
1110
1789
  root: string;
1111
1790
  goal: string;
1112
- worker: ResearchWorker;
1113
- driver: ResearchDriver;
1114
- /**
1115
- * When false (default), the driver ONLY verifies + gates — a pure coordinator
1116
- * that contributes no research of its own (the "doesn't participate in the
1117
- * work" mode). When true, the driver also runs its `research` gap-fill pass
1118
- * each round over the gaps the worker left open.
1119
- */
1120
- driverResearches?: boolean;
1121
- maxRounds?: number;
1122
- actor?: string;
1123
- /** Readiness specs define the gate; an empty list means the loop never gates. */
1791
+ status: KnowledgeImprovementStatus;
1792
+ baseHash: string;
1793
+ createdAt: string;
1794
+ updatedAt: string;
1795
+ ownerId?: string;
1796
+ candidates: KnowledgeImprovementCandidateRecord[];
1797
+ promotedCandidateId?: string;
1798
+ blockedReason?: string;
1799
+ }
1800
+ interface KnowledgeImprovementResult {
1801
+ runId: string;
1802
+ runDir: string;
1803
+ state: KnowledgeImprovementRunState;
1804
+ candidate?: KnowledgeImprovementCandidateRecord;
1805
+ lifecycle?: RunRagKnowledgeImprovementLoopResult;
1806
+ promoted: boolean;
1807
+ blocked: boolean;
1808
+ }
1809
+ interface KnowledgeImprovementRetrievalOptions extends Omit<RunRetrievalImprovementLoopOptions, 'index' | 'runDir'> {
1810
+ runDir?: RunRetrievalImprovementLoopOptions['runDir'];
1811
+ }
1812
+ interface KnowledgeImprovementUpdateInput extends RagKnowledgeUpdateInput {
1813
+ runId: string;
1814
+ iteration: number;
1815
+ candidateId: string;
1816
+ root: string;
1817
+ baselineRoot: string;
1818
+ candidateRoot: string;
1819
+ baseHash: string;
1820
+ }
1821
+ type KnowledgeImprovementUpdate = (input: KnowledgeImprovementUpdateInput) => Promise<RagKnowledgeUpdateResult> | RagKnowledgeUpdateResult;
1822
+ interface KnowledgeImprovementOptions {
1823
+ root: string;
1824
+ goal: string;
1825
+ runId?: string;
1826
+ runDir?: string;
1827
+ ownerId?: string;
1828
+ leaseTtlMs?: number;
1829
+ resume?: boolean;
1830
+ promote?: boolean;
1831
+ maxCandidates?: number;
1832
+ candidateResearchIterations?: number;
1833
+ strict?: ValidateKnowledgeOptions['strict'];
1124
1834
  readinessSpecs?: KnowledgeReadinessSpec[];
1125
1835
  readinessTaskId?: string;
1126
1836
  readiness?: Omit<BuildEvalKnowledgeBundleOptions, 'taskId' | 'index' | 'specs'>;
1127
- sourceOptions?: Pick<AddSourceOptions, 'adapters' | 'now'>;
1837
+ kbQuality?: KnowledgeBaseQualityOptions;
1838
+ step?: RunKnowledgeResearchLoopOptions['step'];
1839
+ knowledgeResearch?: Omit<RagKnowledgeResearchOptions, 'root'>;
1840
+ retrieval?: KnowledgeImprovementRetrievalOptions;
1841
+ diagnose?: NonNullable<RunRagKnowledgeImprovementLoopOptions['diagnose']>;
1842
+ acquireKnowledge?: NonNullable<RunRagKnowledgeImprovementLoopOptions['acquireKnowledge']>;
1843
+ updateKnowledge?: KnowledgeImprovementUpdate;
1844
+ evaluateAnswers?: NonNullable<RunRagKnowledgeImprovementLoopOptions['evaluateAnswers']>;
1845
+ decidePromotion?: NonNullable<RunRagKnowledgeImprovementLoopOptions['promote']>;
1846
+ enabledPhases?: readonly RagKnowledgeImprovementPhase[];
1847
+ requiredPhases?: readonly RagKnowledgeImprovementPhase[];
1848
+ evaluate?: KnowledgeImprovementEvaluator;
1128
1849
  signal?: AbortSignal;
1129
- onRound?: (round: TwoAgentResearchRound) => Promise<void> | void;
1850
+ now?: () => Date;
1851
+ onState?: (state: KnowledgeImprovementRunState) => Promise<void> | void;
1130
1852
  }
1131
- interface TwoAgentResearchRound {
1132
- round: number;
1133
- /** Gaps reported at the START of the round (what the worker targeted). */
1134
- gaps: KnowledgeGap[];
1135
- /** Worker sources accepted by the driver and written to the KB. */
1136
- acceptedWorkerSources: SourceRecord[];
1137
- /** Worker sources the driver rejected (with reasons) — never written. */
1138
- rejectedWorkerSources: RejectedSource[];
1139
- /** Sources the driver itself added in its gap-fill pass. */
1140
- driverSources: SourceRecord[];
1141
- /** Curated pages written this round (worker proposal + driver proposal). */
1142
- writtenPages: string[];
1143
- readiness?: EvalKnowledgeBundleBuildResult;
1144
- /** True once the readiness gate reports no blocking gaps. */
1145
- ready: boolean;
1146
- event: KnowledgeEvent;
1147
- notes: {
1148
- worker?: string;
1149
- driver?: string;
1150
- };
1853
+ declare function knowledgeImprovementRunId(root: string, goal: string): string;
1854
+ declare function knowledgeImprovementRunDir(root: string, runId: string): string;
1855
+ declare function loadKnowledgeImprovementState(root: string, runId: string, runDir?: string): Promise<KnowledgeImprovementRunState | null>;
1856
+ declare function improveKnowledgeBase(options: KnowledgeImprovementOptions): Promise<KnowledgeImprovementResult>;
1857
+ declare function hashKnowledgeBase(root: string): Promise<string>;
1858
+
1859
+ interface KbStore {
1860
+ putSource(source: SourceRecord): Promise<void>;
1861
+ getSource(id: string): Promise<SourceRecord | null>;
1862
+ listSources(): Promise<SourceRecord[]>;
1863
+ putPage(page: KnowledgePage): Promise<void>;
1864
+ getPage(idOrPath: string): Promise<KnowledgePage | null>;
1865
+ listPages(): Promise<KnowledgePage[]>;
1866
+ putIndex(index: KnowledgeIndex): Promise<void>;
1867
+ getIndex(): Promise<KnowledgeIndex | null>;
1868
+ putEvent(event: KnowledgeEvent): Promise<void>;
1869
+ listEvents(query?: KnowledgeEventQuery): Promise<KnowledgeEvent[]>;
1151
1870
  }
1152
- interface TwoAgentResearchLoopResult {
1153
- root: string;
1154
- goal: string;
1155
- rounds: number;
1156
- ready: boolean;
1157
- index: KnowledgeIndex;
1158
- readiness?: EvalKnowledgeBundleBuildResult;
1159
- steps: TwoAgentResearchRound[];
1871
+ declare class MemoryKbStore implements KbStore {
1872
+ private readonly sources;
1873
+ private readonly pages;
1874
+ private readonly events;
1875
+ private index;
1876
+ putSource(source: SourceRecord): Promise<void>;
1877
+ getSource(id: string): Promise<SourceRecord | null>;
1878
+ listSources(): Promise<SourceRecord[]>;
1879
+ putPage(page: KnowledgePage): Promise<void>;
1880
+ getPage(idOrPath: string): Promise<KnowledgePage | null>;
1881
+ listPages(): Promise<KnowledgePage[]>;
1882
+ putIndex(index: KnowledgeIndex): Promise<void>;
1883
+ getIndex(): Promise<KnowledgeIndex | null>;
1884
+ putEvent(event: KnowledgeEvent): Promise<void>;
1885
+ listEvents(query?: KnowledgeEventQuery): Promise<KnowledgeEvent[]>;
1886
+ }
1887
+ declare class FileSystemKbStore extends MemoryKbStore {
1888
+ private readonly dir;
1889
+ constructor(dir: string);
1890
+ putIndex(index: KnowledgeIndex): Promise<void>;
1891
+ getIndex(): Promise<KnowledgeIndex | null>;
1892
+ putEvent(event: KnowledgeEvent): Promise<void>;
1160
1893
  }
1894
+
1895
+ declare function lintKnowledgeIndex(index: KnowledgeIndex): KnowledgeLintFinding[];
1896
+
1161
1897
  /**
1162
- * Two-agent (driver + worker) sibling of `runKnowledgeResearchLoop`.
1163
- *
1164
- * Both agents research to grow ONE knowledge base. The roles are differentiated:
1898
+ * `materialFactsSurfaced` the held-out investment-research METRIC.
1165
1899
  *
1166
- * - **WORKER** = primary research. Each round it reads the open gaps, discovers
1167
- * new sources, and proposes additions (`sources` + `proposalText`). It ADDS.
1168
- * - **DRIVER** = the verifier / gap-filler / gate. It (1) VERIFIES the worker's
1169
- * sources before they commit dedup against the KB, then `verifySource`
1170
- * rejects ones that aren't real/relevant; (2) GAP-FILLS the gaps the worker
1171
- * missed with its own research pass (when `driverResearches`); (3) folds the
1172
- * remaining gaps into the worker's next prompt; and (4) GATES on
1173
- * `scoreKnowledgeReadiness` — the loop stops as soon as there are no blocking
1174
- * gaps.
1900
+ * Given a knowledge base a research loop built for a company and the company's
1901
+ * HELD-OUT material-fact checklist (`tests/eval/investment-thesis-set.ts`, never
1902
+ * shown to the loop), this returns the FRACTION of checklist items the KB's
1903
+ * pages surface + ground. The check is the same `$0`, model-free, deterministic
1904
+ * substring grader the loop's checklist already ships (`gradeFactAgainstText` /
1905
+ * `gradeCompanyAgainstText`) so the answer key never reaches a model the loop
1906
+ * could observe, exactly the firewall the ML deep-question exam uses.
1175
1907
  *
1176
- * Set `driverResearches: false` (default) for the pure-coordinator mode: the
1177
- * driver only verifies + gates and contributes no research itself.
1908
+ * The ONLY thing this file adds over the raw grader is the KB→text join: it reads
1909
+ * the curated pages (and the raw source text) the loop wrote and hands their
1910
+ * concatenation to the grader. That join mirrors `kbText` in the research-quality
1911
+ * A/B (research-driving-ab.test.ts) so the thesis metric and the ML-exam metric
1912
+ * read a KB the same way.
1178
1913
  *
1179
- * Composes existing atoms `initKnowledgeBase`, `addSourceText`,
1180
- * `applyKnowledgeWriteBlocks`, `buildEvalKnowledgeBundle` (the readiness gate),
1181
- * and `searchKnowledge` and reinvents none of them.
1914
+ * WHY pages AND source text: an honest thesis surfaces a buried fact in its
1915
+ * curated thesis PAGE (the judgment), but a loop whose page is thin while its
1916
+ * fetched filings are rich should still get credit for what it actually pulled.
1917
+ * Grading the union is the faithful, not the lenient, choice — it rewards the
1918
+ * loop that REACHED the filing even if its synthesis was terse, and it cannot
1919
+ * manufacture a hit the underlying evidence does not contain.
1182
1920
  */
1183
- declare function runTwoAgentResearchLoop(options: TwoAgentResearchLoopOptions): Promise<TwoAgentResearchLoopResult>;
1921
+
1922
+ /** Per-fact grade plus the fact's id/lens, for the audit trail. */
1923
+ interface FactResult {
1924
+ id: string;
1925
+ lens: CompanyEvalCase['facts'][number]['lens'];
1926
+ surfaced: boolean;
1927
+ groupsFound: number;
1928
+ groupsTotal: number;
1929
+ foundLabels: string[];
1930
+ }
1931
+ /** The metric's result for one company: the surfaced fraction + the per-fact trail. */
1932
+ interface MaterialFactsResult {
1933
+ ticker: string;
1934
+ company: string;
1935
+ /** Held-out facts the KB surfaced + grounded. */
1936
+ surfaced: number;
1937
+ /** Total held-out facts for this company (the denominator). */
1938
+ total: number;
1939
+ /** `surfaced / total` in [0, 1]. */
1940
+ fraction: number;
1941
+ /** Per-fact grade, in checklist order, for the doc / audit. */
1942
+ perFact: FactResult[];
1943
+ }
1184
1944
  /**
1185
- * Helper for verifiers: does the candidate source's text/title overlap any page
1186
- * the readiness search returns for a gap query? A cheap relevance heuristic the
1187
- * driver can compose into `verifySource` (real verifiers can do more).
1945
+ * Join a KB index into the single text blob the grader scans: every curated PAGE
1946
+ * (title + body) followed by every raw SOURCE (title + fetched text). This is the
1947
+ * text read AFTER the loop finished it is never handed to the loop. Identical
1948
+ * in spirit to `kbText` in the research-quality A/B so both metrics read a KB the
1949
+ * same way.
1188
1950
  */
1189
- declare function sourceMatchesGaps(source: ResearchSourceProposal, index: KnowledgeIndex, gaps: KnowledgeGap[]): KnowledgeSearchResult[];
1951
+ declare function kbIndexToText(index: KnowledgeIndex): string;
1952
+ /**
1953
+ * Grade one company's KB against its held-out material-fact checklist, given the
1954
+ * KB's already-joined text. The pure core — no I/O — so calibration can score a
1955
+ * hand-written shallow/deep thesis string directly and the live path can score a
1956
+ * real KB. Returns the surfaced FRACTION plus the per-fact audit trail.
1957
+ */
1958
+ declare function materialFactsSurfacedInText(company: CompanyEvalCase, kbText: string): MaterialFactsResult;
1959
+ /**
1960
+ * `materialFactsSurfaced(kb, checklist)` — the metric in its KB-reading form.
1961
+ *
1962
+ * `kb` is EITHER a knowledge-base root directory (the loop wrote pages there) or
1963
+ * an already-built `KnowledgeIndex`. `checklist` is the company's held-out
1964
+ * `CompanyEvalCase`. Returns the surfaced fraction + per-fact trail.
1965
+ *
1966
+ * The checklist is HELD OUT by construction: it lives in the test eval set, is
1967
+ * never passed to the loop, and is read only here, after the loop finished.
1968
+ */
1969
+ declare function materialFactsSurfaced(kb: string | KnowledgeIndex, checklist: CompanyEvalCase): Promise<MaterialFactsResult>;
1190
1970
 
1191
1971
  /**
1192
- * Real web-research worker + verifying driver for `runTwoAgentResearchLoop`.
1972
+ * Bridge from `AnalystFinding` (agent-eval) to knowledge proposals.
1193
1973
  *
1194
- * This is the GENERAL, any-topic implementation behind the two-agent research
1195
- * loop's live arm. Given the open knowledge gaps the readiness gate surfaces,
1196
- * the worker:
1974
+ * Closes the failure wiki side of the recursive-self-improvement
1975
+ * loop: a knowledge-gap or knowledge-poisoning finding produced by an
1976
+ * analyst becomes a concrete proposal an operator (or auto-merge bot)
1977
+ * can review and apply. The bridge is intentionally lossless on the
1978
+ * fail-loud side — a finding the parser can't classify returns a
1979
+ * `KnowledgeProposalParseError` rather than a silent skip, so the
1980
+ * loop never accepts an underspecified edit.
1197
1981
  *
1198
- * 1. asks an LLM (glm-5.2 by default) to turn each gap into focused web
1199
- * search queries,
1200
- * 2. runs a REAL web search over the Tangle router (`POST /v1/search` — the
1201
- * same endpoint `tcloud mcp`'s `web_search` tool forwards to), so there is
1202
- * no hardcoded corpus,
1203
- * 3. fetches the top results with the repo's polite, cached `politeFetch` and
1204
- * reduces each page to text with `htmlToText`,
1205
- * 4. proposes the readable, verifiable pages as `ResearchSourceProposal`s plus
1206
- * a `buildPages` that writes citing `knowledge/*.md` pages from the sources
1207
- * the driver accepts.
1982
+ * Subject grammar this bridge understands (analyst-side convention,
1983
+ * stamped in the kind prompts):
1208
1984
  *
1209
- * The verifying DRIVER is the differentiated role from the two-agent loop: a
1210
- * second LLM pass that judges each fetched source's on-topic relevance to the
1211
- * goal + open gaps and rejects off-topic / spam / already-covered material. The
1212
- * worker ADDS; the driver GATES. Together they build a cleaner knowledge base
1213
- * than a single agent at the same compute budget.
1985
+ * agent-knowledge:wiki:<page-slug> create / update page
1986
+ * agent-knowledge:wiki:<page-slug>#<heading> insert section under page
1987
+ * agent-knowledge:claim:<topic> draft claim row
1988
+ * agent-knowledge:raw:<source-id> lift raw curated
1989
+ * agent-knowledge:stale:<page-slug> mark page superseded
1214
1990
  *
1215
- * Dependency-free on purpose: it talks to the router over `fetch` directly with
1216
- * the published OpenAI-compatible chat shape and the `/v1/search` shape, so it
1217
- * works whether or not the `tcloud` CLI is installed. Point it at any router by
1218
- * passing `baseUrl`; supply the key via `apiKey` or `TANGLE_API_KEY`.
1991
+ * Anything else (websearch:outdated:*, tool-doc:*, system-prompt:*,
1992
+ * memory:*) is NOT a knowledge-base concern and returns `null` so the
1993
+ * loop's improvement-applier handles it.
1219
1994
  */
1220
1995
 
1221
- /** One live web result, as the router's `/v1/search` returns it. */
1222
- interface WebSearchHit {
1223
- title: string;
1224
- url: string;
1225
- snippet?: string;
1996
+ interface KnowledgeProposal {
1997
+ /**
1998
+ * Stable id derived from the finding so cross-run diffs share an
1999
+ * identity. Re-proposing the same finding produces the same id.
2000
+ */
2001
+ id: string;
2002
+ /** The finding that generated this proposal — useful for audit + revert. */
2003
+ sourceFindingId: string;
2004
+ /** What the proposal does. */
2005
+ kind: 'create-page' | 'update-page' | 'append-section' | 'create-claim' | 'lift-raw' | 'mark-stale';
2006
+ /** Locus on disk (page slug or claim topic). */
2007
+ locus: string;
2008
+ /**
2009
+ * Page write blocks the standard `applyKnowledgeWriteBlocks` consumer
2010
+ * accepts. Empty for proposals that don't change page text (e.g.
2011
+ * `create-claim` produces a `claim` field instead).
2012
+ */
2013
+ writeBlocks: KnowledgeWriteBlock[];
2014
+ /**
2015
+ * Granular claim draft for proposals whose unit-of-change is a claim
2016
+ * row rather than a whole page. `status: 'draft'` until reviewed.
2017
+ */
2018
+ claim?: KnowledgeClaim;
2019
+ /** Per-proposal metadata: severity, confidence, source span. */
2020
+ metadata: {
2021
+ severity: AnalystSeverity;
2022
+ confidence: number;
2023
+ evidence_uri?: string;
2024
+ analyst_id: string;
2025
+ };
2026
+ }
2027
+ declare class KnowledgeProposalParseError extends Error {
2028
+ readonly findingId: string;
2029
+ readonly subject: string;
2030
+ constructor(findingId: string, subject: string, message: string);
1226
2031
  }
1227
2032
  /**
1228
- * The two router capabilities the worker/driver need. Injectable so tests can
1229
- * stub the network; the default talks to the live Tangle router over `fetch`.
2033
+ * Convert one `AnalystFinding` into a knowledge proposal. Returns
2034
+ * `null` when the finding's locus isn't a knowledge-base concern
2035
+ * (`websearch:outdated:*`, `tool-doc:*`, `system-prompt:*`,
2036
+ * `memory:*`, missing subject). Throws when the locus IS a
2037
+ * knowledge-base concern but is malformed — that's a bug in the
2038
+ * analyst prompt and should fail loud.
2039
+ *
2040
+ * Caller convention: feed the function the analyst's full findings
2041
+ * list and filter out the `null`s; the orchestrator passes the
2042
+ * remaining proposals to the existing review / apply pipeline.
1230
2043
  */
1231
- interface RouterClient {
1232
- /** Live web search — returns title/url/snippet hits. */
1233
- search(query: string, opts?: {
1234
- maxResults?: number;
1235
- }): Promise<WebSearchHit[]>;
1236
- /** Chat completion — returns the assistant message's visible text. */
1237
- chat(messages: {
1238
- role: 'system' | 'user';
1239
- content: string;
1240
- }[], maxTokens?: number): Promise<string>;
2044
+ declare function proposeFromFinding(finding: AnalystFinding): KnowledgeProposal | null;
2045
+ /**
2046
+ * Plural convenience: filter + map across an entire findings batch
2047
+ * with one call. Parse errors collect into `errors[]`; the loop
2048
+ * decides per-error whether to abort or continue.
2049
+ */
2050
+ interface ProposeFromFindingsResult {
2051
+ proposals: KnowledgeProposal[];
2052
+ skipped: number;
2053
+ errors: KnowledgeProposalParseError[];
1241
2054
  }
1242
- interface TangleRouterOptions {
1243
- /** Router base URL. Defaults to `https://router.tangle.tools/v1`. */
1244
- baseUrl?: string;
1245
- /** Bearer key. Defaults to `process.env.TANGLE_API_KEY`. */
1246
- apiKey?: string;
1247
- /** Chat model id. Defaults to `glm-5.2`. */
1248
- model?: string;
1249
- /** Optional preferred search provider (exa | you | perplexity | …). */
1250
- searchProvider?: string;
1251
- signal?: AbortSignal;
2055
+ declare function proposeFromFindings(findings: ReadonlyArray<AnalystFinding>): ProposeFromFindingsResult;
2056
+
2057
+ interface EvaluateKnowledgeBaseReadinessOptions {
2058
+ root: string;
2059
+ goal: string;
2060
+ readinessSpecs?: readonly KnowledgeReadinessSpec[];
2061
+ readinessTaskId?: string;
2062
+ readiness?: Omit<BuildEvalKnowledgeBundleOptions, 'taskId' | 'index' | 'specs'>;
2063
+ strict?: ValidateKnowledgeOptions['strict'];
2064
+ kbQuality?: KnowledgeBaseQualityOptions;
1252
2065
  }
1253
- /** A small error so a failed router call fails loud rather than returning junk. */
1254
- declare class RouterError extends Error {
1255
- readonly status: number;
1256
- constructor(status: number, message: string);
2066
+ interface KnowledgeBaseReadinessEvaluation {
2067
+ ready: boolean;
2068
+ summary: string;
2069
+ index: KnowledgeIndex;
2070
+ validation: ValidateKnowledgeResult;
2071
+ readiness?: EvalKnowledgeBundleBuildResult;
2072
+ kbQuality: KnowledgeBaseQualityReport;
2073
+ dimensions: {
2074
+ validation: number;
2075
+ kb_quality: number;
2076
+ blocking_readiness: number;
2077
+ };
2078
+ }
2079
+ declare function evaluateKnowledgeBaseReadiness(options: EvaluateKnowledgeBaseReadinessOptions): Promise<KnowledgeBaseReadinessEvaluation>;
2080
+
2081
+ interface KnowledgeReleaseReport {
2082
+ release: KnowledgeRelease;
2083
+ scorecard: ReleaseConfidenceScorecard;
2084
+ candidateRuns: RunRecord[];
2085
+ baselineRuns: RunRecord[];
1257
2086
  }
1258
2087
  /**
1259
- * Build a dependency-free Tangle router client over `fetch`. This is the same
1260
- * wire surface the `tcloud` SDK + `tcloud mcp` use (`/v1/search` for web search,
1261
- * `/v1/chat/completions` for chat) so it needs no CLI installed.
2088
+ * Campaign-native release report. The caller (a consumer's KB self-improvement
2089
+ * loop) supplies the candidate/baseline `RunRecord[]` (e.g. via
2090
+ * `campaignToRunRecords`) + optional per-instance `ReleaseTraceEvidence` + the
2091
+ * gate decision; this folds them into a `ReleaseConfidenceScorecard` + a
2092
+ * `KnowledgeRelease`. Release confidence is computed from run records + traces,
2093
+ * independent of any optimizer result shape.
2094
+ */
2095
+ interface KnowledgeReleaseInput {
2096
+ candidateId: string;
2097
+ baselineId?: string;
2098
+ candidateRuns: RunRecord[];
2099
+ baselineRuns?: RunRecord[];
2100
+ traces?: ReleaseTraceEvidence[];
2101
+ gateDecision?: GateDecision | null;
2102
+ /**
2103
+ * True when a held-out split was evaluated (drives the holdout threshold).
2104
+ *
2105
+ * Constraint: the substrate gate keys the holdout requirement off a scenario
2106
+ * corpus, which this run-only report does not carry. With `hasHoldout: true`
2107
+ * the gate fails closed (`missing_holdout_split`) even when holdout RunRecords
2108
+ * are supplied. Callers that gate on a real held-out split should drive
2109
+ * `evaluateReleaseConfidence` directly with a dataset/scenarios.
2110
+ */
2111
+ hasHoldout?: boolean;
2112
+ /** Candidate is the search-best variant — a promotion precondition. Default true. */
2113
+ promotedIsBest?: boolean;
2114
+ createdAt?: string;
2115
+ minScore?: number;
2116
+ }
2117
+ declare function knowledgeReleaseReport(input: KnowledgeReleaseInput): KnowledgeReleaseReport;
2118
+
2119
+ /**
2120
+ * Research-DRIVING driver for `runVerifiedResearchLoop`.
2121
+ *
2122
+ * The shipped drivers all FILTER the worker's sources:
2123
+ * - `createVerifyingResearchDriver` judges on-topic relevance,
2124
+ * - `createAdaptiveResearchDriver` dedups then triages then escalates,
2125
+ * - `createClaimGroundingVerifier` rejects misattributed citations.
2126
+ *
2127
+ * This driver does the OPPOSITE job: instead of narrowing the worker's output,
2128
+ * it DRIVES the research DEEPER each round. Its value is not "fewer sources" —
2129
+ * it is "more answered, better-corroborated sub-questions". Concretely, each
2130
+ * round it:
2131
+ *
2132
+ * 1. EXTRACTS the key claims from the worker's new sources (one LLM pass per
2133
+ * source, in `verifySource`; falls back to a deterministic sentence-pull
2134
+ * when the model is unavailable so a round never silently extracts nothing).
2135
+ * 2. TRACKS each claim's support — the set of INDEPENDENT sources (by canonical
2136
+ * host) that assert it — and detects CONTRADICTIONS between a new claim and
2137
+ * one already on the ledger.
2138
+ * 3. GENERATES the next round's DEEP sub-questions from the accumulated claims,
2139
+ * in four kinds — comparative ("how does X's tradeoff differ from Y's?"),
2140
+ * mechanism ("under what precise condition does X fail?"), gap ("what
2141
+ * specific result is missing?"), and contradiction ("does any source
2142
+ * challenge claim Z?").
2143
+ * 4. FLAGS weakly-supported claims (only ONE independent source) and
2144
+ * contradicted claims as INVALIDATION targets and demands the worker find
2145
+ * corroborating / refuting evidence for them.
2146
+ * 5. FOLDS the deep sub-questions + invalidation challenges into the worker's
2147
+ * next prompt via the loop's `foldGaps` → `steer` channel — that is the
2148
+ * mechanism that drives DEPTH and VALIDATION rather than breadth.
2149
+ *
2150
+ * COMPLETION (`isComplete` / the `done` judgment the caller gates on) does NOT
2151
+ * look at source COUNT. It is done only when every deep sub-question it raised
2152
+ * has been addressed AND every key claim is either supported by >= 2 independent
2153
+ * sources OR explicitly marked CONTESTED (a contradiction the loop surfaced and
2154
+ * could not resolve). A KB with twenty sources all asserting one unchallenged
2155
+ * claim is NOT done; a KB whose handful of claims are each corroborated or
2156
+ * contested IS.
2157
+ *
2158
+ * It reuses `runVerifiedResearchLoop` (it is a plain `ResearchDriver`), the web
2159
+ * worker, `sha256` (claim identity), `canonicalizeUrl` (independent-source
2160
+ * identity), and the `RouterClient` chat surface; it reinvents none of them.
1262
2161
  */
1263
- declare function createTangleRouterClient(options?: TangleRouterOptions): RouterClient;
1264
- interface WebResearchWorkerOptions {
1265
- /** Router client. Defaults to a live Tangle router client from env creds. */
2162
+
2163
+ /** The four deep sub-question kinds the driver generates to drive depth. */
2164
+ type DeepQuestionKind = 'comparative' | 'mechanism' | 'gap' | 'contradiction';
2165
+ /** A deep sub-question the driver folds into the worker's next prompt. */
2166
+ interface DeepQuestion {
2167
+ kind: DeepQuestionKind;
2168
+ text: string;
2169
+ /** sha256-derived stable id, so "addressed" can be tracked across rounds. */
2170
+ id: string;
2171
+ /** Claim id(s) this question interrogates (for contradiction/mechanism kinds). */
2172
+ claimIds: string[];
2173
+ /** True once a later round's evidence addressed it (see `markAddressed`). */
2174
+ addressed: boolean;
2175
+ /** The round this question was raised in. */
2176
+ raisedRound: number;
2177
+ }
2178
+ /** One tracked claim plus the independent sources that assert it. */
2179
+ interface TrackedClaim {
2180
+ id: string;
2181
+ /** The claim text as first extracted (kept for prompts/audit). */
2182
+ text: string;
2183
+ /** Canonical hosts of the INDEPENDENT sources that assert this claim. */
2184
+ supportingHosts: Set<string>;
2185
+ /** Source URIs that assert this claim (provenance; may share a host). */
2186
+ supportingUris: string[];
2187
+ /** Claim ids this claim was found to CONTRADICT (and vice versa). */
2188
+ contradicts: Set<string>;
2189
+ /**
2190
+ * CONTESTED = a contradiction the loop surfaced but could not resolve to a
2191
+ * single supported claim. A contested claim counts as "settled enough to be
2192
+ * done" (we report the disagreement) even with < 2 independent sources.
2193
+ */
2194
+ contested: boolean;
2195
+ firstSeenRound: number;
2196
+ }
2197
+ /** The driver's accumulated research state — the completion oracle reads this. */
2198
+ interface ResearchDrivingState {
2199
+ /** Every claim extracted from the worker's sources, by id. */
2200
+ claims: TrackedClaim[];
2201
+ /** Every deep sub-question raised, by id. */
2202
+ questions: DeepQuestion[];
2203
+ /** Claims with exactly one independent source AND not contested. */
2204
+ weaklySupported: TrackedClaim[];
2205
+ /** Claims supported by >= 2 independent sources. */
2206
+ corroborated: TrackedClaim[];
2207
+ /** Claims marked contested (a surfaced, unresolved contradiction). */
2208
+ contested: TrackedClaim[];
2209
+ /** Deep questions still unaddressed. */
2210
+ openQuestions: DeepQuestion[];
2211
+ /** How many rounds the driver has folded steer for. */
2212
+ rounds: number;
2213
+ }
2214
+ interface ResearchDrivingDriverOptions {
2215
+ /** Router client for claim extraction + deep-question generation. */
1266
2216
  router?: RouterClient;
1267
2217
  router_options?: TangleRouterOptions;
1268
- /** Max search queries the LLM may form per gap. Default 2. */
1269
- queriesPerGap?: number;
1270
- /** Max web results fetched per query. Default 3. */
1271
- resultsPerQuery?: number;
1272
- /** Hard cap on sources proposed per round (across all gaps). Default 6. */
1273
- maxSourcesPerRound?: number;
1274
- /** Disk cache dir for `politeFetch`. Optional; speeds repeat runs. */
1275
- cacheDir?: string;
1276
- /** Minimum readable text length to keep a fetched page. Default 200. */
1277
- minTextChars?: number;
1278
- /** Max chars of page text stored per source (keeps pages bounded). Default 4000. */
1279
- maxTextChars?: number;
2218
+ /**
2219
+ * A claim is CORROBORATED at this many INDEPENDENT supporting sources (distinct
2220
+ * canonical hosts). Default 2 the task's ">= 2 independent sources" bar.
2221
+ */
2222
+ minIndependentSources?: number;
2223
+ /** Max deep sub-questions to fold into one round's steer. Default 6. */
2224
+ maxQuestionsPerRound?: number;
2225
+ /** Max claims to extract from a single source. Default 3. */
2226
+ maxClaimsPerSource?: number;
2227
+ /**
2228
+ * When the extractor LLM is unavailable, fall back to a deterministic claim
2229
+ * pull (the source's leading sentences) so the driver still drives. Default
2230
+ * true. Set false to require the model (claims will be empty without it).
2231
+ */
2232
+ deterministicFallback?: boolean;
2233
+ /** Observe each round's generated steer (for instrumentation / the script). */
2234
+ onSteer?: (steer: ResearchDrivingSteer) => void;
2235
+ }
2236
+ /** What the driver folded into one round's worker prompt, surfaced for audit. */
2237
+ interface ResearchDrivingSteer {
2238
+ round: number;
2239
+ deepQuestions: DeepQuestion[];
2240
+ /** Claims it demanded corroborating/refuting evidence for this round. */
2241
+ invalidationTargets: TrackedClaim[];
2242
+ /** The readiness gaps it interleaved (passed through from the loop). */
2243
+ gaps: KnowledgeGap[];
2244
+ /** The full steer text handed to the worker. */
2245
+ text: string;
1280
2246
  }
1281
2247
  /**
1282
- * The real web-research worker. Conforms to the loop's `ResearchWorker`
1283
- * contract: given the open gaps, it returns the sources it found plus a
1284
- * `buildPages` that emits citing pages from the sources the driver accepted.
2248
+ * The research-driving driver. It is a `ResearchDriver` (drops straight into
2249
+ * `runVerifiedResearchLoop`) PLUS a completion oracle and live state, mirroring
2250
+ * how `createAdaptiveResearchDriver` exposes `stats()`.
1285
2251
  */
1286
- declare function createWebResearchWorker(options?: WebResearchWorkerOptions): ResearchWorker;
1287
- interface VerifyingDriverOptions {
1288
- router?: RouterClient;
1289
- router_options?: TangleRouterOptions;
2252
+ interface ResearchDrivingDriver extends ResearchDriver {
2253
+ /** Live snapshot of the claim ledger + deep questions. */
2254
+ researchState(): ResearchDrivingState;
1290
2255
  /**
1291
- * When the LLM verdict can't be parsed, default to REJECT (fail-closed) so a
1292
- * model hiccup never poisons the KB with an unverified source. Set `true` to
1293
- * accept-on-parse-failure only if you have a reason to. Default false.
2256
+ * The completion oracle gate `done` on THIS, not on source count. True when
2257
+ * every deep sub-question is addressed AND every claim is corroborated
2258
+ * (>= `minIndependentSources` independent sources) or explicitly contested.
2259
+ * False while any claim is weakly-supported or any deep question is open.
2260
+ * Returns false before any claim has been seen (nothing researched yet).
1294
2261
  */
1295
- acceptOnParseFailure?: boolean;
2262
+ isComplete(): boolean;
2263
+ /**
2264
+ * The last round's generated steer, or undefined before the first fold. Useful
2265
+ * to assert the driver produced deeper questions / invalidation challenges.
2266
+ */
2267
+ lastSteer(): ResearchDrivingSteer | undefined;
2268
+ }
2269
+ declare function createResearchDrivingDriver(options?: ResearchDrivingDriverOptions): ResearchDrivingDriver;
2270
+
2271
+ declare const SourceAnchorSchema: z.ZodObject<{
2272
+ id: z.ZodString;
2273
+ sourceId: z.ZodString;
2274
+ label: z.ZodOptional<z.ZodString>;
2275
+ page: z.ZodOptional<z.ZodNumber>;
2276
+ lineStart: z.ZodOptional<z.ZodNumber>;
2277
+ lineEnd: z.ZodOptional<z.ZodNumber>;
2278
+ charStart: z.ZodOptional<z.ZodNumber>;
2279
+ charEnd: z.ZodOptional<z.ZodNumber>;
2280
+ timestampMs: z.ZodOptional<z.ZodNumber>;
2281
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
2282
+ }, z.core.$strip>;
2283
+ declare const SourceRecordSchema: z.ZodObject<{
2284
+ id: z.ZodString;
2285
+ uri: z.ZodString;
2286
+ title: z.ZodOptional<z.ZodString>;
2287
+ mediaType: z.ZodOptional<z.ZodString>;
2288
+ contentHash: z.ZodString;
2289
+ text: z.ZodOptional<z.ZodString>;
2290
+ anchors: z.ZodOptional<z.ZodArray<z.ZodObject<{
2291
+ id: z.ZodString;
2292
+ sourceId: z.ZodString;
2293
+ label: z.ZodOptional<z.ZodString>;
2294
+ page: z.ZodOptional<z.ZodNumber>;
2295
+ lineStart: z.ZodOptional<z.ZodNumber>;
2296
+ lineEnd: z.ZodOptional<z.ZodNumber>;
2297
+ charStart: z.ZodOptional<z.ZodNumber>;
2298
+ charEnd: z.ZodOptional<z.ZodNumber>;
2299
+ timestampMs: z.ZodOptional<z.ZodNumber>;
2300
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
2301
+ }, z.core.$strip>>>;
2302
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
2303
+ createdAt: z.ZodString;
2304
+ }, z.core.$strip>;
2305
+ declare const KnowledgePageSchema: z.ZodObject<{
2306
+ id: z.ZodString;
2307
+ path: z.ZodString;
2308
+ title: z.ZodString;
2309
+ text: z.ZodString;
2310
+ frontmatter: z.ZodRecord<z.ZodString, z.ZodUnknown>;
2311
+ sourceIds: z.ZodArray<z.ZodString>;
2312
+ tags: z.ZodArray<z.ZodString>;
2313
+ outLinks: z.ZodArray<z.ZodString>;
2314
+ }, z.core.$strip>;
2315
+ declare const KnowledgeGraphNodeSchema: z.ZodObject<{
2316
+ id: z.ZodString;
2317
+ title: z.ZodString;
2318
+ path: z.ZodString;
2319
+ tags: z.ZodArray<z.ZodString>;
2320
+ sourceIds: z.ZodArray<z.ZodString>;
2321
+ outDegree: z.ZodNumber;
2322
+ inDegree: z.ZodNumber;
2323
+ }, z.core.$strip>;
2324
+ declare const KnowledgeGraphEdgeSchema: z.ZodObject<{
2325
+ source: z.ZodString;
2326
+ target: z.ZodString;
2327
+ weight: z.ZodNumber;
2328
+ reasons: z.ZodArray<z.ZodString>;
2329
+ }, z.core.$strip>;
2330
+ declare const KnowledgeIndexSchema: z.ZodObject<{
2331
+ root: z.ZodString;
2332
+ generatedAt: z.ZodString;
2333
+ sources: z.ZodArray<z.ZodObject<{
2334
+ id: z.ZodString;
2335
+ uri: z.ZodString;
2336
+ title: z.ZodOptional<z.ZodString>;
2337
+ mediaType: z.ZodOptional<z.ZodString>;
2338
+ contentHash: z.ZodString;
2339
+ text: z.ZodOptional<z.ZodString>;
2340
+ anchors: z.ZodOptional<z.ZodArray<z.ZodObject<{
2341
+ id: z.ZodString;
2342
+ sourceId: z.ZodString;
2343
+ label: z.ZodOptional<z.ZodString>;
2344
+ page: z.ZodOptional<z.ZodNumber>;
2345
+ lineStart: z.ZodOptional<z.ZodNumber>;
2346
+ lineEnd: z.ZodOptional<z.ZodNumber>;
2347
+ charStart: z.ZodOptional<z.ZodNumber>;
2348
+ charEnd: z.ZodOptional<z.ZodNumber>;
2349
+ timestampMs: z.ZodOptional<z.ZodNumber>;
2350
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
2351
+ }, z.core.$strip>>>;
2352
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
2353
+ createdAt: z.ZodString;
2354
+ }, z.core.$strip>>;
2355
+ pages: z.ZodArray<z.ZodObject<{
2356
+ id: z.ZodString;
2357
+ path: z.ZodString;
2358
+ title: z.ZodString;
2359
+ text: z.ZodString;
2360
+ frontmatter: z.ZodRecord<z.ZodString, z.ZodUnknown>;
2361
+ sourceIds: z.ZodArray<z.ZodString>;
2362
+ tags: z.ZodArray<z.ZodString>;
2363
+ outLinks: z.ZodArray<z.ZodString>;
2364
+ }, z.core.$strip>>;
2365
+ graph: z.ZodObject<{
2366
+ nodes: z.ZodArray<z.ZodObject<{
2367
+ id: z.ZodString;
2368
+ title: z.ZodString;
2369
+ path: z.ZodString;
2370
+ tags: z.ZodArray<z.ZodString>;
2371
+ sourceIds: z.ZodArray<z.ZodString>;
2372
+ outDegree: z.ZodNumber;
2373
+ inDegree: z.ZodNumber;
2374
+ }, z.core.$strip>>;
2375
+ edges: z.ZodArray<z.ZodObject<{
2376
+ source: z.ZodString;
2377
+ target: z.ZodString;
2378
+ weight: z.ZodNumber;
2379
+ reasons: z.ZodArray<z.ZodString>;
2380
+ }, z.core.$strip>>;
2381
+ }, z.core.$strip>;
2382
+ }, z.core.$strip>;
2383
+ declare const KnowledgeEventSchema: z.ZodObject<{
2384
+ id: z.ZodString;
2385
+ type: z.ZodEnum<{
2386
+ "source.added": "source.added";
2387
+ "proposal.applied": "proposal.applied";
2388
+ "index.built": "index.built";
2389
+ "lint.run": "lint.run";
2390
+ "optimization.run": "optimization.run";
2391
+ "release.promoted": "release.promoted";
2392
+ "release.rejected": "release.rejected";
2393
+ }>;
2394
+ createdAt: z.ZodString;
2395
+ actor: z.ZodOptional<z.ZodString>;
2396
+ target: z.ZodOptional<z.ZodString>;
2397
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
2398
+ }, z.core.$strip>;
2399
+ declare const KnowledgeBaseCandidateSchema: z.ZodObject<{
2400
+ id: z.ZodString;
2401
+ units: z.ZodArray<z.ZodObject<{
2402
+ id: z.ZodString;
2403
+ title: z.ZodString;
2404
+ text: z.ZodString;
2405
+ claims: z.ZodOptional<z.ZodArray<z.ZodObject<{
2406
+ id: z.ZodString;
2407
+ text: z.ZodString;
2408
+ refs: z.ZodArray<z.ZodObject<{
2409
+ sourceId: z.ZodString;
2410
+ anchorId: z.ZodOptional<z.ZodString>;
2411
+ quote: z.ZodOptional<z.ZodString>;
2412
+ }, z.core.$strip>>;
2413
+ confidence: z.ZodOptional<z.ZodNumber>;
2414
+ status: z.ZodOptional<z.ZodEnum<{
2415
+ draft: "draft";
2416
+ active: "active";
2417
+ superseded: "superseded";
2418
+ rejected: "rejected";
2419
+ }>>;
2420
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
2421
+ }, z.core.$strip>>>;
2422
+ relations: z.ZodOptional<z.ZodArray<z.ZodObject<{
2423
+ sourceId: z.ZodString;
2424
+ targetId: z.ZodString;
2425
+ predicate: z.ZodString;
2426
+ weight: z.ZodOptional<z.ZodNumber>;
2427
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
2428
+ }, z.core.$strip>>>;
2429
+ sourceIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
2430
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
2431
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
2432
+ updatedAt: z.ZodOptional<z.ZodString>;
2433
+ }, z.core.$strip>>;
2434
+ retrievalPolicy: z.ZodOptional<z.ZodString>;
2435
+ synthesisPolicy: z.ZodOptional<z.ZodString>;
2436
+ questionPolicy: z.ZodOptional<z.ZodString>;
2437
+ updatePolicy: z.ZodOptional<z.ZodString>;
2438
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
2439
+ }, z.core.$strip>;
2440
+
2441
+ declare function searchKnowledge(index: KnowledgeIndex, query: string, limit?: number): KnowledgeSearchResult[];
2442
+ declare function tokenizeQuery(query: string): string[];
2443
+ declare function reciprocalRankFusion(rankLists: string[][], k?: number): Map<string, number>;
2444
+
2445
+ interface KnowledgeLayout {
2446
+ root: string;
2447
+ knowledgeDir: string;
2448
+ rawSourcesDir: string;
2449
+ sourceRegistryPath: string;
2450
+ indexPath: string;
2451
+ logPath: string;
2452
+ cacheDir: string;
1296
2453
  }
2454
+ declare function layoutFor(root: string): KnowledgeLayout;
1297
2455
  /**
1298
- * The verifying driver: a real LLM pass that judges each candidate source's
1299
- * on-topic relevance to the goal + open gaps and whether it duplicates material
1300
- * already accepted this round. This is the differentiated coordinator role — it
1301
- * GATES the worker's additions; it adds nothing itself.
2456
+ * Filenames that `initKnowledgeBase` writes as human-navigation scaffolding.
2457
+ * These are excluded from the page index they exist on disk so authors can
2458
+ * curate their vault, but they are not searchable content.
1302
2459
  *
1303
- * The loop already dedups exact-uri duplicates and only calls this on genuinely
1304
- * new candidates, so the verifier focuses on relevance + near-duplicate
1305
- * judgement, not bookkeeping.
2460
+ * Add new scaffold filenames here (and only here) to keep lint, validate, viz,
2461
+ * and the indexer consistent.
1306
2462
  */
1307
- declare function createVerifyingResearchDriver(options?: VerifyingDriverOptions): ResearchDriver;
2463
+ declare const SCAFFOLD_PAGE_BASENAMES: readonly string[];
2464
+ /**
2465
+ * True when a knowledge-relative path points at a scaffold file rather than
2466
+ * authored content. Accepts both repo-relative paths (`knowledge/index.md`)
2467
+ * and any nested `<dir>/index.md` or `<dir>/log.md` (for example
2468
+ * `knowledge/concepts/index.md`) so subdirectory README-style scaffolds are
2469
+ * also excluded.
2470
+ */
2471
+ declare function isScaffoldPath(path: string): boolean;
2472
+ declare function initKnowledgeBase(root: string): Promise<KnowledgeLayout>;
2473
+ declare function loadKnowledgePages(root: string): Promise<KnowledgePage[]>;
2474
+ declare function writeJson(path: string, value: unknown): Promise<void>;
1308
2475
 
1309
2476
  declare const WIKILINK_REGEX: RegExp;
1310
2477
  declare function extractWikilinks(content: string): string[];
@@ -1313,4 +2480,4 @@ declare function normalizeLinkTarget(target: string): string;
1313
2480
  declare function isSafeKnowledgePath(path: string, allowedPrefixes?: string[]): boolean;
1314
2481
  declare function parseKnowledgeWriteBlocks(text: string, allowedPrefixes?: string[]): KnowledgeWriteParseResult;
1315
2482
 
1316
- export { type AddSourceOptions, type AddSourceTextInput, type ApplyWriteBlocksResult, type BuildEvalKnowledgeBundleOptions, type ChunkingOptions, type D1Adapter, type DefineReadinessSpecInput, type DetectChangesOptions, type DetectChangesResult, type DiscoveryResult, type DiscoveryTask, type DriverResearchContext, type EvalKnowledgeBundleBuildResult, type FileSystemFreshnessStoreOptions, FileSystemKbStore, type FreshnessKey, type FreshnessMark, type FreshnessRecord, type FreshnessTtl, type KbStore, KnowledgeBaseCandidateSchema, type KnowledgeChange, type KnowledgeChangeKind, type KnowledgeChunk, KnowledgeClaim, type KnowledgeControlLoopAction, type KnowledgeControlLoopActionResult, type KnowledgeControlLoopAdapter, type KnowledgeControlLoopAdapterOptions, type KnowledgeControlLoopState, type KnowledgeDiscoveryDispatcher, type KnowledgeDiscoveryWorker, KnowledgeEvent, type KnowledgeEventQuery, KnowledgeEventSchema, KnowledgeEventType, type KnowledgeExplanation, KnowledgeFragment, type KnowledgeFreshnessStore, type KnowledgeGap, KnowledgeGraph, KnowledgeGraphEdgeSchema, KnowledgeGraphNodeSchema, KnowledgeIndex, KnowledgeIndexSchema, type KnowledgeInspection, type KnowledgeLayout, KnowledgeLintFinding, KnowledgePage, KnowledgePageSchema, type KnowledgeProposal, KnowledgeProposalParseError, type KnowledgeReadinessSpec, KnowledgeRelease, type KnowledgeReleaseInput, type KnowledgeReleaseReport, type KnowledgeResearchLoopContext, type KnowledgeResearchLoopDecision, type KnowledgeResearchLoopResult, type KnowledgeResearchLoopStep, KnowledgeSearchResult, KnowledgeWriteBlock, KnowledgeWriteParseResult, MemoryKbStore, type ParsedFrontmatter, type ProposeFromFindingsResult, READINESS_SPEC_DEFAULTS, RESEARCH_SUPERVISOR_SYSTEM_PROMPT, type RejectedSource, type ResearchContribution, type ResearchDriver, type ResearchSourceProposal, type ResearchSupervisorOptions, type ResearchWorker, type RouterClient, RouterError, type RunKnowledgeResearchLoopOptions, SCAFFOLD_PAGE_BASENAMES, type SourceAdapter, type SourceAdapterInput, type SourceAdapterOutput, SourceAnchorSchema, type SourceFreshnessInspection, SourceRecord, SourceRecordSchema, SourceRegistry, type SourceVerdict, type SourceVerificationContext, type TangleRouterOptions, type TwoAgentResearchLoopOptions, type TwoAgentResearchLoopResult, type TwoAgentResearchRound, type ValidateKnowledgeOptions, type ValidateKnowledgeResult, type VerifyingDriverOptions, WIKILINK_REGEX, type WebResearchWorkerOptions, type WebSearchHit, type WorkerResearchContext, addSourcePath, addSourceText, applyKnowledgeWriteBlocks, applyKnowledgeWriteBlocksFile, buildEvalKnowledgeBundle, buildKnowledgeGraph, buildKnowledgeIndex, chunkMarkdown, createD1FreshnessStoreStub, createFileSystemFreshnessStore, createKnowledgeControlLoopAdapter, createKnowledgeEvent, createLocalDiscoveryDispatcher, createTangleRouterClient, createVerifyingResearchDriver, createWebResearchWorker, defineReadinessSpec, detectChanges, explainKnowledgeTarget, extractWikilinks, formatFrontmatter, initKnowledgeBase, inspectKnowledgeIndex, isSafeKnowledgePath, isScaffoldPath, knowledgeReadinessDeliverable, knowledgeReleaseReport, layoutFor, lintKnowledgeIndex, loadKnowledgePages, loadSourceRegistry, mediaTypeFor, normalizeLinkTarget, parseFrontmatter, parseKnowledgeWriteBlocks, proposeFromFinding, proposeFromFindings, reciprocalRankFusion, runKnowledgeResearchLoop, runResearchSupervisor, runTwoAgentResearchLoop, searchKnowledge, sha256, slugify, sourceMatchesGaps, sourceRegistryPath, stableId, stripFrontmatter, textSourceAdapter, tokenizeQuery, validateKnowledgeIndex, writeJson, writeKnowledgeIndex, writeSourceRegistry };
2483
+ export { type AdaptiveDecision, type AdaptiveDriverOptions, type AdaptiveResearchDriver, type AdaptiveStats, type AddSourceOptions, type AddSourceTextInput, type ApplyWriteBlocksResult, type BuildEvalKnowledgeBundleOptions, type ChunkingOptions, type ClaimGroundingDriverOptions, type CompanyEvalCase, type D1Adapter, type DedupReason, type DeepQuestion, type DeepQuestionKind, type DefineReadinessSpecInput, type DetectChangesOptions, type DetectChangesResult, type DiscoveryResult, type DiscoveryTask, type DriverResearchContext, type EvalKnowledgeBundleBuildResult, type EvaluateKnowledgeBaseReadinessOptions, type ExpectedGroup, type ExternalRagEvalScore, type FactResult, type FileSystemFreshnessStoreOptions, FileSystemKbStore, type FreshnessKey, type FreshnessMark, type FreshnessRecord, type FreshnessTtl, type GroundClaimOptions, type GroundingResult, type KbStore, KnowledgeBaseCandidateSchema, type KnowledgeBaseQualityOptions, type KnowledgeBaseQualityReport, type KnowledgeBaseReadinessEvaluation, type KnowledgeChange, type KnowledgeChangeKind, type KnowledgeChunk, KnowledgeClaim, type KnowledgeControlLoopAction, type KnowledgeControlLoopActionResult, type KnowledgeControlLoopAdapter, type KnowledgeControlLoopAdapterOptions, type KnowledgeControlLoopState, type KnowledgeDiscoveryDispatcher, type KnowledgeDiscoveryWorker, KnowledgeEvent, type KnowledgeEventQuery, KnowledgeEventSchema, KnowledgeEventType, type KnowledgeExplanation, KnowledgeFragment, type KnowledgeFreshnessStore, type KnowledgeGap, KnowledgeGraph, KnowledgeGraphEdgeSchema, KnowledgeGraphNodeSchema, type KnowledgeImprovementCandidateRecord, type KnowledgeImprovementEvaluationInput, type KnowledgeImprovementEvaluator, type KnowledgeImprovementLifecycleRecord, type KnowledgeImprovementMetric, type KnowledgeImprovementOptions, type KnowledgeImprovementResult, type KnowledgeImprovementRetrievalOptions, type KnowledgeImprovementRunState, type KnowledgeImprovementStatus, type KnowledgeImprovementUpdate, type KnowledgeImprovementUpdateInput, KnowledgeIndex, KnowledgeIndexSchema, type KnowledgeInspection, type KnowledgeLayout, KnowledgeLintFinding, KnowledgePage, KnowledgePageSchema, type KnowledgeProposal, KnowledgeProposalParseError, type KnowledgeReadinessSpec, KnowledgeRelease, type KnowledgeReleaseInput, type KnowledgeReleaseReport, type KnowledgeResearchLoopContext, type KnowledgeResearchLoopDecision, type KnowledgeResearchLoopResult, type KnowledgeResearchLoopStep, KnowledgeSearchResult, KnowledgeWriteBlock, KnowledgeWriteParseResult, type MaterialFact, type MaterialFactLens, type MaterialFactsResult, MemoryKbStore, type ParsedFrontmatter, type ProposeFromFindingsResult, READINESS_SPEC_DEFAULTS, type RagAnswerEvalArtifact, type RagAnswerEvalCase, type RagAnswerEvalScenario, type RagAnswerMetricSummary, type RagAnswerQualityHookOptions, type RagAnswerQualityInput, type RagAnswerQualityJudgeOptions, type RagAnswerQualityResult, type RagCalibrationOptions, type RagCalibrationResult, type RagDiagnosisInput, type RagEvalCitation, type RagEvalClaim, type RagEvalContext, type RagEvalMetricKey, type RagEvalProvider, type RagEvalSlice, type RagGapFinding, type RagGapKind, type RagGapSeverity, type RagKnowledgeAcquisitionInput, type RagKnowledgeImprovementPhase, type RagKnowledgeImprovementPhaseResult, type RagKnowledgeImprovementPhaseStatus, type RagKnowledgeResearchOptions, type RagKnowledgeUpdateInput, type RagKnowledgeUpdateResult, type RagPhaseInputBase, type RagPromotionInput, type RagPromotionResult, type RagRequiredContext, type RejectedSource, type ResearchContribution, type ResearchDriver, type ResearchDrivingDriver, type ResearchDrivingDriverOptions, type ResearchDrivingState, type ResearchDrivingSteer, type ResearchSourceProposal, type ResearchWorker, RetrievalConfig, type RouterClient, RouterError, type RouterUsage, type RunKnowledgeResearchLoopOptions, type RunRagKnowledgeImprovementLoopOptions, type RunRagKnowledgeImprovementLoopResult, RunRetrievalImprovementLoopOptions, RunRetrievalImprovementLoopResult, SCAFFOLD_PAGE_BASENAMES, type SourceAdapter, type SourceAdapterInput, type SourceAdapterOutput, SourceAnchorSchema, type SourceFreshnessInspection, SourceRecord, SourceRecordSchema, SourceRegistry, type SourceVerdict, type SourceVerificationContext, type TangleRouterOptions, type ThesisRunOptions, type ThesisRunResult, type ThesisTaskInput, type TrackedClaim, type TriageClass, type TwoAgentResearchLoopOptions, type TwoAgentResearchLoopResult, type TwoAgentResearchRound, type ValidateKnowledgeOptions, type ValidateKnowledgeResult, type VerifiedResearchLoopOptions, type VerifiedResearchLoopResult, type VerifiedResearchRound, type VerifyingDriverOptions, WIKILINK_REGEX, type WebResearchWorkerOptions, type WebSearchHit, type WorkerClaimDecorationOptions, type WorkerResearchContext, addSourcePath, addSourceText, applyKnowledgeWriteBlocks, applyKnowledgeWriteBlocksFile, buildEvalKnowledgeBundle, buildKnowledgeGraph, buildKnowledgeIndex, calibrateRagAnswerJudge, canonicalizeUrl, chunkMarkdown, citedClaimKey, citedClaimOf, contentKey, createAdaptiveResearchDriver, createClaimDecorator, createClaimGroundingVerifier, createCollectionResearchDriver, createD1FreshnessStoreStub, createFileSystemFreshnessStore, createKnowledgeControlLoopAdapter, createKnowledgeEvent, createLocalDiscoveryDispatcher, createRagAnswerQualityHook, createResearchDrivingDriver, createTangleRouterClient, createVerifyingResearchDriver, createWebResearchWorker, defineReadinessSpec, detectChanges, diagnoseRagAnswerFailure, evaluateKnowledgeBaseReadiness, explainKnowledgeTarget, extractWikilinks, formatFrontmatter, gradeCompanyAgainstText, gradeFactAgainstText, groundClaimInText, hashKnowledgeBase, improveKnowledgeBase, initKnowledgeBase, inspectKnowledgeIndex, investmentThesisSet, isSafeKnowledgePath, isScaffoldPath, kbIndexToText, knowledgeImprovementRunDir, knowledgeImprovementRunId, knowledgeReleaseReport, layoutFor, lensDistribution, lintKnowledgeIndex, loadKnowledgeImprovementState, loadKnowledgePages, loadSourceRegistry, materialFactsSurfaced, materialFactsSurfacedInText, mediaTypeFor, normalizeExternalRagScores, normalizeLinkTarget, parseFrontmatter, parseKnowledgeWriteBlocks, proposeFromFinding, proposeFromFindings, ragAnswerQualityJudge, reciprocalRankFusion, runInvestmentThesisTask, runKnowledgeResearchLoop, runRagKnowledgeImprovementLoop, runTwoAgentResearchLoop, runVerifiedResearchLoop, scoreKnowledgeBaseIndex, scoreRagAnswerArtifact, searchKnowledge, sha256, slugify, sourceMatchesGaps, sourceRegistryPath, stableId, stripFrontmatter, textSourceAdapter, thesisReadinessSpecs, toDeepEvalTestCases, toRagCheckerRecords, toRagasEvaluationRows, toTruLensRecords, tokenizeQuery, totalMaterialFacts, triageSource, validateKnowledgeIndex, withCitedClaim, writeJson, writeKnowledgeIndex, writeSourceRegistry };