Assessor_2.0/src/recognition/field-role-evidence.ts

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));
}