Files
safetynet/packages/core/semantic.mjs
T
2026-10-13 10:34:48 +02:00

20 lines
3.2 KiB
JavaScript

import { z } from 'zod';
export const SEMANTIC_MODEL='google/embeddinggemma-2';
export const SEMANTIC_DIMENSIONS=768;
export const SEMANTIC_PROFILE='document-v1';
export const SEMANTIC_ARTIFACT={artifactFormat:'gguf',quantization:'Q8_0',runtimeRevision:'b11461'};
export const ModelRevisionSchema=z.string().regex(/^[a-f0-9]{40}$/,'An exact lowercase model commit SHA is required');
export const EmbeddingVectorSchema=z.array(z.number().finite()).length(SEMANTIC_DIMENSIONS).refine(vector=>{const norm=vector.reduce((sum,v)=>sum+v*v,0);return Number.isFinite(norm)&&Math.abs(norm-1)<=0.002;},'Embedding must be finite, nonzero and normalized');
const CategorySchema=z.object({id:z.string().min(1).max(80),label:z.string().min(1).max(100),threshold:z.number().gt(0).max(1),prototype:EmbeddingVectorSchema,prototypes:z.array(EmbeddingVectorSchema).min(1).max(64).optional()}).strict();
export const defaultSemantic=()=>({enabled:false,model:SEMANTIC_MODEL,revision:'',dimensions:SEMANTIC_DIMENSIONS,profile:SEMANTIC_PROFILE,...SEMANTIC_ARTIFACT,categories:[]});
export const SemanticPolicySchema=z.preprocess(value=>{
// Only inactive legacy configuration is migrated. Old references cannot be assigned a new profile.
if(value&&typeof value==='object'&&value.enabled===false&&value.model===SEMANTIC_MODEL&&(value.profile===undefined||value.dimensions===undefined||value.artifactFormat===undefined))return defaultSemantic();
return value;
},z.object({enabled:z.boolean(),model:z.literal(SEMANTIC_MODEL),revision:z.union([z.literal(''),ModelRevisionSchema]),dimensions:z.literal(SEMANTIC_DIMENSIONS),profile:z.literal(SEMANTIC_PROFILE),artifactFormat:z.literal('gguf'),quantization:z.literal('Q8_0'),runtimeRevision:z.literal('b11461'),categories:z.array(CategorySchema).max(100)}).strict().superRefine((value,ctx)=>{
if((value.enabled||value.categories.length)&&!value.revision)ctx.addIssue({code:'custom',message:'Semantic references require a pinned model revision'});
if(value.enabled&&!value.categories.length)ctx.addIssue({code:'custom',message:'Semantic enforcement requires reference categories'});
if(value.categories.reduce((sum,category)=>sum+(category.prototypes?.length||1),0)>256)ctx.addIssue({code:'custom',message:'Semantic policy supports at most 256 reference vectors'});
})).default(defaultSemantic);
export const EmbeddingReferenceSchema=z.object({model:z.literal(SEMANTIC_MODEL),revision:ModelRevisionSchema,dimensions:z.literal(SEMANTIC_DIMENSIONS),profile:z.literal(SEMANTIC_PROFILE),artifactFormat:z.literal('gguf'),quantization:z.literal('Q8_0'),runtimeRevision:z.literal('b11461'),local:z.literal(true),backend:z.literal('native-gguf').optional(),device:z.literal('cpu').optional(),modalities:z.array(z.enum(['text','image','audio'])).min(1).max(3).optional(),vector:EmbeddingVectorSchema,vectors:z.array(EmbeddingVectorSchema).min(1).max(64),coverage:z.object({complete:z.boolean(),textChunks:z.number().int().min(0).max(64),extractionComplete:z.boolean(),mediaItems:z.number().int().min(0).max(64).optional()}).strict()}).strict().refine(value=>value.coverage.mediaItems?value.coverage.textChunks+value.coverage.mediaItems<=value.vectors.length:value.coverage.textChunks===value.vectors.length,'Chunk count must match supplied vectors');