mirror of
https://github.com/Kaelio/ktx.git
synced 2026-07-01 08:59:39 +02:00
180 lines
5.9 KiB
TypeScript
180 lines
5.9 KiB
TypeScript
|
|
import type { KloLocalProject } from '../project/index.js';
|
||
|
|
import type {
|
||
|
|
KloRelationshipReviewDecisionArtifact,
|
||
|
|
KloRelationshipReviewDecisionEntry,
|
||
|
|
KloRelationshipReviewDecisionValue,
|
||
|
|
} from './relationship-review-decisions.js';
|
||
|
|
|
||
|
|
const DECISION_ARTIFACT_SUFFIX = '/enrichment/relationship-review-decisions.json';
|
||
|
|
const FEEDBACK_SCHEMA_VERSION = 1;
|
||
|
|
|
||
|
|
export type KloRelationshipFeedbackDecisionFilter = KloRelationshipReviewDecisionValue | 'all';
|
||
|
|
|
||
|
|
export interface ExportLocalRelationshipFeedbackLabelsInput {
|
||
|
|
connectionId?: string | null;
|
||
|
|
decision?: KloRelationshipFeedbackDecisionFilter;
|
||
|
|
now?: () => Date;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface KloRelationshipFeedbackLabel {
|
||
|
|
schemaVersion: 1;
|
||
|
|
candidateId: string;
|
||
|
|
decision: KloRelationshipReviewDecisionValue;
|
||
|
|
previousStatus: KloRelationshipReviewDecisionEntry['previousStatus'];
|
||
|
|
connectionId: string;
|
||
|
|
runId: string;
|
||
|
|
syncId: string;
|
||
|
|
decidedAt: string;
|
||
|
|
reviewer: string;
|
||
|
|
note: string | null;
|
||
|
|
relationshipType: KloRelationshipReviewDecisionEntry['relationshipType'];
|
||
|
|
source: string;
|
||
|
|
score: number | null;
|
||
|
|
confidence: number;
|
||
|
|
pkScore: number | null;
|
||
|
|
fkScore: number | null;
|
||
|
|
fromTable: string;
|
||
|
|
fromColumns: string[];
|
||
|
|
toTable: string;
|
||
|
|
toColumns: string[];
|
||
|
|
reasons: string[];
|
||
|
|
artifactPath: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface KloRelationshipFeedbackExportWarning {
|
||
|
|
path: string;
|
||
|
|
message: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface ExportLocalRelationshipFeedbackLabelsResult {
|
||
|
|
generatedAt: string;
|
||
|
|
filters: {
|
||
|
|
connectionId: string | null;
|
||
|
|
decision: KloRelationshipFeedbackDecisionFilter;
|
||
|
|
};
|
||
|
|
summary: {
|
||
|
|
total: number;
|
||
|
|
accepted: number;
|
||
|
|
rejected: number;
|
||
|
|
connections: number;
|
||
|
|
runs: number;
|
||
|
|
};
|
||
|
|
labels: KloRelationshipFeedbackLabel[];
|
||
|
|
warnings: KloRelationshipFeedbackExportWarning[];
|
||
|
|
}
|
||
|
|
|
||
|
|
function qualifiedTableName(entry: KloRelationshipReviewDecisionEntry, side: 'from' | 'to'): string {
|
||
|
|
const table = entry[side].table;
|
||
|
|
return [table.catalog, table.db, table.name].filter((part): part is string => Boolean(part)).join('.');
|
||
|
|
}
|
||
|
|
|
||
|
|
function labelFromDecision(entry: KloRelationshipReviewDecisionEntry, artifactPath: string): KloRelationshipFeedbackLabel {
|
||
|
|
return {
|
||
|
|
schemaVersion: FEEDBACK_SCHEMA_VERSION,
|
||
|
|
candidateId: entry.candidateId,
|
||
|
|
decision: entry.decision,
|
||
|
|
previousStatus: entry.previousStatus,
|
||
|
|
connectionId: entry.connectionId,
|
||
|
|
runId: entry.runId,
|
||
|
|
syncId: entry.syncId,
|
||
|
|
decidedAt: entry.decidedAt,
|
||
|
|
reviewer: entry.reviewer,
|
||
|
|
note: entry.note,
|
||
|
|
relationshipType: entry.relationshipType,
|
||
|
|
source: entry.source,
|
||
|
|
score: entry.score,
|
||
|
|
confidence: entry.confidence,
|
||
|
|
pkScore: entry.pkScore,
|
||
|
|
fkScore: entry.fkScore,
|
||
|
|
fromTable: qualifiedTableName(entry, 'from'),
|
||
|
|
fromColumns: [...entry.from.columns],
|
||
|
|
toTable: qualifiedTableName(entry, 'to'),
|
||
|
|
toColumns: [...entry.to.columns],
|
||
|
|
reasons: [...entry.reasons],
|
||
|
|
artifactPath,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function sortLabels(labels: KloRelationshipFeedbackLabel[]): KloRelationshipFeedbackLabel[] {
|
||
|
|
return [...labels].sort((left, right) => {
|
||
|
|
return (
|
||
|
|
left.connectionId.localeCompare(right.connectionId) ||
|
||
|
|
left.runId.localeCompare(right.runId) ||
|
||
|
|
left.candidateId.localeCompare(right.candidateId) ||
|
||
|
|
left.decidedAt.localeCompare(right.decidedAt)
|
||
|
|
);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function passesFilters(
|
||
|
|
label: KloRelationshipFeedbackLabel,
|
||
|
|
filters: { connectionId: string | null; decision: KloRelationshipFeedbackDecisionFilter },
|
||
|
|
): boolean {
|
||
|
|
if (filters.connectionId && label.connectionId !== filters.connectionId) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return filters.decision === 'all' || label.decision === filters.decision;
|
||
|
|
}
|
||
|
|
|
||
|
|
function messageFromUnknownError(error: unknown): string {
|
||
|
|
return error instanceof Error ? error.message : String(error);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function readDecisionLabels(
|
||
|
|
project: KloLocalProject,
|
||
|
|
artifactPath: string,
|
||
|
|
): Promise<KloRelationshipFeedbackLabel[]> {
|
||
|
|
const raw = await project.fileStore.readFile(artifactPath);
|
||
|
|
const parsed = JSON.parse(raw.content) as KloRelationshipReviewDecisionArtifact;
|
||
|
|
const decisions = Array.isArray(parsed.decisions) ? parsed.decisions : [];
|
||
|
|
return decisions.map((entry) => labelFromDecision(entry, artifactPath));
|
||
|
|
}
|
||
|
|
|
||
|
|
function summarize(labels: KloRelationshipFeedbackLabel[]): ExportLocalRelationshipFeedbackLabelsResult['summary'] {
|
||
|
|
return {
|
||
|
|
total: labels.length,
|
||
|
|
accepted: labels.filter((label) => label.decision === 'accepted').length,
|
||
|
|
rejected: labels.filter((label) => label.decision === 'rejected').length,
|
||
|
|
connections: new Set(labels.map((label) => label.connectionId)).size,
|
||
|
|
runs: new Set(labels.map((label) => `${label.connectionId}:${label.runId}`)).size,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function exportLocalRelationshipFeedbackLabels(
|
||
|
|
project: KloLocalProject,
|
||
|
|
input: ExportLocalRelationshipFeedbackLabelsInput = {},
|
||
|
|
): Promise<ExportLocalRelationshipFeedbackLabelsResult> {
|
||
|
|
const filters = {
|
||
|
|
connectionId: input.connectionId ?? null,
|
||
|
|
decision: input.decision ?? 'all',
|
||
|
|
};
|
||
|
|
const listed = await project.fileStore.listFiles('raw-sources');
|
||
|
|
const artifactPaths = listed.files.filter((path) => path.endsWith(DECISION_ARTIFACT_SUFFIX)).sort();
|
||
|
|
const labels: KloRelationshipFeedbackLabel[] = [];
|
||
|
|
const warnings: KloRelationshipFeedbackExportWarning[] = [];
|
||
|
|
|
||
|
|
for (const artifactPath of artifactPaths) {
|
||
|
|
try {
|
||
|
|
labels.push(...(await readDecisionLabels(project, artifactPath)));
|
||
|
|
} catch (error) {
|
||
|
|
warnings.push({ path: artifactPath, message: messageFromUnknownError(error) });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const filtered = sortLabels(labels.filter((label) => passesFilters(label, filters)));
|
||
|
|
return {
|
||
|
|
generatedAt: (input.now?.() ?? new Date()).toISOString(),
|
||
|
|
filters,
|
||
|
|
summary: summarize(filtered),
|
||
|
|
labels: filtered,
|
||
|
|
warnings,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export function formatKloRelationshipFeedbackLabelsJsonl(result: ExportLocalRelationshipFeedbackLabelsResult): string {
|
||
|
|
if (result.labels.length === 0) {
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
return `${result.labels.map((label) => JSON.stringify(label)).join('\n')}\n`;
|
||
|
|
}
|