spb/autoscholar
Public
1import { STAGES } from "@autoscholar/schemas";2import { fmtDate, shortHash } from "@/lib/format";3import { IconChevron } from "./Icons";4import { StatusBadge } from "./StatusBadge";56type Gate = { gate: string; status: string; severity: string; message: string };78/** ResearchProvenance — expandable, verifiable record: stages, models, hashes, gates, warnings. */9export function ResearchProvenance({10 prov,11 qa,12 texReleased,13 meta,14}: {15 prov: Record<string, unknown>;16 qa: { gates?: Gate[]; warnings?: string[]; policyVersion?: string; evaluatedAt?: string };17 texReleased: boolean;18 meta: Record<string, unknown>;19}) {20 const gates = qa.gates ?? [];21 const passed = gates.filter((g) => g.status === "pass").length;22 const counted = gates.filter((g) => g.status !== "skip").length;23 const stagesDone = (prov.stagesCompleted as string[] | undefined) ?? [];24 const toolchain = (prov.toolchain as Record<string, string> | undefined) ?? {};25 const Row = ({ k, v, mono = false }: { k: string; v: React.ReactNode; mono?: boolean }) => (26 <div className="grid grid-cols-[9rem_1fr] gap-x-3 border-b py-2 text-sm last:border-0 hairline sm:grid-cols-[11rem_1fr]">27 <dt className="label pt-0.5">{k}</dt>28 <dd className={`min-w-0 break-words ${mono ? "font-mono text-xs" : ""}`}>{v}</dd>29 </div>30 );31 return (32 <div className="surface overflow-hidden">33 <div className="grid gap-4 border-b p-5 hairline sm:grid-cols-3">34 <div>35 <p className="label">Quality gates</p>36 <p className="serif mt-1 text-2xl">{counted ? `${passed}/${counted}` : "—"}</p>37 <p className="subtle text-xs">38 {qa.evaluatedAt ? `verified ${fmtDate(qa.evaluatedAt)}` : "not recorded"}39 </p>40 </div>41 <div>42 <p className="label">Stages completed</p>43 <p className="serif mt-1 text-2xl">44 {stagesDone.length ? `${stagesDone.length}/${STAGES.length - 2}` : "—"}45 </p>46 <p className="subtle text-xs">on the research worker</p>47 </div>48 <div>49 <p className="label">Policy</p>50 <p className="serif mt-1 text-2xl">{qa.policyVersion ?? "—"}</p>51 <p className="subtle text-xs">deterministic, server-side</p>52 </div>53 </div>54 <dl className="px-5 py-2">55 <Row k="Research model" v={String(prov.model ?? "—")} mono />56 <Row k="Reviewer model" v={String(prov.reviewerModel ?? "—")} mono />57 <Row58 k="Instruction"59 v={`v${String(prov.instructionVersion ?? "?")} · ${shortHash(prov.instructionSha256 as string)}`}60 mono61 />62 <Row k="Manifest digest" v={shortHash(prov.manifestSha256 as string, 16)} mono />63 <Row64 k="Execution"65 v={66 prov.startedAt67 ? `${fmtDate(prov.startedAt as string)} → ${prov.completedAt ? fmtDate(prov.completedAt as string) : "?"} · ${String(prov.infrastructure ?? "MacLustr")}`68 : "—"69 }70 />71 <Row72 k="Toolchain"73 v={74 Object.keys(toolchain).length75 ? Object.entries(toolchain)76 .map(([k, v]) => `${k} ${v}`)77 .join(" · ")78 : "not recorded"79 }80 mono81 />82 <Row83 k="LaTeX source"84 v={85 texReleased86 ? "Released; the PDF was rebuilt from it in a clean environment during verification."87 : "Not released."88 }89 />90 <Row k="Data availability" v={(meta.dataAvailability as string) || "Not applicable or not stated."} />91 <Row k="Code availability" v={(meta.codeAvailability as string) || "Not applicable or not stated."} />92 <Row93 k="Ethics"94 v={95 (meta.ethics as string) ||96 "No restricted activities; public data, mathematics or simulations only."97 }98 />99 </dl>100 {stagesDone.length > 0 && (101 <div className="border-t px-5 py-4 hairline">102 <p className="label mb-2">Pipeline record</p>103 <ol className="flex flex-wrap gap-1.5">104 {STAGES.map((s) => {105 const done =106 stagesDone.includes(s.key) || (s.key === "quality" && counted > 0) || s.key === "publication";107 return (108 <li key={s.key} className={`badge ${done ? "badge-sci" : ""}`} title={s.name}>109 {s.index} {s.name}110 </li>111 );112 })}113 </ol>114 </div>115 )}116 {gates.length > 0 && (117 <details className="group border-t hairline">118 <summary className="flex items-center justify-between px-5 py-3 text-sm">119 <span>Gate-by-gate evidence ({gates.length})</span>120 <IconChevron size={16} className="subtle transition-transform group-open:rotate-90" />121 </summary>122 <ul className="space-y-1.5 px-5 pb-4 text-xs">123 {gates.map((g) => (124 <li key={g.gate} className="grid grid-cols-[auto_1fr] items-start gap-2">125 <StatusBadge status={g.status} />126 <span>127 <span className="font-mono">{g.gate}</span> <span className="subtle">({g.severity})</span>128 <span className="muted"> — {g.message}</span>129 </span>130 </li>131 ))}132 </ul>133 </details>134 )}135 {qa.warnings && qa.warnings.length > 0 && (136 <details className="group border-t hairline">137 <summary className="flex items-center justify-between px-5 py-3 text-sm">138 <span>Non-blocking warnings recorded at verification ({qa.warnings.length})</span>139 <IconChevron size={16} className="subtle transition-transform group-open:rotate-90" />140 </summary>141 <ul className="muted list-disc space-y-1 px-5 pb-4 pl-9 text-xs">142 {qa.warnings.map((w) => (143 <li key={w}>{w}</li>144 ))}145 </ul>146 </details>147 )}148 <p className="subtle border-t px-5 py-3 text-xs hairline">149 Artifact digests (SHA-256) accompany every download; the complete list is in150 provenance/checksums.sha256.151 </p>152 </div>153 );154}155