feat: define internal semantic workspace schema

This commit is contained in:
2026-08-08 16:43:54 +02:00
parent 4fe4049a24
commit 2f7f923c4d
9 changed files with 463 additions and 154 deletions
+94 -25
View File
@@ -34,7 +34,7 @@ export interface CanonicalDiagnostics {
embedding?: RestDiagnosticRequest & { response: { model: string; dimensions: string } };
}
interface WorkspaceMetadata<Version extends 1 | 2> {
interface WorkspaceMetadata<Version extends 1 | 2 | 3> {
schema_version: Version;
id: string;
name: string;
@@ -72,23 +72,52 @@ interface SemanticIndex<TVectorStore extends VectorStore> {
};
}
interface WorkspaceBase<Version extends 1 | 2, TVectorStore extends VectorStore> {
interface WorkspaceBase<Version extends 1 | 2 | 3, TVectorStore, TDiagnostics = CanonicalDiagnostics> {
workspace: WorkspaceMetadata<Version>;
dwh: WorkspaceDwh;
semantic_index: SemanticIndex<TVectorStore>;
semantic_index: {
vector_store: TVectorStore;
vector_writer?: Record<string, never>;
embedding: Version extends 3 ? {
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?: CanonicalDiagnostics;
diagnostics?: TDiagnostics;
}
export interface CanonicalWorkspace extends WorkspaceBase<2, VectorStore & { database: string; schema: string }> {}
interface QdrantVectorStore {
engine: "qdrant";
collection: string;
dimensions: 1024;
distance: "cosine";
database?: string;
schema?: string;
port?: number;
timeout_ms?: number;
supported_transports?: VectorTransport[];
}
export interface WorkspaceV3 extends WorkspaceBase<3, QdrantVectorStore> {}
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 LegacyWorkspace extends WorkspaceBase<1, VectorStore & { database?: string; schema?: string }> {}
export interface WorkspaceV1 extends WorkspaceBase<1, VectorStore & { database?: string; schema?: string }> {}
export type WorkspaceDescriptor = CanonicalWorkspace | LegacyWorkspace;
export type CanonicalWorkspace = WorkspaceV3;
export type LegacyWorkspace = WorkspaceV1 | WorkspaceV2;
export type WorkspaceDescriptor = WorkspaceV1 | WorkspaceV2 | 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}$",
@@ -159,6 +188,11 @@ const embeddingSchema = z.object({
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,
@@ -178,6 +212,12 @@ const canonicalVectorStoreSchema = z.object({
database: identifier,
schema: identifier,
}).strict();
const qdrantVectorStoreSchema = z.object({
engine: z.literal("qdrant"),
collection: workspaceId,
dimensions: z.literal(1024),
distance: z.literal("cosine"),
}).strict();
const llmPolicySchema = z.object({
default: modelReference.optional(),
allowed: z.array(modelReference).min(1),
@@ -191,11 +231,13 @@ 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"]);
unique(
workspace.semantic_index.vector_store.supported_transports,
context,
["semantic_index", "vector_store", "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.semantic_index.vector_store.dimensions !== workspace.semantic_index.embedding.dimensions) {
@@ -221,7 +263,10 @@ function workspaceInvariants(workspace: any, context: z.RefinementCtx): void {
}
if (
workspace.diagnostics?.vector_rest
&& !workspace.semantic_index.vector_store.supported_transports.includes("rest_api")
&& (
!("supported_transports" in workspace.semantic_index.vector_store)
|| !workspace.semantic_index.vector_store.supported_transports.includes("rest_api")
)
) {
context.addIssue({
code: "custom",
@@ -236,7 +281,7 @@ const workspaceShape = {
llm_policy: llmPolicySchema,
diagnostics: diagnosticsSchema,
};
const LegacyWorkspaceSchema = z.object({
const WorkspaceV1Schema = z.object({
...workspaceShape,
workspace: z.object({
schema_version: z.literal(1), id: workspaceId, name: z.string().trim().min(1),
@@ -248,7 +293,7 @@ const LegacyWorkspaceSchema = z.object({
embedding: embeddingSchema,
}).strict(),
}).strict().superRefine(workspaceInvariants);
const CanonicalWorkspaceSchema = z.object({
const WorkspaceV2Schema = z.object({
...workspaceShape,
workspace: z.object({
schema_version: z.literal(2), id: workspaceId, name: z.string().trim().min(1),
@@ -260,7 +305,22 @@ const CanonicalWorkspaceSchema = z.object({
embedding: embeddingSchema,
}).strict(),
}).strict().superRefine(workspaceInvariants);
const WorkspaceDescriptorSchema = z.union([CanonicalWorkspaceSchema, LegacyWorkspaceSchema]);
const WorkspaceV3Schema = z.object({
dwh: dwhSchema,
llm_policy: llmPolicySchema,
diagnostics: z.object({
dwh_rest: dwhRestDiagnostic.optional(),
}).strict().optional(),
workspace: z.object({
schema_version: z.literal(3), 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: qdrantVectorStoreSchema,
embedding: internalEmbeddingSchema,
}).strict(),
}).strict().superRefine(workspaceInvariants);
const WorkspaceDescriptorSchema = z.union([WorkspaceV3Schema, WorkspaceV2Schema, WorkspaceV1Schema]);
export function parseWorkspaceYaml(source: string): WorkspaceDescriptor {
const documents = parseAllDocuments(source, { uniqueKeys: true });
@@ -278,14 +338,23 @@ export function validateWorkspaceDescriptor(workspace: unknown): WorkspaceDescri
}
export function isCanonicalWorkspace(workspace: WorkspaceDescriptor): workspace is CanonicalWorkspace {
return workspace.workspace.schema_version === 2;
return workspace.workspace.schema_version === 3;
}
export function isOperationalWorkspace(workspace: WorkspaceDescriptor): workspace is WorkspaceV3 {
return workspace.workspace.schema_version === 3;
}
/** 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 (!isCanonicalWorkspace(descriptor)) {
throw new Error("Workspace descriptor requires explicit migration to schema version 2");
if (!isOperationalWorkspace(descriptor)) {
throw new Error("Workspace descriptor requires explicit migration to schema version 3");
}
return descriptor;
}
@@ -295,19 +364,19 @@ export function validateCanonicalWorkspace(workspace: unknown): CanonicalWorkspa
* transformer never derives it from DWH identity, even where both services share a database.
*/
export function migrateWorkspaceV1ToV2(
workspace: LegacyWorkspace,
workspace: WorkspaceV1,
vectorIdentity: { database: string; schema: string },
): CanonicalWorkspace {
const legacy = LegacyWorkspaceSchema.parse(workspace) as LegacyWorkspace;
): WorkspaceV2 {
const legacy = WorkspaceV1Schema.parse(workspace) as WorkspaceV1;
const identity = z.object({ database: identifier, schema: identifier }).strict().parse(vectorIdentity);
return validateCanonicalWorkspace({
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;
}
/** Builds a request URL only after rejecting values that can leave the declared service origin. */
@@ -320,7 +389,7 @@ export function resolveDiagnosticUrl(baseUrl: string, path: string): URL {
}
export function serializeWorkspaceYaml(workspace: CanonicalWorkspace): string {
const canonical = validateCanonicalWorkspace(workspace);
const canonical = validateOperationalWorkspace(workspace);
return stringify(canonical, { lineWidth: 0, sortMapEntries: true });
}