import { SENSITIVE_KEY, redactMetadata } from "../security/metadata" import type { Workspace } from "../domain/types" import { validateWorkspace } from "../validation" import { exportBrain } from "../brain/export" export interface SerializedWorkspace { files:Record; workspaceId:string; schemaVersion:1 } const SECRET = SENSITIVE_KEY const clean = redactMetadata const md=(kind:string,id:string,title:string,data:unknown,body="")=>{const value=clean(data) as Record;const visible=["qualification","evidenceScore","confidence","status","workspaceId","sourceType","strength","objective","priority","supportingEvidence","contradictingEvidence","derivedFrom","hypothesisIds"].flatMap(key=>value?.[key]===undefined?[]:[`${key}: ${JSON.stringify(value[key])}`]);return`---\nschemaVersion: 1\nkind: ${kind}\nid: ${JSON.stringify(id)}\ntitle: ${JSON.stringify(title)}\n${visible.join("\n")}${visible.length?"\n":""}data: ${JSON.stringify(value)}\n---\n\n${body}\n`} export function serializeWorkspace(w:Workspace):SerializedWorkspace{const valid=validateWorkspace(w);if(!valid.valid)throw new Error(`Cannot serialize invalid workspace: ${valid.errors.join("; ")}`);const files:Record={"schema-version.txt":"1\n","manifest.json":JSON.stringify(clean({schemaVersion:1,id:w.id,name:w.name,revision:w.revision,createdAt:w.createdAt,updatedAt:w.updatedAt,backup:w.backup}),null,2)+"\n","context.md":md("workspace",w.id,w.name,{id:w.id,name:w.name},`# ${w.name}`),"graph/edges.ndjson":w.edges.map(e=>JSON.stringify(clean(e))).join("\n")+(w.edges.length?"\n":""),"graph/index.json":JSON.stringify({nodeIds:w.nodes.map(n=>n.id),edgeIds:w.edges.map(e=>e.id)},null,2)+"\n","events/events.ndjson":w.events.map(e=>JSON.stringify(clean(e))).join("\n")+(w.events.length?"\n":"")};if(w.adjudications?.length)files["adjudications/records.ndjson"]=w.adjudications.map(a=>JSON.stringify(clean(a))).join("\n")+"\n";for(const n of w.nodes)files[`graph/nodes/${n.id}.md`]=md("node",n.id,n.title,n,n.body);for(const e of w.evidence)files[`evidence/${e.id}.md`]=md("evidence",e.id,e.claim,e,e.claim);for(const h of w.hypotheses)files[`hypotheses/${h.id}.md`]=md("hypothesis",h.id,h.title,h,h.statement);for(const s of w.syntheses)files[`syntheses/${s.id}.md`]=md("synthesis",s.id,s.title,s,s.statement);for(const t of w.researchTasks)files[`research/tasks/${t.id}.md`]=md("research_task",t.id,t.question,t,t.question);for(const r of w.researchResults)files[`research/results/${r.id}.md`]=md("result",r.id,r.title,r,r.excerpt);const b=exportBrain(w);files["brain/manifest.json"]=JSON.stringify(b.manifest,null,2)+"\n";files["brain/summary.md"]=b.summary;files["brain/graph.json"]=JSON.stringify(clean(b.graph),null,2)+"\n";files["brain/changelog.ndjson"]=b.changelog.map(e=>JSON.stringify(clean(e))).join("\n")+(b.changelog.length?"\n":"");for(const h of b.knowledge)files[`brain/knowledge/${h.id}.md`]=md("brain_knowledge",h.id,h.title,h,h.statement);for(const h of b.hypotheses)files[`brain/hypotheses/${h.id}.md`]=md("hypothesis",h.id,h.title,h,h.statement);for(const h of b.archive)files[`brain/archive/${h.id}.md`]=md("archived_hypothesis",h.id,h.title,h,h.statement);return{workspaceId:w.id,schemaVersion:1,files}} function data(markdown:string){const line=markdown.split("\n").find(x=>x.startsWith("data: "));if(!line)throw new Error("Missing serialized data");return JSON.parse(line.slice(6))} export function restoreSerialized(s:SerializedWorkspace,newId?:string):Workspace{if(s.schemaVersion!==1||s.files["schema-version.txt"]?.trim()!=="1")throw new Error("Unsupported snapshot schema");const manifest=JSON.parse(s.files["manifest.json"]);const list=(prefix:string)=>Object.entries(s.files).filter(([p])=>p.startsWith(prefix)&&p.endsWith(".md")).map(([,v])=>data(v));const parseLines=(p:string)=>(s.files[p]??"").trim().split("\n").filter(Boolean).map(line=>JSON.parse(line));const w:Workspace={schemaVersion:1,id:newId??manifest.id,name:newId?`${manifest.name} (restored)`:manifest.name,revision:manifest.revision??0,nodes:list("graph/nodes/"),edges:parseLines("graph/edges.ndjson"),evidence:list("evidence/"),hypotheses:list("hypotheses/"),syntheses:list("syntheses/"),researchTasks:list("research/tasks/"),researchResults:list("research/results/"),events:parseLines("events/events.ndjson"),...(s.files["adjudications/records.ndjson"]?{adjudications:parseLines("adjudications/records.ndjson")}:{}),brain:{promotedIds:list("brain/knowledge/").map((x:any)=>x.id),changelog:parseLines("brain/changelog.ndjson")},createdAt:manifest.createdAt,updatedAt:manifest.updatedAt};for(const collection of [w.nodes,w.evidence,w.hypotheses,w.syntheses,w.researchTasks])for(const x of collection as any[])x.workspaceId=w.id;for(const e of w.edges)e.workspaceId=w.id;for(const e of w.events)e.workspaceId=w.id;const valid=validateWorkspace(w);if(!valid.valid)throw new Error(`Snapshot validation failed: ${valid.errors.join("; ")}`);return w} export const snapshotContainsSecrets=(s:SerializedWorkspace)=>Object.entries(s.files).some(([p,c])=>SECRET.test(p)||/(github_pat_|ghp_|sk-[A-Za-z0-9]{16,}|Bearer\s+[A-Za-z0-9._-]+)/.test(c))