{"version":3,"file":"research-corpus.d.ts","sourceRoot":"","sources":["../../src/evolve/research-corpus.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAoC,MAAM,wBAAwB,CAAC;AAQ/F,MAAM,WAAW,sBAAsB;IACtC,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IAClC,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,sBAAsB,EAAE,CAAC;IAChC,mEAAmE;IACnE,SAAS,EAAE,MAAM,CAAC;CAClB;AAyJD;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAC9C,MAAM,EAAE,cAAc,EACtB,YAAY,EAAE,MAAM,GAClB,OAAO,CAAC,kBAAkB,CAAC,CA+D7B;AAED,iFAAiF;AACjF,wBAAsB,sBAAsB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAS1G","sourcesContent":["import { mkdir, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { RecordedEvent } from \"../recorder/schema.ts\";\nimport type { EvidenceCorpus, EvidenceFragment, EvidenceSource } from \"../reflect/evidence.ts\";\nimport { atomicWriteFile } from \"../storage.ts\";\n\nconst CLIP_NOTE = \"…[clipped; full content in the matching sessions-raw JSON file]\";\nconst EVENT_TEXT_CLIP = 4_000;\nconst THINKING_CLIP = 600;\nconst TOOL_ARGUMENT_CLIP = 600;\n\nexport interface MaterializedCorpusFile {\n\t/** Path relative to the run directory, e.g. corpus/sessions/<id>.md */\n\tpath: string;\n\tbytes: number;\n\tdescription: string;\n}\n\nexport interface MaterializedCorpus {\n\t/** Absolute path of the corpus directory. */\n\tdirectory: string;\n\tfiles: MaterializedCorpusFile[];\n\t/** Prompt-ready index: one line per file plus reading guidance. */\n\tindexText: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction clip(text: string, maxChars: number): string {\n\tif (text.length <= maxChars) return text;\n\treturn `${text.slice(0, maxChars)}${CLIP_NOTE}`;\n}\n\nfunction compactJson(value: unknown, maxChars: number): string {\n\ttry {\n\t\treturn clip(JSON.stringify(value) ?? \"null\", maxChars);\n\t} catch {\n\t\treturn \"[unserializable]\";\n\t}\n}\n\nfunction formatBytes(bytes: number): string {\n\tif (bytes < 1024) return `${bytes} B`;\n\tif (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n\treturn `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\nfunction renderContentBlocks(content: unknown): string[] {\n\tif (typeof content === \"string\") return [clip(content, EVENT_TEXT_CLIP)];\n\tif (!Array.isArray(content)) return [compactJson(content, EVENT_TEXT_CLIP)];\n\tconst lines: string[] = [];\n\tfor (const block of content) {\n\t\tif (!isRecord(block)) {\n\t\t\tlines.push(compactJson(block, TOOL_ARGUMENT_CLIP));\n\t\t\tcontinue;\n\t\t}\n\t\tswitch (block.type) {\n\t\t\tcase \"text\":\n\t\t\t\tlines.push(clip(typeof block.text === \"string\" ? block.text : \"\", EVENT_TEXT_CLIP));\n\t\t\t\tbreak;\n\t\t\tcase \"thinking\":\n\t\t\t\tlines.push(`(thinking) ${clip(typeof block.thinking === \"string\" ? block.thinking : \"\", THINKING_CLIP)}`);\n\t\t\t\tbreak;\n\t\t\tcase \"toolCall\": {\n\t\t\t\tconst name = isRecord(block.toolCall) ? block.toolCall.name : (block.name ?? \"unknown\");\n\t\t\t\tconst args = isRecord(block.toolCall) ? block.toolCall.arguments : block.arguments;\n\t\t\t\tlines.push(`(tool call) ${String(name)} ${compactJson(args, TOOL_ARGUMENT_CLIP)}`);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\tlines.push(compactJson(block, TOOL_ARGUMENT_CLIP));\n\t\t}\n\t}\n\treturn lines;\n}\n\nfunction renderToolResult(result: unknown): string {\n\tif (typeof result === \"string\") return clip(result, EVENT_TEXT_CLIP);\n\tif (isRecord(result) && Array.isArray(result.content)) {\n\t\tconst texts = result.content.flatMap((item) =>\n\t\t\tisRecord(item) && item.type === \"text\" && typeof item.text === \"string\" ? [item.text] : [],\n\t\t);\n\t\tif (texts.length > 0) return clip(texts.join(\"\\n\"), EVENT_TEXT_CLIP);\n\t}\n\treturn compactJson(result, EVENT_TEXT_CLIP);\n}\n\nfunction renderEvent(event: RecordedEvent & Record<string, unknown>): string[] {\n\tconst head = `### seq ${event.sequence} — ${event.type} (${event.timestamp})`;\n\tswitch (event.type) {\n\t\tcase \"session_start\":\n\t\t\treturn [`${head}\\nreason=${event.reason} cwd=${event.cwd}`];\n\t\tcase \"session_end\":\n\t\t\treturn [`${head}\\nreason=${event.reason}`];\n\t\tcase \"before_agent_start\": {\n\t\t\tconst prompt = typeof event.prompt === \"string\" ? event.prompt : compactJson(event.prompt, EVENT_TEXT_CLIP);\n\t\t\tconst systemPromptBytes =\n\t\t\t\ttypeof event.systemPrompt === \"string\" ? Buffer.byteLength(event.systemPrompt, \"utf8\") : undefined;\n\t\t\treturn [\n\t\t\t\t`${head}\\nprompt:\\n${clip(prompt, EVENT_TEXT_CLIP)}`,\n\t\t\t\tsystemPromptBytes === undefined\n\t\t\t\t\t? \"(system prompt unavailable inline; see raw JSON)\"\n\t\t\t\t\t: `(system prompt: ${formatBytes(systemPromptBytes)}, see raw JSON for full text)`,\n\t\t\t];\n\t\t}\n\t\tcase \"message\": {\n\t\t\tconst message = event.message;\n\t\t\tconst content = isRecord(message) ? message.content : message;\n\t\t\treturn [`${head} role=${event.role}`, ...renderContentBlocks(content)];\n\t\t}\n\t\tcase \"usage\":\n\t\t\treturn [`${head}\\n${compactJson({ provider: event.provider, model: event.model, usage: event.usage }, 800)}`];\n\t\tcase \"tool\": {\n\t\t\tconst status = event.isError ? \"ERROR\" : \"ok\";\n\t\t\treturn [\n\t\t\t\t`${head} ${event.toolName} [${status}, ${event.durationMs}ms]`,\n\t\t\t\t`input: ${compactJson(event.input, TOOL_ARGUMENT_CLIP)}`,\n\t\t\t\t`result:\\n${renderToolResult(event.result)}`,\n\t\t\t];\n\t\t}\n\t\tcase \"git_diff\": {\n\t\t\tconst diff = typeof event.diff === \"string\" ? event.diff : compactJson(event.diff, EVENT_TEXT_CLIP);\n\t\t\treturn [`${head} clean=${event.clean}`, clip(diff, EVENT_TEXT_CLIP)];\n\t\t}\n\t\tcase \"explicit_feedback\": {\n\t\t\tconst text = typeof event.text === \"string\" ? event.text : compactJson(event.text, EVENT_TEXT_CLIP);\n\t\t\treturn [`${head} source=${event.source} inboxFile=${event.inboxFile}`, text];\n\t\t}\n\t\tcase \"compaction\": {\n\t\t\tconst summary =\n\t\t\t\ttypeof event.summary === \"string\" ? event.summary : compactJson(event.summary, EVENT_TEXT_CLIP);\n\t\t\treturn [\n\t\t\t\t`${head} **COMPACTION** reason=${event.reason} willRetry=${event.willRetry} tokensBefore=${event.tokensBefore}`,\n\t\t\t\t`summary:\\n${summary}`,\n\t\t\t];\n\t\t}\n\t\tdefault:\n\t\t\treturn [`${head}\\n${compactJson(event, EVENT_TEXT_CLIP)}`];\n\t}\n}\n\nfunction renderSessionMarkdown(sessionId: string, fragments: readonly EvidenceFragment[]): string {\n\tconst parts: string[] = [`# Session ${sessionId}`, \"\"];\n\tfor (const fragment of fragments) {\n\t\tif (fragment.heading.startsWith(\"## Session digest \")) {\n\t\t\tparts.push(\"## Digest\", \"```json\", JSON.stringify(fragment.value, undefined, \"\\t\"), \"```\", \"\");\n\t\t\tcontinue;\n\t\t}\n\t\tconst value = fragment.value;\n\t\tif (isRecord(value) && typeof value.type === \"string\" && typeof value.sequence === \"number\") {\n\t\t\tparts.push(...renderEvent(value as RecordedEvent & Record<string, unknown>), \"\");\n\t\t\tcontinue;\n\t\t}\n\t\tparts.push(fragment.heading, compactJson(value, EVENT_TEXT_CLIP), \"\");\n\t}\n\treturn `${parts.join(\"\\n\").trimEnd()}\\n`;\n}\n\nfunction renderPlainSource(fragments: readonly EvidenceFragment[]): string {\n\tconst sections = fragments.map(\n\t\t(fragment) =>\n\t\t\t`${fragment.heading}\\n${\n\t\t\t\ttypeof fragment.value === \"string\" ? fragment.value : JSON.stringify(fragment.value, undefined, \"\\t\")\n\t\t\t}`,\n\t);\n\treturn `${sections.join(\"\\n\\n\").trimEnd()}\\n`;\n}\n\n// Inbox is deliberately absent: explicit user inputs are small, carry a\n// must-classify obligation, and therefore stay inline in the prompt index.\nconst PLAIN_SOURCE_DESCRIPTIONS: Record<Exclude<EvidenceSource, \"sessions\" | \"inbox\">, string> = {\n\tbundle: \"stable bundle manifest and bundle files\",\n\thistory: \"registry history and inbox lifecycle entries (newest first)\",\n};\n\n/**\n * Write the evidence corpus into <runDirectory>/corpus as an on-demand file tree and\n * return a compact prompt index. Sessions get a rendered markdown transcript (dense,\n * readable) plus a pretty-printed raw JSON twin for exact quoting — no pathological\n * single-line files.\n */\nexport async function materializeEvidenceCorpus(\n\tcorpus: EvidenceCorpus,\n\trunDirectory: string,\n): Promise<MaterializedCorpus> {\n\tconst directory = join(runDirectory, \"corpus\");\n\tawait mkdir(join(directory, \"sessions\"), { recursive: true });\n\tawait mkdir(join(directory, \"sessions-raw\"), { recursive: true });\n\n\tconst files: MaterializedCorpusFile[] = [];\n\tconst write = async (relativePath: string, content: string, description: string): Promise<void> => {\n\t\tawait atomicWriteFile(join(runDirectory, relativePath), content);\n\t\tfiles.push({ path: relativePath, bytes: Buffer.byteLength(content, \"utf8\"), description });\n\t};\n\n\tfor (const source of [\"bundle\", \"history\"] as const) {\n\t\tconst fragments = corpus.fragments.filter((fragment) => fragment.source === source);\n\t\tif (fragments.length === 0) continue;\n\t\tawait write(`corpus/${source}.md`, renderPlainSource(fragments), PLAIN_SOURCE_DESCRIPTIONS[source]);\n\t}\n\n\tconst sessionFragments = new Map<string, EvidenceFragment[]>();\n\tfor (const fragment of corpus.fragments) {\n\t\tif (fragment.source !== \"sessions\" || !fragment.sessionId) continue;\n\t\tconst existing = sessionFragments.get(fragment.sessionId);\n\t\tif (existing) existing.push(fragment);\n\t\telse sessionFragments.set(fragment.sessionId, [fragment]);\n\t}\n\tfor (const [sessionId, fragments] of sessionFragments) {\n\t\tawait write(\n\t\t\t`corpus/sessions/${sessionId}.md`,\n\t\t\trenderSessionMarkdown(sessionId, fragments),\n\t\t\t\"rendered session transcript (messages, tools, usage, compaction boundaries)\",\n\t\t);\n\t\tconst rawEvents = fragments\n\t\t\t.filter((fragment) => !fragment.heading.startsWith(\"## Session digest \"))\n\t\t\t.map((fragment) => fragment.value);\n\t\tawait write(\n\t\t\t`corpus/sessions-raw/${sessionId}.json`,\n\t\t\t`${JSON.stringify(rawEvents, undefined, \"\\t\")}\\n`,\n\t\t\t\"full recorded events for exact quoting (pretty-printed JSON)\",\n\t\t);\n\t}\n\n\tconst indexLines = files.map((file) => `- ${file.path} (${formatBytes(file.bytes)}) — ${file.description}`);\n\tconst inboxFragments = corpus.fragments.filter((fragment) => fragment.source === \"inbox\");\n\tconst indexText = [\n\t\t`The evidence corpus is materialized as files under: ${directory}`,\n\t\t\"Read them on demand with read/grep/find/ls instead of assuming their content.\",\n\t\t\"Prefer the rendered corpus/sessions/*.md transcripts; open corpus/sessions-raw/*.json only to verify exact quotes or payload details.\",\n\t\t\"\",\n\t\t...indexLines,\n\t\t\"\",\n\t\t// Explicit user inputs stay inline: every entry below must be classified.\n\t\t...(inboxFragments.length > 0\n\t\t\t? [\n\t\t\t\t\t\"Open explicit user inbox inputs (classify every file below in inboxDecisions):\",\n\t\t\t\t\trenderPlainSource(inboxFragments),\n\t\t\t\t]\n\t\t\t: [\"There are no open explicit user inbox inputs.\"]),\n\t].join(\"\\n\");\n\n\t// Persist the index so later phases (evidence resumption) can rebuild the\n\t// prompt block without recollecting the corpus.\n\tawait atomicWriteFile(join(directory, \"INDEX.md\"), `${indexText}\\n`);\n\n\treturn { directory, files, indexText };\n}\n\n/** Rehydrate a materialized corpus from its persisted index, when one exists. */\nexport async function readMaterializedCorpus(runDirectory: string): Promise<MaterializedCorpus | undefined> {\n\tconst directory = join(runDirectory, \"corpus\");\n\ttry {\n\t\tconst indexText = (await readFile(join(directory, \"INDEX.md\"), \"utf8\")).trimEnd();\n\t\treturn { directory, files: [], indexText };\n\t} catch (error) {\n\t\tif ((error as { code?: string }).code === \"ENOENT\") return undefined;\n\t\tthrow error;\n\t}\n}\n"]}