253 lines
8.8 KiB
TypeScript
253 lines
8.8 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { normalize } from "node:path";
|
|
|
|
export interface CanonicalEffectiveConfig {
|
|
schemaVersion: 3;
|
|
dwh: CanonicalDwhConfig;
|
|
vector: CanonicalVectorConfig;
|
|
embedding: CanonicalEmbeddingConfig;
|
|
roots: CanonicalRootsConfig;
|
|
}
|
|
|
|
export interface CanonicalDwhConfig {
|
|
engine: "postgres";
|
|
database: string;
|
|
schema: string;
|
|
transport: "postgres_direct" | "rest_api" | "ssh_tunnel";
|
|
host?: string;
|
|
port?: number;
|
|
baseUrl?: string;
|
|
user?: string;
|
|
}
|
|
|
|
export interface CanonicalVectorConfig {
|
|
collections: {
|
|
reference: string;
|
|
memory: string;
|
|
};
|
|
dimensions: number;
|
|
distance: string;
|
|
}
|
|
|
|
export interface CanonicalEmbeddingConfig {
|
|
id: string;
|
|
model: string;
|
|
dimensions: number;
|
|
}
|
|
|
|
export interface CanonicalRootsConfig {
|
|
artifacts: string;
|
|
indexes: string;
|
|
}
|
|
|
|
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
return value as Record<string, unknown>;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function requireString(value: Record<string, unknown>, key: string): string {
|
|
const candidate = value[key];
|
|
if (typeof candidate !== "string" || candidate.length === 0) {
|
|
throw new TypeError(`effective config is missing ${key}`);
|
|
}
|
|
return candidate;
|
|
}
|
|
|
|
function optionalString(value: Record<string, unknown>, key: string): string | undefined {
|
|
const candidate = value[key];
|
|
if (candidate === undefined || candidate === null) return undefined;
|
|
if (typeof candidate !== "string") return undefined;
|
|
return candidate;
|
|
}
|
|
|
|
function optionalNumber(value: Record<string, unknown>, key: string): number | undefined {
|
|
const candidate = value[key];
|
|
if (candidate === undefined || candidate === null) return undefined;
|
|
if (typeof candidate !== "number" || Number.isNaN(candidate)) return undefined;
|
|
return candidate;
|
|
}
|
|
|
|
function requireNumber(value: Record<string, unknown>, key: string): number {
|
|
const candidate = value[key];
|
|
if (typeof candidate !== "number" || Number.isNaN(candidate)) {
|
|
throw new TypeError(`effective config is missing numeric ${key}`);
|
|
}
|
|
return candidate;
|
|
}
|
|
|
|
function normalizeRoot(value: string): string {
|
|
return normalize(value);
|
|
}
|
|
|
|
function dwhTransport(rendered: Record<string, unknown>): CanonicalDwhConfig["transport"] {
|
|
const dwh = asRecord(rendered.dwh);
|
|
const type = dwh ? requireString(dwh, "type") : undefined;
|
|
if (type === "postgres_direct") return "postgres_direct";
|
|
if (type === "thoth_rest") return "rest_api";
|
|
if (type === "ssh_tunnel") return "ssh_tunnel";
|
|
throw new TypeError(`effective config has unsupported dwh transport ${type}`);
|
|
}
|
|
|
|
function buildDwhConfig(rendered: Record<string, unknown>): CanonicalDwhConfig {
|
|
const transport = dwhTransport(rendered);
|
|
const databaseRecord = asRecord(rendered.database) ?? asRecord(asRecord(asRecord(rendered.dwh)?.connection)?.database);
|
|
if (!databaseRecord) {
|
|
throw new TypeError("effective config is missing database identity");
|
|
}
|
|
const engine = "postgres";
|
|
const database = requireString(databaseRecord, "database");
|
|
const schema = requireString(databaseRecord, "schema");
|
|
const dwh: Record<string, unknown> = { engine, database, schema, transport };
|
|
|
|
if (transport === "postgres_direct") {
|
|
const host = optionalString(databaseRecord, "host");
|
|
const port = optionalNumber(databaseRecord, "port");
|
|
const user = optionalString(databaseRecord, "user");
|
|
if (host !== undefined) dwh.host = host;
|
|
if (port !== undefined) dwh.port = port;
|
|
if (user !== undefined) dwh.user = user;
|
|
} else if (transport === "rest_api") {
|
|
const rest = asRecord(rendered.rest) ?? asRecord(asRecord(asRecord(rendered.dwh)?.endpoint));
|
|
const baseUrl = rest ? optionalString(rest, "base_url") : undefined;
|
|
if (baseUrl !== undefined) dwh.baseUrl = baseUrl;
|
|
}
|
|
|
|
return dwh as unknown as CanonicalDwhConfig;
|
|
}
|
|
|
|
function buildVectorConfig(rendered: Record<string, unknown>): CanonicalVectorConfig {
|
|
const resources = asRecord(rendered.resources);
|
|
const vector = resources ? asRecord(resources.vector) : undefined;
|
|
if (!vector) {
|
|
throw new TypeError("effective config is missing vector resources");
|
|
}
|
|
const collections = asRecord(vector.collections);
|
|
if (!collections) {
|
|
throw new TypeError("effective config is missing vector collections");
|
|
}
|
|
const semanticIndex = asRecord(rendered.semantic_index);
|
|
const vectorStore = semanticIndex ? asRecord(semanticIndex.vector_store) : undefined;
|
|
const dimensions = vectorStore
|
|
? requireNumber(vectorStore, "dimensions")
|
|
: requireNumber(vector, "dimensions");
|
|
const distance = vectorStore
|
|
? requireString(vectorStore, "distance")
|
|
: (optionalString(vector, "distance") ?? "cosine");
|
|
return {
|
|
collections: {
|
|
reference: requireString(collections, "reference"),
|
|
memory: requireString(collections, "memory"),
|
|
},
|
|
dimensions,
|
|
distance,
|
|
};
|
|
}
|
|
|
|
function buildEmbeddingConfig(rendered: Record<string, unknown>): CanonicalEmbeddingConfig {
|
|
const resources = asRecord(rendered.resources);
|
|
const embeddings = resources ? asRecord(resources.embeddings) : undefined;
|
|
if (!embeddings) {
|
|
throw new TypeError("effective config is missing embedding resources");
|
|
}
|
|
const model = requireString(embeddings, "model");
|
|
return {
|
|
id: optionalString(embeddings, "id") ?? `ollama/${model}`,
|
|
model,
|
|
dimensions: requireNumber(embeddings, "dimensions"),
|
|
};
|
|
}
|
|
|
|
function buildRootsConfig(rendered: Record<string, unknown>): CanonicalRootsConfig {
|
|
const roots = asRecord(rendered.roots) ?? asRecord(rendered.paths);
|
|
if (!roots) {
|
|
throw new TypeError("effective config is missing roots");
|
|
}
|
|
return {
|
|
artifacts: normalizeRoot(requireString(roots, "artifacts")),
|
|
indexes: normalizeRoot(requireString(roots, "indexes")),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Build the versioned, non-secret effective DWH/preprocessing configuration from a
|
|
* rendered runtime configuration object. The result contains only the fields that
|
|
* affect DWH generation identity; credentials, runtime identity, session storage,
|
|
* evidence, and service endpoints are excluded.
|
|
*/
|
|
export function buildCanonicalEffectiveConfig(renderedConfig: unknown): CanonicalEffectiveConfig {
|
|
const rendered = asRecord(renderedConfig);
|
|
if (!rendered) {
|
|
throw new TypeError("effective config requires a rendered configuration object");
|
|
}
|
|
return {
|
|
schemaVersion: 3,
|
|
dwh: buildDwhConfig(rendered),
|
|
vector: buildVectorConfig(rendered),
|
|
embedding: buildEmbeddingConfig(rendered),
|
|
roots: buildRootsConfig(rendered),
|
|
};
|
|
}
|
|
|
|
function sha256(value: string | Buffer): string {
|
|
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
}
|
|
|
|
/**
|
|
* Serialize the canonical effective config to a deterministic JSON string with the
|
|
* fixed key order defined by the shared contract. No whitespace is included.
|
|
*/
|
|
export function canonicalEffectiveConfigJson(config: CanonicalEffectiveConfig): string {
|
|
const ordered: Record<string, unknown> = { schemaVersion: config.schemaVersion };
|
|
ordered.dwh = { ...config.dwh };
|
|
ordered.vector = { ...config.vector };
|
|
ordered.embedding = { ...config.embedding };
|
|
ordered.roots = { ...config.roots };
|
|
return JSON.stringify(ordered);
|
|
}
|
|
|
|
/**
|
|
* Return the stable logical config-source identity for a workspace revision.
|
|
* This is `workspace://<workspaceId>@v1:<sha256-hex-of-canonical-json>`.
|
|
*/
|
|
export function effectiveConfigIdentity(workspaceId: string, renderedConfig: unknown): string {
|
|
const canonical = buildCanonicalEffectiveConfig(renderedConfig);
|
|
const digest = createHash("sha256").update(canonicalEffectiveConfigJson(canonical)).digest("hex");
|
|
return `workspace://${workspaceId}@v1:${digest}`;
|
|
}
|
|
|
|
/**
|
|
* Return the config fingerprint: `sha256:` + the SHA-256 of the canonical effective
|
|
* config JSON bytes.
|
|
*/
|
|
export function configFingerprint(renderedConfig: unknown): string {
|
|
const canonical = buildCanonicalEffectiveConfig(renderedConfig);
|
|
return sha256(canonicalEffectiveConfigJson(canonical));
|
|
}
|
|
|
|
/**
|
|
* Return the input fingerprint: `sha256:` + the SHA-256 of the logical config-source
|
|
* identity string.
|
|
*/
|
|
export function inputFingerprint(workspaceId: string, renderedConfig: unknown): string {
|
|
return sha256(effectiveConfigIdentity(workspaceId, renderedConfig));
|
|
}
|
|
|
|
/**
|
|
* Fingerprint every non-Catalog input consumed by complete preprocessing. The immutable Git
|
|
* revision covers the workspace descriptor and its revision-pinned Evidence tree; the effective
|
|
* configuration identity covers the Catalog-derived DWH binding and semantic runtime contract.
|
|
*/
|
|
export function preprocessingInputFingerprint(
|
|
workspaceId: string,
|
|
workspaceRevision: string,
|
|
renderedConfig: unknown,
|
|
): string {
|
|
return sha256(JSON.stringify({
|
|
workspaceId,
|
|
workspaceRevision,
|
|
effectiveConfigIdentity: effectiveConfigIdentity(workspaceId, renderedConfig),
|
|
}));
|
|
}
|