{"version":3,"file":"operability.d.ts","sourceRoot":"","sources":["../../src/core/operability.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { existsSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { basename, resolve } from \"node:path\";\nimport type { AgentMessage } from \"@apholdings/jensen-agent-core\";\nimport type { FileEntry, SessionEntry, SessionHeader, SessionMessageEntry } from \"./session-manager.js\";\nimport { workspaceDoctorChecks } from \"./workspace/index.js\";\n\nexport const OBSERVABILITY_SCHEMA_VERSION = 1;\nexport const DEFAULT_PAGE_SIZE = 50;\nexport const MAX_PAGE_SIZE = 500;\n\nexport type CorruptionClass =\n\t| \"recoverable_tail_corruption\"\n\t| \"recoverable_index_corruption\"\n\t| \"snapshot_rebuild_required\"\n\t| \"evidence_missing\"\n\t| \"manual_recovery_required\"\n\t| \"integrity_failure\";\n\nexport interface JensenEventEnvelope<TPayload = unknown> {\n\teventId: string;\n\teventType: string;\n\tschemaVersion: number;\n\trunId: string;\n\tsessionId?: string;\n\tparentEventId?: string;\n\tcausationId?: string;\n\tcorrelationId?: string;\n\tsequence: number;\n\trecordedAt: string;\n\tmonotonicOffsetMs?: number;\n\tsource: { component: string; instanceId?: string };\n\tpayload: TPayload;\n\tpayloadSha256: string;\n}\n\nexport interface EventReadIssue {\n\tline: number;\n\tclass: CorruptionClass;\n\treasonCode: string;\n\tdetail: string;\n}\n\nexport interface EventReadResult {\n\theader?: SessionHeader;\n\tevents: JensenEventEnvelope<FileEntry>[];\n\tissues: EventReadIssue[];\n\tcomplete: boolean;\n}\n\nexport interface EventQuery {\n\tpageSize?: number;\n\tpageToken?: string;\n\teventType?: string;\n\tcomponent?: string;\n\tfailureOnly?: boolean;\n\tfrom?: string;\n\tto?: string;\n}\n\nexport interface EventPage {\n\tevents: JensenEventEnvelope<FileEntry>[];\n\tnextPageToken?: string;\n\tissues: EventReadIssue[];\n}\n\nexport interface RunProjection {\n\trunId: string;\n\tsessionId: string;\n\tcwd: string;\n\tobjective?: string;\n\tentryCount: number;\n\tmessageCount: number;\n\ttoolCallCount: number;\n\ttoolResultCount: number;\n\tmodels: Array<{ provider: string; modelId: string }>;\n\ttoolNames: string[];\n\tmutations: number;\n\tevidenceIds: string[];\n\twarnings: string[];\n\tunknownEventTypes: string[];\n\tfirstRecordedAt?: string;\n\tlastRecordedAt?: string;\n}\n\nexport interface RenderReplay {\n\tlines: string[];\n\teventCount: number;\n\tmissingArtifacts: string[];\n\texternalEffects: { modelCalls: 0; toolCalls: 0; networkCalls: 0; mutations: 0 };\n}\n\nexport interface ProjectionReplay {\n\tprojection: RunProjection;\n\tissues: EventReadIssue[];\n\tsnapshotMatches?: boolean;\n}\n\nexport interface RunDiffField {\n\tpath: string;\n\tcategory: \"improved\" | \"regressed\" | \"changed\" | \"unchanged\" | \"incomparable\";\n\tbefore: unknown;\n\tafter: unknown;\n}\n\nexport interface RunDiff {\n\tleftRunId: string;\n\trightRunId: string;\n\tcomparable: boolean;\n\tfields: RunDiffField[];\n}\n\nfunction canonical(value: unknown): string {\n\tif (value === null || typeof value !== \"object\") return JSON.stringify(value);\n\tif (Array.isArray(value)) return `[${value.map(canonical).join(\",\")}]`;\n\tconst record = value as Record<string, unknown>;\n\treturn `{${Object.keys(record)\n\t\t.sort()\n\t\t.map((key) => `${JSON.stringify(key)}:${canonical(record[key])}`)\n\t\t.join(\",\")}}`;\n}\n\nexport function sha256(value: unknown): string {\n\treturn createHash(\"sha256\")\n\t\t.update(typeof value === \"string\" ? value : canonical(value))\n\t\t.digest(\"hex\");\n}\n\nfunction isFileEntry(value: unknown): value is FileEntry {\n\tif (!value || typeof value !== \"object\") return false;\n\tconst record = value as Record<string, unknown>;\n\treturn typeof record.type === \"string\";\n}\n\nfunction isPersistedEnvelope(value: unknown): value is JensenEventEnvelope<FileEntry> {\n\tif (!value || typeof value !== \"object\") return false;\n\tconst record = value as Record<string, unknown>;\n\treturn (\n\t\ttypeof record.eventId === \"string\" &&\n\t\ttypeof record.eventType === \"string\" &&\n\t\ttypeof record.payloadSha256 === \"string\" &&\n\t\tisFileEntry(record.payload)\n\t);\n}\n\nfunction entryType(entry: FileEntry): string {\n\treturn entry.type;\n}\n\nfunction entryId(entry: FileEntry): string | undefined {\n\treturn entry.type === \"session\" ? undefined : typeof entry.id === \"string\" ? entry.id : undefined;\n}\n\nfunction entryTimestamp(entry: FileEntry): string {\n\treturn typeof entry.timestamp === \"string\" ? entry.timestamp : new Date(0).toISOString();\n}\n\nexport function envelopeForEntry(entry: FileEntry, runId: string, sequence: number): JensenEventEnvelope<FileEntry> {\n\tconst id = entryId(entry) ?? `${runId}:session`;\n\treturn {\n\t\teventId: `${runId}:${id}`,\n\t\teventType: entryType(entry),\n\t\tschemaVersion: OBSERVABILITY_SCHEMA_VERSION,\n\t\trunId,\n\t\tsessionId: runId,\n\t\tparentEventId: entry.type === \"session\" ? undefined : entry.parentId ? `${runId}:${entry.parentId}` : undefined,\n\t\tsequence,\n\t\trecordedAt: entryTimestamp(entry),\n\t\tsource: { component: \"session-manager\" },\n\t\tpayload: entry,\n\t\tpayloadSha256: sha256(entry),\n\t};\n}\n\n/** Read a session JSONL file without loading more than the configured event count. */\nexport function readSessionEvents(filePath: string, maxEvents = 100_000): EventReadResult {\n\tif (!existsSync(filePath))\n\t\treturn {\n\t\t\tevents: [],\n\t\t\tissues: [{ line: 0, class: \"evidence_missing\", reasonCode: \"file_missing\", detail: filePath }],\n\t\t\tcomplete: false,\n\t\t};\n\tconst lines = readFileSync(filePath, \"utf8\").split(/\\n/);\n\tconst events: JensenEventEnvelope<FileEntry>[] = [];\n\tconst issues: EventReadIssue[] = [];\n\tlet header: SessionHeader | undefined;\n\tlet runId = basename(filePath, \".jsonl\");\n\tlet sawNonEmpty = false;\n\tconst eventIds = new Set<string>();\n\tfor (let index = 0; index < lines.length && events.length < maxEvents; index++) {\n\t\tconst text = lines[index]?.trim() ?? \"\";\n\t\tif (!text) continue;\n\t\tsawNonEmpty = true;\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = JSON.parse(text);\n\t\t} catch {\n\t\t\tconst tail = lines.slice(index + 1).every((line) => !line.trim());\n\t\t\tissues.push({\n\t\t\t\tline: index + 1,\n\t\t\t\tclass: tail ? \"recoverable_tail_corruption\" : \"manual_recovery_required\",\n\t\t\t\treasonCode: \"malformed_json\",\n\t\t\t\tdetail: tail ? \"truncated final record\" : \"malformed record in canonical history\",\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tif (isPersistedEnvelope(parsed)) {\n\t\t\tif (parsed.payloadSha256 !== sha256(parsed.payload))\n\t\t\t\tissues.push({\n\t\t\t\t\tline: index + 1,\n\t\t\t\t\tclass: \"integrity_failure\",\n\t\t\t\t\treasonCode: \"payload_hash_mismatch\",\n\t\t\t\t\tdetail: parsed.eventId,\n\t\t\t\t});\n\t\t\tif (eventIds.has(parsed.eventId))\n\t\t\t\tissues.push({\n\t\t\t\t\tline: index + 1,\n\t\t\t\t\tclass: \"integrity_failure\",\n\t\t\t\t\treasonCode: \"duplicate_event_id\",\n\t\t\t\t\tdetail: parsed.eventId,\n\t\t\t\t});\n\t\t\teventIds.add(parsed.eventId);\n\t\t\tevents.push(parsed);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!isFileEntry(parsed)) {\n\t\t\tissues.push({\n\t\t\t\tline: index + 1,\n\t\t\t\tclass: \"integrity_failure\",\n\t\t\t\treasonCode: \"invalid_entry\",\n\t\t\t\tdetail: \"record has no string type\",\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tif (parsed.type === \"session\") {\n\t\t\theader = parsed;\n\t\t\trunId = parsed.id;\n\t\t}\n\t\tconst envelope = envelopeForEntry(parsed, runId, events.length);\n\t\tconst persistedHash = parsed.type === \"session\" ? undefined : parsed.payloadSha256;\n\t\tif (typeof persistedHash === \"string\" && persistedHash !== sha256({ ...parsed, payloadSha256: undefined }))\n\t\t\tissues.push({\n\t\t\t\tline: index + 1,\n\t\t\t\tclass: \"integrity_failure\",\n\t\t\t\treasonCode: \"payload_hash_mismatch\",\n\t\t\t\tdetail: envelope.eventId,\n\t\t\t});\n\t\tif (eventIds.has(envelope.eventId))\n\t\t\tissues.push({\n\t\t\t\tline: index + 1,\n\t\t\t\tclass: \"integrity_failure\",\n\t\t\t\treasonCode: \"duplicate_event_id\",\n\t\t\t\tdetail: envelope.eventId,\n\t\t\t});\n\t\tif (envelope.payloadSha256 !== sha256(envelope.payload))\n\t\t\tissues.push({\n\t\t\t\tline: index + 1,\n\t\t\t\tclass: \"integrity_failure\",\n\t\t\t\treasonCode: \"payload_hash_mismatch\",\n\t\t\t\tdetail: envelope.eventId,\n\t\t\t});\n\t\teventIds.add(envelope.eventId);\n\t\tevents.push(envelope);\n\t}\n\tif (!sawNonEmpty) issues.push({ line: 0, class: \"evidence_missing\", reasonCode: \"empty_session\", detail: filePath });\n\tif (events.length >= maxEvents && lines.some((line) => line.trim()))\n\t\tissues.push({\n\t\t\tline: events.length,\n\t\t\tclass: \"manual_recovery_required\",\n\t\t\treasonCode: \"event_limit_reached\",\n\t\t\tdetail: `maximum ${maxEvents} events reached`,\n\t\t});\n\treturn { header, events, issues, complete: issues.every((issue) => issue.class === \"recoverable_tail_corruption\") };\n}\n\nexport function queryEvents(result: EventReadResult, query: EventQuery = {}): EventPage {\n\tconst size = Math.min(Math.max(query.pageSize ?? DEFAULT_PAGE_SIZE, 1), MAX_PAGE_SIZE);\n\tconst start = query.pageToken === undefined ? 0 : /^\\d+$/.test(query.pageToken) ? Number(query.pageToken) : 0;\n\tconst filtered = result.events.filter((event) => {\n\t\tif (query.eventType && event.eventType !== query.eventType) return false;\n\t\tif (query.component && event.source.component !== query.component) return false;\n\t\tif (query.from && event.recordedAt < query.from) return false;\n\t\tif (query.to && event.recordedAt > query.to) return false;\n\t\tif (query.failureOnly && !/(error|fail|blocked|abort|deny|corrupt)/i.test(canonical(event.payload))) return false;\n\t\treturn true;\n\t});\n\tconst page = filtered.slice(Number.isFinite(start) ? start : 0, (Number.isFinite(start) ? start : 0) + size);\n\tconst next = (Number.isFinite(start) ? start : 0) + page.length;\n\treturn { events: page, nextPageToken: next < filtered.length ? String(next) : undefined, issues: result.issues };\n}\n\nfunction messageOf(entry: FileEntry): AgentMessage | undefined {\n\treturn entry.type === \"message\" ? (entry as SessionMessageEntry).message : undefined;\n}\n\nfunction textOfMessage(message: AgentMessage): string {\n\tif (typeof (message as { content?: unknown }).content === \"string\") return (message as { content: string }).content;\n\tconst content = (message as { content?: unknown }).content;\n\tif (!Array.isArray(content)) return \"\";\n\treturn content\n\t\t.filter((block): block is { type: \"text\"; text: string } =>\n\t\t\tBoolean(\n\t\t\t\tblock &&\n\t\t\t\t\ttypeof block === \"object\" &&\n\t\t\t\t\t(block as { type?: unknown }).type === \"text\" &&\n\t\t\t\t\ttypeof (block as { text?: unknown }).text === \"string\",\n\t\t\t),\n\t\t)\n\t\t.map((block) => block.text)\n\t\t.join(\"\");\n}\n\nfunction isToolCall(message: AgentMessage): boolean {\n\tconst content = (message as { content?: unknown }).content;\n\treturn (\n\t\tArray.isArray(content) &&\n\t\tcontent.some((block) =>\n\t\t\tBoolean(block && typeof block === \"object\" && (block as { type?: unknown }).type === \"toolCall\"),\n\t\t)\n\t);\n}\n\nfunction toolNameFromMessage(message: AgentMessage): string | undefined {\n\tconst content = (message as { content?: unknown }).content;\n\tif (!Array.isArray(content)) return undefined;\n\tconst block = content.find((item) =>\n\t\tBoolean(item && typeof item === \"object\" && (item as { type?: unknown }).type === \"toolCall\"),\n\t) as { name?: unknown } | undefined;\n\treturn typeof block?.name === \"string\" ? block.name : undefined;\n}\n\nfunction evidenceIdsFrom(value: unknown, ids: Set<string>): void {\n\tif (!value || typeof value !== \"object\") return;\n\tif (Array.isArray(value)) {\n\t\tfor (const item of value) evidenceIdsFrom(item, ids);\n\t\treturn;\n\t}\n\tconst record = value as Record<string, unknown>;\n\tfor (const [key, child] of Object.entries(record)) {\n\t\tif (/(evidence|artifact)id/i.test(key) && typeof child === \"string\") ids.add(child);\n\t\telse evidenceIdsFrom(child, ids);\n\t}\n}\n\nexport function projectRun(result: EventReadResult): RunProjection {\n\tconst runId = result.header?.id ?? result.events[0]?.runId ?? \"unknown\";\n\tconst models = new Map<string, { provider: string; modelId: string }>();\n\tconst tools = new Set<string>();\n\tconst evidence = new Set<string>();\n\tlet messageCount = 0;\n\tlet toolCallCount = 0;\n\tlet toolResultCount = 0;\n\tlet mutations = 0;\n\tlet objective: string | undefined;\n\tfor (const event of result.events) {\n\t\tconst entry = event.payload;\n\t\tif (event.eventType === \"message\") {\n\t\t\tconst message = messageOf(entry);\n\t\t\tif (message) {\n\t\t\t\tmessageCount++;\n\t\t\t\tif (message.role === \"assistant\") {\n\t\t\t\t\tconst provider = (message as { provider?: unknown }).provider;\n\t\t\t\t\tconst modelId = (message as { model?: unknown }).model;\n\t\t\t\t\tif (typeof provider === \"string\" && typeof modelId === \"string\")\n\t\t\t\t\t\tmodels.set(`${provider}/${modelId}`, { provider, modelId });\n\t\t\t\t\tif (isToolCall(message)) {\n\t\t\t\t\t\ttoolCallCount++;\n\t\t\t\t\t\tconst name = toolNameFromMessage(message);\n\t\t\t\t\t\tif (name) tools.add(name);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (message.role === \"toolResult\") {\n\t\t\t\t\ttoolResultCount++;\n\t\t\t\t\tconst name = (message as { toolName?: unknown }).toolName;\n\t\t\t\t\tif (typeof name === \"string\") {\n\t\t\t\t\t\ttools.add(name);\n\t\t\t\t\t\tif (/write|edit|delete|bash|powershell|mutat/i.test(name)) mutations++;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (message.role === \"user\" && !objective) objective = \"[recorded user objective]\";\n\t\t\t}\n\t\t}\n\t\tif (/(mutation|transaction|checkpoint)/i.test(event.eventType)) mutations++;\n\t\tevidenceIdsFrom(entry, evidence);\n\t}\n\treturn {\n\t\trunId,\n\t\tsessionId: runId,\n\t\tcwd: result.header?.cwd ?? \"\",\n\t\tobjective,\n\t\tentryCount: result.events.length,\n\t\tmessageCount,\n\t\ttoolCallCount,\n\t\ttoolResultCount,\n\t\tmodels: [...models.values()].sort((a, b) =>\n\t\t\t`${a.provider}/${a.modelId}`.localeCompare(`${b.provider}/${b.modelId}`),\n\t\t),\n\t\ttoolNames: [...tools].sort(),\n\t\tmutations,\n\t\tevidenceIds: [...evidence].sort(),\n\t\twarnings: result.issues.map((issue) => `${issue.reasonCode}:${issue.detail}`),\n\t\tunknownEventTypes: result.events\n\t\t\t.filter(\n\t\t\t\t(event) =>\n\t\t\t\t\t![\n\t\t\t\t\t\t\"session\",\n\t\t\t\t\t\t\"message\",\n\t\t\t\t\t\t\"thinking_level_change\",\n\t\t\t\t\t\t\"model_change\",\n\t\t\t\t\t\t\"compaction\",\n\t\t\t\t\t\t\"branch_summary\",\n\t\t\t\t\t\t\"custom\",\n\t\t\t\t\t\t\"custom_message\",\n\t\t\t\t\t\t\"label\",\n\t\t\t\t\t\t\"session_info\",\n\t\t\t\t\t].includes(event.eventType),\n\t\t\t)\n\t\t\t.map((event) => event.eventType)\n\t\t\t.filter((type, index, values) => values.indexOf(type) === index)\n\t\t\t.sort(),\n\t\tfirstRecordedAt: result.events[0]?.recordedAt,\n\t\tlastRecordedAt: result.events.at(-1)?.recordedAt,\n\t};\n}\n\nexport function renderReplay(result: EventReadResult): RenderReplay {\n\tconst lines: string[] = [];\n\tconst missingArtifacts: string[] = [];\n\tfor (const event of result.events) {\n\t\tconst entry = event.payload;\n\t\tif (entry.type === \"message\") {\n\t\t\tconst message = messageOf(entry);\n\t\t\tif (!message) continue;\n\t\t\tconst text = textOfMessage(message);\n\t\t\tif (message.role === \"user\") lines.push(`user: ${text}`);\n\t\t\telse if (message.role === \"assistant\") lines.push(`assistant: ${text}`);\n\t\t\telse if (message.role === \"toolResult\")\n\t\t\t\tlines.push(`tool ${message.toolName}: ${text || (message.isError ? \"[error]\" : \"[recorded result]\")}`);\n\t\t}\n\t\tif (entry.type === \"custom\" || entry.type === \"custom_message\") {\n\t\t\tconst record = entry as unknown as Record<string, unknown>;\n\t\t\tif (record.data && typeof record.data === \"object\" && \"artifactPath\" in (record.data as object)) {\n\t\t\t\tconst artifactPath = (record.data as { artifactPath?: unknown }).artifactPath;\n\t\t\t\tif (typeof artifactPath === \"string\" && !existsSync(artifactPath)) missingArtifacts.push(artifactPath);\n\t\t\t}\n\t\t}\n\t}\n\treturn {\n\t\tlines,\n\t\teventCount: result.events.length,\n\t\tmissingArtifacts: [...new Set(missingArtifacts)].sort(),\n\t\texternalEffects: { modelCalls: 0, toolCalls: 0, networkCalls: 0, mutations: 0 },\n\t};\n}\n\nexport function projectionReplay(result: EventReadResult, snapshot?: RunProjection): ProjectionReplay {\n\tconst projection = projectRun(result);\n\treturn {\n\t\tprojection,\n\t\tissues: result.issues,\n\t\tsnapshotMatches: snapshot ? canonical(snapshot) === canonical(projection) : undefined,\n\t};\n}\n\nfunction comparableValue(value: unknown): unknown {\n\tif (typeof value === \"string\") return value.replace(/[0-9a-f]{8,}/gi, \"<id>\");\n\tif (Array.isArray(value)) return value.map(comparableValue);\n\tif (value && typeof value === \"object\") {\n\t\tconst out: Record<string, unknown> = {};\n\t\tfor (const key of Object.keys(value as object).sort()) {\n\t\t\tif (!/(timestamp|recordedAt|eventId|sessionId|runId)/i.test(key))\n\t\t\t\tout[key] = comparableValue((value as Record<string, unknown>)[key]);\n\t\t}\n\t\treturn out;\n\t}\n\treturn value;\n}\n\nexport function diffRuns(left: RunProjection, right: RunProjection): RunDiff {\n\tconst fields: RunDiffField[] = [];\n\tconst values: Array<[string, unknown, unknown]> = [\n\t\t[\"objective\", left.objective, right.objective],\n\t\t[\"cwd\", left.cwd, right.cwd],\n\t\t[\"entryCount\", left.entryCount, right.entryCount],\n\t\t[\"messageCount\", left.messageCount, right.messageCount],\n\t\t[\"toolCallCount\", left.toolCallCount, right.toolCallCount],\n\t\t[\"toolResultCount\", left.toolResultCount, right.toolResultCount],\n\t\t[\"models\", left.models, right.models],\n\t\t[\"toolNames\", left.toolNames, right.toolNames],\n\t\t[\"mutations\", left.mutations, right.mutations],\n\t\t[\"evidenceIds\", left.evidenceIds, right.evidenceIds],\n\t];\n\tfor (const [path, before, after] of values) {\n\t\tconst a = comparableValue(before);\n\t\tconst b = comparableValue(after);\n\t\tlet category: RunDiffField[\"category\"] = \"changed\";\n\t\tif (canonical(a) === canonical(b)) category = \"unchanged\";\n\t\telse if (typeof before === \"number\" && typeof after === \"number\")\n\t\t\tcategory = after < before ? \"improved\" : \"regressed\";\n\t\telse if (before === undefined || after === undefined) category = \"incomparable\";\n\t\tfields.push({ path, category, before: a, after: b });\n\t}\n\treturn {\n\t\tleftRunId: left.runId,\n\t\trightRunId: right.runId,\n\t\tcomparable: Boolean(left.objective && right.objective ? true : left.cwd === right.cwd),\n\t\tfields,\n\t};\n}\n\nexport interface EvidenceRecord {\n\tevidenceId: string;\n\ttype: string;\n\torigin: string;\n\tcreatedAt: string;\n\tcontentSha256?: string;\n\tlocator?: string;\n\tclaimIds: string[];\n\tredacted: boolean;\n\tintegrity: \"verified\" | \"unverified\" | \"missing\";\n}\n\nexport function listEvidence(result: EventReadResult): EvidenceRecord[] {\n\tconst records: EvidenceRecord[] = [];\n\tfor (const event of result.events) {\n\t\tconst payload = event.payload as unknown as Record<string, unknown>;\n\t\tconst candidates = [payload, payload.data].filter((candidate): candidate is Record<string, unknown> =>\n\t\t\tBoolean(candidate && typeof candidate === \"object\"),\n\t\t);\n\t\tfor (const candidate of candidates) {\n\t\t\tconst id = candidate.evidenceId ?? candidate.artifactId;\n\t\t\tif (typeof id !== \"string\") continue;\n\t\t\tconst content = candidate.content ?? candidate.text;\n\t\t\trecords.push({\n\t\t\t\tevidenceId: id,\n\t\t\t\ttype: typeof candidate.type === \"string\" ? candidate.type : event.eventType,\n\t\t\t\torigin: event.source.component,\n\t\t\t\tcreatedAt: event.recordedAt,\n\t\t\t\tcontentSha256:\n\t\t\t\t\ttypeof candidate.contentSha256 === \"string\"\n\t\t\t\t\t\t? candidate.contentSha256\n\t\t\t\t\t\t: typeof content === \"string\"\n\t\t\t\t\t\t\t? sha256(content)\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\tlocator:\n\t\t\t\t\ttypeof candidate.locator === \"string\"\n\t\t\t\t\t\t? candidate.locator\n\t\t\t\t\t\t: typeof candidate.path === \"string\"\n\t\t\t\t\t\t\t? candidate.path\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\tclaimIds: Array.isArray(candidate.claimIds)\n\t\t\t\t\t? candidate.claimIds.filter((claim): claim is string => typeof claim === \"string\").sort()\n\t\t\t\t\t: [],\n\t\t\t\tredacted: candidate.redacted === true,\n\t\t\t\tintegrity: typeof content === \"string\" ? \"verified\" : \"unverified\",\n\t\t\t});\n\t\t}\n\t}\n\tconst byId = new Map<string, EvidenceRecord>();\n\tfor (const record of records) byId.set(record.evidenceId, record);\n\treturn [...byId.values()].sort((a, b) => a.evidenceId.localeCompare(b.evidenceId));\n}\n\nexport interface DiagnosticCheckResult {\n\tcheckId: string;\n\tcomponent: string;\n\tstatus: \"pass\" | \"warn\" | \"fail\" | \"unavailable\" | \"skipped\";\n\treasonCode: string;\n\tsummary: string;\n\tevidenceIds?: string[];\n\tremediation?: { safeAutomatic?: boolean; command?: string; description: string };\n}\n\nexport interface DiagnosticReport {\n\tchecks: DiagnosticCheckResult[];\n\tgeneratedAt: string;\n\treadOnly: true;\n\texitCode: 0 | 1 | 2 | 3;\n}\n\nexport function collectDiagnostics(\n\toptions: { cwd?: string; sessionFile?: string; checkMcp?: boolean } = {},\n): DiagnosticReport {\n\tconst checks: DiagnosticCheckResult[] = [];\n\tconst cwd = options.cwd ?? process.cwd();\n\tchecks.push({\n\t\tcheckId: \"runtime.node\",\n\t\tcomponent: \"runtime\",\n\t\tstatus: process.versions.node ? \"pass\" : \"fail\",\n\t\treasonCode: \"node_detected\",\n\t\tsummary: `Node.js ${process.versions.node}`,\n\t});\n\tchecks.push({\n\t\tcheckId: \"workspace.cwd\",\n\t\tcomponent: \"workspace\",\n\t\tstatus: cwd ? \"pass\" : \"fail\",\n\t\treasonCode: cwd ? \"cwd_detected\" : \"cwd_missing\",\n\t\tsummary: cwd ? \"Workspace path is available\" : \"Workspace path is unavailable\",\n\t});\n\tif (options.sessionFile) {\n\t\tconst events = readSessionEvents(resolve(options.sessionFile));\n\t\tchecks.push({\n\t\t\tcheckId: \"events.integrity\",\n\t\t\tcomponent: \"events\",\n\t\t\tstatus: events.issues.some(\n\t\t\t\t(issue) => issue.class === \"manual_recovery_required\" || issue.class === \"integrity_failure\",\n\t\t\t)\n\t\t\t\t? \"fail\"\n\t\t\t\t: events.issues.length\n\t\t\t\t\t? \"warn\"\n\t\t\t\t\t: \"pass\",\n\t\t\treasonCode: events.issues[0]?.reasonCode ?? \"event_store_readable\",\n\t\t\tsummary: events.issues.length\n\t\t\t\t? `${events.issues.length} event-store issue(s) surfaced`\n\t\t\t\t: `${events.events.length} events readable`,\n\t\t});\n\t\tchecks.push({\n\t\t\tcheckId: \"evidence.integrity\",\n\t\t\tcomponent: \"evidence\",\n\t\t\tstatus: \"pass\",\n\t\t\treasonCode: \"evidence_projection_available\",\n\t\t\tsummary: `${listEvidence(events).length} addressable evidence record(s)`,\n\t\t});\n\t} else\n\t\tchecks.push({\n\t\t\tcheckId: \"events.integrity\",\n\t\t\tcomponent: \"events\",\n\t\t\tstatus: \"unavailable\",\n\t\t\treasonCode: \"session_not_selected\",\n\t\t\tsummary: \"No session selected; event checks skipped\",\n\t\t});\n\tif (options.checkMcp)\n\t\tchecks.push({\n\t\t\tcheckId: \"mcp.configuration\",\n\t\t\tcomponent: \"mcp\",\n\t\t\tstatus: \"skipped\",\n\t\t\treasonCode: \"live_probe_not_enabled\",\n\t\t\tsummary: \"MCP live probes are opt-in and not run by default\",\n\t\t});\n\t// Workspace intelligence index/embedding/retrieval checks (1.7.0, read-only).\n\tchecks.push(...workspaceDoctorChecks(cwd));\n\tconst hasFail = checks.some((check) => check.status === \"fail\");\n\tconst hasWarn = checks.some((check) => check.status === \"warn\" || check.status === \"unavailable\");\n\treturn { checks, generatedAt: new Date().toISOString(), readOnly: true, exitCode: hasFail ? 2 : hasWarn ? 1 : 0 };\n}\n\nconst SECRET_KEY = /(api[-_]?key|token|secret|password|authorization|cookie|private[-_]?key)/i;\nconst SECRET_VALUE =\n\t/(?:bearer\\s+|sk-[A-Za-z0-9_-]{8,}|gh[pousr]_[A-Za-z0-9_]{8,}|-----BEGIN [A-Z ]+-----|eyJ[A-Za-z0-9_-]{8,}\\.)/i;\nexport function sanitize(value: unknown): unknown {\n\tif (typeof value === \"string\") return SECRET_VALUE.test(value) ? \"[REDACTED]\" : value;\n\tif (Array.isArray(value)) return value.map(sanitize);\n\tif (!value || typeof value !== \"object\") return value;\n\tconst out: Record<string, unknown> = {};\n\tfor (const [key, child] of Object.entries(value as Record<string, unknown>))\n\t\tout[key] = SECRET_KEY.test(key) ? \"[REDACTED]\" : sanitize(child);\n\treturn out;\n}\n\nexport interface SupportBundleManifest {\n\tbundleVersion: 1;\n\tcreatedAt: string;\n\tfiles: Array<{ path: string; bytes: number; sha256: string }>;\n\tredactions: string[];\n\tmaxBytes: number;\n}\n\nexport function supportBundlePreview(\n\treport: DiagnosticReport,\n\tprojection?: RunProjection,\n): { manifest: SupportBundleManifest; files: Record<string, string> } {\n\tconst files: Record<string, string> = {\n\t\t\"doctor.json\": JSON.stringify(sanitize(report), null, 2),\n\t\t\"projection.json\": JSON.stringify(sanitize(projection ?? null), null, 2),\n\t};\n\tconst manifest: SupportBundleManifest = {\n\t\tbundleVersion: 1,\n\t\tcreatedAt: new Date().toISOString(),\n\t\tfiles: Object.entries(files).map(([path, content]) => ({\n\t\t\tpath,\n\t\t\tbytes: Buffer.byteLength(content),\n\t\t\tsha256: sha256(content),\n\t\t})),\n\t\tredactions: [\"credentials\", \"authorization headers\", \"full environment\", \"raw prompt history\"],\n\t\tmaxBytes: 5 * 1024 * 1024,\n\t};\n\treturn { manifest, files };\n}\n\nexport function createSupportBundle(\n\tdestination: string,\n\treport: DiagnosticReport,\n\tprojection?: RunProjection,\n): SupportBundleManifest {\n\tconst preview = supportBundlePreview(report, projection);\n\tconst payload = `${JSON.stringify(preview.manifest, null, 2)}\\n${Object.entries(preview.files)\n\t\t.map(([name, content]) => `\\n--- ${name} ---\\n${content}`)\n\t\t.join(\"\\n\")}\\n`;\n\tif (Buffer.byteLength(payload) > preview.manifest.maxBytes) throw new Error(\"support bundle exceeds maximum size\");\n\twriteFileSync(destination, payload, { encoding: \"utf8\", flag: \"wx\" });\n\treturn preview.manifest;\n}\n\nexport function sessionFileFromId(id: string, sessionDir?: string): string | undefined {\n\tif (!sessionDir) return undefined;\n\tconst path = resolve(sessionDir, id.endsWith(\".jsonl\") ? id : `${id}.jsonl`);\n\treturn existsSync(path) && statSync(path).isFile() ? path : undefined;\n}\n\nexport function loadProjectionFromSession(filePath: string): { events: EventReadResult; projection: RunProjection } {\n\tconst events = readSessionEvents(filePath);\n\treturn { events, projection: projectRun(events) };\n}\n\nexport type { FileEntry, SessionEntry, SessionHeader };\n"]}