@odla-ai/kg 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/types.ts","../src/ontology.ts","../src/graph/merge.ts","../src/graph/writer.ts","../src/graph/resolve.ts","../src/llm/provider.ts","../src/llm/json.ts","../src/util/content.ts","../src/connectors/rss.ts","../src/connectors/url.ts","../src/connectors/gdelt.ts","../src/util/identity.ts","../src/connectors/fred.ts","../src/connectors/edgar.ts","../src/connectors/govdata.ts","../src/connectors/web-search.ts","../src/connectors/registry.ts","../src/sources.ts","../src/state/kv.ts","../src/workflow/helpers.ts","../src/pipeline/run-items.ts"],"sourcesContent":["/**\n * @odla-ai/kg — ontology-driven knowledge-graph builder.\n *\n * Pipeline shape: Connector → RawItem[] → extract → GraphFragment →\n * writeFragment → Op[] → odla-db. The ontology is config-as-data; it compiles\n * to the odla-db schema, the writer's natural-key/merge-rule maps, and (by\n * convention) the extractor's tool schema. Consumers supply the ontology\n * instance, the extractor, the AdminDb, and a KV store — see PipelineDeps.\n */\n\n// Core shapes\nexport type { RawItem, GraphNode, GraphEdge, GraphFragment, Usage, ExtractResult } from \"./types\";\nexport { emptyUsage, addUsage } from \"./types\";\n\n// Ontology: types + compilers (ontology instances are consumer data)\nexport type { Ontology, EntitySpec, EdgeSpec, AttrSpec, AttrType, MergeRule } from \"./ontology\";\nexport { naturalKeysOf, mergeRulesOf, humanAttrsOf, toSerializedSchema } from \"./ontology\";\n\n// Graph writer + entity resolution\nexport { fragmentToOps, writeFragment, type NaturalKeys } from \"./graph/writer\";\nexport { mergeScalar, mergeAttrs } from \"./graph/merge\";\nexport { searchEntities, resolveName, canonicalizeFragment } from \"./graph/resolve\";\n\n// LLM seam (extract / search / write, adapted over @odla-ai/ai)\nexport type { Provider, ProviderFamily, ExtractCall, SearchCall, WriteCall, LlmConfig, LlmEnv } from \"./llm/provider\";\nexport { createProvider, getProvider, MODEL_TIERS } from \"./llm/provider\";\nexport { extractJson, extractJsonArray } from \"./llm/json\";\n\n// Connectors\nexport type { SourceConnector, ConnectorContext, ConnectorJob } from \"./connectors/types\";\nexport { getConnector, connectorIds, makeContext, type ContextConfig } from \"./connectors/registry\";\nexport { fetchUrl } from \"./connectors/url\";\n\n// Source scheduling (the source list itself lives with the consumer)\nexport type { SourceEntry } from \"./sources\";\nexport { enabledSources, jobsForHour, allJobs } from \"./sources\";\n\n// Operational state (seen-URL dedup, lastrun stamps, runtime source config)\nexport { Store, type KVLike } from \"./state/kv\";\n\n// Pipeline\nexport { ingestFresh, type PipelineDeps } from \"./pipeline/run-items\";\nexport {\n resilientStep,\n asStepLike,\n IO_STEP_CONFIG,\n COMMIT_STEP_CONFIG,\n LLM_STEP_CONFIG,\n type StepLike,\n type StepConfig,\n} from \"./workflow/helpers\";\n\n// Utilities\nexport { slugify, normalizeUrl, urlHash, isLikelyPersonName } from \"./util/identity\";\nexport { fetchText, htmlToText } from \"./util/content\";\n","import type { Value } from \"@odla-ai/db\";\n\n/**\n * The output of a connector: one acquired item. Exactly one of `text` or\n * `fragment` is set — `text` goes through LLM extraction, `fragment` is already\n * ontology-typed and goes straight to the graph writer (structured feeds).\n */\nexport interface RawItem {\n url: string;\n title: string;\n date?: string;\n sourceName: string;\n sourceKind: string; // connector id: \"url\" | \"rss\" | \"edgar\" | ...\n watch?: string | null;\n text?: string;\n fragment?: GraphFragment;\n}\n\n/** A graph node = an odla-db entity in a namespace, keyed by a natural key. */\nexport interface GraphNode {\n type: string; // namespace, e.g. \"company\"\n key: string; // natural-key value (slug, urlHash, ...) — used for Lookup upsert\n attrs: Record<string, Value>; // node properties (the natural-key attr is added by the writer)\n}\n\n/** A graph edge = an odla-db `link` from one node to another by forward label. */\nexport interface GraphEdge {\n from: { type: string; key: string };\n to: { type: string; key: string };\n label: string; // forward link label, e.g. \"mentions\"\n}\n\n/** The single shape the graph writer consumes, whatever produced it. */\nexport interface GraphFragment {\n nodes: GraphNode[];\n edges: GraphEdge[];\n}\n\n/** What an extractor turns a text RawItem into: a fragment plus a prose summary. */\nexport interface ExtractResult {\n fragment: GraphFragment;\n summary: string;\n}\n\n/** Provider-agnostic LLM usage accounting. */\nexport interface Usage {\n calls: number;\n inputTokens: number;\n outputTokens: number;\n}\n\nexport function emptyUsage(): Usage {\n return { calls: 0, inputTokens: 0, outputTokens: 0 };\n}\n\nexport function addUsage(into: Usage, more: Usage): void {\n into.calls += more.calls;\n into.inputTokens += more.inputTokens;\n into.outputTokens += more.outputTokens;\n}\n","import type { SerializedAttr, SerializedEntity, SerializedLink, SerializedSchema, Cardinality } from \"@odla-ai/db\";\nimport type { NaturalKeys } from \"./graph/writer\";\n\n/** How a scalar attribute is reconciled when a node is re-discovered. */\nexport type MergeRule =\n | \"replace\" // last write wins (default)\n | \"prefer\" // first non-empty value sticks (stable facts like a name)\n | \"earliest\" // keep the smaller value (first_seen)\n | \"latest\"; // keep the larger value (last_seen)\n\nexport type AttrType = SerializedAttr[\"type\"]; // \"string\" | \"number\" | \"boolean\" | \"date\" | \"json\"\n\nexport interface AttrSpec {\n type: AttrType;\n unique?: boolean;\n indexed?: boolean;\n optional?: boolean;\n /** The pipeline NEVER writes this attr (hand-edited in a UI); omitted from every update. */\n human?: boolean;\n /** Provenance rule for re-discovery. Default \"replace\". */\n merge?: MergeRule;\n /** Shown to the LLM when this attr appears in an extraction schema. */\n desc?: string;\n}\n\nexport interface EntitySpec {\n /** Unique attr used for Lookup upsert (MERGE key). Must be `unique` in attrs. */\n naturalKey: string;\n attrs: Record<string, AttrSpec>;\n /** FTS fields for fuzzy resolution (P5). */\n searchable?: string[];\n}\n\nexport interface EdgeSpec {\n from: string; // entity type\n to: string; // entity type\n forwardLabel: string; // link label on the `from` side (what the `link` op carries)\n reverseLabel: string;\n forwardHas?: Cardinality; // default \"many\"\n reverseHas?: Cardinality; // default \"many\"\n}\n\nexport interface Ontology {\n entities: Record<string, EntitySpec>;\n edges: Record<string, EdgeSpec>; // keyed by edge name\n}\n\n/** node type → natural-key attr name (for the graph writer's Lookup refs). */\nexport function naturalKeysOf(o: Ontology): NaturalKeys {\n const m: NaturalKeys = {};\n for (const [type, e] of Object.entries(o.entities)) m[type] = e.naturalKey;\n return m;\n}\n\n/** Attr name → merge rule, for every entity type (used by the provenance writer). */\nexport function mergeRulesOf(o: Ontology): Record<string, Record<string, MergeRule>> {\n const out: Record<string, Record<string, MergeRule>> = {};\n for (const [type, e] of Object.entries(o.entities)) {\n out[type] = {};\n for (const [attr, spec] of Object.entries(e.attrs)) out[type][attr] = spec.merge ?? \"replace\";\n }\n return out;\n}\n\n/** Attr names the pipeline must never write, per entity type. */\nexport function humanAttrsOf(o: Ontology): Record<string, Set<string>> {\n const out: Record<string, Set<string>> = {};\n for (const [type, e] of Object.entries(o.entities)) {\n out[type] = new Set(Object.entries(e.attrs).filter(([, a]) => a.human).map(([n]) => n));\n }\n return out;\n}\n\n/** Compile the ontology to an odla-db SerializedSchema to POST at /app/:id/schema. */\nexport function toSerializedSchema(o: Ontology): SerializedSchema {\n const entities: Record<string, SerializedEntity> = {};\n for (const [type, e] of Object.entries(o.entities)) {\n const attrs: Record<string, SerializedAttr> = {};\n for (const [name, a] of Object.entries(e.attrs)) {\n attrs[name] = {\n type: a.type,\n unique: !!a.unique,\n indexed: !!a.indexed,\n optional: !!a.optional,\n };\n }\n entities[type] = { attrs };\n }\n const links: Record<string, SerializedLink> = {};\n for (const [name, ed] of Object.entries(o.edges)) {\n links[name] = {\n forward: { on: ed.from, has: ed.forwardHas ?? \"many\", label: ed.forwardLabel },\n reverse: { on: ed.to, has: ed.reverseHas ?? \"many\", label: ed.reverseLabel },\n };\n }\n return { entities, links };\n}\n","import type { Value } from \"@odla-ai/db\";\nimport type { MergeRule } from \"../ontology\";\n\nfunction isEmpty(v: Value | undefined): boolean {\n return v === undefined || v === null || v === \"\";\n}\n\n/**\n * Reconcile one scalar attribute when a node is re-discovered. Port of agnacl's\n * provenance rules (utils/markdown.ts). Empty incoming never clobbers; empty\n * existing is filled. Ordering rules compare stringwise (ISO dates sort right).\n */\nexport function mergeScalar(\n rule: MergeRule,\n existing: Value | undefined,\n incoming: Value | undefined,\n): Value | undefined {\n if (isEmpty(incoming)) return existing;\n if (isEmpty(existing)) return incoming;\n switch (rule) {\n case \"prefer\":\n return existing; // first non-empty value sticks\n case \"earliest\":\n return String(incoming) < String(existing) ? incoming : existing;\n case \"latest\":\n return String(incoming) > String(existing) ? incoming : existing;\n case \"replace\":\n default:\n return incoming;\n }\n}\n\n/**\n * Merge an incoming attr set into an existing one per the type's rules. `human`\n * attrs are excluded upstream (never in `incoming`); this only reconciles what\n * the pipeline owns. Returns the attrs to write (update leaves unlisted attrs intact).\n */\nexport function mergeAttrs(\n rules: Record<string, MergeRule>,\n existing: Record<string, Value>,\n incoming: Record<string, Value>,\n): Record<string, Value> {\n const out: Record<string, Value> = {};\n for (const [attr, val] of Object.entries(incoming)) {\n const merged = mergeScalar(rules[attr] ?? \"replace\", existing[attr], val);\n if (merged !== undefined) out[attr] = merged;\n }\n return out;\n}\n","import type {\n AdminDb,\n Op,\n EntityRef,\n Value,\n InstaQLQuery,\n QueryResult,\n} from \"@odla-ai/db\";\nimport type { GraphFragment } from \"../types\";\nimport {\n naturalKeysOf,\n mergeRulesOf,\n humanAttrsOf,\n type Ontology,\n} from \"../ontology\";\nimport { mergeAttrs } from \"./merge\";\n\n/** node type → unique natural-key attribute name. */\nexport type NaturalKeys = Record<string, string>;\n\n/**\n * Pure fragment → ops mapping (no read/merge). Node upserts by Lookup natural\n * key, then links — nodes always before links so `link` targets resolve.\n * Used by tests and as the building block for the provenance path below.\n */\nexport function fragmentToOps(fragment: GraphFragment, keys: NaturalKeys): Op[] {\n const ref = (type: string, key: string): EntityRef => {\n const attr = keys[type];\n if (!attr) throw new Error(`No natural key configured for node type \"${type}\"`);\n return { ns: type, attr, value: key };\n };\n const ops: Op[] = [];\n for (const n of fragment.nodes) {\n const attr = keys[n.type];\n if (!attr) throw new Error(`No natural key configured for node type \"${n.type}\"`);\n ops.push({ t: \"update\", ns: n.type, id: { ns: n.type, attr, value: n.key }, attrs: { ...n.attrs, [attr]: n.key } });\n }\n for (const e of fragment.edges) {\n ops.push({ t: \"link\", ns: e.from.type, id: ref(e.from.type, e.from.key), label: e.label, target: ref(e.to.type, e.to.key) });\n }\n return ops;\n}\n\n/**\n * Provenance-preserving write. Reads existing nodes by natural key (one batched\n * query), merges scalar attrs per the ontology's rules, drops human-owned\n * attrs, then writes merged node updates + all links in one atomic, idempotent\n * transaction. `mutationId` must be deterministic for exactly-once.\n */\nexport async function writeFragment(\n db: AdminDb,\n fragment: GraphFragment,\n ontology: Ontology,\n mutationId: string,\n): Promise<{ txId: number; ops: number }> {\n const keys = naturalKeysOf(ontology);\n const rules = mergeRulesOf(ontology);\n const humans = humanAttrsOf(ontology);\n\n // 1. Collapse duplicate nodes within the fragment (same type+key), dropping human attrs.\n const byType = new Map<string, Map<string, Record<string, Value>>>();\n for (const n of fragment.nodes) {\n if (!keys[n.type]) throw new Error(`No natural key configured for node type \"${n.type}\"`);\n const human = humans[n.type] ?? new Set<string>();\n const clean: Record<string, Value> = {};\n for (const [k, v] of Object.entries(n.attrs)) if (!human.has(k)) clean[k] = v;\n const typeMap = byType.get(n.type) ?? new Map<string, Record<string, Value>>();\n const prev = typeMap.get(n.key);\n typeMap.set(n.key, prev ? mergeAttrs(rules[n.type]!, prev, clean) : clean);\n byType.set(n.type, typeMap);\n }\n\n // 2. Batch-read existing rows per type by natural key (one query, filtered per level).\n const query: InstaQLQuery = {};\n for (const [type, m] of byType) {\n query[type] = { $: { where: { [keys[type]!]: { $in: [...m.keys()] } } } };\n }\n const existing: QueryResult = Object.keys(query).length ? await db.query(query) : {};\n const priorOf = (type: string, key: string): Record<string, Value> => {\n const attr = keys[type]!;\n for (const row of existing[type] ?? []) {\n if (row[attr] === key) return row as unknown as Record<string, Value>;\n }\n return {};\n };\n\n // 3. Merged node updates first, then links.\n const ops: Op[] = [];\n for (const [type, m] of byType) {\n const attr = keys[type]!;\n for (const [key, incoming] of m) {\n const merged = mergeAttrs(rules[type]!, priorOf(type, key), incoming);\n ops.push({ t: \"update\", ns: type, id: { ns: type, attr, value: key }, attrs: { ...merged, [attr]: key } });\n }\n }\n const ref = (type: string, key: string): EntityRef => ({ ns: type, attr: keys[type]!, value: key });\n for (const e of fragment.edges) {\n ops.push({ t: \"link\", ns: e.from.type, id: ref(e.from.type, e.from.key), label: e.label, target: ref(e.to.type, e.to.key) });\n }\n\n if (ops.length === 0) return { txId: 0, ops: 0 };\n const txId = await db.transact(ops, { mutationId });\n return { txId, ops: ops.length };\n}\n","import type { AdminDb, Entity } from \"@odla-ai/db\";\nimport type { GraphFragment } from \"../types\";\nimport type { Ontology } from \"../ontology\";\n\n/** FTS5 search over a namespace's text fields (odla-db QueryOpts.search). */\nexport async function searchEntities(db: AdminDb, type: string, q: string, limit = 5): Promise<Entity[]> {\n if (!q.trim()) return [];\n const res = await db.query({ [type]: { $: { search: q, limit } } });\n return (res[type] ?? []) as Entity[];\n}\n\nconst norm = (s: string): string => s.toLowerCase().replace(/[^a-z0-9]+/g, \"\");\n\n/**\n * Resolve a name to an existing entity's natural key via FTS — for merging a\n * re-discovery that slugified differently. Conservative: only returns a hit when\n * the top match's name/key normalizes identically to the query.\n */\nexport async function resolveName(\n db: AdminDb,\n type: string,\n keyAttr: string,\n name: string,\n): Promise<string | null> {\n const target = norm(name);\n if (!target) return null;\n for (const h of await searchEntities(db, type, name, 3)) {\n const key = h[keyAttr];\n const hname = h[\"name\"];\n if (typeof key !== \"string\") continue;\n if ((typeof hname === \"string\" && norm(hname) === target) || norm(key) === target) return key;\n }\n return null;\n}\n\n/**\n * Canonicalize a fragment's searchable nodes against existing entities via FTS,\n * remapping node keys (and the edges that reference them) to the canonical key.\n * Used on the lower-volume discovery/enrich paths, not high-volume ingest.\n */\nexport async function canonicalizeFragment(\n db: AdminDb,\n fragment: GraphFragment,\n ontology: Ontology,\n): Promise<GraphFragment> {\n const remap = new Map<string, string>(); // `${type}:${key}` -> canonical key\n for (const n of fragment.nodes) {\n const spec = ontology.entities[n.type];\n if (!spec?.searchable?.length) continue;\n const name = typeof n.attrs.name === \"string\" ? n.attrs.name : \"\";\n if (!name) continue;\n const canon = await resolveName(db, n.type, spec.naturalKey, name);\n if (canon && canon !== n.key) remap.set(`${n.type}:${n.key}`, canon);\n }\n if (remap.size === 0) return fragment;\n\n const rk = (type: string, key: string): string => remap.get(`${type}:${key}`) ?? key;\n return {\n nodes: fragment.nodes.map((n) => ({ ...n, key: rk(n.type, n.key) })),\n edges: fragment.edges.map((e) => ({\n from: { type: e.from.type, key: rk(e.from.type, e.from.key) },\n to: { type: e.to.type, key: rk(e.to.type, e.to.key) },\n label: e.label,\n })),\n };\n}\n","import { init, extractText, type Ai } from \"@odla-ai/ai\";\nimport type { Usage } from \"../types\";\n\n/**\n * The LLM seam, now a thin adapter over @odla-ai/ai (which was itself\n * generalized from this package's original Provider). Three verbs stay:\n * extract (forced structured tool call), search (web-search-enabled text),\n * write (plain generation). Callers never see vendor or oracle types.\n */\n\nexport type ProviderFamily = \"claude\" | \"openai\";\n\n/** Role → model tiers per family — config as data. `cheap` is for gate-style\n * calls, `extract` for structured extraction (and prose writing), `search`\n * for web-search-enabled calls. */\nexport const MODEL_TIERS: Record<ProviderFamily, { cheap: string; extract: string; search: string }> = {\n claude: { cheap: \"claude-haiku-4-5\", extract: \"claude-sonnet-5\", search: \"claude-sonnet-5\" },\n openai: { cheap: \"gpt-5-nano\", extract: \"gpt-5-mini\", search: \"gpt-5-search-api\" },\n};\n\n/** A forced structured tool call: schema in, validated object out. */\nexport interface ExtractCall {\n system: string;\n user: string;\n tool: { name: string; description?: string; parameters: Record<string, unknown> };\n model?: string;\n maxTokens?: number;\n}\n\n/** A web-search-enabled call: the model searches and returns text (caller tolerant-parses). */\nexport interface SearchCall {\n system: string;\n user: string;\n model?: string;\n maxTokens?: number;\n}\n\n/** A plain text-generation call (no tools, no search) — e.g. digest prose. */\nexport interface WriteCall {\n system: string;\n user: string;\n model?: string;\n maxTokens?: number;\n}\n\nexport interface Provider {\n id: ProviderFamily;\n /** The underlying @odla-ai/ai client — escape hatch for advanced use. */\n ai: Ai;\n extract<T = unknown>(c: ExtractCall): Promise<{ value: T; usage: Usage }>;\n search(c: SearchCall): Promise<{ text: string; usage: Usage }>;\n write(c: WriteCall): Promise<{ text: string; usage: Usage }>;\n}\n\nexport interface LlmConfig {\n provider: ProviderFamily;\n anthropicKey?: string;\n openaiKey?: string;\n /** Inject a pre-built Ai (tests, custom catalog); keys are ignored when set. */\n ai?: Ai;\n}\n\nfunction toUsage(u: { inputTokens: number; outputTokens: number }): Usage {\n return { calls: 1, inputTokens: u.inputTokens, outputTokens: u.outputTokens };\n}\n\n/** The library primitive: an explicit config in, the three-verb seam out. */\nexport function createProvider(cfg: LlmConfig): Provider {\n const tiers = MODEL_TIERS[cfg.provider];\n const ai = cfg.ai ?? init({ keys: { anthropic: cfg.anthropicKey, openai: cfg.openaiKey } });\n return {\n id: cfg.provider,\n ai,\n async extract<T = unknown>(c: ExtractCall) {\n const { value, usage } = await ai.extract<T>({\n model: c.model ?? tiers.extract,\n system: c.system,\n user: c.user,\n tool: c.tool,\n maxTokens: c.maxTokens,\n });\n return { value, usage: toUsage(usage) };\n },\n async search(c: SearchCall) {\n const { text, usage } = await ai.search({\n model: c.model ?? tiers.search,\n system: c.system,\n user: c.user,\n maxTokens: c.maxTokens,\n });\n return { text, usage: toUsage(usage) };\n },\n async write(c: WriteCall) {\n const res = await ai.chat({\n model: c.model ?? tiers.extract,\n system: c.system,\n messages: [{ role: \"user\", content: c.user }],\n maxTokens: c.maxTokens ?? 2048,\n });\n return { text: extractText(res.content), usage: toUsage(res.usage) };\n },\n };\n}\n\n/** The env vars the provider factory reads — a structural slice, not the\n * worker Env, so any host (worker, script, test) can supply plain strings. */\nexport interface LlmEnv {\n LLM_PROVIDER?: string; // \"claude\" | \"openai\"\n ANTHROPIC_API_KEY?: string;\n OPENAI_API_KEY?: string;\n}\n\nexport function getProvider(env: LlmEnv): Provider {\n const which: ProviderFamily = (env.LLM_PROVIDER || \"claude\").toLowerCase() === \"openai\" ? \"openai\" : \"claude\";\n if (which === \"openai\" && !env.OPENAI_API_KEY) throw new Error(\"LLM_PROVIDER=openai but OPENAI_API_KEY is unset\");\n if (which === \"claude\" && !env.ANTHROPIC_API_KEY) throw new Error(\"LLM_PROVIDER=claude but ANTHROPIC_API_KEY is unset\");\n return createProvider({ provider: which, anthropicKey: env.ANTHROPIC_API_KEY, openaiKey: env.OPENAI_API_KEY });\n}\n","/**\n * Tolerant JSON extraction from LLM prose — web-search models return JSON inside\n * explanatory text. Ported from agnacl services/enrichment.ts.\n */\n\nexport function extractJson<T = unknown>(text: string): T | null {\n const m = text.match(/\\{[\\s\\S]*\\}/);\n if (!m) return null;\n try {\n return JSON.parse(m[0]) as T;\n } catch {\n return null;\n }\n}\n\nexport function extractJsonArray<T = unknown>(text: string): T[] {\n const m = text.match(/\\[[\\s\\S]*\\]/);\n if (!m) return [];\n try {\n const v = JSON.parse(m[0]);\n return Array.isArray(v) ? (v as T[]) : [];\n } catch {\n return [];\n }\n}\n","/**\n * Dependency-free HTML → text extraction (Workers-isolate friendly: no DOM/jsdom).\n * Ported from agnacl services/content-extractor.ts.\n */\n\nconst MAX_CHARS = 8000;\n\n/** Strip scripts/styles/tags and collapse whitespace; cap length. */\nexport function htmlToText(html: string, maxChars = MAX_CHARS): string {\n let t = html;\n t = t.replace(/<script[\\s\\S]*?<\\/script>/gi, \" \");\n t = t.replace(/<style[\\s\\S]*?<\\/style>/gi, \" \");\n t = t.replace(/<!--[\\s\\S]*?-->/g, \" \");\n t = t.replace(/<\\/(p|div|section|article|li|h[1-6]|br)>/gi, \"\\n\");\n t = t.replace(/<[^>]+>/g, \" \");\n t = decodeEntities(t);\n t = t.replace(/[ \\t\\f\\r]+/g, \" \").replace(/\\n\\s*\\n\\s*/g, \"\\n\\n\").trim();\n return t.slice(0, maxChars);\n}\n\nfunction decodeEntities(s: string): string {\n return s\n .replace(/&nbsp;/gi, \" \")\n .replace(/&amp;/gi, \"&\")\n .replace(/&lt;/gi, \"<\")\n .replace(/&gt;/gi, \">\")\n .replace(/&quot;/gi, '\"')\n .replace(/&#39;|&apos;/gi, \"'\")\n .replace(/&#(\\d+);/g, (_, d) => String.fromCharCode(Number(d)))\n .replace(/&#x([0-9a-f]+);/gi, (_, h) => String.fromCharCode(parseInt(h, 16)));\n}\n\n/** Fetch a URL and return readable text. Best-effort: returns \"\" on any failure. */\nexport async function fetchText(url: string, maxChars = MAX_CHARS): Promise<string> {\n try {\n const res = await fetch(url, {\n headers: {\n \"user-agent\": \"odla-kg/0.1 (+https://github.com/cory/odla-kg)\",\n accept: \"text/html,application/xhtml+xml\",\n },\n });\n if (!res.ok) return \"\";\n const ct = res.headers.get(\"content-type\") || \"\";\n const body = await res.text();\n if (ct.includes(\"html\") || /<\\w+[\\s>]/.test(body.slice(0, 200))) {\n return htmlToText(body, maxChars);\n }\n return body.slice(0, maxChars);\n } catch {\n return \"\";\n }\n}\n","import type { RawItem } from \"../types\";\nimport type { SourceConnector, ConnectorContext } from \"./types\";\nimport { htmlToText } from \"../util/content\";\n\n/** Dependency-free RSS/Atom parsing, ported from agnacl services/rss.ts. */\nconst UA = \"Mozilla/5.0 (compatible; odla-kg/0.1)\";\n\nfunction extractTag(xml: string, tag: string): string {\n const cdata = xml.match(new RegExp(`<${tag}[^>]*>\\\\s*<!\\\\[CDATA\\\\[([\\\\s\\\\S]*?)\\\\]\\\\]>\\\\s*</${tag}>`, \"i\"));\n if (cdata) return cdata[1]!.trim();\n const m = xml.match(new RegExp(`<${tag}[^>]*>([\\\\s\\\\S]*?)</${tag}>`, \"i\"));\n return m ? m[1]!.trim() : \"\";\n}\n\nfunction extractAttr(xml: string, tag: string, attr: string): string {\n const m = xml.match(new RegExp(`<${tag}[^>]*\\\\s${attr}=[\"']([^\"']*)[\"']`, \"i\"));\n return m ? m[1]!.trim() : \"\";\n}\n\nfunction splitItems(xml: string): string[] {\n return xml.match(/<item[\\s>][\\s\\S]*?<\\/item>/gi) || xml.match(/<entry[\\s>][\\s\\S]*?<\\/entry>/gi) || [];\n}\n\ninterface RssCfg {\n url: string;\n name: string;\n watch?: string | null;\n}\n\nexport const rssConnector: SourceConnector<RssCfg> = {\n id: \"rss\",\n capabilities: { fetch: true, structured: false },\n\n parseConfig(raw): RssCfg {\n const r = raw as Record<string, unknown>;\n if (typeof r.url !== \"string\" || !r.url) throw new Error(\"rss connector requires a `url`\");\n return { url: r.url, name: String(r.name ?? r.url), watch: (r.watch as string | null) ?? null };\n },\n\n async fetch(cfg, ctx): Promise<RawItem[]> {\n let xml: string;\n try {\n const res = await fetch(cfg.url, { headers: { \"User-Agent\": UA } });\n if (!res.ok) {\n if (res.status === 429 || res.status === 403 || res.status >= 500) return [];\n throw new Error(`feed ${cfg.url} → ${res.status}`);\n }\n xml = await res.text();\n } catch (e) {\n console.log(`rss fetch failed (non-blocking): ${cfg.url}: ${e}`);\n return [];\n }\n\n const out: RawItem[] = [];\n for (const raw of splitItems(xml)) {\n const title = extractTag(raw, \"title\");\n let url = extractTag(raw, \"link\") || extractAttr(raw, \"link\", \"href\");\n if (!title || !url) continue;\n url = url.trim();\n const date =\n extractTag(raw, \"pubDate\") || extractTag(raw, \"published\") || extractTag(raw, \"updated\") || undefined;\n const content =\n extractTag(raw, \"content:encoded\") ||\n extractTag(raw, \"content\") ||\n extractTag(raw, \"description\") ||\n extractTag(raw, \"summary\") ||\n \"\";\n let text = htmlToText(content);\n // Thin feed body → fetch the article page.\n if (text.length < 200) {\n const page = await ctx.fetchText(url);\n if (page.length > text.length) text = page;\n }\n out.push({ url, title, date, sourceName: cfg.name, sourceKind: \"rss\", watch: cfg.watch, text });\n }\n return out;\n },\n};\n","import type { RawItem } from \"../types\";\nimport type { SourceConnector } from \"./types\";\nimport { fetchText } from \"../util/content\";\n\n/** Fetch one URL → a text-bearing RawItem. Used both directly (/trigger) and as a connector. */\nexport async function fetchUrl(url: string): Promise<RawItem> {\n const text = await fetchText(url);\n const firstLine = text.split(\"\\n\").find((l) => l.trim().length > 0) ?? \"\";\n let host = url;\n try {\n host = new URL(url).hostname;\n } catch {\n /* keep raw url as the source label */\n }\n return { url, title: (firstLine || url).slice(0, 200).trim(), sourceName: host, sourceKind: \"url\", text };\n}\n\ninterface UrlCfg {\n url: string;\n watch?: string | null;\n}\n\nexport const urlConnector: SourceConnector<UrlCfg> = {\n id: \"url\",\n capabilities: { fetch: true, structured: false },\n parseConfig(raw): UrlCfg {\n const r = raw as Record<string, unknown>;\n if (typeof r.url !== \"string\" || !r.url) throw new Error(\"url connector requires a `url`\");\n return { url: r.url, watch: (r.watch as string | null) ?? null };\n },\n async fetch(cfg): Promise<RawItem[]> {\n const item = await fetchUrl(cfg.url);\n if (cfg.watch !== undefined) item.watch = cfg.watch;\n return [item];\n },\n};\n","import type { RawItem } from \"../types\";\nimport type { SourceConnector, ConnectorContext } from \"./types\";\n\n/** GDELT DOC 2.0 standing-query connector (text). Ported from agnacl services/gdelt.ts. */\nconst UA = \"Mozilla/5.0 (compatible; odla-kg/0.1)\";\nconst MAX_ITEMS = 15;\n\ninterface GdeltCfg {\n query: string;\n name: string;\n watch?: string | null;\n}\n\nfunction isoFromSeendate(s: string): string | undefined {\n const m = s.match(/^(\\d{4})(\\d{2})(\\d{2})T(\\d{2})(\\d{2})(\\d{2})Z?$/);\n return m ? `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}Z` : undefined;\n}\n\nexport const gdeltConnector: SourceConnector<GdeltCfg> = {\n id: \"gdelt\",\n capabilities: { fetch: true, structured: false },\n\n parseConfig(raw): GdeltCfg {\n const r = raw as Record<string, unknown>;\n if (typeof r.query !== \"string\" || !r.query) throw new Error(\"gdelt connector requires a `query`\");\n return { query: r.query, name: String(r.name ?? `GDELT: ${r.query}`), watch: (r.watch as string | null) ?? null };\n },\n\n async fetch(cfg, ctx): Promise<RawItem[]> {\n const api =\n `https://api.gdeltproject.org/api/v2/doc/doc?query=${encodeURIComponent(cfg.query)}` +\n `&mode=artlist&format=json&maxrecords=40&timespan=2d`;\n\n let articles: Array<{ url?: string; title?: string; seendate?: string; language?: string }> = [];\n try {\n const res = await fetch(api, { headers: { \"User-Agent\": UA } });\n if (!res.ok) return [];\n const text = await res.text(); // GDELT sometimes returns text errors with 200\n try {\n articles = (JSON.parse(text) as { articles?: typeof articles }).articles ?? [];\n } catch {\n return [];\n }\n } catch (e) {\n console.log(`gdelt fetch failed (non-blocking): ${e}`);\n return [];\n }\n\n const seen = new Set<string>();\n const picked = articles\n .filter((a) => a?.url && a?.title && a.language === \"English\" && !seen.has(a.url!) && (seen.add(a.url!), true))\n .slice(0, MAX_ITEMS);\n\n const out: RawItem[] = [];\n for (const a of picked) {\n const body = await ctx.fetchText(a.url!); // GDELT gives no body; fetch the article\n if (body.length < 200) continue;\n out.push({\n url: a.url!,\n title: a.title!,\n date: isoFromSeendate(a.seendate ?? \"\"),\n sourceName: cfg.name,\n sourceKind: \"gdelt\",\n watch: cfg.watch,\n text: body,\n });\n }\n return out;\n },\n};\n","/**\n * Identity + dedup helpers. The slug / urlHash a node resolves to IS its\n * natural key for Lookup-based upserts, so these must be stable and pure.\n * Ported from agnacl (utils/markdown.ts slugify, services/kv.ts normalizeUrl).\n */\n\n/** NFKD-normalize, strip accents, lowercase, non-alphanumerics → \"-\", cap 60. */\nexport function slugify(input: string): string {\n return input\n .normalize(\"NFKD\")\n .replace(/[\\u0300-\\u036f]/g, \"\")\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 60)\n .replace(/-+$/g, \"\");\n}\n\n/** Drop tracking params, hash, and trailing slash so the same page maps to one key. */\nexport function normalizeUrl(raw: string): string {\n try {\n const u = new URL(raw);\n u.hash = \"\";\n const drop = [/^utm_/i, /^ref$/i, /^ref_/i, /^fbclid$/i, /^gclid$/i, /^mc_/i];\n for (const key of [...u.searchParams.keys()]) {\n if (drop.some((re) => re.test(key))) u.searchParams.delete(key);\n }\n let s = u.toString();\n s = s.replace(/\\/$/, \"\");\n return s;\n } catch {\n return raw.trim().replace(/#.*$/, \"\").replace(/\\/$/, \"\");\n }\n}\n\n/** Stable short key for a URL (FNV-1a 32-bit, base36). Used as the `signal` natural key. */\nexport function urlHash(raw: string): string {\n const s = normalizeUrl(raw);\n let h = 0x811c9dc5;\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return (h >>> 0).toString(36);\n}\n\n/**\n * Reject descriptive phrases masquerading as people (\"three craftsmen\",\n * \"the founders\"). A real name is 2-4 capitalized tokens, no digits.\n * Ported from agnacl utils/names.ts (isLikelyPersonName).\n */\nexport function isLikelyPersonName(name: string): boolean {\n const n = name.trim();\n if (!n || /\\d/.test(n)) return false;\n const parts = n.split(/\\s+/);\n if (parts.length < 2 || parts.length > 4) return false;\n return parts.every((p) => /^[A-Z][a-zA-Z'’.-]*$/.test(p));\n}\n","import type { Value } from \"@odla-ai/db\";\nimport type { RawItem, GraphFragment } from \"../types\";\nimport type { SourceConnector, ConnectorContext } from \"./types\";\nimport { slugify } from \"../util/identity\";\n\n/**\n * FRED economic indicators — a STRUCTURED connector: it maps series directly to\n * graph nodes (indicator + region) and emits RawItem.fragment, so the ingest\n * workflow writes it without any LLM extraction. Ported from agnacl services/fred.ts.\n */\ninterface FredCfg {\n series: Array<{ id: string; label: string; unit?: string }>;\n region?: string;\n regionName?: string;\n}\n\nasync function fetchSeries(\n seriesId: string,\n apiKey: string,\n start: string,\n): Promise<Array<{ date: string; value: number }>> {\n const url =\n `https://api.stlouisfed.org/fred/series/observations?series_id=${encodeURIComponent(seriesId)}` +\n `&api_key=${encodeURIComponent(apiKey)}&file_type=json&observation_start=${encodeURIComponent(start)}`;\n try {\n const res = await fetch(url);\n if (!res.ok) return [];\n const data = (await res.json()) as { observations?: Array<{ date?: string; value?: string }> };\n const out: Array<{ date: string; value: number }> = [];\n for (const o of data.observations ?? []) {\n if (!o?.date || o.value === undefined || o.value === \".\") continue;\n const v = Number(o.value);\n if (Number.isFinite(v)) out.push({ date: o.date, value: v });\n }\n return out;\n } catch (e) {\n console.log(`fred fetch failed for ${seriesId} (non-blocking): ${e}`);\n return [];\n }\n}\n\nexport const fredConnector: SourceConnector<FredCfg> = {\n id: \"fred\",\n capabilities: { fetch: true, structured: true },\n\n parseConfig(raw): FredCfg {\n const r = raw as Record<string, unknown>;\n const series = Array.isArray(r.series) ? (r.series as FredCfg[\"series\"]) : [];\n if (!series.length) throw new Error(\"fred connector requires a non-empty `series`\");\n return { series, region: r.region as string | undefined, regionName: r.regionName as string | undefined };\n },\n\n async fetch(cfg, ctx): Promise<RawItem[]> {\n const apiKey = ctx.secret(\"FRED_API_KEY\");\n if (!apiKey) {\n console.log(\"fred: FRED_API_KEY unset — skipping\");\n return [];\n }\n const start = new Date(ctx.now.getTime() - 400 * 24 * 3600 * 1000).toISOString().slice(0, 10);\n const out: RawItem[] = [];\n\n for (const s of cfg.series) {\n const obs = await fetchSeries(s.id, apiKey, start);\n if (!obs.length) continue;\n const latest = obs[obs.length - 1]!;\n const prior = obs.length > 1 ? obs[obs.length - 2] : undefined;\n\n const attrs: Record<string, Value> = { label: s.label, latest: String(latest.value), as_of: latest.date };\n if (prior) attrs.prior = String(prior.value);\n if (s.unit) attrs.unit = s.unit;\n\n const fragment: GraphFragment = { nodes: [{ type: \"indicator\", key: s.id, attrs }], edges: [] };\n if (cfg.region) {\n const rslug = slugify(cfg.region);\n if (rslug) {\n fragment.nodes.push({ type: \"region\", key: rslug, attrs: { name: cfg.regionName ?? cfg.region } });\n fragment.edges.push({ from: { type: \"region\", key: rslug }, to: { type: \"indicator\", key: s.id }, label: \"has_indicator\" });\n }\n }\n\n out.push({\n url: `https://fred.stlouisfed.org/series/${s.id}#${latest.date}`,\n title: `${s.label}: ${latest.value} (${latest.date})`,\n date: latest.date,\n sourceName: \"FRED\",\n sourceKind: \"fred\",\n fragment,\n });\n }\n return out;\n },\n};\n","import type { Value } from \"@odla-ai/db\";\nimport type { RawItem, GraphFragment } from \"../types\";\nimport type { SourceConnector, ConnectorContext } from \"./types\";\nimport { slugify, isLikelyPersonName } from \"../util/identity\";\n\n/**\n * SEC EDGAR Form D — a STRUCTURED connector (RawItem.fragment, no LLM). A lean\n * port of agnacl services/edgar.ts: first page of Form D filings for the given\n * states, each mapped to company + funding_round (+ people, region). SEC\n * requires an identifying User-Agent (env.EDGAR_USER_AGENT) on every request.\n */\nconst SEARCH_BASE = \"https://efts.sec.gov/LATEST/search-index\";\nconst MAX_FILINGS = 10;\n\ninterface EdgarCfg {\n locationCodes: string[];\n cityFilter?: string[];\n days?: number;\n}\n\nconst cikTrim = (cik: string): string => cik.replace(/^0+/, \"\") || cik;\nconst indexUrl = (cik: string, adsh: string): string =>\n `https://www.sec.gov/Archives/edgar/data/${cikTrim(cik)}/${adsh.replace(/-/g, \"\")}/${adsh}-index.htm`;\n\nfunction tagText(scope: string, tag: string): string {\n const m = scope.match(new RegExp(`<${tag}[^>]*>\\\\s*([\\\\s\\\\S]*?)\\\\s*</${tag}>`, \"i\"));\n return m ? m[1]!.trim() : \"\";\n}\n\ninterface EdgarHit {\n _source?: {\n adsh?: string;\n ciks?: string[];\n display_names?: string[];\n file_date?: string;\n biz_states?: string[];\n biz_locations?: string[];\n };\n}\n\nexport const edgarConnector: SourceConnector<EdgarCfg> = {\n id: \"edgar\",\n capabilities: { fetch: true, structured: true },\n\n parseConfig(raw): EdgarCfg {\n const r = raw as Record<string, unknown>;\n const codes = Array.isArray(r.locationCodes) ? (r.locationCodes as string[]) : [];\n if (!codes.length) throw new Error('edgar connector requires `locationCodes` (e.g. [\"UT\"])');\n return {\n locationCodes: codes,\n cityFilter: Array.isArray(r.cityFilter) ? (r.cityFilter as string[]) : undefined,\n days: typeof r.days === \"number\" ? r.days : 7,\n };\n },\n\n async fetch(cfg, ctx): Promise<RawItem[]> {\n const ua = ctx.secret(\"EDGAR_USER_AGENT\");\n if (!ua) {\n console.log(\"edgar: EDGAR_USER_AGENT unset — skipping\");\n return [];\n }\n const enddt = ctx.now.toISOString().slice(0, 10);\n const startdt = new Date(ctx.now.getTime() - (cfg.days ?? 7) * 24 * 3600 * 1000).toISOString().slice(0, 10);\n const searchUrl =\n `${SEARCH_BASE}?q=&forms=D&locationCodes=${encodeURIComponent(cfg.locationCodes.join(\",\"))}` +\n `&startdt=${startdt}&enddt=${enddt}`;\n\n let hits: EdgarHit[] = [];\n try {\n const res = await fetch(searchUrl, { headers: { \"User-Agent\": ua } });\n if (!res.ok) return [];\n hits = ((await res.json()) as { hits?: { hits?: EdgarHit[] } }).hits?.hits ?? [];\n } catch (e) {\n console.log(`edgar search failed (non-blocking): ${e}`);\n return [];\n }\n\n const out: RawItem[] = [];\n const seen = new Set<string>();\n for (const hit of hits.slice(0, MAX_FILINGS)) {\n const src = hit._source ?? {};\n const adsh = src.adsh ?? \"\";\n if (!adsh || seen.has(adsh)) continue;\n seen.add(adsh);\n const cik = src.ciks?.[0] ?? \"\";\n const name = (src.display_names?.[0] ?? \"\").replace(/\\s*\\(CIK\\s*\\d+\\)\\s*$/i, \"\").trim();\n if (!name || !cik) continue;\n\n const loc = src.biz_locations?.[0] ?? \"\";\n let city = loc.trim();\n let state = src.biz_states?.[0] ?? \"\";\n const comma = loc.lastIndexOf(\",\");\n if (comma >= 0) {\n city = loc.slice(0, comma).trim();\n state = loc.slice(comma + 1).trim() || state;\n }\n if (cfg.cityFilter?.length && !cfg.cityFilter.some((c) => c.toLowerCase() === city.toLowerCase())) continue;\n\n const url = indexUrl(cik, adsh);\n const fileDate = src.file_date ?? \"\";\n\n // Filing detail (offering amount, industry, related persons)\n let industry = \"\";\n let offering = \"\";\n const persons: Array<{ name: string; roles: string[] }> = [];\n try {\n const detailUrl = `https://www.sec.gov/Archives/edgar/data/${cikTrim(cik)}/${adsh.replace(/-/g, \"\")}/primary_doc.xml`;\n const dres = await fetch(detailUrl, { headers: { \"User-Agent\": ua } });\n if (dres.ok) {\n const xml = await dres.text();\n industry = tagText(xml, \"industryGroupType\");\n offering = tagText(xml, \"totalOfferingAmount\");\n for (const block of xml.match(/<relatedPersonInfo>[\\s\\S]*?<\\/relatedPersonInfo>/gi) ?? []) {\n const pname = [tagText(block, \"firstName\"), tagText(block, \"lastName\")]\n .filter((p) => p && p !== \"-\")\n .join(\" \")\n .trim();\n if (!pname) continue;\n const roles: string[] = [];\n const relRe = /<relationship>\\s*([\\s\\S]*?)\\s*<\\/relationship>/gi;\n let m: RegExpExecArray | null;\n while ((m = relRe.exec(block)) !== null) {\n const r = m[1]!.trim();\n if (r && !roles.includes(r)) roles.push(r);\n }\n persons.push({ name: pname, roles });\n }\n }\n } catch {\n /* fall back to search-hit metadata */\n }\n\n const cslug = slugify(name);\n if (!cslug) continue;\n const f: GraphFragment = { nodes: [], edges: [] };\n const cattrs: Record<string, Value> = { name };\n if (city) cattrs.city = city;\n if (state) cattrs.region = state;\n if (industry) cattrs.sector = industry;\n if (fileDate) {\n cattrs.first_seen = fileDate;\n cattrs.last_seen = fileDate;\n }\n f.nodes.push({ type: \"company\", key: cslug, attrs: cattrs });\n\n if (state) {\n const rslug = slugify(state);\n if (rslug) {\n f.nodes.push({ type: \"region\", key: rslug, attrs: { name: state } });\n f.edges.push({ from: { type: \"company\", key: cslug }, to: { type: \"region\", key: rslug }, label: \"located_in\" });\n }\n }\n\n const rkey = `${cslug}:form-d:${fileDate}`;\n const rattrs: Record<string, Value> = { round: \"Form D\", source_url: url };\n if (offering) rattrs.amount = offering;\n if (fileDate) rattrs.date = fileDate;\n f.nodes.push({ type: \"funding_round\", key: rkey, attrs: rattrs });\n f.edges.push({ from: { type: \"funding_round\", key: rkey }, to: { type: \"company\", key: cslug }, label: \"round_of\" });\n\n for (const p of persons) {\n if (!isLikelyPersonName(p.name)) continue;\n const pslug = slugify(p.name);\n if (!pslug) continue;\n const pattrs: Record<string, Value> = { name: p.name };\n if (p.roles.length) pattrs.role = p.roles.join(\", \");\n f.nodes.push({ type: \"person\", key: pslug, attrs: pattrs });\n f.edges.push({ from: { type: \"person\", key: pslug }, to: { type: \"company\", key: cslug }, label: \"works_at\" });\n }\n\n out.push({\n url,\n title: `Form D: ${name}${offering ? ` — ${offering}` : \"\"}`,\n date: fileDate || undefined,\n sourceName: \"SEC EDGAR\",\n sourceKind: \"edgar\",\n fragment: f,\n });\n }\n return out;\n },\n};\n","import type { RawItem } from \"../types\";\nimport type { SourceConnector, ConnectorContext } from \"./types\";\n\n/**\n * TEMPLATE — a government / open-data connector.\n *\n * This exists to demonstrate the extensibility contract: adding a new source is\n * a self-contained module implementing SourceConnector + ONE line in the\n * registry + config entries in config/sources.ts. The cron, ingest workflow,\n * extractor, and graph writer are untouched — they only ever speak RawItem /\n * GraphFragment.\n *\n * To make it real, fetch your dataset and return either:\n * - text-bearing RawItems ({ ...meta, text }) → the LLM extracts a graph\n * - fragment-bearing RawItems ({ ...meta, fragment }) → straight to the writer\n * (map records to ontology nodes/edges yourself — see fred.ts / edgar.ts).\n * Returns [] until implemented, so wiring it up is harmless.\n */\ninterface GovDataCfg {\n dataset: string;\n watch?: string | null;\n}\n\nexport const govDataConnector: SourceConnector<GovDataCfg> = {\n id: \"gov-data\",\n capabilities: { fetch: true, structured: true },\n\n parseConfig(raw): GovDataCfg {\n const r = raw as Record<string, unknown>;\n return { dataset: String(r.dataset ?? \"\"), watch: (r.watch as string | null) ?? null };\n },\n\n async fetch(cfg, _ctx: ConnectorContext): Promise<RawItem[]> {\n console.log(`gov-data: template connector for dataset \"${cfg.dataset}\" — implement fetch() to emit RawItems`);\n return [];\n },\n};\n","import type { RawItem } from \"../types\";\nimport type { SourceConnector, ConnectorContext } from \"./types\";\nimport { urlHash } from \"../util/identity\";\n\n/**\n * LLM-native web search — a DISCOVERY connector. Each standing query is run\n * through the provider's web-search tool; the prose result becomes a text\n * RawItem that flows through the normal extractor (so discovered companies land\n * in the graph the same way feed items do). Discovery-only: fetch() is a no-op.\n */\ninterface WebSearchCfg {\n queries: string[];\n name: string;\n watch?: string | null;\n}\n\nconst SWEEP_SYSTEM =\n \"You are a research scout building a knowledge graph of entrepreneurs, startups, and business trends. Search the web and report concrete, real companies and the people leading them, with specifics (what they do, where, who founded them). Tie every claim to something you actually found — never invent names, numbers, or companies. If a search turns up nothing solid, say so plainly.\";\n\nexport const webSearchConnector: SourceConnector<WebSearchCfg> = {\n id: \"web-search\",\n capabilities: { fetch: false, discover: true, structured: false },\n\n parseConfig(raw): WebSearchCfg {\n const r = raw as Record<string, unknown>;\n const queries = Array.isArray(r.queries)\n ? (r.queries.filter((q) => typeof q === \"string\") as string[])\n : typeof r.query === \"string\"\n ? [r.query]\n : [];\n if (!queries.length) throw new Error(\"web-search connector requires `queries` (or a `query`)\");\n return { queries, name: String(r.name ?? \"web-search\"), watch: (r.watch as string | null) ?? null };\n },\n\n async fetch(): Promise<RawItem[]> {\n return []; // discovery-only\n },\n\n async discover(cfg, ctx: ConnectorContext): Promise<RawItem[]> {\n const provider = ctx.provider();\n const today = ctx.now.toISOString().slice(0, 10);\n const out: RawItem[] = [];\n for (const q of cfg.queries) {\n const { text } = await provider.search({\n system: SWEEP_SYSTEM,\n user: `Search task: ${q}\\nToday's date: ${today}\\n\\nReport what you find, naming specific companies and the real people leading them.`,\n });\n if (text.trim().length < 80) continue;\n out.push({\n url: `search://web-search/${urlHash(q)}#${today}`, // synthetic; changes daily so re-sweeps aren't deduped away\n title: q,\n date: today,\n sourceName: cfg.name,\n sourceKind: \"web-search\",\n watch: cfg.watch,\n text,\n });\n }\n return out;\n },\n};\n","import type { SourceConnector, ConnectorContext } from \"./types\";\nimport type { Ontology } from \"../ontology\";\nimport type { Provider } from \"../llm/provider\";\nimport { fetchText } from \"../util/content\";\nimport { rssConnector } from \"./rss\";\nimport { urlConnector } from \"./url\";\nimport { gdeltConnector } from \"./gdelt\";\nimport { fredConnector } from \"./fred\";\nimport { edgarConnector } from \"./edgar\";\nimport { govDataConnector } from \"./govdata\";\nimport { webSearchConnector } from \"./web-search\";\n\n/**\n * The connector registry. Adding a source (RSS, a government feed, a new API)\n * is: implement SourceConnector in a module, add one line here, add config\n * entries to config/sources.ts. The cron, workflow, extractor, and writer are\n * untouched — they only speak RawItem / GraphFragment.\n */\nconst REGISTRY = new Map<string, SourceConnector>([\n [rssConnector.id, rssConnector as SourceConnector],\n [urlConnector.id, urlConnector as SourceConnector],\n [gdeltConnector.id, gdeltConnector as SourceConnector],\n [fredConnector.id, fredConnector as SourceConnector], // structured (RawItem.fragment)\n [edgarConnector.id, edgarConnector as SourceConnector], // structured (RawItem.fragment)\n [webSearchConnector.id, webSearchConnector as SourceConnector], // discovery (web search)\n [govDataConnector.id, govDataConnector as SourceConnector], // template — zero core change to add\n]);\n\nexport function getConnector(id: string): SourceConnector | undefined {\n return REGISTRY.get(id);\n}\n\nexport function connectorIds(): string[] {\n return [...REGISTRY.keys()];\n}\n\n/** Everything a ConnectorContext needs, supplied by the host (worker, script, test). */\nexport interface ContextConfig {\n ontology: Ontology;\n provider: () => Provider;\n /** Named secrets (API keys, user agents) — e.g. the worker Env, or a plain object. */\n secrets?: Record<string, string | undefined>;\n now?: Date;\n}\n\nexport function makeContext(cfg: ContextConfig): ConnectorContext {\n let provider: Provider | undefined; // lazy: only discovery connectors need it\n return {\n now: cfg.now ?? new Date(),\n fetchText: (u) => fetchText(u),\n secret: (name) => cfg.secrets?.[name],\n ontology: cfg.ontology,\n provider: () => (provider ??= cfg.provider()),\n };\n}\n","import type { ConnectorJob } from \"./connectors/types\";\n\n/**\n * Sources are DATA, not code. Each entry names a connector (`kind`) + its config\n * + scheduling. Add a feed → add an entry; no redeploy of the pipeline. P5 moves\n * this into KV behind a /config admin endpoint so edits don't need a deploy at all.\n */\nexport interface SourceEntry {\n kind: string; // connector id (registry key)\n name: string;\n enabled?: boolean; // default true\n cadenceHours?: number; // default 1 — runs when UTC hour % cadence === 0\n watch?: string | null;\n // connector-specific config (rss: url; more in P3):\n url?: string;\n query?: string;\n [key: string]: unknown;\n}\n\nexport function enabledSources(sources: SourceEntry[]): SourceEntry[] {\n return sources.filter((s) => s.enabled !== false);\n}\n\nfunction toJob(s: SourceEntry): ConnectorJob {\n return { kind: s.kind, name: s.name, watch: s.watch ?? null, cfg: s };\n}\n\n/** Enabled sources whose cadence lands on this UTC hour. */\nexport function jobsForHour(sources: SourceEntry[], hour: number): ConnectorJob[] {\n return enabledSources(sources)\n .filter((s) => hour % Math.max(1, Math.floor(s.cadenceHours ?? 1)) === 0)\n .map(toJob);\n}\n\n/** Every enabled source, ignoring cadence (manual /trigger?kind=sources). */\nexport function allJobs(sources: SourceEntry[]): ConnectorJob[] {\n return enabledSources(sources).map(toJob);\n}\n","import type { RawItem } from \"../types\";\nimport type { SourceEntry } from \"../sources\";\nimport { urlHash } from \"../util/identity\";\n\nconst SEEN_TTL_S = 120 * 24 * 60 * 60; // 120 days\nconst ENRICHED_TTL_S = 30 * 24 * 60 * 60; // 30-day enrichment cooldown\n\n/**\n * The slice of a KV store the pipeline needs. Cloudflare's KVNamespace\n * satisfies it structurally; tests pass an in-memory map. Keeping the library\n * off Workers types is what lets it publish as a plain isomorphic package.\n */\nexport interface KVLike {\n get(key: string): Promise<string | null>;\n put(key: string, value: string, opts?: { expirationTtl?: number }): Promise<void>;\n}\n\n/**\n * Operational state in KV (never the graph itself — that lives in odla-db).\n * Ported/trimmed from agnacl services/kv.ts: seen-URL dedup + lastrun stamps.\n */\nexport class Store {\n constructor(private kv: KVLike) {}\n\n private seenKey(url: string): string {\n return `seen:${urlHash(url)}`;\n }\n\n async hasSeen(url: string): Promise<boolean> {\n return (await this.kv.get(this.seenKey(url))) !== null;\n }\n\n async markSeen(url: string): Promise<void> {\n await this.kv.put(this.seenKey(url), \"1\", { expirationTtl: SEEN_TTL_S });\n }\n\n /** Drop items whose URL we've already processed (checked BEFORE any LLM work). */\n async filterNew(items: RawItem[]): Promise<RawItem[]> {\n const flags = await Promise.all(items.map((it) => this.hasSeen(it.url)));\n return items.filter((_, i) => !flags[i]);\n }\n\n async setLastRun(kind: string, at = new Date().toISOString()): Promise<void> {\n await this.kv.put(`lastrun:${kind}`, at);\n }\n\n async getLastRun(kind: string): Promise<string | null> {\n return this.kv.get(`lastrun:${kind}`);\n }\n\n /** Per-entity enrichment cooldown so we don't re-search the same node constantly. */\n async isEnrichCooling(key: string): Promise<boolean> {\n return (await this.kv.get(`enriched:${key}`)) !== null;\n }\n\n async markEnriched(key: string): Promise<void> {\n await this.kv.put(`enriched:${key}`, new Date().toISOString(), { expirationTtl: ENRICHED_TTL_S });\n }\n\n /** Runtime source config (edit without redeploy). null = fall back to the static SOURCES. */\n async getConfigSources(): Promise<SourceEntry[] | null> {\n const raw = await this.kv.get(\"config:sources\");\n if (!raw) return null;\n try {\n const v = JSON.parse(raw);\n return Array.isArray(v) ? (v as SourceEntry[]) : null;\n } catch {\n return null;\n }\n }\n\n async setConfigSources(list: SourceEntry[]): Promise<void> {\n await this.kv.put(\"config:sources\", JSON.stringify(list));\n }\n}\n","/**\n * Cloudflare Workflow hardening — ported verbatim from agnacl\n * (utils/workflow-helpers.ts). resilientStep adds structured error logging\n * without changing retry semantics; the step configs replace the engine's slow\n * default retry with fast exponential backoff.\n */\n\nexport async function resilientStep<T>(stepName: string, fn: () => Promise<T>): Promise<T> {\n try {\n return await fn();\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n const stack = err instanceof Error ? err.stack : undefined;\n console.error(`[STEP_ERROR] ${stepName}: ${msg}`);\n if (stack) console.error(`[STEP_STACK] ${stepName}: ${stack}`);\n throw err;\n }\n}\n\n/** I/O-bound steps: KV reads, feed fetches. */\nexport const IO_STEP_CONFIG = {\n retries: { limit: 3, delay: \"5 seconds\" as const, backoff: \"exponential\" as const },\n timeout: \"2 minutes\" as const,\n};\n\n/** Graph write steps. */\nexport const COMMIT_STEP_CONFIG = {\n retries: { limit: 3, delay: \"3 seconds\" as const, backoff: \"exponential\" as const },\n timeout: \"90 seconds\" as const,\n};\n\n/** LLM generation steps (can be slow). */\nexport const LLM_STEP_CONFIG = {\n retries: { limit: 2, delay: \"5 seconds\" as const, backoff: \"exponential\" as const },\n timeout: \"3 minutes\" as const,\n};\n\n/** Retry/timeout shape of the step configs above. */\nexport interface StepConfig {\n retries?: { limit: number; delay: string; backoff?: string };\n timeout?: string;\n}\n\n/**\n * Structural view of Cloudflare's WorkflowStep. WorkflowStep.do constrains its\n * result to Rpc.Serializable<T>, which cannot terminate (TS2589) on the\n * recursive Value type now that the @odla/* workspace packages give\n * @odla-ai/db's re-exports real definitions. A live WorkflowStep satisfies\n * this shape at runtime; widen through asStepLike at the workflow boundary.\n */\nexport interface StepLike {\n do<T>(name: string, fn: () => Promise<T>): Promise<T>;\n do<T>(name: string, config: StepConfig, fn: () => Promise<T>): Promise<T>;\n}\n\n/** Widen a WorkflowStep to StepLike (type-level only, no runtime cost). */\nexport function asStepLike(step: unknown): StepLike {\n return step as StepLike;\n}\n","import { LLM_STEP_CONFIG, type StepLike, resilientStep } from \"../workflow/helpers\";\nimport type { ExtractResult, RawItem } from \"../types\";\nimport type { AdminDb } from \"@odla-ai/db\";\nimport type { Store } from \"../state/kv\";\nimport type { Provider } from \"../llm/provider\";\nimport type { Ontology } from \"../ontology\";\nimport { writeFragment } from \"../graph/writer\";\nimport { canonicalizeFragment } from \"../graph/resolve\";\nimport { urlHash } from \"../util/identity\";\n\n/** Everything the shared ingest loop needs, supplied by the host. The\n * extractor is the ontology's domain craft (e.g. the OSINT extractor); the\n * library stays ontology-agnostic. */\nexport interface PipelineDeps {\n db: AdminDb;\n provider: Provider;\n ontology: Ontology;\n extract: (provider: Provider, item: RawItem) => Promise<ExtractResult | null>;\n store: Store;\n}\n\n/**\n * Shared per-item processing: each RawItem is its own step (non-blocking,\n * replay-cached). Structured items (fragment) skip the LLM; text items extract.\n * `resolve` FTS-canonicalizes new nodes against existing ones (discovery path).\n * Used by both the ingest (fetch) and discover (web-search) workflows.\n */\nexport async function ingestFresh(\n step: StepLike,\n deps: PipelineDeps,\n fresh: RawItem[],\n kind: string,\n resolve = false,\n): Promise<void> {\n const { db, provider, ontology, extract, store } = deps;\n if (fresh.length === 0) {\n await store.setLastRun(kind);\n return;\n }\n\n for (const item of fresh) {\n const id = urlHash(item.url);\n await step.do(`item-${id}`, LLM_STEP_CONFIG, () =>\n resilientStep(`item-${id}`, async () => {\n try {\n let fragment = item.fragment;\n if (!fragment) {\n const r = await extract(provider, item);\n if (!r) {\n await store.markSeen(item.url);\n return { skipped: true };\n }\n fragment = r.fragment;\n }\n if (resolve) fragment = await canonicalizeFragment(db, fragment, ontology);\n const { txId, ops } = await writeFragment(db, fragment, ontology, `ingest:${id}:v1`);\n await store.markSeen(item.url);\n return { txId, ops };\n } catch (e) {\n console.log(`ingest item failed (non-blocking): ${item.url}: ${e instanceof Error ? e.message : e}`);\n return { error: true };\n }\n }),\n );\n }\n\n await step.do(`stamp-${kind}`, () =>\n resilientStep(`stamp-${kind}`, async () => {\n await store.setLastRun(kind);\n return { ok: true };\n }),\n );\n}\n"],"mappings":"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","names":["UA"]}
@@ -0,0 +1,435 @@
1
+ import { Value, Op, AdminDb, SerializedAttr, Cardinality, SerializedSchema, Entity } from '@odla-ai/db';
2
+ import { Ai } from '@odla-ai/ai';
3
+
4
+ /**
5
+ * The output of a connector: one acquired item. Exactly one of `text` or
6
+ * `fragment` is set — `text` goes through LLM extraction, `fragment` is already
7
+ * ontology-typed and goes straight to the graph writer (structured feeds).
8
+ */
9
+ interface RawItem {
10
+ url: string;
11
+ title: string;
12
+ date?: string;
13
+ sourceName: string;
14
+ sourceKind: string;
15
+ watch?: string | null;
16
+ text?: string;
17
+ fragment?: GraphFragment;
18
+ }
19
+ /** A graph node = an odla-db entity in a namespace, keyed by a natural key. */
20
+ interface GraphNode {
21
+ type: string;
22
+ key: string;
23
+ attrs: Record<string, Value>;
24
+ }
25
+ /** A graph edge = an odla-db `link` from one node to another by forward label. */
26
+ interface GraphEdge {
27
+ from: {
28
+ type: string;
29
+ key: string;
30
+ };
31
+ to: {
32
+ type: string;
33
+ key: string;
34
+ };
35
+ label: string;
36
+ }
37
+ /** The single shape the graph writer consumes, whatever produced it. */
38
+ interface GraphFragment {
39
+ nodes: GraphNode[];
40
+ edges: GraphEdge[];
41
+ }
42
+ /** What an extractor turns a text RawItem into: a fragment plus a prose summary. */
43
+ interface ExtractResult {
44
+ fragment: GraphFragment;
45
+ summary: string;
46
+ }
47
+ /** Provider-agnostic LLM usage accounting. */
48
+ interface Usage {
49
+ calls: number;
50
+ inputTokens: number;
51
+ outputTokens: number;
52
+ }
53
+ declare function emptyUsage(): Usage;
54
+ declare function addUsage(into: Usage, more: Usage): void;
55
+
56
+ /** node type → unique natural-key attribute name. */
57
+ type NaturalKeys = Record<string, string>;
58
+ /**
59
+ * Pure fragment → ops mapping (no read/merge). Node upserts by Lookup natural
60
+ * key, then links — nodes always before links so `link` targets resolve.
61
+ * Used by tests and as the building block for the provenance path below.
62
+ */
63
+ declare function fragmentToOps(fragment: GraphFragment, keys: NaturalKeys): Op[];
64
+ /**
65
+ * Provenance-preserving write. Reads existing nodes by natural key (one batched
66
+ * query), merges scalar attrs per the ontology's rules, drops human-owned
67
+ * attrs, then writes merged node updates + all links in one atomic, idempotent
68
+ * transaction. `mutationId` must be deterministic for exactly-once.
69
+ */
70
+ declare function writeFragment(db: AdminDb, fragment: GraphFragment, ontology: Ontology, mutationId: string): Promise<{
71
+ txId: number;
72
+ ops: number;
73
+ }>;
74
+
75
+ /** How a scalar attribute is reconciled when a node is re-discovered. */
76
+ type MergeRule = "replace" | "prefer" | "earliest" | "latest";
77
+ type AttrType = SerializedAttr["type"];
78
+ interface AttrSpec {
79
+ type: AttrType;
80
+ unique?: boolean;
81
+ indexed?: boolean;
82
+ optional?: boolean;
83
+ /** The pipeline NEVER writes this attr (hand-edited in a UI); omitted from every update. */
84
+ human?: boolean;
85
+ /** Provenance rule for re-discovery. Default "replace". */
86
+ merge?: MergeRule;
87
+ /** Shown to the LLM when this attr appears in an extraction schema. */
88
+ desc?: string;
89
+ }
90
+ interface EntitySpec {
91
+ /** Unique attr used for Lookup upsert (MERGE key). Must be `unique` in attrs. */
92
+ naturalKey: string;
93
+ attrs: Record<string, AttrSpec>;
94
+ /** FTS fields for fuzzy resolution (P5). */
95
+ searchable?: string[];
96
+ }
97
+ interface EdgeSpec {
98
+ from: string;
99
+ to: string;
100
+ forwardLabel: string;
101
+ reverseLabel: string;
102
+ forwardHas?: Cardinality;
103
+ reverseHas?: Cardinality;
104
+ }
105
+ interface Ontology {
106
+ entities: Record<string, EntitySpec>;
107
+ edges: Record<string, EdgeSpec>;
108
+ }
109
+ /** node type → natural-key attr name (for the graph writer's Lookup refs). */
110
+ declare function naturalKeysOf(o: Ontology): NaturalKeys;
111
+ /** Attr name → merge rule, for every entity type (used by the provenance writer). */
112
+ declare function mergeRulesOf(o: Ontology): Record<string, Record<string, MergeRule>>;
113
+ /** Attr names the pipeline must never write, per entity type. */
114
+ declare function humanAttrsOf(o: Ontology): Record<string, Set<string>>;
115
+ /** Compile the ontology to an odla-db SerializedSchema to POST at /app/:id/schema. */
116
+ declare function toSerializedSchema(o: Ontology): SerializedSchema;
117
+
118
+ /**
119
+ * Reconcile one scalar attribute when a node is re-discovered. Port of agnacl's
120
+ * provenance rules (utils/markdown.ts). Empty incoming never clobbers; empty
121
+ * existing is filled. Ordering rules compare stringwise (ISO dates sort right).
122
+ */
123
+ declare function mergeScalar(rule: MergeRule, existing: Value | undefined, incoming: Value | undefined): Value | undefined;
124
+ /**
125
+ * Merge an incoming attr set into an existing one per the type's rules. `human`
126
+ * attrs are excluded upstream (never in `incoming`); this only reconciles what
127
+ * the pipeline owns. Returns the attrs to write (update leaves unlisted attrs intact).
128
+ */
129
+ declare function mergeAttrs(rules: Record<string, MergeRule>, existing: Record<string, Value>, incoming: Record<string, Value>): Record<string, Value>;
130
+
131
+ /** FTS5 search over a namespace's text fields (odla-db QueryOpts.search). */
132
+ declare function searchEntities(db: AdminDb, type: string, q: string, limit?: number): Promise<Entity[]>;
133
+ /**
134
+ * Resolve a name to an existing entity's natural key via FTS — for merging a
135
+ * re-discovery that slugified differently. Conservative: only returns a hit when
136
+ * the top match's name/key normalizes identically to the query.
137
+ */
138
+ declare function resolveName(db: AdminDb, type: string, keyAttr: string, name: string): Promise<string | null>;
139
+ /**
140
+ * Canonicalize a fragment's searchable nodes against existing entities via FTS,
141
+ * remapping node keys (and the edges that reference them) to the canonical key.
142
+ * Used on the lower-volume discovery/enrich paths, not high-volume ingest.
143
+ */
144
+ declare function canonicalizeFragment(db: AdminDb, fragment: GraphFragment, ontology: Ontology): Promise<GraphFragment>;
145
+
146
+ /**
147
+ * The LLM seam, now a thin adapter over @odla-ai/ai (which was itself
148
+ * generalized from this package's original Provider). Three verbs stay:
149
+ * extract (forced structured tool call), search (web-search-enabled text),
150
+ * write (plain generation). Callers never see vendor or oracle types.
151
+ */
152
+ type ProviderFamily = "claude" | "openai";
153
+ /** Role → model tiers per family — config as data. `cheap` is for gate-style
154
+ * calls, `extract` for structured extraction (and prose writing), `search`
155
+ * for web-search-enabled calls. */
156
+ declare const MODEL_TIERS: Record<ProviderFamily, {
157
+ cheap: string;
158
+ extract: string;
159
+ search: string;
160
+ }>;
161
+ /** A forced structured tool call: schema in, validated object out. */
162
+ interface ExtractCall {
163
+ system: string;
164
+ user: string;
165
+ tool: {
166
+ name: string;
167
+ description?: string;
168
+ parameters: Record<string, unknown>;
169
+ };
170
+ model?: string;
171
+ maxTokens?: number;
172
+ }
173
+ /** A web-search-enabled call: the model searches and returns text (caller tolerant-parses). */
174
+ interface SearchCall {
175
+ system: string;
176
+ user: string;
177
+ model?: string;
178
+ maxTokens?: number;
179
+ }
180
+ /** A plain text-generation call (no tools, no search) — e.g. digest prose. */
181
+ interface WriteCall {
182
+ system: string;
183
+ user: string;
184
+ model?: string;
185
+ maxTokens?: number;
186
+ }
187
+ interface Provider {
188
+ id: ProviderFamily;
189
+ /** The underlying @odla-ai/ai client — escape hatch for advanced use. */
190
+ ai: Ai;
191
+ extract<T = unknown>(c: ExtractCall): Promise<{
192
+ value: T;
193
+ usage: Usage;
194
+ }>;
195
+ search(c: SearchCall): Promise<{
196
+ text: string;
197
+ usage: Usage;
198
+ }>;
199
+ write(c: WriteCall): Promise<{
200
+ text: string;
201
+ usage: Usage;
202
+ }>;
203
+ }
204
+ interface LlmConfig {
205
+ provider: ProviderFamily;
206
+ anthropicKey?: string;
207
+ openaiKey?: string;
208
+ /** Inject a pre-built Ai (tests, custom catalog); keys are ignored when set. */
209
+ ai?: Ai;
210
+ }
211
+ /** The library primitive: an explicit config in, the three-verb seam out. */
212
+ declare function createProvider(cfg: LlmConfig): Provider;
213
+ /** The env vars the provider factory reads — a structural slice, not the
214
+ * worker Env, so any host (worker, script, test) can supply plain strings. */
215
+ interface LlmEnv {
216
+ LLM_PROVIDER?: string;
217
+ ANTHROPIC_API_KEY?: string;
218
+ OPENAI_API_KEY?: string;
219
+ }
220
+ declare function getProvider(env: LlmEnv): Provider;
221
+
222
+ /**
223
+ * Tolerant JSON extraction from LLM prose — web-search models return JSON inside
224
+ * explanatory text. Ported from agnacl services/enrichment.ts.
225
+ */
226
+ declare function extractJson<T = unknown>(text: string): T | null;
227
+ declare function extractJsonArray<T = unknown>(text: string): T[];
228
+
229
+ /** Ambient services a connector may use. Host-agnostic: secrets come through
230
+ * secret(), never a worker Env, so the library runs anywhere. */
231
+ interface ConnectorContext {
232
+ now: Date;
233
+ fetchText(url: string): Promise<string>;
234
+ secret(name: string): string | undefined;
235
+ ontology: Ontology;
236
+ /** The configured LLM provider (lazy — only web-search/discovery connectors need it). */
237
+ provider(): Provider;
238
+ }
239
+ /**
240
+ * The extensibility seam. A connector emits normalized RawItems — text-bearing
241
+ * (→ LLM extraction) or fragment-bearing (structured → straight to the writer).
242
+ * Adding a source = a new module implementing this + one registry line.
243
+ */
244
+ interface SourceConnector<Cfg = unknown> {
245
+ id: string;
246
+ capabilities: {
247
+ fetch: boolean;
248
+ discover?: boolean;
249
+ structured: boolean;
250
+ };
251
+ parseConfig(raw: unknown): Cfg;
252
+ fetch(cfg: Cfg, ctx: ConnectorContext): Promise<RawItem[]>;
253
+ /** Web-search-style net-new discovery (P4). */
254
+ discover?(cfg: Cfg, ctx: ConnectorContext): Promise<RawItem[]>;
255
+ }
256
+ /** One unit of work for the ingest workflow: which connector + its config. */
257
+ interface ConnectorJob {
258
+ kind: string;
259
+ name: string;
260
+ watch?: string | null;
261
+ cfg: unknown;
262
+ }
263
+
264
+ declare function getConnector(id: string): SourceConnector | undefined;
265
+ declare function connectorIds(): string[];
266
+ /** Everything a ConnectorContext needs, supplied by the host (worker, script, test). */
267
+ interface ContextConfig {
268
+ ontology: Ontology;
269
+ provider: () => Provider;
270
+ /** Named secrets (API keys, user agents) — e.g. the worker Env, or a plain object. */
271
+ secrets?: Record<string, string | undefined>;
272
+ now?: Date;
273
+ }
274
+ declare function makeContext(cfg: ContextConfig): ConnectorContext;
275
+
276
+ /** Fetch one URL → a text-bearing RawItem. Used both directly (/trigger) and as a connector. */
277
+ declare function fetchUrl(url: string): Promise<RawItem>;
278
+
279
+ /**
280
+ * Sources are DATA, not code. Each entry names a connector (`kind`) + its config
281
+ * + scheduling. Add a feed → add an entry; no redeploy of the pipeline. P5 moves
282
+ * this into KV behind a /config admin endpoint so edits don't need a deploy at all.
283
+ */
284
+ interface SourceEntry {
285
+ kind: string;
286
+ name: string;
287
+ enabled?: boolean;
288
+ cadenceHours?: number;
289
+ watch?: string | null;
290
+ url?: string;
291
+ query?: string;
292
+ [key: string]: unknown;
293
+ }
294
+ declare function enabledSources(sources: SourceEntry[]): SourceEntry[];
295
+ /** Enabled sources whose cadence lands on this UTC hour. */
296
+ declare function jobsForHour(sources: SourceEntry[], hour: number): ConnectorJob[];
297
+ /** Every enabled source, ignoring cadence (manual /trigger?kind=sources). */
298
+ declare function allJobs(sources: SourceEntry[]): ConnectorJob[];
299
+
300
+ /**
301
+ * The slice of a KV store the pipeline needs. Cloudflare's KVNamespace
302
+ * satisfies it structurally; tests pass an in-memory map. Keeping the library
303
+ * off Workers types is what lets it publish as a plain isomorphic package.
304
+ */
305
+ interface KVLike {
306
+ get(key: string): Promise<string | null>;
307
+ put(key: string, value: string, opts?: {
308
+ expirationTtl?: number;
309
+ }): Promise<void>;
310
+ }
311
+ /**
312
+ * Operational state in KV (never the graph itself — that lives in odla-db).
313
+ * Ported/trimmed from agnacl services/kv.ts: seen-URL dedup + lastrun stamps.
314
+ */
315
+ declare class Store {
316
+ private kv;
317
+ constructor(kv: KVLike);
318
+ private seenKey;
319
+ hasSeen(url: string): Promise<boolean>;
320
+ markSeen(url: string): Promise<void>;
321
+ /** Drop items whose URL we've already processed (checked BEFORE any LLM work). */
322
+ filterNew(items: RawItem[]): Promise<RawItem[]>;
323
+ setLastRun(kind: string, at?: string): Promise<void>;
324
+ getLastRun(kind: string): Promise<string | null>;
325
+ /** Per-entity enrichment cooldown so we don't re-search the same node constantly. */
326
+ isEnrichCooling(key: string): Promise<boolean>;
327
+ markEnriched(key: string): Promise<void>;
328
+ /** Runtime source config (edit without redeploy). null = fall back to the static SOURCES. */
329
+ getConfigSources(): Promise<SourceEntry[] | null>;
330
+ setConfigSources(list: SourceEntry[]): Promise<void>;
331
+ }
332
+
333
+ /**
334
+ * Cloudflare Workflow hardening — ported verbatim from agnacl
335
+ * (utils/workflow-helpers.ts). resilientStep adds structured error logging
336
+ * without changing retry semantics; the step configs replace the engine's slow
337
+ * default retry with fast exponential backoff.
338
+ */
339
+ declare function resilientStep<T>(stepName: string, fn: () => Promise<T>): Promise<T>;
340
+ /** I/O-bound steps: KV reads, feed fetches. */
341
+ declare const IO_STEP_CONFIG: {
342
+ retries: {
343
+ limit: number;
344
+ delay: "5 seconds";
345
+ backoff: "exponential";
346
+ };
347
+ timeout: "2 minutes";
348
+ };
349
+ /** Graph write steps. */
350
+ declare const COMMIT_STEP_CONFIG: {
351
+ retries: {
352
+ limit: number;
353
+ delay: "3 seconds";
354
+ backoff: "exponential";
355
+ };
356
+ timeout: "90 seconds";
357
+ };
358
+ /** LLM generation steps (can be slow). */
359
+ declare const LLM_STEP_CONFIG: {
360
+ retries: {
361
+ limit: number;
362
+ delay: "5 seconds";
363
+ backoff: "exponential";
364
+ };
365
+ timeout: "3 minutes";
366
+ };
367
+ /** Retry/timeout shape of the step configs above. */
368
+ interface StepConfig {
369
+ retries?: {
370
+ limit: number;
371
+ delay: string;
372
+ backoff?: string;
373
+ };
374
+ timeout?: string;
375
+ }
376
+ /**
377
+ * Structural view of Cloudflare's WorkflowStep. WorkflowStep.do constrains its
378
+ * result to Rpc.Serializable<T>, which cannot terminate (TS2589) on the
379
+ * recursive Value type now that the @odla/* workspace packages give
380
+ * @odla-ai/db's re-exports real definitions. A live WorkflowStep satisfies
381
+ * this shape at runtime; widen through asStepLike at the workflow boundary.
382
+ */
383
+ interface StepLike {
384
+ do<T>(name: string, fn: () => Promise<T>): Promise<T>;
385
+ do<T>(name: string, config: StepConfig, fn: () => Promise<T>): Promise<T>;
386
+ }
387
+ /** Widen a WorkflowStep to StepLike (type-level only, no runtime cost). */
388
+ declare function asStepLike(step: unknown): StepLike;
389
+
390
+ /** Everything the shared ingest loop needs, supplied by the host. The
391
+ * extractor is the ontology's domain craft (e.g. the OSINT extractor); the
392
+ * library stays ontology-agnostic. */
393
+ interface PipelineDeps {
394
+ db: AdminDb;
395
+ provider: Provider;
396
+ ontology: Ontology;
397
+ extract: (provider: Provider, item: RawItem) => Promise<ExtractResult | null>;
398
+ store: Store;
399
+ }
400
+ /**
401
+ * Shared per-item processing: each RawItem is its own step (non-blocking,
402
+ * replay-cached). Structured items (fragment) skip the LLM; text items extract.
403
+ * `resolve` FTS-canonicalizes new nodes against existing ones (discovery path).
404
+ * Used by both the ingest (fetch) and discover (web-search) workflows.
405
+ */
406
+ declare function ingestFresh(step: StepLike, deps: PipelineDeps, fresh: RawItem[], kind: string, resolve?: boolean): Promise<void>;
407
+
408
+ /**
409
+ * Identity + dedup helpers. The slug / urlHash a node resolves to IS its
410
+ * natural key for Lookup-based upserts, so these must be stable and pure.
411
+ * Ported from agnacl (utils/markdown.ts slugify, services/kv.ts normalizeUrl).
412
+ */
413
+ /** NFKD-normalize, strip accents, lowercase, non-alphanumerics → "-", cap 60. */
414
+ declare function slugify(input: string): string;
415
+ /** Drop tracking params, hash, and trailing slash so the same page maps to one key. */
416
+ declare function normalizeUrl(raw: string): string;
417
+ /** Stable short key for a URL (FNV-1a 32-bit, base36). Used as the `signal` natural key. */
418
+ declare function urlHash(raw: string): string;
419
+ /**
420
+ * Reject descriptive phrases masquerading as people ("three craftsmen",
421
+ * "the founders"). A real name is 2-4 capitalized tokens, no digits.
422
+ * Ported from agnacl utils/names.ts (isLikelyPersonName).
423
+ */
424
+ declare function isLikelyPersonName(name: string): boolean;
425
+
426
+ /**
427
+ * Dependency-free HTML → text extraction (Workers-isolate friendly: no DOM/jsdom).
428
+ * Ported from agnacl services/content-extractor.ts.
429
+ */
430
+ /** Strip scripts/styles/tags and collapse whitespace; cap length. */
431
+ declare function htmlToText(html: string, maxChars?: number): string;
432
+ /** Fetch a URL and return readable text. Best-effort: returns "" on any failure. */
433
+ declare function fetchText(url: string, maxChars?: number): Promise<string>;
434
+
435
+ export { type AttrSpec, type AttrType, COMMIT_STEP_CONFIG, type ConnectorContext, type ConnectorJob, type ContextConfig, type EdgeSpec, type EntitySpec, type ExtractCall, type ExtractResult, type GraphEdge, type GraphFragment, type GraphNode, IO_STEP_CONFIG, type KVLike, LLM_STEP_CONFIG, type LlmConfig, type LlmEnv, MODEL_TIERS, type MergeRule, type NaturalKeys, type Ontology, type PipelineDeps, type Provider, type ProviderFamily, type RawItem, type SearchCall, type SourceConnector, type SourceEntry, type StepConfig, type StepLike, Store, type Usage, type WriteCall, addUsage, allJobs, asStepLike, canonicalizeFragment, connectorIds, createProvider, emptyUsage, enabledSources, extractJson, extractJsonArray, fetchText, fetchUrl, fragmentToOps, getConnector, getProvider, htmlToText, humanAttrsOf, ingestFresh, isLikelyPersonName, jobsForHour, makeContext, mergeAttrs, mergeRulesOf, mergeScalar, naturalKeysOf, normalizeUrl, resilientStep, resolveName, searchEntities, slugify, toSerializedSchema, urlHash, writeFragment };