export interface Company { scrip_code: string; name: string; cin: string | null; isin: string | null; industry: string; sector: string; year: string; pages: number; pl_fields: number; bs_fields: number; dr_fields?: number; identities: string[]; isin_corroborated: boolean; warnings?: string[]; } export type FactStatus = | "verified" | "unverified" | "conflicted" | "resolved" | "attested" | "derived"; export interface Provenance { source_id: string; source_title: string; source_type: string; locator: string; page: number | null; raw_line: string; section: string; url: string | null; extractor: string; retrieved_at: string; } export interface Fact { id: string; label: string; value: unknown; unit: string; status: FactStatus; confidence: number; provenance: Provenance[]; section_refs: string[]; derived_from: string[]; note: string | null; period: string | null; identities: string[]; history: Record[]; } export interface Evidence { label: string; value: unknown; display: string; fact_id: string | null; section: string; locator: string; raw_line: string; } export interface Resolution { id: string; label: string; description: string; recommended: boolean; kind: string; justification: string; } export interface Finding { rule_id: string; severity: "blocker" | "high" | "medium" | "info"; title: string; explanation: string; icdr_ref: string; evidence: Evidence[]; delta: string | null; resolutions: Resolution[]; fact_ids: string[]; resolved: boolean; resolved_by: string | null; resolved_at: string | null; } export interface RuleResult { rule_id: string; title: string; icdr_ref: string; status: "pass" | "fail" | "not_applicable"; detail: string; findings: Finding[]; } export interface DraftSection { id: string; number: string; title: string; icdr_clause: string; html: string; text: string; fact_ids: string[]; word_count: number; narrative_source: string; } export interface ClauseCoverage { clause_id: string; clause: string; title: string; covered: boolean; section_id: string | null; reason: string; } export interface Decision { at: string; actor: string; action: string; rule_id: string | null; resolution_id: string | null; fact_ids: string[]; detail: string; } export interface IssueObject { id: string; label: string; amount: number; is_general_corporate: boolean; schedule: Record; } export interface IssueTerms { face_value: number; price_per_share: number; fresh_issue_shares: number; ofs_shares: number; issue_expenses: number; objects: IssueObject[]; lead_manager: string; listing_platform: string; fresh_issue_value: number; ofs_value: number; total_issue_value: number; total_issue_shares: number; net_proceeds: number; } export interface RunSummary { id: string; scrip_code: string; company_name: string; cin: string | null; status: string; created_at: string; fact_count: number; fact_status: Record; severity: Record; section_count: number; coverage_pct: number; rule_count: number; rules_passed: number; rules_failed: number; rules_na: number; open_findings: number; resolved_findings: number; meta: Record; source_documents: Array>; } export interface Run extends RunSummary { facts: Record; sections: DraftSection[]; rule_results: RuleResult[]; coverage: ClauseCoverage[]; decisions: Decision[]; } export class ApiError extends Error {} async function req(path: string, init?: RequestInit): Promise { let res: Response; try { res = await fetch(path, { ...init, headers: { "Content-Type": "application/json", ...(init?.headers ?? {}) }, }); } catch { throw new ApiError( "Could not reach the PrimaryDraft service. Check that the backend is running on port 8000.", ); } if (!res.ok) { let detail = `${res.status} ${res.statusText}`; try { const body = await res.json(); if (body?.detail) detail = String(body.detail); } catch { /* response had no JSON body */ } throw new ApiError(detail); } return res.json() as Promise; } export const api = { health: () => req("/api/health"), companies: (q = "") => req<{ companies: Company[]; count: number }>( `/api/companies?q=${encodeURIComponent(q)}`, ), createRun: (scrip_code: string) => req("/api/runs", { method: "POST", body: JSON.stringify({ scrip_code }) }), getRun: (id: string) => req(`/api/runs/${id}`), getIssue: (id: string) => req(`/api/runs/${id}/issue`), putIssue: (id: string, terms: Partial) => req(`/api/runs/${id}/issue`, { method: "PUT", body: JSON.stringify(terms) }), draft: (id: string) => req(`/api/runs/${id}/draft`, { method: "POST" }), check: (id: string) => req(`/api/runs/${id}/check`, { method: "POST" }), resolve: (id: string, rule_id: string, resolution_id: string, actor: string) => req(`/api/runs/${id}/resolve`, { method: "POST", body: JSON.stringify({ rule_id, resolution_id, actor }), }), attest: (id: string, fact_ids: string[], actor: string) => req(`/api/runs/${id}/attest`, { method: "POST", body: JSON.stringify({ fact_ids, actor }), }), rules: () => req("/api/rules"), search: (q: string, scope: "sme" | "all" = "sme", limit = 40) => req(`/api/search?q=${encodeURIComponent(q)}&scope=${scope}&limit=${limit}`), ingest: (scrip_code: string, name = "") => req("/api/ingest", { method: "POST", body: JSON.stringify({ scrip_code, name }) }), ingestStatus: (job_id: string) => req(`/api/ingest/${job_id}`), inspectCin: (cin: string) => req(`/api/cin/${encodeURIComponent(cin)}`), createRunFromCin: (cin: string, name: string) => req("/api/runs/from-cin", { method: "POST", body: JSON.stringify({ cin, name }), }), manifestUrl: (id: string) => `/api/runs/${id}/export/manifest`, docxUrl: (id: string) => `/api/runs/${id}/export/docx`, }; /** Format rupees the way an Indian offer document does. */ export function inr(v: number | null | undefined): string { if (v === null || v === undefined || Number.isNaN(v)) return "n/a"; const a = Math.abs(v); if (a >= 1e7) return `₹${(v / 1e7).toLocaleString("en-IN", { maximumFractionDigits: 2 })} cr`; if (a >= 1e5) return `₹${(v / 1e5).toLocaleString("en-IN", { maximumFractionDigits: 2 })} lakh`; return `₹${v.toLocaleString("en-IN", { maximumFractionDigits: 2 })}`; } export function factValue(f: Fact): string { const v = f.value; if (typeof v === "number") { if (f.unit === "INR") return inr(v); if (f.unit === "PCT") return `${v.toFixed(2)}%`; if (f.unit === "SHARES") return v.toLocaleString("en-IN"); if (f.unit === "RATIO") return v.toFixed(2); return v.toLocaleString("en-IN"); } return String(v ?? "n/a"); }