import type { ConnectorConfig, ConnectorContext, DiscoveredUrl, ExtractedRecord, Parser, RawDocument } from "@dci/connectors"; import { GenericConnector } from "@dci/connectors"; import { cleanText } from "@dci/core"; import { EXTRACTOR_VERSION } from "./extract-project.js"; import { detectOperators } from "./operators-lexicon.js"; import { US_STATES } from "./locations-lexicon.js"; /** * SEC EDGAR full-text search (efts.sec.gov/LATEST/search-index) — public JSON API, no key. The API ignores * `dateRange=30d`, so the connector computes `dateRange=custom&startdt=…&enddt=…` at discovery time * (`implementation: news_edgar_fts`, params.queries / params.lookbackDays / params.forms). One `news_event` * per filing (exhibit preferred), no fetch of the filing itself — links point to the SEC archive. * SEC fair-access policy: identify the crawler with a contact address (DEFAULT_UA does), ≤ 10 req/s. */ interface EdgarHit { _id: string; // "0001079973-26-000696:ex99x2.htm" _source: { ciks?: string[]; display_names?: string[]; file_date?: string; form?: string; root_forms?: string[]; file_type?: string; file_description?: string; adsh?: string; items?: string[]; biz_locations?: string[]; biz_states?: string[]; period_ending?: string; sics?: string[] }; } interface EdgarResponse { hits?: { total?: { value?: number }; hits?: EdgarHit[] }; query?: unknown } const iso = (d: Date): string => d.toISOString().slice(0, 10); export class EdgarFtsConnector extends GenericConnector { async discover(_ctx: ConnectorContext): Promise { const p = (this.cfg.params ?? {}) as { queries?: unknown; forms?: unknown; lookbackDays?: unknown }; const queries = Array.isArray(p.queries) && p.queries.length ? p.queries.map(String) : ['"data center"']; const forms = typeof p.forms === "string" && p.forms ? p.forms : "8-K"; const lookback = Number.isFinite(Number(p.lookbackDays)) && Number(p.lookbackDays) > 0 ? Number(p.lookbackDays) : 30; const end = new Date(); const start = new Date(end.getTime() - lookback * 86_400_000); return queries.map((q, i) => ({ url: `https://efts.sec.gov/LATEST/search-index?q=${encodeURIComponent(q)}&forms=${encodeURIComponent(forms)}&dateRange=custom&startdt=${iso(start)}&enddt=${iso(end)}`, group: "filings", pageType: "financial_disclosure", priority: 90 - i, discoveredFrom: null, meta: { query: q, forms, lookbackDays: lookback }, })); } } export function edgarFilingUrl(hit: EdgarHit): { url: string; adsh: string; file: string } | null { const [adshRaw, file] = hit._id.split(":"); const adsh = hit._source.adsh ?? adshRaw; const cik = hit._source.ciks?.[0]; if (!adsh || !cik) return null; const folder = adsh.replace(/-/g, ""); const url = file ? `https://www.sec.gov/Archives/edgar/data/${Number(cik)}/${folder}/${file}` : `https://www.sec.gov/Archives/edgar/data/${Number(cik)}/${folder}/`; return { url, adsh, file: file ?? "" }; } function companyName(display: string | undefined): string | null { if (!display) return null; return cleanText(display.replace(/\s*\([A-Z0-9.]{1,10}\)\s*\(CIK \d+\)\s*$/, "").replace(/\s*\(CIK \d+\)\s*$/, "")); } /** Rank exhibits above the cover 8-K so one filing yields the press-release exhibit when it exists. */ function rank(hit: EdgarHit): number { const ft = (hit._source.file_type ?? "").toUpperCase(); if (/^EX-99/.test(ft)) return 3; if (/^EX-/.test(ft)) return 2; return 1; } export const edgarFtsParser: Parser = { name: "news_edgar_fts_v1", version: EXTRACTOR_VERSION, parse(doc: RawDocument, ctx: ConnectorContext): ExtractedRecord[] { let res: EdgarResponse; try { res = JSON.parse(doc.text) as EdgarResponse; } catch { ctx.log("warn", `news_edgar_fts_v1: response is not JSON (${doc.finalUrl})`); return []; } const hits = res.hits?.hits ?? []; const query = typeof doc.meta?.query === "string" ? doc.meta.query : new URL(doc.finalUrl).searchParams.get("q") ?? ""; const byFiling = new Map(); for (const h of hits) { const adsh = h._source.adsh ?? h._id.split(":")[0]!; const prev = byFiling.get(adsh); if (!prev || rank(h) > rank(prev)) byFiling.set(adsh, h); } const out: ExtractedRecord[] = []; for (const h of byFiling.values()) { const link = edgarFilingUrl(h); if (!link) continue; const s = h._source; const company = companyName(s.display_names?.[0]); if (!company) continue; const form = s.root_forms?.[0] ?? s.form ?? "8-K"; const fileType = s.file_type && s.file_type.toUpperCase() !== form.toUpperCase() ? s.file_type : null; const title = `${company}: Form ${form}${fileType ? ` (${fileType}${s.file_description && !/^EXHIBIT/i.test(s.file_description) ? ` — ${s.file_description}` : ""})` : ""}`; const loc = s.biz_locations?.[0] ?? null; const state = s.biz_states?.[0] ?? null; const summary = cleanText(`${form} filed ${s.file_date ?? "(date unknown)"} by ${(s.display_names ?? [company]).map((d) => companyName(d)).filter(Boolean).join(", ")}${s.items?.length ? `; items ${s.items.join(", ")}` : ""}${loc ? `; registrant located in ${loc}` : ""}. Matched EDGAR full-text search ${query}.`); const operators = detectOperators(company).map((o) => o.name); const isUs = state ? Object.prototype.hasOwnProperty.call(US_STATES, state.toUpperCase()) : false; const city = loc ? cleanText(loc.split(",")[0]) : null; out.push({ kind: "news_event", key: `${ctx.connectorId}:news:${link.adsh}`, url: link.url, pageType: "financial_disclosure", certainty: 0.7, data: { title, url: link.url, publishedAt: s.file_date ?? null, summary, pageType: "financial_disclosure", eventType: "news", mw: [], status: null, money: null, text: null, operators, countries: isUs ? ["US"] : [], cities: city && isUs ? [city] : [], filing: { adsh: link.adsh, form, fileType: s.file_type ?? null, items: s.items ?? [], cik: s.ciks?.[0] ?? null, periodEnding: s.period_ending ?? null, query }, relevance: "strong", extractor: EXTRACTOR_VERSION, }, methods: { title: "edgar:display_names", publishedAt: "edgar:file_date", summary: "edgar:metadata", operators: "lexicon:operator" }, }); } ctx.log("info", `news_edgar_fts_v1: ${hits.length} hits → ${out.length} filings (${query})`); return out; }, }; export function edgarImplementation(cfg: ConnectorConfig): EdgarFtsConnector { return new EdgarFtsConnector(cfg); }