package store import ( "context" "fmt" "time" ) // Finding ist das Ergebnis einer Regel für einen Beitrag. Append-only: // siehe Migration. ExtractionID ist optional (nil wenn ein Finding nicht // direkt aus einer Extraktion, sondern z. B. manuell erzeugt wurde). // Title/Fix/Sources sind zum Zeitpunkt der Regelauswertung fixiert // gespeichert (nicht nur rule_id/rule_version referenziert), weil ein // späteres Update der Regel-YAML eine ältere Version sonst nicht mehr // nachträglich auflösen könnte — der Wortlaut zum Zeitpunkt des // Findings ist der Beweis, kein Verweis darauf. type Finding struct { ID string SubmissionID string ExtractionID *string RuleID string RuleVersion int Severity string Title string Fix string Sources []string CreatedAt time.Time } // CreateFinding speichert ein Finding. func (s *Store) CreateFinding(ctx context.Context, submissionID string, extractionID *string, ruleID string, ruleVersion int, severity, title, fix string, sources []string) (Finding, error) { var f Finding err := s.Pool.QueryRow(ctx, ` INSERT INTO finding (submission_id, extraction_id, rule_id, rule_version, severity, title, fix, sources) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, submission_id, extraction_id, rule_id, rule_version, severity, title, fix, sources, created_at `, submissionID, extractionID, ruleID, ruleVersion, severity, title, fix, sources).Scan( &f.ID, &f.SubmissionID, &f.ExtractionID, &f.RuleID, &f.RuleVersion, &f.Severity, &f.Title, &f.Fix, &f.Sources, &f.CreatedAt, ) if err != nil { return Finding{}, fmt.Errorf("store: create finding: %w", err) } return f, nil } // ListCurrentFindings liefert die aktuell gültigen Findings eines // Beitrags — Zeilen, die von keiner anderen Zeile per supersedes // ersetzt wurden (siehe Migrationskommentar zu finding.supersedes). func (s *Store) ListCurrentFindings(ctx context.Context, submissionID string) ([]Finding, error) { rows, err := s.Pool.Query(ctx, ` SELECT f.id, f.submission_id, f.extraction_id, f.rule_id, f.rule_version, f.severity, f.title, f.fix, f.sources, f.created_at FROM finding f WHERE f.submission_id = $1 AND NOT EXISTS (SELECT 1 FROM finding f2 WHERE f2.supersedes = f.id) ORDER BY f.created_at `, submissionID) if err != nil { return nil, fmt.Errorf("store: list current findings: %w", err) } defer rows.Close() var findings []Finding for rows.Next() { var f Finding if err := rows.Scan(&f.ID, &f.SubmissionID, &f.ExtractionID, &f.RuleID, &f.RuleVersion, &f.Severity, &f.Title, &f.Fix, &f.Sources, &f.CreatedAt); err != nil { return nil, fmt.Errorf("store: scan finding: %w", err) } findings = append(findings, f) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("store: list current findings: %w", err) } return findings, nil }