refactor: make browser workspace writes bootstrap-only
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import { beforeEach, expect, test } from "vitest";
|
||||
import type { CanonicalWorkspace } from "../api/workspaces";
|
||||
import {
|
||||
sanitizeCanonicalWorkspace, workspaceDeletionDrafts, workspaceDrafts, workspacePreferences,
|
||||
sanitizeCanonicalWorkspace,
|
||||
workspaceBootstrapDrafts,
|
||||
workspacePreferences,
|
||||
type WorkspaceBootstrapDraft,
|
||||
} from "./drafts";
|
||||
|
||||
const workspace: CanonicalWorkspace = {
|
||||
@@ -19,68 +22,29 @@ const workspace: CanonicalWorkspace = {
|
||||
llm_policy: { allowed: ["zai/glm-5.2"] },
|
||||
};
|
||||
|
||||
const bootstrapDraft: WorkspaceBootstrapDraft = {
|
||||
workspaceId: "psd-clinical",
|
||||
baseCommit: "a".repeat(40),
|
||||
workspace,
|
||||
updatedAt: "2026-08-04T10:00:00.000Z",
|
||||
};
|
||||
|
||||
const policy = { max_chunk_chars: 8_000, retain_published_generations: 5 };
|
||||
|
||||
const evidenceWorkspaces = [
|
||||
{
|
||||
name: "filesystem",
|
||||
workspace: {
|
||||
...workspace,
|
||||
evidence: {
|
||||
source: {
|
||||
type: "filesystem",
|
||||
uri: "workspace-content/psd-clinical/evidence",
|
||||
patterns: ["documents/**/*.pdf", "notes/*.md"],
|
||||
max_bytes: 12_000_000,
|
||||
},
|
||||
policy,
|
||||
},
|
||||
} satisfies CanonicalWorkspace,
|
||||
const evidenceWorkspace: CanonicalWorkspace = {
|
||||
...workspace,
|
||||
evidence: {
|
||||
source: {
|
||||
type: "filesystem",
|
||||
uri: "psd-clinical/evidence",
|
||||
patterns: ["documents/**/*.pdf", "notes/*.md"],
|
||||
max_bytes: 12_000_000,
|
||||
},
|
||||
policy,
|
||||
},
|
||||
{
|
||||
name: "http",
|
||||
workspace: {
|
||||
...workspace,
|
||||
evidence: {
|
||||
source: {
|
||||
type: "http",
|
||||
uris: ["https://evidence.example/manifest.json", "http://evidence.example/files/list.txt"],
|
||||
authentication: "signed_urls_file",
|
||||
connect_timeout_ms: 2_000,
|
||||
read_timeout_ms: 20_000,
|
||||
max_bytes: 12_000_000,
|
||||
max_redirects: 2,
|
||||
allow_private_hosts: false,
|
||||
max_cache_bytes: 24_000_000,
|
||||
},
|
||||
policy,
|
||||
},
|
||||
} satisfies CanonicalWorkspace,
|
||||
},
|
||||
{
|
||||
name: "s3",
|
||||
workspace: {
|
||||
...workspace,
|
||||
evidence: {
|
||||
source: {
|
||||
type: "s3",
|
||||
uri: "s3://clinical-evidence/published/",
|
||||
endpoint_url: "https://objects.example",
|
||||
region: "eu-west-1",
|
||||
credentials: "static_files",
|
||||
trusted_endpoint: true,
|
||||
allow_private_endpoint: false,
|
||||
allow_insecure_endpoint: false,
|
||||
max_bytes: 12_000_000,
|
||||
max_objects: 2_000,
|
||||
max_pages: 20,
|
||||
page_size: 100,
|
||||
},
|
||||
policy,
|
||||
},
|
||||
} satisfies CanonicalWorkspace,
|
||||
},
|
||||
] as const;
|
||||
};
|
||||
|
||||
beforeEach(() => localStorage.clear());
|
||||
|
||||
test("keeps an anonymous user's model selection in browser storage", () => {
|
||||
workspacePreferences.save({
|
||||
@@ -90,191 +54,73 @@ test("keeps an anonymous user's model selection in browser storage", () => {
|
||||
expect(workspacePreferences.load()).toMatchObject({ model: "glm-5.2" });
|
||||
});
|
||||
|
||||
test("reloads a canonical workspace draft and discards it by workspace ID", () => {
|
||||
workspaceDrafts.save({
|
||||
test("reloads a bootstrap draft from the v2 browser-storage key and discards it by workspace ID", () => {
|
||||
workspaceBootstrapDrafts.save(bootstrapDraft);
|
||||
|
||||
expect(workspaceBootstrapDrafts.load("psd-clinical")).toEqual(bootstrapDraft);
|
||||
expect(localStorage.getItem("thothii.workspace-registry.v2.bootstrap.psd-clinical")).toContain('"baseCommit"');
|
||||
expect(localStorage.getItem("thothii.workspace-registry.v2.bootstrap.psd-clinical")).not.toContain("baseBlob");
|
||||
|
||||
workspaceBootstrapDrafts.discard("psd-clinical");
|
||||
expect(workspaceBootstrapDrafts.load("psd-clinical")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("purges known v1 update and deletion keys without touching unrelated localStorage", () => {
|
||||
localStorage.setItem("thothii.workspace-registry.v1.draft.psd-clinical", JSON.stringify({
|
||||
workspaceId: "psd-clinical",
|
||||
baseCommit: "a".repeat(40),
|
||||
baseBlob: "b".repeat(40),
|
||||
workspace,
|
||||
updatedAt: "2026-08-04T10:00:00.000Z",
|
||||
});
|
||||
|
||||
expect(workspaceDrafts.load("psd-clinical")).toMatchObject({
|
||||
baseCommit: "a".repeat(40), workspace,
|
||||
});
|
||||
workspaceDrafts.discard("psd-clinical");
|
||||
expect(workspaceDrafts.load("psd-clinical")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("persists a deletion draft without retaining a workspace definition", () => {
|
||||
workspaceDeletionDrafts.save({
|
||||
}));
|
||||
localStorage.setItem("thothii.workspace-registry.v1.delete.psd-clinical", JSON.stringify({
|
||||
id: "psd-clinical",
|
||||
baseCommit: "a".repeat(40),
|
||||
baseBlob: "b".repeat(40),
|
||||
updatedAt: "2026-08-04T10:00:00.000Z",
|
||||
});
|
||||
}));
|
||||
localStorage.setItem("unrelated", "keep-me");
|
||||
|
||||
expect(workspaceDeletionDrafts.load("psd-clinical")).toEqual({
|
||||
id: "psd-clinical",
|
||||
baseCommit: "a".repeat(40),
|
||||
expect(workspaceBootstrapDrafts.load("psd-clinical")).toBeUndefined();
|
||||
expect(localStorage.getItem("thothii.workspace-registry.v1.draft.psd-clinical")).toBeNull();
|
||||
expect(localStorage.getItem("thothii.workspace-registry.v1.delete.psd-clinical")).toBeNull();
|
||||
expect(localStorage.getItem("unrelated")).toBe("keep-me");
|
||||
});
|
||||
|
||||
test("rejects draft payloads that still carry baseBlob from the removed update flow", () => {
|
||||
localStorage.setItem("thothii.workspace-registry.v2.bootstrap.psd-clinical", JSON.stringify({
|
||||
...bootstrapDraft,
|
||||
baseBlob: "b".repeat(40),
|
||||
updatedAt: "2026-08-04T10:00:00.000Z",
|
||||
});
|
||||
expect(localStorage.getItem("thothii.workspace-registry.v1.delete.psd-clinical")).not.toContain("PSD Clinical");
|
||||
}));
|
||||
|
||||
expect(workspaceBootstrapDrafts.load("psd-clinical")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("keeps valid canonical diagnostic configuration", () => {
|
||||
const configured = {
|
||||
...workspace,
|
||||
dwh: { ...workspace.dwh, supported_transports: ["postgres_direct", "rest_api"] },
|
||||
diagnostics: {
|
||||
dwh_rest: {
|
||||
method: "POST",
|
||||
path: "/rpc/ping",
|
||||
auth: "bearer",
|
||||
response: { database: "database", schema: "schema" },
|
||||
test("accepts only the workspace directory evidence root for filesystem sources", () => {
|
||||
const sanitized = sanitizeCanonicalWorkspace(evidenceWorkspace);
|
||||
|
||||
expect(sanitized).toEqual(evidenceWorkspace);
|
||||
expect(sanitized).not.toBe(evidenceWorkspace);
|
||||
expect(sanitized?.evidence).not.toBe(evidenceWorkspace.evidence);
|
||||
});
|
||||
|
||||
test("rejects filesystem Evidence that still points at workspace-content", () => {
|
||||
const invalid: CanonicalWorkspace = {
|
||||
...evidenceWorkspace,
|
||||
evidence: {
|
||||
source: {
|
||||
type: "filesystem",
|
||||
uri: "workspace-content/psd-clinical/evidence",
|
||||
patterns: ["documents/**/*.pdf", "notes/*.md"],
|
||||
max_bytes: 12_000_000,
|
||||
},
|
||||
policy,
|
||||
},
|
||||
} satisfies CanonicalWorkspace;
|
||||
workspaceDrafts.save({
|
||||
workspaceId: "psd-clinical",
|
||||
baseCommit: "a".repeat(40),
|
||||
workspace: configured,
|
||||
updatedAt: "2026-08-04T10:00:00.000Z",
|
||||
});
|
||||
};
|
||||
|
||||
expect(workspaceDrafts.load("psd-clinical")?.workspace.diagnostics).toEqual(configured.diagnostics);
|
||||
});
|
||||
|
||||
beforeEach(() => localStorage.clear());
|
||||
|
||||
test.each([
|
||||
["an unsupported schema version", { ...workspace, workspace: { ...workspace.workspace, schema_version: 1 } }],
|
||||
["an invalid DWH engine", { ...workspace, dwh: { ...workspace.dwh, engine: "mysql" } }],
|
||||
["a diagnostic path with a query", {
|
||||
...workspace,
|
||||
diagnostics: {
|
||||
dwh_rest: {
|
||||
method: "POST", path: "/rpc/ping?token=secret", auth: "bearer",
|
||||
response: { database: "database", schema: "schema" },
|
||||
},
|
||||
},
|
||||
}],
|
||||
["a diagnostic path with a fragment", {
|
||||
...workspace,
|
||||
diagnostics: {
|
||||
dwh_rest: {
|
||||
method: "POST", path: "/rpc/ping#token", auth: "bearer",
|
||||
response: { database: "database", schema: "schema" },
|
||||
},
|
||||
},
|
||||
}],
|
||||
["a diagnostic path outside the declared origin", {
|
||||
...workspace,
|
||||
diagnostics: {
|
||||
dwh_rest: {
|
||||
method: "POST", path: "https://outside.example/rpc/ping", auth: "bearer",
|
||||
response: { database: "database", schema: "schema" },
|
||||
},
|
||||
},
|
||||
}],
|
||||
["an unknown secret field", { ...workspace, secret: "must-not-be-persisted" }],
|
||||
])("rejects a draft with %s", (_reason, invalidWorkspace) => {
|
||||
workspaceDrafts.save({
|
||||
workspaceId: "psd-clinical",
|
||||
baseCommit: "a".repeat(40),
|
||||
workspace: invalidWorkspace as CanonicalWorkspace,
|
||||
updatedAt: "2026-08-04T10:00:00.000Z",
|
||||
});
|
||||
|
||||
expect(workspaceDrafts.load("psd-clinical")).toBeUndefined();
|
||||
expect(localStorage.getItem("thothii.workspace-registry.v1.draft.psd-clinical")).toBeNull();
|
||||
});
|
||||
|
||||
test("rejects a draft that tries to persist removed external semantic configuration fields", () => {
|
||||
workspaceDrafts.save({
|
||||
workspaceId: "psd-clinical",
|
||||
baseCommit: "a".repeat(40),
|
||||
workspace: {
|
||||
...workspace,
|
||||
semantic_index: {
|
||||
vector_store: {
|
||||
...workspace.semantic_index.vector_store,
|
||||
database: "vectors",
|
||||
},
|
||||
embedding: workspace.semantic_index.embedding,
|
||||
},
|
||||
} as CanonicalWorkspace,
|
||||
updatedAt: "2026-08-04T10:00:00.000Z",
|
||||
});
|
||||
|
||||
expect(workspaceDrafts.load("psd-clinical")).toBeUndefined();
|
||||
expect(localStorage.getItem("thothii.workspace-registry.v1.draft.psd-clinical")).toBeNull();
|
||||
});
|
||||
|
||||
|
||||
test.each(evidenceWorkspaces)("deep-sanitizes canonical $name Evidence", ({ workspace: configured }) => {
|
||||
const sanitized = sanitizeCanonicalWorkspace(configured);
|
||||
|
||||
expect(sanitized).toEqual(configured);
|
||||
expect(sanitized).not.toBe(configured);
|
||||
expect(sanitized?.evidence).not.toBe(configured.evidence);
|
||||
expect(sanitized?.evidence?.source).not.toBe(configured.evidence.source);
|
||||
expect(sanitized?.evidence?.policy).not.toBe(configured.evidence.policy);
|
||||
});
|
||||
|
||||
test.each(evidenceWorkspaces)("saves and reloads canonical $name Evidence", ({ workspace: configured }) => {
|
||||
workspaceDrafts.save({
|
||||
workspaceId: "psd-clinical",
|
||||
baseCommit: "a".repeat(40),
|
||||
workspace: configured,
|
||||
updatedAt: "2026-08-04T10:00:00.000Z",
|
||||
});
|
||||
|
||||
expect(workspaceDrafts.load("psd-clinical")?.workspace.evidence).toEqual(configured.evidence);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["an unknown Evidence key", { ...evidenceWorkspaces[0].workspace.evidence, extra: "unexpected" }],
|
||||
["a secret-shaped source key", {
|
||||
...evidenceWorkspaces[1].workspace.evidence,
|
||||
source: { ...evidenceWorkspaces[1].workspace.evidence.source, signed_urls_file: "/run/secrets/urls" },
|
||||
}],
|
||||
["an HTTP URI with credentials", {
|
||||
...evidenceWorkspaces[1].workspace.evidence,
|
||||
source: { ...evidenceWorkspaces[1].workspace.evidence.source, uris: ["https://user:secret@evidence.example/file"] },
|
||||
}],
|
||||
["an HTTP URI with a signed query", {
|
||||
...evidenceWorkspaces[1].workspace.evidence,
|
||||
source: { ...evidenceWorkspaces[1].workspace.evidence.source, uris: ["https://evidence.example/file?token=secret"] },
|
||||
}],
|
||||
["an unsafe S3 URI", {
|
||||
...evidenceWorkspaces[2].workspace.evidence,
|
||||
source: { ...evidenceWorkspaces[2].workspace.evidence.source, uri: "s3://user:secret@clinical-evidence/published/" },
|
||||
}],
|
||||
["an invalid zero policy value", {
|
||||
...evidenceWorkspaces[0].workspace.evidence,
|
||||
policy: { ...policy, max_chunk_chars: 0 },
|
||||
}],
|
||||
["an unsafe integer policy value", {
|
||||
...evidenceWorkspaces[0].workspace.evidence,
|
||||
policy: { ...policy, retain_published_generations: Number.MAX_SAFE_INTEGER + 1 },
|
||||
}],
|
||||
["a malformed source union", {
|
||||
...evidenceWorkspaces[0].workspace.evidence,
|
||||
source: { ...evidenceWorkspaces[0].workspace.evidence.source, uris: ["https://evidence.example/file"] },
|
||||
}],
|
||||
])("rejects %s instead of putting it in browser state", (_reason, evidence) => {
|
||||
const invalid = { ...workspace, evidence };
|
||||
expect(sanitizeCanonicalWorkspace(invalid)).toBeUndefined();
|
||||
|
||||
workspaceDrafts.save({
|
||||
workspaceId: "psd-clinical",
|
||||
baseCommit: "a".repeat(40),
|
||||
workspace: invalid as CanonicalWorkspace,
|
||||
updatedAt: "2026-08-04T10:00:00.000Z",
|
||||
});
|
||||
expect(workspaceDrafts.load("psd-clinical")).toBeUndefined();
|
||||
workspaceBootstrapDrafts.save({ ...bootstrapDraft, workspace: invalid });
|
||||
expect(workspaceBootstrapDrafts.load("psd-clinical")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("continues to sanitize workspaces without Evidence", () => {
|
||||
|
||||
@@ -4,21 +4,15 @@ import type {
|
||||
} from "../api/workspaces";
|
||||
export { workspacePreferences, type WorkspacePreference } from "./preferences";
|
||||
|
||||
export interface WorkspaceDraft {
|
||||
export interface WorkspaceBootstrapDraft {
|
||||
workspaceId: string;
|
||||
baseCommit: string;
|
||||
baseBlob?: string;
|
||||
workspace: CanonicalWorkspace;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** A publishable deletion intent; it deliberately carries no workspace body. */
|
||||
export interface WorkspaceDeletionDraft {
|
||||
id: string;
|
||||
baseCommit: string;
|
||||
baseBlob: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
/** @deprecated Use WorkspaceBootstrapDraft. */
|
||||
export type WorkspaceDraft = WorkspaceBootstrapDraft;
|
||||
|
||||
export const WORKSPACE_SUMMARY_ERROR = "Could not load workspace registry. Please retry.";
|
||||
export const WORKSPACE_POLICY_ERROR = "Could not load selected workspace policy. Please retry.";
|
||||
@@ -123,9 +117,9 @@ export const workspacePolicyGate = {
|
||||
},
|
||||
};
|
||||
|
||||
const PREFIX = "thothii.workspace-registry.v1";
|
||||
const DRAFT_PREFIX = `${PREFIX}.draft.`;
|
||||
const DELETE_DRAFT_PREFIX = `${PREFIX}.delete.`;
|
||||
const LEGACY_PREFIX = "thothii.workspace-registry.v1";
|
||||
const PREFIX = "thothii.workspace-registry.v2";
|
||||
const DRAFT_PREFIX = `${PREFIX}.bootstrap.`;
|
||||
|
||||
function storage(): Storage | undefined {
|
||||
try { return window.localStorage; } catch { return undefined; }
|
||||
@@ -234,7 +228,7 @@ function copyFilesystemEvidence(value: unknown, id: string): EvidenceSource | un
|
||||
const maxBytes = positiveInteger(source?.max_bytes);
|
||||
if (
|
||||
source?.type !== "filesystem"
|
||||
|| uri !== `workspace-content/${id}/evidence`
|
||||
|| uri !== `${id}/evidence`
|
||||
|| !Array.isArray(patterns)
|
||||
|| patterns.length === 0
|
||||
|| !patterns.every((pattern) => typeof pattern === "string" && isSafeEvidencePattern(pattern))
|
||||
@@ -480,20 +474,47 @@ export function sanitizeCanonicalWorkspace(value: unknown): CanonicalWorkspace |
|
||||
};
|
||||
}
|
||||
|
||||
function normalize(value: unknown): WorkspaceDraft | undefined {
|
||||
const source = exactRecord(value, ["workspaceId", "baseCommit", "baseBlob", "workspace", "updatedAt"]);
|
||||
const workspace = sanitizeCanonicalWorkspace(source?.workspace);
|
||||
function repairBootstrapWorkspace(value: unknown, id: string): CanonicalWorkspace | undefined {
|
||||
const direct = sanitizeCanonicalWorkspace(value);
|
||||
if (direct) return direct;
|
||||
const source = exactRecord(value, [
|
||||
"workspace", "dwh", "semantic_index", "llm_policy", "diagnostics", "evidence",
|
||||
]);
|
||||
const evidence = exactRecord(source?.evidence, ["source", "policy"]);
|
||||
const evidenceSource = exactRecord(evidence?.source, ["type", "uri", "patterns", "max_bytes"]);
|
||||
if (
|
||||
source
|
||||
&& evidence
|
||||
&& evidenceSource?.type === "filesystem"
|
||||
&& typeof evidenceSource.uri === "string"
|
||||
&& /^[a-z][a-z0-9-]{2,62}\/evidence$/.test(evidenceSource.uri)
|
||||
) {
|
||||
return sanitizeCanonicalWorkspace({
|
||||
...source,
|
||||
evidence: {
|
||||
...evidence,
|
||||
source: {
|
||||
...evidenceSource,
|
||||
uri: `${id}/evidence`,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function normalize(value: unknown): WorkspaceBootstrapDraft | undefined {
|
||||
const source = exactRecord(value, ["workspaceId", "baseCommit", "workspace", "updatedAt"]);
|
||||
const id = workspaceId(source?.workspaceId);
|
||||
const workspace = id ? repairBootstrapWorkspace(source?.workspace, id) : undefined;
|
||||
const baseCommit = typeof source?.baseCommit === "string" && /^[0-9a-f]{40}$/.test(source.baseCommit) ? source.baseCommit : undefined;
|
||||
const baseBlob = source?.baseBlob === undefined ? undefined : typeof source.baseBlob === "string" && /^[0-9a-f]{40}$/.test(source.baseBlob) ? source.baseBlob : undefined;
|
||||
const updatedAt = typeof source?.updatedAt === "string" && Number.isFinite(Date.parse(source.updatedAt)) ? source.updatedAt : undefined;
|
||||
if (!source || !workspace || !id || id !== workspace.workspace.id || !baseCommit || (source.baseBlob !== undefined && !baseBlob) || !updatedAt) {
|
||||
if (!source || !workspace || !id || id !== workspace.workspace.id || !baseCommit || !updatedAt) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
workspaceId: id,
|
||||
baseCommit,
|
||||
...(baseBlob ? { baseBlob } : {}),
|
||||
workspace,
|
||||
updatedAt,
|
||||
};
|
||||
@@ -503,28 +524,21 @@ function key(id: string): string {
|
||||
return `${DRAFT_PREFIX}${encodeURIComponent(id)}`;
|
||||
}
|
||||
|
||||
function deletionKey(id: string): string {
|
||||
return `${DELETE_DRAFT_PREFIX}${encodeURIComponent(id)}`;
|
||||
function purgeLegacyRegistryDrafts(): void {
|
||||
const store = storage();
|
||||
if (!store) return;
|
||||
const keys = Array.from({ length: store.length }, (_, index) => store.key(index)).filter((value): value is string => value !== null);
|
||||
for (const entry of keys) {
|
||||
if (entry.startsWith(`${LEGACY_PREFIX}.draft.`) || entry.startsWith(`${LEGACY_PREFIX}.delete.`)) {
|
||||
store.removeItem(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeDeletion(value: unknown): WorkspaceDeletionDraft | undefined {
|
||||
const source = exactRecord(value, ["id", "baseCommit", "baseBlob", "updatedAt"]);
|
||||
const id = workspaceId(source?.id);
|
||||
const baseCommit = typeof source?.baseCommit === "string" && /^[0-9a-f]{40}$/.test(source.baseCommit)
|
||||
? source.baseCommit
|
||||
: undefined;
|
||||
const baseBlob = typeof source?.baseBlob === "string" && /^[0-9a-f]{40}$/.test(source.baseBlob)
|
||||
? source.baseBlob
|
||||
: undefined;
|
||||
const updatedAt = typeof source?.updatedAt === "string" && Number.isFinite(Date.parse(source.updatedAt))
|
||||
? source.updatedAt
|
||||
: undefined;
|
||||
return id && baseCommit && baseBlob && updatedAt ? { id, baseCommit, baseBlob, updatedAt } : undefined;
|
||||
}
|
||||
|
||||
/** Browser-only workspace drafts. Saving or editing one never calls the server. */
|
||||
export const workspaceDrafts = {
|
||||
load(id: string): WorkspaceDraft | undefined {
|
||||
/** Browser-only bootstrap drafts. Saving or editing one never calls the server. */
|
||||
export const workspaceBootstrapDrafts = {
|
||||
load(id: string): WorkspaceBootstrapDraft | undefined {
|
||||
purgeLegacyRegistryDrafts();
|
||||
try {
|
||||
const raw = storage()?.getItem(key(id));
|
||||
return raw ? normalize(JSON.parse(raw)) : undefined;
|
||||
@@ -533,35 +547,34 @@ export const workspaceDrafts = {
|
||||
}
|
||||
},
|
||||
|
||||
save(draft: WorkspaceDraft): void {
|
||||
save(draft: WorkspaceBootstrapDraft): void {
|
||||
purgeLegacyRegistryDrafts();
|
||||
const safe = normalize(draft);
|
||||
if (!safe) return;
|
||||
try { storage()?.setItem(key(safe.workspaceId), JSON.stringify(safe)); } catch { /* storage is optional */ }
|
||||
},
|
||||
|
||||
discard(id: string): void {
|
||||
purgeLegacyRegistryDrafts();
|
||||
try { storage()?.removeItem(key(id)); } catch { /* storage is optional */ }
|
||||
},
|
||||
};
|
||||
|
||||
/** Browser-only deletion drafts. Task 10 alone may publish one. */
|
||||
/** @deprecated Use workspaceBootstrapDrafts. */
|
||||
export const workspaceDrafts = workspaceBootstrapDrafts;
|
||||
|
||||
|
||||
/** @deprecated Removed in P1.1; existing workspaces are curator-owned and deletions are not drafted in-browser. */
|
||||
export interface WorkspaceDeletionDraft {
|
||||
id: string;
|
||||
baseCommit: string;
|
||||
baseBlob: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** @deprecated Removed in P1.1; kept temporarily so legacy imports compile during the UI transition. */
|
||||
export const workspaceDeletionDrafts = {
|
||||
load(id: string): WorkspaceDeletionDraft | undefined {
|
||||
try {
|
||||
const raw = storage()?.getItem(deletionKey(id));
|
||||
return raw ? normalizeDeletion(JSON.parse(raw)) : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
save(draft: WorkspaceDeletionDraft): void {
|
||||
const safe = normalizeDeletion(draft);
|
||||
if (!safe) return;
|
||||
try { storage()?.setItem(deletionKey(safe.id), JSON.stringify(safe)); } catch { /* storage is optional */ }
|
||||
},
|
||||
|
||||
discard(id: string): void {
|
||||
try { storage()?.removeItem(deletionKey(id)); } catch { /* storage is optional */ }
|
||||
},
|
||||
load(_id: string): WorkspaceDeletionDraft | undefined { return undefined; },
|
||||
save(_draft: WorkspaceDeletionDraft): void {},
|
||||
discard(_id: string): void {},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user