refactor: remove legacy workspace UI state

This commit is contained in:
2026-08-10 22:56:00 +02:00
parent fc19b0d67c
commit 90684c3280
12 changed files with 283 additions and 130 deletions
+45 -7
View File
@@ -99,7 +99,6 @@ export interface WorkspaceRevision {
commit: string;
blob: string;
snapshotPath: string;
state: "operational" | "migration_required";
}
export interface WorkspaceSummary {
@@ -110,7 +109,7 @@ export interface WorkspaceSummary {
displayName: string;
description?: string;
language: "en" | "it";
revision: WorkspaceRevision;
revision?: WorkspaceRevision;
}
export interface WorkspaceRecord {
@@ -206,9 +205,10 @@ function exactObject(value: unknown, keys: readonly string[]): Record<string, un
}
function workspaceRevision(value: unknown, expectedId: string): WorkspaceRevision | undefined {
const source = exactObject(value, ["id", "commit", "blob", "snapshotPath", "state"]);
// Frontend and core deploy atomically: accept only the schema-v3, state-free contract.
const source = exactObject(value, ["id", "commit", "blob", "snapshotPath"]);
if (!source) return undefined;
const { id, commit, blob, snapshotPath, state } = source;
const { id, commit, blob, snapshotPath } = source;
if (
id !== expectedId
|| typeof id !== "string" || !/^[a-z][a-z0-9-]{2,62}$/.test(id)
@@ -216,9 +216,39 @@ function workspaceRevision(value: unknown, expectedId: string): WorkspaceRevisio
|| typeof blob !== "string" || !/^[0-9a-f]{40}$/.test(blob)
|| typeof snapshotPath !== "string" || snapshotPath.length === 0
|| snapshotPath.trim() !== snapshotPath || /[\u0000-\u001f\u007f]/u.test(snapshotPath)
|| (state !== "operational" && state !== "migration_required")
) return undefined;
return { id, commit, blob, snapshotPath, state };
return { id, commit, blob, snapshotPath };
}
function workspaceSummary(value: unknown): WorkspaceSummary | undefined {
const source = object(value);
if (!source) return undefined;
const { id, name, file, displayName, description, language } = source;
// Summary prose follows the producer's z.string().trim().min(1) contract;
// path-like revision fields retain their stricter control-character checks above.
const validText = (candidate: unknown) => typeof candidate === "string"
&& candidate.length > 0
&& candidate.trim() === candidate;
if (
typeof id !== "string" || !/^[a-z][a-z0-9-]{2,62}$/.test(id)
|| name !== id || file !== `${id}.yaml`
|| !validText(displayName)
|| (description !== undefined && !validText(description))
|| (language !== "en" && language !== "it")
) return undefined;
const revision = source.revision === undefined
? undefined
: workspaceRevision(source.revision, id);
if (source.revision !== undefined && !revision) return undefined;
return {
id,
name: name as string,
file: file as string,
displayName: displayName as string,
...(description === undefined ? {} : { description: description as string }),
language,
...(revision ? { revision } : {}),
};
}
function requireWorkspaceRevision(value: unknown, expectedId: string): WorkspaceRevision {
@@ -280,7 +310,15 @@ function requireCanonicalWorkspace(value: unknown): CanonicalWorkspace {
return workspace;
}
export const listWorkspaces = () => apiFetch<WorkspaceSummary[]>("/workspaces");
export const listWorkspaces = async (): Promise<WorkspaceSummary[]> => {
const response = await apiFetch<unknown>("/workspaces");
if (!Array.isArray(response)) throw new Error("Workspace API returned an invalid workspace summary");
const summaries = response.map(workspaceSummary);
if (summaries.some((summary) => !summary)) {
throw new Error("Workspace API returned an invalid workspace summary");
}
return summaries as WorkspaceSummary[];
};
export const getWorkspace = async (id: string): Promise<WorkspaceRecord> => {
const response = await apiFetch<unknown>(`/workspaces/${encodeURIComponent(id)}`);
const source = object(response);