Files
pikasTech-HWLAB/internal/cloud/workbench-session-detail-response.ts

121 lines
5.2 KiB
TypeScript

/*
* 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<string, unknown>;
session: Record<string, unknown> | null | undefined;
sessionId: string | null | undefined;
includeMessages: boolean;
messages: unknown[];
}): Record<string, unknown> {
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<string, unknown> | 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<string, unknown> | 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<string, unknown> {
const record = objectRecord(value);
if (!record) return {};
const out: Record<string, unknown> = {};
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<string, unknown> | null | undefined): Record<string, unknown> | null {
return objectRecord(session?.sessionJson);
}
function objectRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : 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";
}