/* * SPEC: pikasTech/HWLAB#2359 Workbench session detail metadata-only contract. * Responsibility: Build Workbench session detail payloads and parse detail payload mode flags. */ export function includeMessagesForSessionDetail(params: URLSearchParams): boolean { if (params.get("includeMessages") !== null) return truthyQueryFlag(params.get("includeMessages")); if (params.get("messages") !== null) return truthyQueryFlag(params.get("messages")); return false; } export function buildWorkbenchSessionDetail(input: { summary: Record; session: Record | null | undefined; sessionId: string | null | undefined; includeMessages: boolean; messages: unknown[]; }): Record { const detail = { ...input.summary, metadata: { startedAt: input.session?.startedAt ?? input.session?.createdAt ?? null, endedAt: input.session?.endedAt ?? null, ownerUserId: input.session?.ownerUserId ?? null, launchContext: compactLaunchContext(sessionJson(input.session)?.launchContext) }, messagePageUrl: input.sessionId ? `/v1/workbench/sessions/${encodeURIComponent(input.sessionId)}/messages` : null, valuesRedacted: true, secretMaterialStored: false }; return input.includeMessages ? { ...detail, messages: input.messages } : detail; } export function compactLaunchContext(value: unknown): Record | null { const record = objectRecord(value); if (!record) return null; const projectId = textValue(record.projectId) || null; const taskRef = textValue(record.taskRef) || null; if (!projectId && !taskRef) return null; const execution = compactLaunchExecutionContext(record.executionContext ?? record.execution ?? record); return { source: textValue(record.source) || "project-management", projectId, taskRef, sourceId: textValue(record.sourceId) || null, sourceKind: textValue(record.sourceKind) || execution?.sourceKind || null, fileRef: textValue(record.fileRef) || null, relativePath: textValue(record.relativePath) || execution?.relativePath || null, taskId: textValue(record.taskId) || null, title: textValue(record.title) || null, status: textValue(record.status) || null, hwpodId: (execution?.hwpodId ?? textValue(record.hwpodId)) || null, nodeId: (execution?.nodeId ?? textValue(record.nodeId)) || null, mdtodoRootRef: (execution?.mdtodoRootRef ?? textValue(record.mdtodoRootRef)) || null, workspaceRootHash: (execution?.workspaceRootHash ?? textValue(record.workspaceRootHash)) || null, workspaceRootLabel: (execution?.workspaceRootLabel ?? textValue(record.workspaceRootLabel)) || null, hwpodWorkspaceArgs: (execution?.hwpodWorkspaceArgs ?? textValue(record.hwpodWorkspaceArgs)) || null, contextFingerprint: (execution?.contextFingerprint ?? textValue(record.contextFingerprint)) || null, capabilities: compactPlainObject(record.capabilities), executionContext: execution, route: textValue(record.route) || "/projects/mdtodo", valuesRedacted: true }; } function compactLaunchExecutionContext(value: unknown): Record | null { const record = objectRecord(value); if (!record) return null; const sourceId = textValue(record.sourceId) || null; const taskRef = textValue(record.taskRef) || null; const hwpodId = textValue(record.hwpodId) || null; if (!sourceId && !taskRef && !hwpodId) return null; return { sourceId, sourceKind: textValue(record.sourceKind) || null, projectId: textValue(record.projectId) || null, taskRef, fileRef: textValue(record.fileRef) || null, relativePath: textValue(record.relativePath) || null, taskId: textValue(record.taskId) || null, hwpodId, nodeId: textValue(record.nodeId) || null, mdtodoRootRef: textValue(record.mdtodoRootRef) || null, workspaceRootRef: textValue(record.workspaceRootRef) || null, workspaceRootHash: textValue(record.workspaceRootHash) || null, workspaceRootLabel: textValue(record.workspaceRootLabel) || null, hwpodWorkspaceArgs: textValue(record.hwpodWorkspaceArgs) || null, contextFingerprint: textValue(record.contextFingerprint) || null, capabilities: compactPlainObject(record.capabilities), valuesRedacted: true }; } function compactPlainObject(value: unknown): Record { const record = objectRecord(value); if (!record) return {}; const out: Record = {}; for (const [key, item] of Object.entries(record)) { if (!/^[A-Za-z0-9_.:-]{1,80}$/u.test(key)) continue; if (["string", "number", "boolean"].includes(typeof item) || item === null) out[key] = item; } return out; } function sessionJson(session: Record | null | undefined): Record | null { return objectRecord(session?.sessionJson); } function objectRecord(value: unknown): Record | null { return value && typeof value === "object" && !Array.isArray(value) ? value as Record : null; } function textValue(value: unknown): string { return typeof value === "string" && value.trim() ? value.trim() : ""; } function truthyQueryFlag(value: string | null): boolean { const normalized = textValue(value).toLowerCase(); return normalized === "1" || normalized === "true" || normalized === "yes"; }