feat: add AI catalog description generation

This commit is contained in:
Codex
2026-08-29 16:42:56 +02:00
parent b0afba81ca
commit 376dd5a09d
76 changed files with 14860 additions and 102 deletions
+174
View File
@@ -2,7 +2,10 @@ import { randomUUID } from "node:crypto";
import {
CatalogConflictError,
CatalogConnectorError,
DescriptionGenerationRunActiveError,
type CatalogColumn,
type CatalogDescriptionConsolidationCounts,
type CatalogDescriptionTarget,
type CatalogDatabaseMetadataDeleteTarget,
type CatalogMetadataDeleteCounts,
type CatalogRelationship,
@@ -19,6 +22,10 @@ import {
type DatabaseTestResult,
type ObservedCatalogTable,
type ObservedSchemaSnapshot,
type DescriptionGenerationEvent,
type DescriptionGenerationRun,
type DescriptionGenerationRunUpdate,
type DescriptionGenerationScope,
type TableSyncRepositoryResult,
type WorkspaceDatabase,
} from "./types.js";
@@ -33,6 +40,8 @@ export class MemoryCatalogRepository implements CatalogRepository {
private readonly tables = new Map<string, CatalogTable>();
private readonly columns = new Map<string, CatalogColumn>();
private readonly relationships = new Map<string, CatalogRelationship>();
private readonly descriptionGenerationRuns = new Map<string, DescriptionGenerationRun>();
private readonly descriptionGenerationEvents = new Map<string, DescriptionGenerationEvent[]>();
private readonly syncRuns = new Map<string, CatalogSyncRun>();
private readonly syncEvents = new Map<string, CatalogSyncEvent[]>();
@@ -122,6 +131,11 @@ export class MemoryCatalogRepository implements CatalogRepository {
for (const [relationshipId, relationship] of this.relationships) {
if (relationship.databaseId === id) this.relationships.delete(relationshipId);
}
for (const [runId, run] of this.descriptionGenerationRuns) {
if (run.databaseId !== id) continue;
this.descriptionGenerationRuns.delete(runId);
this.descriptionGenerationEvents.delete(runId);
}
return this.records.delete(id);
}
async listTables(databaseId: string): Promise<CatalogTable[]> {
@@ -196,6 +210,166 @@ export class MemoryCatalogRepository implements CatalogRepository {
return structuredClone(updated);
}
async consolidateGeneratedDescriptions(
databaseId: string,
target: CatalogDescriptionTarget,
targetIds: readonly string[],
): Promise<CatalogDescriptionConsolidationCounts | undefined> {
const selectedTargetIds = [...new Set(targetIds)];
if (!this.records.has(databaseId) || selectedTargetIds.length === 0) {
return undefined;
}
const now = new Date().toISOString();
if (target === "tables") {
const targets = selectedTargetIds.map((id) => this.tables.get(id));
if (targets.some((table) => !table || table.databaseId !== databaseId)) return undefined;
const copied = targets.filter((table) => Boolean(table!.generatedDescription?.trim()));
for (const table of copied) {
this.tables.set(table!.id, {
...table!,
description: table!.generatedDescription,
version: table!.version + 1,
updatedAt: now,
});
}
return { copied: copied.length, skipped: targets.length - copied.length };
}
const targets = selectedTargetIds.map((id) => this.columns.get(id));
if (targets.some((column) => (
!column || this.tables.get(column.tableId)?.databaseId !== databaseId
))) return undefined;
const copied = targets.filter((column) => Boolean(column!.generatedDescription?.trim()));
for (const column of copied) {
this.columns.set(column!.id, {
...column!,
description: column!.generatedDescription,
version: column!.version + 1,
updatedAt: now,
});
}
return { copied: copied.length, skipped: targets.length - copied.length };
}
async createDescriptionGenerationRun(
databaseId: string,
scope: DescriptionGenerationScope,
modelId: string,
language: DescriptionGenerationRun["language"],
total: number,
): Promise<DescriptionGenerationRun> {
if ([...this.descriptionGenerationRuns.values()].some((run) => (
run.status === "queued" || run.status === "running"
))) {
throw new DescriptionGenerationRunActiveError("A description generation run is already active");
}
const now = new Date().toISOString();
const run: DescriptionGenerationRun = {
id: randomUUID(),
databaseId,
scope,
modelId,
language,
status: "queued",
total,
processed: 0,
generated: 0,
nonGeneratable: 0,
failed: 0,
createdAt: now,
startedAt: null,
updatedAt: now,
finishedAt: null,
errorSummary: null,
};
this.descriptionGenerationRuns.set(run.id, run);
return structuredClone(run);
}
async getDescriptionGenerationRun(runId: string): Promise<DescriptionGenerationRun | undefined> {
const run = this.descriptionGenerationRuns.get(runId);
return run ? structuredClone(run) : undefined;
}
async listDescriptionGenerationRuns(limit = 50): Promise<DescriptionGenerationRun[]> {
return [...this.descriptionGenerationRuns.values()]
.sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.id.localeCompare(a.id))
.slice(0, limit)
.map((run) => structuredClone(run));
}
async getActiveDescriptionGenerationRun(): Promise<DescriptionGenerationRun | undefined> {
const run = [...this.descriptionGenerationRuns.values()]
.filter((candidate) => candidate.status === "queued" || candidate.status === "running")
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0];
return run ? structuredClone(run) : undefined;
}
async interruptActiveDescriptionGenerationRuns(
errorSummary: string,
): Promise<DescriptionGenerationRun[]> {
const interrupted: DescriptionGenerationRun[] = [];
for (const run of this.descriptionGenerationRuns.values()) {
if (run.status !== "queued" && run.status !== "running") continue;
const now = new Date().toISOString();
const updated: DescriptionGenerationRun = {
...run,
status: "interrupted",
updatedAt: now,
finishedAt: now,
errorSummary,
};
this.descriptionGenerationRuns.set(run.id, updated);
interrupted.push(structuredClone(updated));
}
return interrupted;
}
async updateDescriptionGenerationRun(
runId: string,
update: DescriptionGenerationRunUpdate,
): Promise<DescriptionGenerationRun | undefined> {
const current = this.descriptionGenerationRuns.get(runId);
if (!current) return undefined;
const updated = {
...current,
...structuredClone(update),
updatedAt: new Date().toISOString(),
};
this.descriptionGenerationRuns.set(runId, updated);
return structuredClone(updated);
}
async appendDescriptionGenerationEvent(
runId: string,
level: DescriptionGenerationEvent["level"],
message: string,
): Promise<DescriptionGenerationEvent> {
if (!this.descriptionGenerationRuns.has(runId)) {
throw new CatalogConflictError("Description Generation Run does not exist");
}
const events = this.descriptionGenerationEvents.get(runId) ?? [];
const event: DescriptionGenerationEvent = {
runId,
sequence: events.length + 1,
level,
message,
createdAt: new Date().toISOString(),
};
events.push(event);
this.descriptionGenerationEvents.set(runId, events);
return structuredClone(event);
}
async listDescriptionGenerationEvents(
runId: string,
afterSequence = 0,
): Promise<DescriptionGenerationEvent[]> {
return (this.descriptionGenerationEvents.get(runId) ?? [])
.filter((event) => event.sequence > afterSequence)
.map((event) => structuredClone(event));
}
async listRelationships(databaseId: string): Promise<CatalogRelationship[]> {
return [...this.relationships.values()].filter((relationship) => relationship.databaseId === databaseId)
.sort((a, b) => `${a.sourceTableName}.${a.constraintName}`.localeCompare(`${b.sourceTableName}.${b.constraintName}`))