refactor: make workspace descriptors schema v3 only

This commit is contained in:
2026-08-10 20:21:20 +02:00
parent 5d016ce635
commit 512b0261b1
12 changed files with 187 additions and 295 deletions
+15 -7
View File
@@ -15,11 +15,10 @@ import {
import { resolveRuntimeBindings } from "../workspaces/bindings.js";
import { buildInstallationContract, renderWorkspaceDocs } from "../workspaces/contracts.js";
import {
isCanonicalWorkspace,
parseWorkspaceYaml,
serializeWorkspaceYaml,
validateCanonicalWorkspace,
validateOperationalWorkspace,
validateWorkspaceDescriptor,
type CanonicalWorkspace,
type WorkspaceDescriptor,
} from "../workspaces/schema.js";
@@ -177,7 +176,7 @@ async function importDraft(source: Buffer, config: WorkspaceRegistryConfig): Pro
}
try {
const descriptor = parseWorkspaceYaml(utf8(files["workspace.yaml"]));
const workspace = validateCanonicalWorkspace(descriptor);
const workspace = validateWorkspaceDescriptor(descriptor);
const docs = renderWorkspaceDocs(workspace);
if (
workspace.workspace.id !== manifest.workspace_id
@@ -231,6 +230,14 @@ function workspaceErrorStatus(code: keyof typeof SAFE_MESSAGES): number {
return 400;
}
function validatedWorkspace(value: unknown): WorkspaceDescriptor | undefined {
try {
return validateWorkspaceDescriptor(value);
} catch {
return undefined;
}
}
function errorReply(reply: FastifyReply, error: unknown) {
const code = workspaceErrorCode(error);
const body: Record<string, unknown> = { code, message: SAFE_MESSAGES[code] };
@@ -253,7 +260,8 @@ function errorReply(reply: FastifyReply, error: unknown) {
) body[key] = revision;
}
for (const key of ["base", "local", "remote"] as const) {
if (conflict[key] && isCanonicalWorkspace(conflict[key] as WorkspaceDescriptor)) body[key] = conflict[key];
const workspace = validatedWorkspace(conflict[key]);
if (workspace) body[key] = workspace;
}
}
return reply.code(workspaceErrorStatus(code)).send(body);
@@ -262,7 +270,7 @@ function errorReply(reply: FastifyReply, error: unknown) {
function publishRequest(value: unknown): PublishWorkspaceRequest {
const parsed = publishPayload.parse(value);
if (parsed.action === "delete") return parsed;
return { ...parsed, workspace: validateCanonicalWorkspace(parsed.workspace) };
return { ...parsed, workspace: validateWorkspaceDescriptor(parsed.workspace) };
}
export function workspaceRoutes(app: FastifyInstance, deps: WorkspaceRoutesDeps): void {
@@ -321,7 +329,7 @@ export function workspaceRoutes(app: FastifyInstance, deps: WorkspaceRoutesDeps)
app.post("/workspaces/validate", async (request, reply) => {
try {
const { workspace } = workspacePayload.parse(request.body);
const canonical = validateCanonicalWorkspace(workspace);
const canonical = validateWorkspaceDescriptor(workspace);
return { workspace: canonical, contract: buildInstallationContract(canonical) };
} catch (error) {
return errorReply(reply, error);
@@ -365,7 +373,7 @@ export function workspaceRoutes(app: FastifyInstance, deps: WorkspaceRoutesDeps)
try {
const { id } = z.object({ id: workspaceId }).parse(request.params);
const { workspace } = await deps.registry.read(id);
const canonical = validateCanonicalWorkspace(workspace);
const canonical = validateWorkspaceDescriptor(workspace);
const bundle = await exportBundle(canonical);
return reply
.type("application/zip")
+4 -4
View File
@@ -9,6 +9,7 @@ import {
type VectorTransport,
type WorkspaceDescriptor,
} from "./schema.js";
import type { DeprecatedV2Descriptor } from "./deprecated-v2-descriptor.js";
export interface ResolvedEvidenceBinding {
values: Record<string, string>;
@@ -85,7 +86,7 @@ function requiredSuffixes(
const required = REQUIRED_SUFFIXES[role][transport] ?? [];
const diagnostic = role === "DWH"
? workspace.diagnostics?.dwh_rest
: workspace.diagnostics?.vector_rest?.metadata;
: (workspace as unknown as DeprecatedV2Descriptor).diagnostics?.vector_rest?.metadata;
return transport === "rest_api" && diagnostic?.auth === "none"
? required.filter((suffix) => suffix !== "API_KEY_FILE")
: required;
@@ -107,14 +108,13 @@ export function resolveBinding(
}
const contract = buildInstallationContract(descriptor);
const legacy = descriptor as unknown as DeprecatedV2Descriptor;
const variables = contract.variables.filter((variable) => variable.role === role);
const transportVariable = variables.find((variable) => variable.suffix === "TRANSPORT");
const supported = role === "DWH"
? descriptor.dwh.supported_transports
: role === "VECTOR"
? ("supported_transports" in descriptor.semantic_index.vector_store
? descriptor.semantic_index.vector_store.supported_transports
: [])
? legacy.semantic_index.vector_store.supported_transports
: ["rest_api"] as const;
const selectedValue = transportVariable ? env[transportVariable.name] : undefined;
const selectedTransport = isTransport(selectedValue) ? selectedValue : supported[0];
+7 -6
View File
@@ -1,5 +1,6 @@
import { validateWorkspaceDescriptor } from "./schema.js";
import type { DwhTransport, VectorTransport, WorkspaceDescriptor } from "./schema.js";
import type { DeprecatedV2Descriptor } from "./deprecated-v2-descriptor.js";
export type InstallationRole = "DWH" | "VECTOR" | "VECTOR_WRITER" | "EMBEDDING" | "EVIDENCE";
export type InstallationSuffix =
@@ -145,25 +146,25 @@ function evidenceVariables(
export function buildInstallationContract(workspace: WorkspaceDescriptor): InstallationContract {
const descriptor = validateWorkspaceDescriptor(workspace);
const namespace = namespaceFor(descriptor);
const schemaVersion = Number(descriptor.workspace.schema_version);
const legacy = descriptor as unknown as DeprecatedV2Descriptor;
return {
workspaceId: descriptor.workspace.id,
namespace,
variables: [
...connectorVariables(namespace, "DWH", descriptor.dwh.supported_transports),
...(descriptor.workspace.schema_version === 2
...(schemaVersion === 2
? connectorVariables(
namespace,
"VECTOR",
"supported_transports" in descriptor.semantic_index.vector_store
? descriptor.semantic_index.vector_store.supported_transports
: [],
legacy.semantic_index.vector_store.supported_transports,
)
: []),
...(descriptor.workspace.schema_version === 2 && descriptor.semantic_index.vector_writer
...(schemaVersion === 2 && legacy.semantic_index.vector_writer
? [createVariable(namespace, "VECTOR_WRITER", "API_KEY_FILE")]
: []),
...(descriptor.workspace.schema_version === 2
...(schemaVersion === 2
? EMBEDDING_SUFFIXES.map((suffix) => createVariable(namespace, "EMBEDDING", suffix))
: []),
...evidenceVariables(namespace, descriptor),
@@ -0,0 +1,52 @@
import type {
CanonicalDiagnostics,
DwhTransport,
VectorTransport,
} from "./schema.js";
/**
* Temporary compile-time shape for legacy runtime branches that will be removed separately.
* It is deliberately not part of the accepted workspace descriptor schema.
*/
export interface DeprecatedV2Descriptor {
workspace: {
schema_version: 2;
id: string;
name: string;
description?: string;
language: "en" | "it";
};
dwh: {
engine: "postgres";
database: string;
schema: string;
port?: number;
timeout_ms?: number;
supported_transports: DwhTransport[];
};
semantic_index: {
vector_store: {
engine: "pgvector";
database: string;
schema: string;
collection: string;
dimensions: number;
distance: "cosine" | "l2" | "inner_product";
port?: number;
timeout_ms?: number;
supported_transports: VectorTransport[];
};
vector_writer?: Record<string, never>;
embedding: {
provider: "ollama_compatible" | "openai_compatible";
model: string;
dimensions: number;
timeout_ms?: number;
};
};
llm_policy: {
default?: `${string}/${string}`;
allowed: `${string}/${string}`[];
};
diagnostics?: CanonicalDiagnostics;
}
+9 -9
View File
@@ -12,9 +12,9 @@ import {
resolveDiagnosticUrl,
validateWorkspaceDescriptor,
type RestDiagnosticRequest,
type WorkspaceV2,
type WorkspaceDescriptor,
} from "./schema.js";
import type { DeprecatedV2Descriptor } from "./deprecated-v2-descriptor.js";
import type { WorkspaceErrorCode } from "./types.js";
import type { SemanticRuntimeConfig } from "./runtime-renderer.js";
@@ -543,7 +543,7 @@ export function createProductionWorkspaceDiagnoser(
options: { writeProbe: boolean },
): Promise<WorkspaceDiagnostics> => {
const descriptor = validateWorkspaceDescriptor(workspace);
if (descriptor.workspace.schema_version !== 3) {
if (Number(descriptor.workspace.schema_version) !== 3) {
return await legacyDiagnoser(descriptor, bindings, options);
}
return await diagnoseSchemaV3Workspace(
@@ -618,11 +618,11 @@ function diagnosticError(code: WorkspaceErrorCode, field?: string): Diagnostic {
}
function bindingName(
workspace: WorkspaceDescriptor,
workspace: WorkspaceDescriptor | DeprecatedV2Descriptor,
role: "DWH" | "VECTOR" | "VECTOR_WRITER" | "EMBEDDING",
suffix: string,
): string {
const entry = buildInstallationContract(workspace).variables.find((variable) => (
const entry = buildInstallationContract(workspace as WorkspaceDescriptor).variables.find((variable) => (
variable.role === role && variable.suffix === suffix
));
if (!entry) throw new Error(`workspace contract is missing ${role}_${suffix}`);
@@ -798,7 +798,7 @@ async function diagnoseSchemaV3Workspace(
}
function diagnosticsForMissingBindings(
workspace: WorkspaceV2,
workspace: DeprecatedV2Descriptor,
bindings: RuntimeBindings,
): Diagnostic[] {
const missing = new Set([
@@ -815,7 +815,7 @@ function diagnosticsForMissingBindings(
}
function connectorRequest(
workspace: WorkspaceV2,
workspace: DeprecatedV2Descriptor,
role: ConnectorRole,
bindings: RuntimeBindings,
timeoutMs: number,
@@ -889,7 +889,7 @@ function connectorRequest(
}
function tunnelProbeRequest(
workspace: WorkspaceV2,
workspace: DeprecatedV2Descriptor,
role: ConnectorRole,
bindings: RuntimeBindings,
timeoutMs: number,
@@ -934,10 +934,10 @@ export function createWorkspaceDiagnoser(
options: { writeProbe: boolean },
): Promise<WorkspaceDiagnostics> {
const descriptor = validateWorkspaceDescriptor(workspace);
if (descriptor.workspace.schema_version !== 2) {
if (Number(descriptor.workspace.schema_version) !== 2) {
return { activatable: false, diagnostics: [diagnosticError("workspace_not_activatable")] };
}
const canonical = descriptor as WorkspaceV2;
const canonical = descriptor as unknown as DeprecatedV2Descriptor;
const diagnostics = diagnosticsForMissingBindings(canonical, bindings);
if (diagnostics.length > 0) return { activatable: false, diagnostics };
+3 -2
View File
@@ -1,7 +1,8 @@
import { validateOperationalWorkspace, type WorkspaceV2, type WorkspaceV3 } from "./schema.js";
import { validateOperationalWorkspace, type WorkspaceV3 } from "./schema.js";
import type { DeprecatedV2Descriptor } from "./deprecated-v2-descriptor.js";
export function migrateWorkspaceV2ToV3(
legacy: WorkspaceV2,
legacy: DeprecatedV2Descriptor,
collection: string,
): WorkspaceV3 {
return validateOperationalWorkspace({
+4 -4
View File
@@ -4,9 +4,9 @@ import { buildInstallationContract } from "./contracts.js";
import {
validateWorkspaceDescriptor,
type WorkspaceDescriptor,
type WorkspaceV2,
type WorkspaceV3,
} from "./schema.js";
import type { DeprecatedV2Descriptor } from "./deprecated-v2-descriptor.js";
import type { ResolvedBinding, ResolvedEvidenceBinding, RuntimeBindings } from "./bindings.js";
export type { RuntimeBindings } from "./bindings.js";
@@ -216,8 +216,8 @@ export function renderRuntimeConfig(
if (!variable) throw new Error(`workspace contract is missing ${role}_${suffix}`);
return variable.name;
};
if (descriptor.workspace.schema_version !== 2) {
if (descriptor.workspace.schema_version === 1) {
if (Number(descriptor.workspace.schema_version) !== 2) {
if (Number(descriptor.workspace.schema_version) === 1) {
throw new Error("Workspace descriptor requires explicit migration to schema version 2");
}
const canonicalV3 = descriptor as WorkspaceV3;
@@ -290,7 +290,7 @@ export function renderRuntimeConfig(
return stringify(renderedV3, { lineWidth: 0, sortMapEntries: false });
}
const canonical = descriptor as WorkspaceV2;
const canonical = descriptor as unknown as DeprecatedV2Descriptor;
if ([...bindings.dwh.missing, ...bindings.vector.missing, ...bindings.embedding.missing].length > 0) {
throw new Error("runtime configuration requires complete bindings");
}
+14 -175
View File
@@ -34,8 +34,8 @@ export interface CanonicalDiagnostics {
embedding?: RestDiagnosticRequest & { response: { model: string; dimensions: string } };
}
interface WorkspaceMetadata<Version extends 1 | 2 | 3> {
schema_version: Version;
interface WorkspaceMetadata {
schema_version: 3;
id: string;
name: string;
description?: string;
@@ -51,50 +51,22 @@ interface WorkspaceDwh {
supported_transports: DwhTransport[];
}
interface VectorStore {
engine: "pgvector";
collection: string;
dimensions: number;
distance: "cosine" | "l2" | "inner_product";
port?: number;
timeout_ms?: number;
supported_transports: VectorTransport[];
}
interface SemanticIndex<TVectorStore extends VectorStore> {
vector_store: TVectorStore;
vector_writer?: Record<string, never>;
embedding: {
provider: "ollama_compatible" | "openai_compatible";
model: string;
dimensions: number;
timeout_ms?: number;
};
}
interface WorkspaceBase<Version extends 1 | 2 | 3, TVectorStore, TDiagnostics = CanonicalDiagnostics> {
workspace: WorkspaceMetadata<Version>;
interface WorkspaceBase<TVectorStore> {
workspace: WorkspaceMetadata;
dwh: WorkspaceDwh;
semantic_index: {
vector_store: TVectorStore;
vector_writer?: Record<string, never>;
embedding: Version extends 3 ? {
embedding: {
provider: "ollama_internal";
model: "qwen3-embedding:0.6b";
dimensions: 1024;
timeout_ms?: number;
} : {
provider: "ollama_compatible" | "openai_compatible";
model: string;
dimensions: number;
timeout_ms?: number;
};
};
llm_policy: {
default?: `${string}/${string}`;
allowed: `${string}/${string}`[];
};
diagnostics?: TDiagnostics;
diagnostics?: Pick<CanonicalDiagnostics, "dwh_rest">;
}
interface QdrantVectorStore {
@@ -147,17 +119,12 @@ export interface WorkspaceEvidence {
policy: EvidencePolicy;
}
export interface WorkspaceV3 extends WorkspaceBase<3, QdrantVectorStore> {
export interface WorkspaceV3 extends WorkspaceBase<QdrantVectorStore> {
evidence?: WorkspaceEvidence;
}
export interface WorkspaceV2 extends WorkspaceBase<2, VectorStore & { database: string; schema: string }> {}
/** A readable, non-operational v1 descriptor. It must be explicitly migrated before use. */
export interface WorkspaceV1 extends WorkspaceBase<1, VectorStore & { database?: string; schema?: string }> {}
export type CanonicalWorkspace = WorkspaceV3;
export type LegacyWorkspace = WorkspaceV1 | WorkspaceV2;
export type WorkspaceDescriptor = WorkspaceV1 | WorkspaceV2 | WorkspaceV3;
export type WorkspaceDescriptor = WorkspaceV3;
const workspaceId = z.string().regex(/^[a-z][a-z0-9-]{2,62}$/, {
message: "workspace id must match ^[a-z][a-z0-9-]{2,62}$",
@@ -165,7 +132,6 @@ const workspaceId = z.string().regex(/^[a-z][a-z0-9-]{2,62}$/, {
const identifier = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, {
message: "database identifiers must start with a letter or underscore",
});
const dimensions = z.number().int().positive().max(32_768);
const port = z.number().int().min(1).max(65_535);
const timeoutMs = z.number().int().positive();
const modelReference = z.string().regex(/^[^/\s]+\/[^/\s]+$/, {
@@ -190,29 +156,6 @@ const restDiagnosticRequest = z.object({
const dwhRestDiagnostic = restDiagnosticRequest.extend({
response: z.object({ database: responseField, schema: responseField }).strict(),
}).strict();
const vectorMetadataDiagnostic = restDiagnosticRequest.extend({
response: z.object({
collection: responseField,
dimensions: responseField,
distance: responseField,
}).strict(),
}).strict();
const reversibleVectorProbe = restDiagnosticRequest.extend({
method: z.literal("POST"),
auth: z.enum(["bearer", "x-api-key"]),
response: z.object({ operation: responseField }).strict(),
}).strict();
const embeddingDiagnostic = restDiagnosticRequest.extend({
response: z.object({ model: responseField, dimensions: responseField }).strict(),
}).strict();
const diagnosticsSchema = z.object({
dwh_rest: dwhRestDiagnostic.optional(),
vector_rest: z.object({
metadata: vectorMetadataDiagnostic,
reversible_probe: reversibleVectorProbe.optional(),
}).strict().optional(),
embedding: embeddingDiagnostic.optional(),
}).strict().optional();
const dwhSchema = z.object({
engine: z.literal("postgres"),
@@ -222,36 +165,11 @@ const dwhSchema = z.object({
timeout_ms: timeoutMs.optional(),
supported_transports: z.array(z.enum(DWH_TRANSPORTS)).min(1),
}).strict();
const embeddingSchema = z.object({
provider: z.enum(["ollama_compatible", "openai_compatible"]),
model: z.string().trim().min(1),
dimensions,
timeout_ms: timeoutMs.optional(),
}).strict();
const internalEmbeddingSchema = z.object({
provider: z.literal("ollama_internal"),
model: z.literal("qwen3-embedding:0.6b"),
dimensions: z.literal(1024),
}).strict();
const vectorStoreShape = {
engine: z.literal("pgvector"),
collection: identifier,
dimensions,
distance: z.enum(["cosine", "l2", "inner_product"]),
port: port.optional(),
timeout_ms: timeoutMs.optional(),
supported_transports: z.array(z.enum(VECTOR_TRANSPORTS)).min(1),
};
const legacyVectorStoreSchema = z.object({
...vectorStoreShape,
database: identifier.optional(),
schema: identifier.optional(),
}).strict();
const canonicalVectorStoreSchema = z.object({
...vectorStoreShape,
database: identifier,
schema: identifier,
}).strict();
const qdrantVectorStoreSchema = z.object({
engine: z.literal("qdrant"),
collection: workspaceId,
@@ -420,13 +338,6 @@ function unique<T>(values: readonly T[], context: z.RefinementCtx, path: Propert
function workspaceInvariants(workspace: any, context: z.RefinementCtx): void {
unique(workspace.dwh.supported_transports, context, ["dwh", "supported_transports"]);
if ("supported_transports" in workspace.semantic_index.vector_store) {
unique(
workspace.semantic_index.vector_store.supported_transports,
context,
["semantic_index", "vector_store", "supported_transports"],
);
}
unique(workspace.llm_policy.allowed, context, ["llm_policy", "allowed"]);
if (workspace.evidence?.source.type === "filesystem") {
@@ -461,50 +372,8 @@ function workspaceInvariants(workspace: any, context: z.RefinementCtx): void {
message: "diagnostics.dwh_rest requires dwh rest_api transport support",
});
}
if (
workspace.diagnostics?.vector_rest
&& (
!("supported_transports" in workspace.semantic_index.vector_store)
|| !workspace.semantic_index.vector_store.supported_transports.includes("rest_api")
)
) {
context.addIssue({
code: "custom",
path: ["diagnostics", "vector_rest"],
message: "diagnostics.vector_rest requires vector_store rest_api transport support",
});
}
}
const workspaceShape = {
dwh: dwhSchema,
llm_policy: llmPolicySchema,
diagnostics: diagnosticsSchema,
};
const WorkspaceV1Schema = z.object({
...workspaceShape,
workspace: z.object({
schema_version: z.literal(1), id: workspaceId, name: z.string().trim().min(1),
description: z.string().trim().min(1).optional(), language: z.enum(["en", "it"]),
}).strict(),
semantic_index: z.object({
vector_store: legacyVectorStoreSchema,
vector_writer: z.object({}).strict().optional(),
embedding: embeddingSchema,
}).strict(),
}).strict().superRefine(workspaceInvariants);
const WorkspaceV2Schema = z.object({
...workspaceShape,
workspace: z.object({
schema_version: z.literal(2), id: workspaceId, name: z.string().trim().min(1),
description: z.string().trim().min(1).optional(), language: z.enum(["en", "it"]),
}).strict(),
semantic_index: z.object({
vector_store: canonicalVectorStoreSchema,
vector_writer: z.object({}).strict().optional(),
embedding: embeddingSchema,
}).strict(),
}).strict().superRefine(workspaceInvariants);
const WorkspaceV3Schema = z.object({
dwh: dwhSchema,
llm_policy: llmPolicySchema,
@@ -521,7 +390,7 @@ const WorkspaceV3Schema = z.object({
embedding: internalEmbeddingSchema,
}).strict(),
}).strict().superRefine(workspaceInvariants);
const WorkspaceDescriptorSchema = z.union([WorkspaceV3Schema, WorkspaceV2Schema, WorkspaceV1Schema]);
const WorkspaceDescriptorSchema = WorkspaceV3Schema;
export function parseWorkspaceYaml(source: string): WorkspaceDescriptor {
const documents = parseAllDocuments(source, { uniqueKeys: true });
@@ -538,46 +407,16 @@ export function validateWorkspaceDescriptor(workspace: unknown): WorkspaceDescri
return WorkspaceDescriptorSchema.parse(workspace) as WorkspaceDescriptor;
}
export function isCanonicalWorkspace(workspace: WorkspaceDescriptor): workspace is CanonicalWorkspace {
return workspace.workspace.schema_version === 3;
export function isCanonicalWorkspace(workspace: unknown): workspace is CanonicalWorkspace {
return WorkspaceDescriptorSchema.safeParse(workspace).success;
}
export function isOperationalWorkspace(workspace: WorkspaceDescriptor): workspace is WorkspaceV3 {
return workspace.workspace.schema_version === 3;
export function isOperationalWorkspace(workspace: unknown): workspace is WorkspaceV3 {
return WorkspaceDescriptorSchema.safeParse(workspace).success;
}
/** Rejects readable v1 descriptors at every operational boundary until a caller migrates them. */
export function validateCanonicalWorkspace(workspace: unknown): CanonicalWorkspace {
return validateOperationalWorkspace(workspace);
}
/** Rejects readable v1/v2 descriptors at every operational boundary until a caller migrates them. */
export function validateOperationalWorkspace(workspace: unknown): WorkspaceV3 {
const descriptor = validateWorkspaceDescriptor(workspace);
if (!isOperationalWorkspace(descriptor)) {
throw new Error("Workspace descriptor requires explicit migration to schema version 3");
}
return descriptor;
}
/**
* Explicitly upgrades a readable v1 descriptor. The caller must supply vector identity; the
* transformer never derives it from DWH identity, even where both services share a database.
*/
export function migrateWorkspaceV1ToV2(
workspace: WorkspaceV1,
vectorIdentity: { database: string; schema: string },
): WorkspaceV2 {
const legacy = WorkspaceV1Schema.parse(workspace) as WorkspaceV1;
const identity = z.object({ database: identifier, schema: identifier }).strict().parse(vectorIdentity);
return WorkspaceV2Schema.parse({
...legacy,
workspace: { ...legacy.workspace, schema_version: 2 },
semantic_index: {
...legacy.semantic_index,
vector_store: { ...legacy.semantic_index.vector_store, ...identity },
},
}) as WorkspaceV2;
return validateWorkspaceDescriptor(workspace);
}
/** Builds a request URL only after rejecting values that can leave the declared service origin. */
+1 -1
View File
@@ -16,4 +16,4 @@ export type WorkspaceErrorCode =
| "git_auth_failed" | "git_non_fast_forward" | "git_push_rejected"
| "connector_unavailable" | "semantic_index_incompatible";
export type { WorkspaceV2, WorkspaceV3 } from "./schema.js";
export type { WorkspaceV3 } from "./schema.js";