Files
deklarix/internal/rules/loader.go
noroot bec34b5988 feat: add rules engine (internal/rules) with first two disclosure rules
internal/rules implements Stufe 2 from the core principle: the LLM
extracts facts, this deterministic engine judges them against versioned
YAML rules. Facts/Condition/Rule/Finding types, a loader that refuses to
load on a missing id/version or a duplicate rule id rather than silently
skipping a bad file, and Evaluate() matching facts against rules.

Two real rules grounded in verified research (see rules/OPEN.md for the
open questions that surfaced along the way):
- WK-001: no disclosure at all despite consideration (§ 5a Abs. 4 UWG,
  § 22 Abs. 1 MStV)
- WK-004: disclosure present but hidden behind a "mehr anzeigen" cut
  (§ 5a Abs. 4 UWG, Leitfaden der Medienanstalten, LG Köln 12.05.2026)

The two are deliberately disjoint (WK-004 requires disclosure_present=
true) so a post with no disclosure at all doesn't double-fire both
rules. Golden suite in testdata/golden/ covers both rules plus two clean
cases; it's this suite, not the UI, that's the actual asset per
CLAUDE.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 13:35:07 +02:00

50 lines
1.3 KiB
Go

package rules
import (
"fmt"
"io/fs"
"path/filepath"
"gopkg.in/yaml.v3"
)
// Load liest alle *.yaml-Dateien aus fsys (nicht rekursiv) und parst sie
// als Regeln. Ein Fehler in einer Datei (Parse-Fehler, fehlende ID/
// Version) bricht das Laden komplett ab, statt die Datei stillschweigend
// zu überspringen — ein halb geladenes Regelwerk ist gefährlicher als
// ein Start, der mit einem klaren Fehler abbricht.
func Load(fsys fs.FS) ([]Rule, error) {
entries, err := fs.ReadDir(fsys, ".")
if err != nil {
return nil, fmt.Errorf("rules: read dir: %w", err)
}
seen := make(map[string]bool)
var result []Rule
for _, entry := range entries {
if entry.IsDir() || filepath.Ext(entry.Name()) != ".yaml" {
continue
}
data, err := fs.ReadFile(fsys, entry.Name())
if err != nil {
return nil, fmt.Errorf("rules: read %s: %w", entry.Name(), err)
}
var r Rule
if err := yaml.Unmarshal(data, &r); err != nil {
return nil, fmt.Errorf("rules: parse %s: %w", entry.Name(), err)
}
if r.ID == "" {
return nil, fmt.Errorf("rules: %s: missing id", entry.Name())
}
if r.Version == 0 {
return nil, fmt.Errorf("rules: %s: missing version", entry.Name())
}
if seen[r.ID] {
return nil, fmt.Errorf("rules: %s: duplicate rule id %s", entry.Name(), r.ID)
}
seen[r.ID] = true
result = append(result, r)
}
return result, nil
}