// Responsibility: Typed YAML profile parsing for repo-owned Cloud Console browser closeout. export interface HwlabRuntimeWebProbeConsoleViewportSpec { readonly id: string; readonly width: number; readonly height: number; } export interface HwlabRuntimeWebProbeConsoleRouteSpec { readonly id: string; readonly path: string; readonly readySelector: string; readonly screenshot: boolean; } export interface HwlabRuntimeWebProbeConsoleProjectsWorkflowSpec { readonly routeId: string; readonly listButtonName: string; readonly viewQueryKey: string; } export interface HwlabRuntimeWebProbeConsoleHwpodWorkflowSpec { readonly devicesRouteId: string; readonly nodesRouteId: string; readonly onboardingRouteId: string; readonly listButtonName: string; readonly cardButtonName: string; readonly devicesListSelector: string; readonly devicesCardSelector: string; readonly nodesListSelector: string; readonly pcbTraceSelector: string; readonly detailButtonName: string; readonly detailPathPrefix: string; readonly onboardingCommandSelector: string; readonly requiredCommandPrefixes: readonly string[]; readonly downloadSelector: string; readonly forbiddenDownloadSubstrings: readonly string[]; readonly forbiddenInternalSelector: string; } export interface HwlabRuntimeWebProbeConsoleMdtodoWorkflowSpec { readonly routeId: string; readonly taskSelector: string; readonly taskDeepLinkSegment: string; readonly taskDetailSelector: string; readonly reportLinkSelector: string; readonly reportPreviewSelector: string; readonly fullscreenButtonSelector: string; readonly fullscreenDialogSelector: string; } export interface HwlabRuntimeWebProbeConsoleTasktreeWorkflowSpec { readonly routeId: string; readonly taskSelector: string; readonly barSelector: string; readonly milestoneSelector: string; readonly ganttSelector: string; readonly scrollSelector: string; readonly labelPaneSelector: string; readonly scaleSelector: string; readonly tickSelector: string; readonly zoomInButtonName: string; readonly secondsScaleLabel: string; readonly zoomScreenshotName: string; readonly dialogSelector: string; readonly reportSelector: string; readonly detailScreenshotName: string; } export interface HwlabRuntimeWebProbeConsoleAgentViewSpec { readonly buttonName: string; readonly queryValue: string; readonly screenshotName: string; } export interface HwlabRuntimeWebProbeConsoleAgentSelectionSpec { readonly selector: string; readonly inspectorSelector: string; readonly screenshotName: string; } export interface HwlabRuntimeWebProbeConsoleAgentObserverWorkflowSpec { readonly routeId: string; readonly eventSourcePath: string; readonly phaseSelector: string; readonly blockedPhases: readonly string[]; readonly streamSettleMs: number; readonly views: readonly HwlabRuntimeWebProbeConsoleAgentViewSpec[]; readonly selection?: HwlabRuntimeWebProbeConsoleAgentSelectionSpec; } export interface HwlabRuntimeWebProbeConsoleVerificationProfileSpec { readonly navigationTimeoutMs: number; readonly settleMs: number; readonly commandTimeoutSeconds: number; readonly minTextLength: number; readonly maxHorizontalOverflowPx: number; readonly outputLimits: { readonly failures: number; readonly consoleErrors: number; readonly responseErrors: number; readonly screenshots: number; readonly textPreviewChars: number; }; readonly viewports: readonly HwlabRuntimeWebProbeConsoleViewportSpec[]; readonly routes: readonly HwlabRuntimeWebProbeConsoleRouteSpec[]; readonly workflows: { readonly projects?: HwlabRuntimeWebProbeConsoleProjectsWorkflowSpec; readonly hwpod?: HwlabRuntimeWebProbeConsoleHwpodWorkflowSpec; readonly mdtodo?: HwlabRuntimeWebProbeConsoleMdtodoWorkflowSpec; readonly tasktree?: HwlabRuntimeWebProbeConsoleTasktreeWorkflowSpec; readonly agentObserver?: HwlabRuntimeWebProbeConsoleAgentObserverWorkflowSpec; }; } export function parseWebProbeConsoleVerificationProfiles( value: unknown, path: string, ): Readonly> { const raw = record(value, path); const entries = Object.entries(raw); if (entries.length < 1 || entries.length > 20) throw new Error(`${path} must contain 1-20 profiles`); return Object.fromEntries(entries.map(([name, profile]) => { simpleId(name, `${path}.${name}`); return [name, consoleProfile(profile, `${path}.${name}`)]; })); } function consoleProfile(value: unknown, path: string): HwlabRuntimeWebProbeConsoleVerificationProfileSpec { const raw = record(value, path); onlyKeys(raw, [ "navigationTimeoutMs", "settleMs", "commandTimeoutSeconds", "minTextLength", "maxHorizontalOverflowPx", "outputLimits", "viewports", "routes", "workflows", ], path); const viewports = array(raw.viewports, `${path}.viewports`, 1, 8).map((item, index) => viewport(item, `${path}.viewports[${index}]`)); const routes = array(raw.routes, `${path}.routes`, 1, 100).map((item, index) => route(item, `${path}.routes[${index}]`)); unique(viewports.map((item) => item.id), `${path}.viewports[].id`); unique(routes.map((item) => item.id), `${path}.routes[].id`); unique(routes.map((item) => item.path), `${path}.routes[].path`); const routeIds = new Set(routes.map((item) => item.id)); const workflows = workflowConfig(raw.workflows, `${path}.workflows`, routeIds); const output = record(raw.outputLimits, `${path}.outputLimits`); onlyKeys(output, ["failures", "consoleErrors", "responseErrors", "screenshots", "textPreviewChars"], `${path}.outputLimits`); const screenshotBudget = integer(output.screenshots, `${path}.outputLimits.screenshots`, 1, 1000); const matrixScreenshots = routes.filter((item) => item.screenshot).length * viewports.length; const workflowScreenshots = (workflows.agentObserver?.views.length ?? 0) + (workflows.agentObserver?.selection === undefined ? 0 : 1) + (workflows.tasktree === undefined ? 0 : 1); if (matrixScreenshots + workflowScreenshots > screenshotBudget) { throw new Error(`${path}.outputLimits.screenshots=${screenshotBudget} is below the declared matrix/workflow screenshot count ${matrixScreenshots + workflowScreenshots}`); } return { navigationTimeoutMs: integer(raw.navigationTimeoutMs, `${path}.navigationTimeoutMs`, 1000, 120_000), settleMs: integer(raw.settleMs, `${path}.settleMs`, 0, 30_000), commandTimeoutSeconds: integer(raw.commandTimeoutSeconds, `${path}.commandTimeoutSeconds`, 60, 3600), minTextLength: integer(raw.minTextLength, `${path}.minTextLength`, 1, 100_000), maxHorizontalOverflowPx: integer(raw.maxHorizontalOverflowPx, `${path}.maxHorizontalOverflowPx`, 0, 1000), outputLimits: { failures: integer(output.failures, `${path}.outputLimits.failures`, 1, 1000), consoleErrors: integer(output.consoleErrors, `${path}.outputLimits.consoleErrors`, 1, 1000), responseErrors: integer(output.responseErrors, `${path}.outputLimits.responseErrors`, 1, 1000), screenshots: screenshotBudget, textPreviewChars: integer(output.textPreviewChars, `${path}.outputLimits.textPreviewChars`, 20, 2000), }, viewports, routes, workflows, }; } function viewport(value: unknown, path: string): HwlabRuntimeWebProbeConsoleViewportSpec { const raw = record(value, path); onlyKeys(raw, ["id", "width", "height"], path); return { id: simpleId(text(raw.id, `${path}.id`), `${path}.id`), width: integer(raw.width, `${path}.width`, 240, 7680), height: integer(raw.height, `${path}.height`, 240, 4320), }; } function route(value: unknown, path: string): HwlabRuntimeWebProbeConsoleRouteSpec { const raw = record(value, path); onlyKeys(raw, ["id", "path", "readySelector", "screenshot"], path); return { id: simpleId(text(raw.id, `${path}.id`), `${path}.id`), path: absolutePath(text(raw.path, `${path}.path`), `${path}.path`), readySelector: boundedText(raw.readySelector, `${path}.readySelector`, 500), screenshot: boolean(raw.screenshot, `${path}.screenshot`), }; } function workflowConfig( value: unknown, path: string, routeIds: ReadonlySet, ): HwlabRuntimeWebProbeConsoleVerificationProfileSpec["workflows"] { const raw = record(value, path); onlyKeys(raw, ["projects", "hwpod", "mdtodo", "tasktree", "agentObserver"], path); return { ...(raw.projects === undefined ? {} : { projects: projectsWorkflow(raw.projects, `${path}.projects`, routeIds) }), ...(raw.hwpod === undefined ? {} : { hwpod: hwpodWorkflow(raw.hwpod, `${path}.hwpod`, routeIds) }), ...(raw.mdtodo === undefined ? {} : { mdtodo: mdtodoWorkflow(raw.mdtodo, `${path}.mdtodo`, routeIds) }), ...(raw.tasktree === undefined ? {} : { tasktree: tasktreeWorkflow(raw.tasktree, `${path}.tasktree`, routeIds) }), ...(raw.agentObserver === undefined ? {} : { agentObserver: agentObserverWorkflow(raw.agentObserver, `${path}.agentObserver`, routeIds) }), }; } function tasktreeWorkflow(value: unknown, path: string, routeIds: ReadonlySet): HwlabRuntimeWebProbeConsoleTasktreeWorkflowSpec { const raw = record(value, path); onlyKeys(raw, [ "routeId", "taskSelector", "barSelector", "milestoneSelector", "ganttSelector", "scrollSelector", "labelPaneSelector", "scaleSelector", "tickSelector", "zoomInButtonName", "secondsScaleLabel", "zoomScreenshotName", "dialogSelector", "reportSelector", "detailScreenshotName", ], path); const detailScreenshotName = boundedText(raw.detailScreenshotName, `${path}.detailScreenshotName`, 120); if (!/^[A-Za-z0-9._-]+\.png$/u.test(detailScreenshotName)) throw new Error(`${path}.detailScreenshotName must be a safe PNG filename`); const zoomScreenshotName = boundedText(raw.zoomScreenshotName, `${path}.zoomScreenshotName`, 120); if (!/^[A-Za-z0-9._-]+\.png$/u.test(zoomScreenshotName)) throw new Error(`${path}.zoomScreenshotName must be a safe PNG filename`); return { routeId: routeReference(raw.routeId, `${path}.routeId`, routeIds), taskSelector: boundedText(raw.taskSelector, `${path}.taskSelector`, 500), barSelector: boundedText(raw.barSelector, `${path}.barSelector`, 500), milestoneSelector: boundedText(raw.milestoneSelector, `${path}.milestoneSelector`, 500), ganttSelector: boundedText(raw.ganttSelector, `${path}.ganttSelector`, 500), scrollSelector: boundedText(raw.scrollSelector, `${path}.scrollSelector`, 500), labelPaneSelector: boundedText(raw.labelPaneSelector, `${path}.labelPaneSelector`, 500), scaleSelector: boundedText(raw.scaleSelector, `${path}.scaleSelector`, 500), tickSelector: boundedText(raw.tickSelector, `${path}.tickSelector`, 500), zoomInButtonName: boundedText(raw.zoomInButtonName, `${path}.zoomInButtonName`, 120), secondsScaleLabel: boundedText(raw.secondsScaleLabel, `${path}.secondsScaleLabel`, 40), zoomScreenshotName, dialogSelector: boundedText(raw.dialogSelector, `${path}.dialogSelector`, 500), reportSelector: boundedText(raw.reportSelector, `${path}.reportSelector`, 500), detailScreenshotName, }; } function projectsWorkflow(value: unknown, path: string, routeIds: ReadonlySet): HwlabRuntimeWebProbeConsoleProjectsWorkflowSpec { const raw = record(value, path); onlyKeys(raw, ["routeId", "listButtonName", "viewQueryKey"], path); return { routeId: routeReference(raw.routeId, `${path}.routeId`, routeIds), listButtonName: boundedText(raw.listButtonName, `${path}.listButtonName`, 100), viewQueryKey: simpleId(text(raw.viewQueryKey, `${path}.viewQueryKey`), `${path}.viewQueryKey`), }; } function hwpodWorkflow(value: unknown, path: string, routeIds: ReadonlySet): HwlabRuntimeWebProbeConsoleHwpodWorkflowSpec { const raw = record(value, path); onlyKeys(raw, [ "devicesRouteId", "nodesRouteId", "onboardingRouteId", "listButtonName", "cardButtonName", "devicesListSelector", "devicesCardSelector", "nodesListSelector", "pcbTraceSelector", "detailButtonName", "detailPathPrefix", "onboardingCommandSelector", "requiredCommandPrefixes", "downloadSelector", "forbiddenDownloadSubstrings", "forbiddenInternalSelector", ], path); return { devicesRouteId: routeReference(raw.devicesRouteId, `${path}.devicesRouteId`, routeIds), nodesRouteId: routeReference(raw.nodesRouteId, `${path}.nodesRouteId`, routeIds), onboardingRouteId: routeReference(raw.onboardingRouteId, `${path}.onboardingRouteId`, routeIds), listButtonName: boundedText(raw.listButtonName, `${path}.listButtonName`, 100), cardButtonName: boundedText(raw.cardButtonName, `${path}.cardButtonName`, 100), devicesListSelector: boundedText(raw.devicesListSelector, `${path}.devicesListSelector`, 500), devicesCardSelector: boundedText(raw.devicesCardSelector, `${path}.devicesCardSelector`, 500), nodesListSelector: boundedText(raw.nodesListSelector, `${path}.nodesListSelector`, 500), pcbTraceSelector: boundedText(raw.pcbTraceSelector, `${path}.pcbTraceSelector`, 500), detailButtonName: boundedText(raw.detailButtonName, `${path}.detailButtonName`, 100), detailPathPrefix: absolutePath(text(raw.detailPathPrefix, `${path}.detailPathPrefix`), `${path}.detailPathPrefix`), onboardingCommandSelector: boundedText(raw.onboardingCommandSelector, `${path}.onboardingCommandSelector`, 500), requiredCommandPrefixes: textArray(raw.requiredCommandPrefixes, `${path}.requiredCommandPrefixes`, 1, 20, 300), downloadSelector: boundedText(raw.downloadSelector, `${path}.downloadSelector`, 500), forbiddenDownloadSubstrings: textArray(raw.forbiddenDownloadSubstrings, `${path}.forbiddenDownloadSubstrings`, 1, 20, 300), forbiddenInternalSelector: boundedText(raw.forbiddenInternalSelector, `${path}.forbiddenInternalSelector`, 500), }; } function mdtodoWorkflow(value: unknown, path: string, routeIds: ReadonlySet): HwlabRuntimeWebProbeConsoleMdtodoWorkflowSpec { const raw = record(value, path); onlyKeys(raw, [ "routeId", "taskSelector", "taskDeepLinkSegment", "taskDetailSelector", "reportLinkSelector", "reportPreviewSelector", "fullscreenButtonSelector", "fullscreenDialogSelector", ], path); const segment = boundedText(raw.taskDeepLinkSegment, `${path}.taskDeepLinkSegment`, 100); if (!segment.startsWith("/")) throw new Error(`${path}.taskDeepLinkSegment must start with /`); return { routeId: routeReference(raw.routeId, `${path}.routeId`, routeIds), taskSelector: boundedText(raw.taskSelector, `${path}.taskSelector`, 500), taskDeepLinkSegment: segment, taskDetailSelector: boundedText(raw.taskDetailSelector, `${path}.taskDetailSelector`, 500), reportLinkSelector: boundedText(raw.reportLinkSelector, `${path}.reportLinkSelector`, 500), reportPreviewSelector: boundedText(raw.reportPreviewSelector, `${path}.reportPreviewSelector`, 500), fullscreenButtonSelector: boundedText(raw.fullscreenButtonSelector, `${path}.fullscreenButtonSelector`, 500), fullscreenDialogSelector: boundedText(raw.fullscreenDialogSelector, `${path}.fullscreenDialogSelector`, 500), }; } function agentObserverWorkflow(value: unknown, path: string, routeIds: ReadonlySet): HwlabRuntimeWebProbeConsoleAgentObserverWorkflowSpec { const raw = record(value, path); onlyKeys(raw, ["routeId", "eventSourcePath", "phaseSelector", "blockedPhases", "streamSettleMs", "views", "selection"], path); const views = array(raw.views, `${path}.views`, 1, 10).map((item, index) => agentView(item, `${path}.views[${index}]`)); const selection = raw.selection === undefined ? undefined : agentSelection(raw.selection, `${path}.selection`); unique(views.map((item) => item.queryValue), `${path}.views[].queryValue`); unique([...views.map((item) => item.screenshotName), ...(selection === undefined ? [] : [selection.screenshotName])], `${path}.screenshots`); return { routeId: routeReference(raw.routeId, `${path}.routeId`, routeIds), eventSourcePath: absolutePath(text(raw.eventSourcePath, `${path}.eventSourcePath`), `${path}.eventSourcePath`), phaseSelector: boundedText(raw.phaseSelector, `${path}.phaseSelector`, 500), blockedPhases: textArray(raw.blockedPhases, `${path}.blockedPhases`, 1, 20, 100), streamSettleMs: integer(raw.streamSettleMs, `${path}.streamSettleMs`, 0, 120_000), views, ...(selection === undefined ? {} : { selection }), }; } function agentSelection(value: unknown, path: string): HwlabRuntimeWebProbeConsoleAgentSelectionSpec { const raw = record(value, path); onlyKeys(raw, ["selector", "inspectorSelector", "screenshotName"], path); const screenshotName = boundedText(raw.screenshotName, `${path}.screenshotName`, 120); if (!/^[A-Za-z0-9._-]+\.png$/u.test(screenshotName)) throw new Error(`${path}.screenshotName must be a safe PNG filename`); return { selector: boundedText(raw.selector, `${path}.selector`, 500), inspectorSelector: boundedText(raw.inspectorSelector, `${path}.inspectorSelector`, 500), screenshotName, }; } function agentView(value: unknown, path: string): HwlabRuntimeWebProbeConsoleAgentViewSpec { const raw = record(value, path); onlyKeys(raw, ["buttonName", "queryValue", "screenshotName"], path); const screenshotName = boundedText(raw.screenshotName, `${path}.screenshotName`, 120); if (!/^[A-Za-z0-9._-]+\.png$/u.test(screenshotName)) throw new Error(`${path}.screenshotName must be a safe PNG filename`); return { buttonName: boundedText(raw.buttonName, `${path}.buttonName`, 100), queryValue: simpleId(text(raw.queryValue, `${path}.queryValue`), `${path}.queryValue`), screenshotName, }; } function routeReference(value: unknown, path: string, routeIds: ReadonlySet): string { const id = simpleId(text(value, path), path); if (!routeIds.has(id)) throw new Error(`${path} references unknown route ${id}`); return id; } function record(value: unknown, path: string): Record { if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`); return value as Record; } function array(value: unknown, path: string, min: number, max: number): unknown[] { if (!Array.isArray(value) || value.length < min || value.length > max) throw new Error(`${path} must contain ${min}-${max} items`); return value; } function text(value: unknown, path: string): string { if (typeof value !== "string" || value.trim().length === 0 || value.includes("\0")) throw new Error(`${path} must be a non-empty string`); return value; } function boundedText(value: unknown, path: string, max: number): string { const result = text(value, path); if (result.length > max) throw new Error(`${path} must be at most ${max} chars`); return result; } function textArray(value: unknown, path: string, min: number, max: number, maxChars: number): readonly string[] { return array(value, path, min, max).map((item, index) => boundedText(item, `${path}[${index}]`, maxChars)); } function simpleId(value: string, path: string): string { if (!/^[A-Za-z0-9._-]+$/u.test(value) || value.length > 100) throw new Error(`${path} must be a simple id up to 100 chars`); return value; } function absolutePath(value: string, path: string): string { if (!value.startsWith("/") || value.length > 300) throw new Error(`${path} must be an absolute path up to 300 chars`); return value; } function integer(value: unknown, path: string, min: number, max: number): number { if (!Number.isInteger(value) || (value as number) < min || (value as number) > max) throw new Error(`${path} must be an integer ${min}-${max}`); return value as number; } function boolean(value: unknown, path: string): boolean { if (typeof value !== "boolean") throw new Error(`${path} must be boolean`); return value; } function onlyKeys(raw: Record, allowed: readonly string[], path: string): void { const allow = new Set(allowed); const unknown = Object.keys(raw).filter((key) => !allow.has(key)); if (unknown.length > 0) throw new Error(`${path} has unsupported fields: ${unknown.join(", ")}`); } function unique(values: readonly string[], path: string): void { const seen = new Set(); for (const value of values) { if (seen.has(value)) throw new Error(`${path} contains duplicate ${value}`); seen.add(value); } }