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