Bot-Derpy's picture
PrimaryDraft: SEBI TechSprint prototype (27 deterministic checks, 40 real BSE SME issuers)
baf8a7b verified
Raw History Blame Contribute Delete
7.13 kB
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<string, unknown>[];
}
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<string, number>;
}
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<string, number>;
severity: Record<string, number>;
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<string, any>;
source_documents: Array<Record<string, any>>;
}
export interface Run extends RunSummary {
facts: Record<string, Fact>;
sections: DraftSection[];
rule_results: RuleResult[];
coverage: ClauseCoverage[];
decisions: Decision[];
}
export class ApiError extends Error {}
async function req<T>(path: string, init?: RequestInit): Promise<T> {
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<T>;
}
export const api = {
health: () => req<any>("/api/health"),
companies: (q = "") => req<{ companies: Company[]; count: number }>(
`/api/companies?q=${encodeURIComponent(q)}`,
),
createRun: (scrip_code: string) =>
req<RunSummary>("/api/runs", { method: "POST", body: JSON.stringify({ scrip_code }) }),
getRun: (id: string) => req<Run>(`/api/runs/${id}`),
getIssue: (id: string) => req<IssueTerms>(`/api/runs/${id}/issue`),
putIssue: (id: string, terms: Partial<IssueTerms>) =>
req<IssueTerms>(`/api/runs/${id}/issue`, { method: "PUT", body: JSON.stringify(terms) }),
draft: (id: string) => req<any>(`/api/runs/${id}/draft`, { method: "POST" }),
check: (id: string) => req<any>(`/api/runs/${id}/check`, { method: "POST" }),
resolve: (id: string, rule_id: string, resolution_id: string, actor: string) =>
req<any>(`/api/runs/${id}/resolve`, {
method: "POST",
body: JSON.stringify({ rule_id, resolution_id, actor }),
}),
attest: (id: string, fact_ids: string[], actor: string) =>
req<any>(`/api/runs/${id}/attest`, {
method: "POST",
body: JSON.stringify({ fact_ids, actor }),
}),
rules: () => req<any>("/api/rules"),
search: (q: string, scope: "sme" | "all" = "sme", limit = 40) =>
req<any>(`/api/search?q=${encodeURIComponent(q)}&scope=${scope}&limit=${limit}`),
ingest: (scrip_code: string, name = "") =>
req<any>("/api/ingest", { method: "POST", body: JSON.stringify({ scrip_code, name }) }),
ingestStatus: (job_id: string) => req<any>(`/api/ingest/${job_id}`),
inspectCin: (cin: string) => req<any>(`/api/cin/${encodeURIComponent(cin)}`),
createRunFromCin: (cin: string, name: string) =>
req<RunSummary>("/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");
}