Merge remote-tracking branch 'origin/v0.3' into feat/2668-tasktree-github-backup

This commit is contained in:
root
2026-07-19 04:33:35 +02:00
14 changed files with 201 additions and 43 deletions
+2 -1
View File
@@ -101,6 +101,7 @@ import { handleTaskTreeProxyHttp } from "./tasktree-proxy.ts";
import { handleDashboardSummaryHttp } from "./server-dashboard-http.ts";
import { HWPOD_NODE_OPS, HWPOD_NODE_OPS_CONTRACT_VERSION } from "../../tools/src/hwpod-node-ops-contract.ts";
import { proxyWorkbenchCommandHttp, workbenchActivityDispatchRequested, workbenchTemporalProxyEnabled } from "./workbench-command-proxy.ts";
import { normalizeWorkbenchCloudAuthorization } from "../workbench/cloud-authorization.ts";
const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
const HWLAB_WEB_SESSION_COOKIE = "hwlab_session";
@@ -1078,7 +1079,7 @@ function navIdForRestPath(pathname, method = "GET") {
async function codeAgentOptions(request, response, options, authOptions = {}) {
if (workbenchActivityDispatchRequested(request)) {
const expected = String(options.env?.WORKBENCH_CLOUD_API_AUTHORIZATION ?? "").trim();
const expected = normalizeWorkbenchCloudAuthorization(String(options.env?.WORKBENCH_CLOUD_API_AUTHORIZATION ?? ""));
const actual = String(request.headers?.authorization ?? "").trim();
const actorId = String(request.headers?.["x-hwlab-actor-id"] ?? "").trim();
if (!expected || actual !== expected || !actorId) {
@@ -1,9 +1,16 @@
import { expect, test } from "bun:test";
import { workbenchActivityDispatchRequested } from "./workbench-command-proxy.ts";
import { normalizeWorkbenchCloudAuthorization } from "../workbench/cloud-authorization.ts";
test("accepts activity dispatch header from Node and Fetch request shapes", () => {
expect(workbenchActivityDispatchRequested({ headers: { "x-workbench-activity-dispatch": "1" } })).toBe(true);
expect(workbenchActivityDispatchRequested({ headers: new Headers({ "x-workbench-activity-dispatch": "1" }) })).toBe(true);
expect(workbenchActivityDispatchRequested({ headers: new Headers({ "x-workbench-activity-dispatch": "0" }) })).toBe(false);
});
test("normalizes the unified Workbench Cloud authorization on both sides", () => {
expect(normalizeWorkbenchCloudAuthorization(" unified-internal-key ")).toBe("Bearer unified-internal-key");
expect(normalizeWorkbenchCloudAuthorization("Bearer unified-internal-key")).toBe("Bearer unified-internal-key");
expect(normalizeWorkbenchCloudAuthorization(" ")).toBe("");
});
@@ -8,6 +8,7 @@ const POST_PROXY_ROUTES = new Set([
"/v1/agent/chat/cancel",
"/v1/agent/chat/steer",
"/v1/agent/sessions",
"/v1/workbench/commands",
"/v1/workbench/launches",
"/v1/hwpod-node-ops",
"/v1/web-performance",
@@ -236,3 +236,12 @@ test("cloud web proxies authenticated Workbench launch writes to cloud-api", ()
routeKey: "POST /v1/workbench/launches"
});
});
test("cloud web proxies authenticated Workbench commands to cloud-api", () => {
assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/v1/workbench/commands"), {
proxy: true,
authRequired: true,
publicRoute: false,
routeKey: "POST /v1/workbench/commands"
});
});
@@ -105,7 +105,7 @@ test("cloud web auth requests are proxied once", async () => {
}
});
test("cloud web proxies Workbench launches with trace context", async () => {
test("cloud web proxies Workbench writes with trace context", async () => {
const upstreamRequests = [];
const upstream = createServer(async (request, response) => {
let body = "";
@@ -145,27 +145,29 @@ test("cloud web proxies Workbench launches with trace context", async () => {
try {
const body = JSON.stringify({ projectId: "project_d601", taskRef: "task_2136" });
const response = await fetch(`${serverUrl(cloudWeb)}/v1/workbench/launches`, {
for (const route of ["/v1/workbench/launches", "/v1/workbench/commands"]) {
const response = await fetch(`${serverUrl(cloudWeb)}${route}`, {
method: "POST",
headers: {
"content-type": "application/json",
cookie: "hwlab_session=session-launch",
traceparent: "00-11111111111111111111111111111111-2222222222222222-01",
"x-hwlab-otel-trace-id": "11111111111111111111111111111111"
},
body
});
assert.equal(response.status, 404);
assert.equal(response.headers.get("x-hwlab-otel-trace-id"), "11111111111111111111111111111111");
assert.deepEqual(await response.json(), { error: "not_found" });
}
assert.deepEqual(upstreamRequests, ["/v1/workbench/launches", "/v1/workbench/commands"].map((url) => ({
method: "POST",
headers: {
"content-type": "application/json",
cookie: "hwlab_session=session-launch",
traceparent: "00-11111111111111111111111111111111-2222222222222222-01",
"x-hwlab-otel-trace-id": "11111111111111111111111111111111"
},
body
});
assert.equal(response.status, 404);
assert.equal(response.headers.get("x-hwlab-otel-trace-id"), "11111111111111111111111111111111");
assert.deepEqual(await response.json(), { error: "not_found" });
assert.deepEqual(upstreamRequests, [{
method: "POST",
url: "/v1/workbench/launches",
url,
cookie: "hwlab_session=session-launch",
traceparent: "00-11111111111111111111111111111111-2222222222222222-01",
otelTraceId: "11111111111111111111111111111111",
body
}]);
})));
} finally {
await close(cloudWeb);
await close(upstream);
+4 -2
View File
@@ -1,4 +1,5 @@
import type { WorkbenchApplication } from "./contracts.ts";
import { normalizeWorkbenchCloudAuthorization } from "./cloud-authorization.ts";
export function createCloudWorkbenchApplication(options: {
baseUrl: string;
@@ -6,13 +7,14 @@ export function createCloudWorkbenchApplication(options: {
fetchImpl?: typeof fetch;
}): WorkbenchApplication {
if (!options.baseUrl) throw codedError("workbench_cloud_api_url_required", "WORKBENCH_CLOUD_API_URL is required");
if (!options.authorization) throw codedError("workbench_cloud_api_auth_required", "WORKBENCH_CLOUD_API_AUTHORIZATION is required");
const authorization = normalizeWorkbenchCloudAuthorization(options.authorization);
if (!authorization) throw codedError("workbench_cloud_api_auth_required", "WORKBENCH_CLOUD_API_AUTHORIZATION is required");
const fetchImpl = options.fetchImpl ?? fetch;
const request = async (method: string, route: string, body: Record<string, unknown> | undefined, actor: { id: string; role?: string | null }) => {
const response = await fetchImpl(`${options.baseUrl.replace(/\/$/u, "")}${route}`, {
method,
headers: {
authorization: options.authorization,
authorization,
"content-type": "application/json",
"x-workbench-activity-dispatch": "1",
"x-hwlab-actor-id": actor.id,
@@ -0,0 +1,5 @@
export function normalizeWorkbenchCloudAuthorization(value: string) {
const authorization = value.trim();
if (!authorization) return "";
return /^Bearer\s+/iu.test(authorization) ? authorization : `Bearer ${authorization}`;
}
+16 -2
View File
@@ -54,10 +54,24 @@ describe("Workbench Cloud application adapter", () => {
await application.admitTurn(input);
await application.dispatchTurn(input);
expect(calls).toEqual([
{ path: "/v1/internal/workbench/admit", authorization: "unified-internal-key" },
{ path: "/v1/agent/chat", authorization: "unified-internal-key" }
{ path: "/v1/internal/workbench/admit", authorization: "Bearer unified-internal-key" },
{ path: "/v1/agent/chat", authorization: "Bearer unified-internal-key" }
]);
});
test("preserves an existing Bearer authorization scheme", async () => {
let authorization: string | null = null;
const application = createCloudWorkbenchApplication({
baseUrl: "http://cloud.internal",
authorization: "Bearer unified-internal-key",
fetchImpl: (async (_url: string | URL | Request, init?: RequestInit) => {
authorization = new Headers(init?.headers).get("authorization");
return Response.json({ ok: true, session: { sessionId: "ses_test" } });
}) as typeof fetch
});
await application.createSession({ actor: { id: "usr_test" }, params: {} });
expect(authorization).toBe("Bearer unified-internal-key");
});
});
describe("Workbench native HTTP adapter", () => {
@@ -1,7 +1,7 @@
import assert from "node:assert/strict";
import test from "node:test";
import { providerProfileOptionsFromPayload } from "../src/stores/workbench-session.ts";
import { preferredProviderProfile, providerProfileOptionsFromPayload } from "../src/stores/workbench-session.ts";
test("Workbench provider catalog marks missing selected profile as unavailable", () => {
const options = providerProfileOptionsFromPayload({ profiles: [{ profile: "dsflash-go", label: "DeepSeek V4 Flash", configured: true }] }, "codex");
@@ -11,4 +11,11 @@ test("Workbench provider catalog marks missing selected profile as unavailable",
assert.equal(available?.configured, true);
assert.equal(missingSelected?.configured, false);
assert.equal(preferredProviderProfile(options, "codex"), "dsflash-go");
});
test("Workbench provider selection stays non-blocking without a configured catalog option", () => {
const options = providerProfileOptionsFromPayload({ profiles: [{ profile: "codex", configured: false }] }, "codex");
assert.equal(preferredProviderProfile(options, "codex"), "codex");
assert.equal(preferredProviderProfile(providerProfileOptionsFromPayload({}, "codex"), "codex"), "codex");
});
@@ -21,7 +21,7 @@ import { messageDiagnosticView } from "../src/utils/workbench-error-runtime.ts";
import { projectRejectedWorkbenchAdmission } from "../src/stores/workbench-admission-failure.ts";
import { WORKBENCH_TIMELINE_OPENCODE_PARITY, buildWorkbenchTimelineRows, normalizeWorkbenchTimelineMessages, workbenchTimelineSignature } from "../src/stores/workbench-timeline-model.ts";
import { reduceWorkbenchRealtimeEvent } from "../src/stores/workbench-event-reducer.ts";
import { planWorkbenchRealtimeApply, planWorkbenchRealtimeRecovery } from "../src/stores/workbench-realtime-plan.ts";
import { assessWorkbenchRealtimeConnected, planWorkbenchRealtimeApply, planWorkbenchRealtimeRecovery } from "../src/stores/workbench-realtime-plan.ts";
import { WORKBENCH_REALTIME_AUTHORITY_VERSION, workbenchRealtimePrimaryAuthorityDecision } from "../src/stores/workbench-realtime-authority.ts";
import { cleanupWorkbenchServerStateDroppedSessions, cleanupWorkbenchServerStateSessions, createWorkbenchServerState, reduceWorkbenchServerState } from "../src/stores/workbench-server-state.ts";
import { cleanupDroppedWorkbenchSessionCaches, trimWorkbenchSessionCache } from "../src/stores/workbench-session-cache.ts";
@@ -93,6 +93,40 @@ test("the active projection trace scope follows the current request", () => {
assert.equal(workbenchRealtimeTraceIdForCapabilities({ liveKafkaSse: true, kafkaRefreshReplay: true, projectionRealtime: false }, "trc_current_request"), null);
});
test("connected realtime metadata drift warns without blocking the active session", () => {
const assessment = assessWorkbenchRealtimeConnected({
deliverySemantics: "live-only",
liveOnly: true,
replay: false,
filters: { sessionId: "ses_active" }
}, "ses_active", false);
assert.equal(assessment.ready, true);
assert.equal(assessment.connectedSessionId, "ses_active");
assert.equal(assessment.blocker, null);
assert.deepEqual(assessment.warning?.fields, [
"capabilities.liveKafkaSse",
"capabilities.kafkaRefreshReplay"
]);
assert.equal(assessment.warning?.severity, "warning");
});
test("connected realtime session scope mismatch remains blocking", () => {
const assessment = assessWorkbenchRealtimeConnected({
deliverySemantics: "live-only",
liveOnly: true,
replay: false,
capabilities: { liveKafkaSse: true, kafkaRefreshReplay: false, projectionRealtime: false },
filters: { sessionId: "ses_other" }
}, "ses_active", false);
assert.equal(assessment.ready, false);
assert.equal(assessment.warning, null);
assert.equal(assessment.blocker?.code, "workbench_realtime_session_scope_mismatch");
assert.equal(assessment.blocker?.expectedSessionId, "ses_active");
assert.equal(assessment.blocker?.connectedSessionId, "ses_other");
});
test("Error runtime owns Workbench message diagnostic view model", () => {
const degraded = messageDiagnosticView(agentMessage({
status: "running",
@@ -51,6 +51,23 @@ export interface WorkbenchRealtimeRecoveryPlan {
};
}
export interface WorkbenchRealtimeConnectedAssessment {
connectedSessionId: string | null;
ready: boolean;
warning: {
code: "workbench_realtime_metadata_drift_warning";
fields: string[];
severity: "warning";
valuesRedacted: true;
} | null;
blocker: {
code: "workbench_realtime_session_scope_mismatch";
expectedSessionId: string | null;
connectedSessionId: string | null;
valuesRedacted: true;
} | null;
}
export function planWorkbenchRealtimeApply(action: WorkbenchRealtimeAction): WorkbenchRealtimeApplyPlan {
const steps = applySteps(action);
return {
@@ -65,6 +82,40 @@ export function planWorkbenchRealtimeApply(action: WorkbenchRealtimeAction): Wor
};
}
export function assessWorkbenchRealtimeConnected(
event: WorkbenchRealtimeEvent,
expectedSessionId: string | null | undefined,
kafkaRefreshReplay: boolean
): WorkbenchRealtimeConnectedAssessment {
const filters = event.filters && typeof event.filters === "object" ? event.filters as Record<string, unknown> : null;
const connectedSessionId = normalizeWorkbenchSessionId(filters?.sessionId);
const normalizedExpectedSessionId = normalizeWorkbenchSessionId(expectedSessionId);
const scopeMatches = Boolean(connectedSessionId && normalizedExpectedSessionId && connectedSessionId === normalizedExpectedSessionId);
const fields: string[] = [];
const expectedDelivery = kafkaRefreshReplay ? "kafka-retention-then-live" : "live-only";
if (event.deliverySemantics !== expectedDelivery) fields.push("deliverySemantics");
if (event.liveOnly !== !kafkaRefreshReplay) fields.push("liveOnly");
if (event.replay !== kafkaRefreshReplay) fields.push("replay");
if (kafkaRefreshReplay && event.lossPossible !== false) fields.push("lossPossible");
if (event.capabilities?.liveKafkaSse !== true) fields.push("capabilities.liveKafkaSse");
if (event.capabilities?.kafkaRefreshReplay !== kafkaRefreshReplay) fields.push("capabilities.kafkaRefreshReplay");
return {
connectedSessionId,
ready: scopeMatches,
warning: fields.length > 0
? { code: "workbench_realtime_metadata_drift_warning", fields, severity: "warning", valuesRedacted: true }
: null,
blocker: scopeMatches
? null
: {
code: "workbench_realtime_session_scope_mismatch",
expectedSessionId: normalizedExpectedSessionId,
connectedSessionId,
valuesRedacted: true
}
};
}
export function planWorkbenchRealtimeRecovery(recovery: WorkbenchStreamTransportRecovery, context: WorkbenchRealtimeRecoveryContext): WorkbenchRealtimeRecoveryPlan {
const sessionId = normalizeWorkbenchSessionId(recovery.sessionId ?? context.selectedSessionId);
const traceId = firstNonEmptyString(recovery.traceId, context.fallbackTraceId);
@@ -259,6 +259,12 @@ export function defaultProviderProfileOptions(selectedProfile: ProviderProfile):
return ensureSelectedProviderProfile(BUILTIN_PROVIDER_PROFILE_ORDER.map((profile) => ({ value: profile, label: providerProfileLabel(profile) })), selectedProfile);
}
export function preferredProviderProfile(options: ProviderProfileOption[], selectedProfile: ProviderProfile): ProviderProfile {
const selected = options.find((option) => option.value === selectedProfile);
if (selected?.configured !== false) return selectedProfile;
return options.find((option) => option.configured === true)?.value ?? selectedProfile;
}
function activeSession(sessions: WorkbenchSessionRecord[], sessionId: string | null): WorkbenchSessionRecord | null {
return sessionId ? sessions.find((item) => item.sessionId === sessionId) ?? null : null;
}
+33 -15
View File
@@ -17,7 +17,7 @@ import type { AgentChatResponse, AgentChatResultResponse, AgentRunProvenance, Ap
import { firstNonEmptyString, nextProtocolId, normalizeWorkbenchSessionId, normalizeWorkbenchSessionRouteId } from "@/utils";
import { composeWorkbenchScopedKey, workbenchRealtimeScopeKey } from "@/utils/workbench-key";
import { failWorkbenchSessionSwitch, failWorkbenchSubmitJourney, finishWorkbenchSessionSwitchFullLoad, markWorkbenchSubmitApiAccepted, markWorkbenchTraceEventsReceived, markWorkbenchTraceProjected, recordWorkbenchLoadingState, recordWorkbenchRuntimeDiagnostic, startWorkbenchSessionSwitch, startWorkbenchSubmitJourney } from "@/utils/workbench-performance";
import { RECENT_DRAFTS_STORAGE_KEY, appendSessionPage, defaultProviderProfileOptions, isArchivedSession, mergeSessionIntoList, normalizeChatMessageStatus, normalizeRecentDrafts, normalizeWorkbenchMessageTitle, providerProfileOptionsFromPayload, recordRecentDraft, resolveCancelableAgentMessage, resolveComposerState, shouldReadTerminalTraceDetail, shouldShowSessionListLoading, sortSessionTabs, stableSessionList, type DraftEntry, type ProviderProfileOption, type TurnStatusAuthority } from "./workbench-session";
import { RECENT_DRAFTS_STORAGE_KEY, appendSessionPage, defaultProviderProfileOptions, isArchivedSession, mergeSessionIntoList, normalizeChatMessageStatus, normalizeRecentDrafts, normalizeWorkbenchMessageTitle, preferredProviderProfile, providerProfileOptionsFromPayload, recordRecentDraft, resolveCancelableAgentMessage, resolveComposerState, shouldReadTerminalTraceDetail, shouldShowSessionListLoading, sortSessionTabs, stableSessionList, type DraftEntry, type ProviderProfileOption, type TurnStatusAuthority } from "./workbench-session";
import { initialWorkbenchSessionIdFromLocation } from "./workbench-projection";
import { cleanupWorkbenchServerStateSessions, selectActiveMessages, selectActiveSession, selectSessionList, selectSessionStatusAuthority, selectTraceAuthorityById, selectTurnStatusAuthority, type WorkbenchServerAction } from "./workbench-server-state";
import { cleanupDroppedWorkbenchSessionCaches, trimWorkbenchSessionCache } from "./workbench-session-cache";
@@ -65,7 +65,7 @@ import {
traceHasEvents,
traceResultHasTerminalEvidence,
} from "./workbench-message-projection-runtime";
import { planWorkbenchRealtimeApply, planWorkbenchRealtimeRecovery, type WorkbenchRealtimeApplyStep, type WorkbenchRealtimeRecoveryStep } from "./workbench-realtime-plan";
import { assessWorkbenchRealtimeConnected, planWorkbenchRealtimeApply, planWorkbenchRealtimeRecovery, type WorkbenchRealtimeApplyStep, type WorkbenchRealtimeRecoveryStep } from "./workbench-realtime-plan";
import { useWorkbenchColadaMutations } from "./workbench-colada-mutations";
import { useWorkbenchColadaQueries } from "./workbench-colada-queries";
import { useWorkbenchColadaReducer } from "./workbench-colada-reducer";
@@ -921,7 +921,13 @@ export const useWorkbenchStore = defineStore("workbench", () => {
async function refreshProviderOptions(): Promise<boolean> {
const response = await workbenchColadaQueries.fetchProviderProfiles({ minIntervalMs: runtimePolicy.workbenchReadFailureCooldownMs });
providerOptions.value = response.ok ? providerProfileOptionsFromPayload(response.data, providerProfile.value) : defaultProviderProfileOptions(providerProfile.value);
if (response.ok) {
providerOptions.value = providerProfileOptionsFromPayload(response.data, providerProfile.value);
const preferred = preferredProviderProfile(providerOptions.value, providerProfile.value);
if (preferred !== providerProfile.value) setProviderProfile(preferred);
} else {
providerOptions.value = defaultProviderProfileOptions(providerProfile.value);
}
return response.ok === true;
}
@@ -983,18 +989,30 @@ export const useWorkbenchStore = defineStore("workbench", () => {
onRecovery: (recovery) => handleRealtimeRecovery(recovery),
onEvent: (event, eventName) => {
if (realtimeCapabilities.liveKafkaSse && eventName === "workbench.connected") {
const filters = recordValue(event.filters);
const connectedSessionId = normalizeWorkbenchSessionId(filters?.sessionId);
const refreshReplay = realtimeCapabilities.kafkaRefreshReplay;
const deliveryValid = refreshReplay
? event.deliverySemantics === "kafka-retention-then-live" && event.liveOnly === false && event.replay === true && event.lossPossible === false
: event.deliverySemantics === "live-only" && event.liveOnly === true && event.replay === false;
const valid = deliveryValid
&& event.capabilities?.liveKafkaSse === true
&& event.capabilities?.kafkaRefreshReplay === refreshReplay
&& connectedSessionId === sessionId;
liveRealtimeReadySessionId.value = valid ? connectedSessionId : null;
if (!valid) error.value = "workbench_live_realtime_contract_invalid";
const assessment = assessWorkbenchRealtimeConnected(event, sessionId, realtimeCapabilities.kafkaRefreshReplay);
liveRealtimeReadySessionId.value = assessment.ready ? assessment.connectedSessionId : null;
if (assessment.warning) {
recordWorkbenchRuntimeDiagnostic({
module: "workbench-stream-contract",
sessionId,
traceId,
outcome: "partial",
level: "warning",
diagnostic: assessment.warning
});
}
if (assessment.blocker) {
error.value = assessment.blocker.code;
recordWorkbenchRuntimeDiagnostic({
module: "workbench-stream-contract",
sessionId,
traceId,
outcome: "error",
diagnostic: assessment.blocker
});
} else if (error.value === "workbench_live_realtime_contract_invalid" || error.value === "workbench_realtime_session_scope_mismatch") {
error.value = null;
}
}
applyRealtimeEvent(event, eventName);
}
@@ -365,25 +365,26 @@ export function recordWorkbenchSseLifecycle(input: { state: "connect" | "open" |
});
}
export function recordWorkbenchRuntimeDiagnostic(input: { module?: string | null; diagnostic?: Record<string, unknown> | null; sessionId?: string | null; traceId?: string | null; outcome?: WorkbenchOutcome | null }): void {
export function recordWorkbenchRuntimeDiagnostic(input: { module?: string | null; diagnostic?: Record<string, unknown> | null; sessionId?: string | null; traceId?: string | null; outcome?: WorkbenchOutcome | null; level?: "error" | "warning" | "sample" }): void {
ensureInstalled();
const diagnostic = recordValue(input.diagnostic);
const endedAt = wallNow();
const code = diagnosticText(diagnostic, "code") ?? "runtime_diagnostic";
const flushDurationMs = diagnosticNumber(diagnostic, "flushDurationMs");
const isSseFlush = code === "workbench_sse_flush";
const warning = input.level === "warning" || diagnosticText(diagnostic, "severity", "level") === "warning";
enqueueWorkbenchUiEvent({
eventType: "runtime_diagnostic",
loadingScope: "sse",
state: isSseFlush ? "sample" : "error",
reason: isSseFlush ? "sse_flush" : "sse_error",
state: isSseFlush || warning ? "sample" : "error",
reason: isSseFlush ? "sse_flush" : warning ? "unknown" : "sse_error",
route: pageRoute(),
valueMs: flushDurationMs ?? 0,
startedAtEpochMs: Math.max(0, endedAt - (flushDurationMs ?? 0)),
endedAtEpochMs: endedAt,
sessionHash: hashIdentifier(input.sessionId ?? diagnosticText(diagnostic, "sessionId"), "ses"),
traceHash: hashIdentifier(input.traceId ?? diagnosticText(diagnostic, "traceId"), "trc"),
outcome: input.outcome ?? normalizeUiOutcome(diagnosticText(diagnostic, "outcome")),
outcome: input.outcome ?? (warning ? "partial" : normalizeUiOutcome(diagnosticText(diagnostic, "outcome"))),
errorName: code.slice(0, 80),
module: diagnosticLabel(input.module ?? diagnosticText(diagnostic, "module") ?? diagnosticText(diagnostic, "layer")),
diagnosticCode: diagnosticLabel(code),