66 lines
3.8 KiB
TypeScript
66 lines
3.8 KiB
TypeScript
import type { CanonicalFieldRole } from '../domain/canonical-model';
|
|
import type { RecognitionConfidence, RecognitionEvidence } from '../domain/import-recognition';
|
|
import type { ProfileFieldRule } from '../domain/tool-profile-definition';
|
|
import { normalizeNumericValue } from '../shared/text';
|
|
import { recognitionConfidence } from './confidence';
|
|
|
|
export interface FieldRoleEvidenceAlternative {
|
|
role: CanonicalFieldRole;
|
|
confidence: RecognitionConfidence;
|
|
evidence: RecognitionEvidence[];
|
|
}
|
|
|
|
function matches(pattern: RegExp, value: string): boolean {
|
|
pattern.lastIndex = 0;
|
|
return pattern.test(value);
|
|
}
|
|
|
|
function valueEvidence(role: CanonicalFieldRole, values: string[]): RecognitionEvidence[] {
|
|
if (!values.length) return [];
|
|
const ratio = (predicate: (value: string) => boolean) => values.filter(predicate).length / values.length;
|
|
const evidence: RecognitionEvidence[] = [];
|
|
if (role === 'severity' && ratio(value => /^(?:critical|high|medium|low|info|kritisch|hoch|mittel|niedrig)$/i.test(value)) >= 0.6) {
|
|
evidence.push({ signal: 'value-distribution', contribution: 0.88, message: 'Die Werte entsprechen überwiegend bekannten Schweregraden.' });
|
|
}
|
|
if (role === 'status' && ratio(value => /^(?:pass(?:ed)?|fail(?:ed)?|open|closed|resolved|error|unknown|manual|not selected|not run|ioes? found)$/i.test(value)) >= 0.6) {
|
|
evidence.push({ signal: 'value-distribution', contribution: 0.86, message: 'Die Werte entsprechen überwiegend bekannten Ergebnis- oder Statuswerten.' });
|
|
}
|
|
if (role === 'rule-id' && ratio(value => /^(?:[A-Z]{1,8}[.-])?[A-Z0-9]+(?:[._-][A-Z0-9]+)+$/i.test(value)) >= 0.6) {
|
|
evidence.push({ signal: 'value-shape', contribution: 0.7, message: 'Die Werte besitzen überwiegend die Form stabiler Regelbezeichner.' });
|
|
}
|
|
if (role === 'score' && ratio(value => normalizeNumericValue(value) !== null) >= 0.8) {
|
|
evidence.push({ signal: 'detected-type', contribution: 0.36, message: 'Die Spalte besteht überwiegend aus Zahlenwerten.' });
|
|
}
|
|
if (role === 'framework-reference' && ratio(value => /^(?:https?:\/\/|[A-Z]+[- ]?\d)/i.test(value)) >= 0.5) {
|
|
evidence.push({ signal: 'value-shape', contribution: 0.48, message: 'Die Werte ähneln URLs oder Control-Referenzen.' });
|
|
}
|
|
const averageLength = values.reduce((sum, value) => sum + value.length, 0) / values.length;
|
|
if (role === 'description' && averageLength >= 80) {
|
|
evidence.push({ signal: 'value-length', contribution: 0.52, message: 'Die Werte sind überwiegend längere beschreibende Texte.' });
|
|
}
|
|
if (role === 'evidence' && averageLength >= 40) {
|
|
evidence.push({ signal: 'value-length', contribution: 0.4, message: 'Die Werte besitzen die typische Länge von Evidenzdetails.' });
|
|
}
|
|
return evidence;
|
|
}
|
|
|
|
export function evaluateFieldRoleEvidence(
|
|
header: string,
|
|
values: string[],
|
|
fieldRoleRules: ProfileFieldRule[],
|
|
sourceHeaders: string[] = [header]
|
|
): FieldRoleEvidenceAlternative[] {
|
|
return fieldRoleRules.filter(rule =>
|
|
(rule.whenHeadersPresent ?? []).every(pattern => sourceHeaders.some(value => matches(pattern, value.trim())))
|
|
&& (rule.whenHeadersAbsent ?? []).every(pattern => !sourceHeaders.some(value => matches(pattern, value.trim())))
|
|
).map(rule => {
|
|
const headerEvidence = rule.headers
|
|
.filter(item => matches(item.pattern, header))
|
|
.map(item => ({ signal: 'header', contribution: item.contribution, message: item.message }));
|
|
const evidence = [...headerEvidence, ...valueEvidence(rule.role, values)];
|
|
const score = evidence.length ? Math.min(1, evidence.reduce((sum, item) => sum + item.contribution, 0)) : 0;
|
|
return { role: rule.role, evidence, confidence: recognitionConfidence(score, evidence) };
|
|
}).filter(item => item.confidence.score >= 0.2)
|
|
.sort((left, right) => right.confidence.score - left.confidence.score || left.role.localeCompare(right.role));
|
|
}
|