Compare commits

...

8 Commits

Author SHA1 Message Date
fegger 50ba153904 feat(agent): tune retrieval for decision support 2026-09-16 09:34:29 +02:00
fegger f525fb4862 docs: record rechtsprechung eval baseline 2026-09-15 16:42:49 +02:00
fegger a11f2740f7 chore: consolidate local source directories 2026-09-15 15:58:22 +02:00
fegger 5209539ba3 feat(kb): ingest rechtsprechung corpus 2026-09-15 15:27:28 +02:00
fegger 1594aa9cbd length-Retry: abgeschnittene Antworten sind die q-024-Ursache, kein Thinking (D14)
- Diagnose: q-024-Flakiness war KEIN Think-Ghost (thinking-Feld leer,
  keine Tags), sondern num_predict=1024 — lange belegte Antworten
  brachen bei done_reason=length ab (3/3 Sondenlaeufe), Zitationen
  wurden unvollstaendig, CITE_RE matchte partielle IDs -> Verletzung ->
  Regenerierungs-Eskalation -> UNCERTAIN.
- Fix: num_predict 1024 -> 2048; OllamaClient.chat_full() liefert
  (content, done_reason); chat_with_length_retry() wiederholt bei
  length einmal mit 2x Budget (technischer Retry, kein Regel-
  Regenerierungszaehler) - im Antwort-, Map- und Regenerierungspfad.
- Voll-Eval (46 Fragen): Zitier-Praezision 100 %, Verweigerung korrekt
  100 % (46/46) - erstmals alle M3-Gates erfuellt; erwartete Quelle
  92,7 %; Latenz mean 39,6 s / p95 78 s (vollstaendige statt
  abgeschnittener Antworten). q-024: 4/4 stabil.
- Tests 59 -> 60. Report lokal data/eval-qwen38-lengthfix.json.
2026-09-15 14:14:27 +02:00
fegger 4a9e06f66e Stufe 2: Antworttyp-Routing — Survey Map-Reduce + Rueckfrage-Regel (D13, M6)
- Planer liefert jetzt type: survey|specific. Survey-Fragen (Uebersicht
  ueber viele Dokumente) laufen ueber Map-Reduce: Retrieval auf
  survey_blocks=16 erweitert, ein Map-Call destilliert JE Block als
  Stichpunkte mit seiner KB-ID (MAP_SYSTEM_PROMPT), ein Reduce-Call
  synthetisiert die Endantwort. Grounding unveraendert: Zitier-Validierung
  strikt ueber die Retrieved-Union; leerer Map-Output -> Fallback auf
  Einzelantwort.
- Systemprompt-Regel 9: haengt die Antwort wesentlich von nicht genanntem
  Kontext ab (Branche, Bundesland, Zeitraum), belegte allgemeine Aussage
  plus EINE Rueckfrage statt Verweigerung (API-first; Odoo-Chat kann die
  Rueckfrage als Follow-up nutzen).
- Ergebnis: q-029 (WIKU-Survey, bisher hartnaeckigste Fehlverweigerung)
  geheilt - Teilantwort mit 5 belegten Heften; q-015 antwortet mit
  expliziter KV-Abhaengigkeit + Rueckfrage statt Branchen-Noise.
- Eval (46 Fragen): Zitier-Praezision 97,8 %, Verweigerung 97,8 % (Gate
  erfuellt), erwartete Quelle 90,2 %, Latenz mean 34,2 s (Map-Reduce nur
  bei Survey-Fragen, ~95 s). Report lokal data/eval-qwen38-stage2.json.
- Tests 57 -> 59 (Survey-Integration, Typ-Parsing).
2026-09-15 11:16:53 +02:00
fegger 062e010d7b Stufe 1: komplexe Fragen via Query-Planer, Per-Query-Slots, Scope-Filter (D12, M6)
- Query-Planer (agent/query_planner.py): Heuristik-Gate (Jahr, Vergleichs-/
  Aggregationsmarker, Laenge) entscheidet ueber einen kleinen LLM-Call,
  der komplexe Fragen in 1-3 Sub-Queries zerlegt (JSON, temp 0; Fehler ->
  Original als Einzel-Query). Je Sub-Query optional stand_year und scope.
- Multi-Query-Retrieval: je Sub-Query BM25+Dense mit RRF-Summe;
  Per-Query-Slots (2 je Sub-Query) sichern jeden Frageaspekt im Kontext
  (sonst dominieren Eintraege, die in mehreren Sub-Queries mittelgut
  matchen — q-113-Befund). Scope-Filter: 'gesetz' (nur Lexis/WIKU/RIS)
  und 'kv' (nur Branchen-KV) mit Fallback auf unscoped bei leerem
  Ergebnis. Temporal-Intent: stand_year + temporal_boost (default 0,
  FTS-jahr-Tag-Signal reichte).
- Grounding unveraendert: eine Retrieved-Menge (Union), eine Antwort,
  Post-Validierung ueber die Union, Verweigerungspflicht unveraendert.
- Goldset 42 -> 46: q-110 (Temporal 2023; 2024er-Lohnordnung existiert
  im Korpus nicht - Mantelvertrag ohne Lohntabelle, korrekt verweigert),
  q-111 (2025), q-112 (Abfertigung-Vergleich), q-113 (Gesetz+KV).
- Eval (46 Fragen): Zitier-Praezision 97,8 %, Verweigerung 97,8 %
  (Gate >94,3 % erfuellt), erwartete Quelle 90,2 %, Latenz mean 33,5 s.
- Tests 50 -> 57. Reports lokal: data/eval-qwen38-stage1*.json.
  API-first festgehalten (D13-Vorbereitung): Odoo bleibt duenner Client;
  Lohndaten-Zugriff in M4 erfordert Privacy-Neubewertung.
2026-09-15 09:58:25 +02:00
fegger c594c553b5 Tuning: Prompt-Budget + cross_ref-Erweiterung heilt Fehlverweigerungen (D11)
- Ursache der 4 Fehlverweigerungen nach KV/RIS-Erweiterung: lange
  KV-Chunks ueberlieferten num_ctx=16384 (bis 62,7 KB Prompt) — Ollama
  trunciert den Systemprompt vorn, das Modell verliert die Zitierregeln
  ('Block-N'-Zitate statt IDs -> Regenerierung -> Verweigerung).
- Fix: num_ctx 32768; trim_results (max_context_chars=90.000, niedrig
  gerankte Bloecke ganz weglassen statt truncieren, Mindestbestand 6);
  cross_ref_expand 3 -> 6 (Top-3 sind oft Branchen-KV-Bloecke mit leeren
  cross_refs - kuratierte Nachbarn kamen sonst nie nach).
- Eval: alle 4 Faelle geheilt, 10/10 Bestaetigungslaeufe OK.
  Retrieval Recall@8 0,851 -> 0,946 (cross_ref-Extras bringen
  Expected-IDs nach). Antworten: Zitier-Praezision 97,6 %, Verweigerung
  97,6 % (>94,3 %-Gate), erwartete Quelle 83,8 -> 91,9 %, Latenz mean 33 s.
- Tests 49 -> 50 (trim_results); Reports lokal (data/eval-*).
2026-09-15 08:31:02 +02:00
1044 changed files with 35636 additions and 1469 deletions
+147 -14
View File
@@ -5,10 +5,12 @@ Rollender Übergabe-Log für agent-Threads. Workflow: `.agents/SKILL.md`
## Current focus
M1, M2 und **M3 (Bake-off) sind abgeschlossen**`qwen3.8:27b` ist als
Antwortmodell fixiert (D7). Offen: Fehlverweigerungs-Tuning (q-008,
q-031 — beide Finalisten betreffend) und Odoo-Integration (M4, separat
zu planen).
Retrieval-/Antwort-Tuning für komplexe Gestaltungsfragen läuft (D16).
Goldset q-124126 und deterministischer Decision-Support-Pfad sind umgesetzt;
offen ist der vollständige 53-Fragen-Antwortmodus-Eval. Danach folgen die
bewusste Publikations-/Lizenzfreigabe der quellentreuen Rechtsprechungs-
Volltexte und M4 (API-first Odoo-Integration inkl. separat zu planender
Privacy-Grenze für Lohndaten).
## Completed (2026-09-14)
@@ -57,8 +59,8 @@ zu planen).
## Completed (2026-09-15)
- **KB-Erweiterung um WKO-Kollektivverträge + RIS-Gesetze** (User-Auftrag):
`.firecrawl/wko-kv/docs/` (614 KV-Dokumente: Lohn-/Gehaltsordnungen mit
Lohntabellen, KV-Abschlüsse, Erläuterungen, Zusatz-KVs) und `.ris/` (59
`.firecrawl/kv-portal/wko-kv/docs/` (614 KV-Dokumente: Lohn-/Gehaltsordnungen mit
Lohntabellen, KV-Abschlüsse, Erläuterungen, Zusatz-KVs) und `.firecrawl/ris/gesetze/` (59
Gesetze, nur die im Lexis360-Bestand zitierten §-Ausschnitte, Stand
2026-09-13) → **1 274 Layer-2-Einträge** (`kv-kvt-001…614`,
`ris-<prefix>-nn`), 14 984 Chunks, Reindex 8,5 min (11 947 neue
@@ -97,6 +99,55 @@ zu planen).
- **Tests:** 41 → 49, alle grün (neue ID-Räume, html-Source, Konverter,
LAW_MAP-Vollabdeckung, Korpus-Integrationszahl 1 274).
- **D15 (Rechtsprechungs-Intake, 2026-09-15):** `.rechtsprechung/` → neuer
ID-Raum `rj-rjs-*`, gemeinsamer Cluster `rechtsprechung`: 320 RIS-OGD-
Entscheidungen/Rechtssätze, 23 zitierte RIS-Normauszüge und 5 als
nichtamtlich markierte EuGH-lexetius-Textwiedergaben. Parser:
`tools/ingest_sources.py --source rj`; lange Volltexte werden an
Absatzgrenzen in H2-Chunks geteilt. `rj_catalog.json` friert die IDs ein;
Registry/Index und Agent-Zitatvalidierung kennen `rj-*`. Korpus 1.274 →
**1.622** Einträge / 77 Cluster; Reindex: 16.480 Chunks, 1.496 neue
Embeddings in 75 s. Retrieval-Goldset erweitert um q-120123:
Hit-Rate 0,956 · Recall@8 **0,922** (Gate ✓) · MRR 0,661, alle vier
neuen Fälle gefunden. Gezielte Antwortläufe q-120123: alle zitiergültig,
nicht verweigert, keine Regenerierung; Systemprompt verlangt für als
nicht amtlich markierte Quellen die gleiche Einschränkung in der Antwort
(q-123 verifiziert). Voller Antwortmodus-Eval (50 Fragen): **100 %
Zitier-Präzision**, **100 % Verweigerung korrekt**, erwartete Quelle
93,3 %, mean 39,4 s / p95 82,2 s, 5 Regenerierungen; Report
`data/eval-qwen38-rj.json` (lokal/unversioniert). Offline-Tests: 65
grün. **Nicht als generelle
Commit-Freigabe lesen:** Die gelieferten OGH/VwGH/VfGH-Dateien nennen
überwiegend RIS-OGD, der User sprach jedoch von Lexis/opendataloader;
die Publikations-/Lizenzfreigabe der quellentreuen Volltexte ist vor
Commit ausdrücklich zu prüfen. EuGH-Wiedergaben sind als nichtamtlich
markiert und verweisen auf EUR-Lex.
## Completed (2026-09-16)
- **D16 (Gestaltungsfragen / Retrieval-Tuning):** Goldset um q-124126
erweitert (500-Euro-Zusatzzahlung, Alternativen, Mitarbeiterprämie 2026).
`agent/query_planner.py` erkennt Decision-Support deterministisch und zerlegt
in drei gesetzlich gescopte Queries: Mitarbeiterprämie samt §-49-Konflikt,
Zukunftssicherung sowie Sach-/zweckgebundene Leistungen. Decision-Support
läuft als `specific`, nicht als Survey/Map-Reduce. `Retriever.search()` nutzt
denselben Plan im Offline-Eval; `search_multi()` behält Scope/Jahr auch bei
einer einzelnen Query. Ergebnis (53 Goldsetfragen / 48 Retrieval-Fälle):
q-124/q-125/q-126 jeweils Recall@8 **1,00**, gesamt Recall@8 **0,9271**
(Gate >0,9), Hit-Rate 0,9583, MRR 0,6158. Bekannte Altfehler q-015/q-113
bleiben unverändert.
- **Decision-Support-Grounding:** Systemprompt trennt Barzahlung von
zweckgebundenen Leistungen, verbietet pauschale Sieger ohne Kontext,
verlangt vergleichbare Abgabendimensionen und genau eine Rückfrage. Der
bekannte Konflikt Mitarbeiterprämie 2026 wird offen dargestellt:
`wk-akt-04` nennt SV/BV/DB/DZ/KommSt-Pflicht, `lb-sva-03` ordnet die Prämie
systematisch in den taxativen Katalog beitragsfreier Bezüge ein. Zusätzlich
erzwingt ein semantischer Post-Validation-Gate ⚠ + beide IDs + korrekte
Rollen und verhindert interne Regelverweise bzw. Quellenpriorisierung.
Gezielte Real-Läufe: q-124/q-125 nicht verweigert, zitiergültig; q-126 war
bereits sauber. Tests **72 grün**; `git diff --check` sauber. Vollständiger
Antwortmodus-Eval steht noch aus.
## Open issues / blockers
- **Push**: Remote localhost:3003 aus der Sandbox nicht erreichbar — User
@@ -118,13 +169,16 @@ zu planen).
- **M4 Odoo**: native LLM-Module des konkreten Odoo-19-Stands verifizieren
(keine API-Annahmen); Option A (dünnes Custom-Modul + Service-API) ist
Default.
- **Nach KV/RIS-Erweiterung (2026-09-15)**: Antwortmodus unter den Gates
(Zitier-Präzision 95,2 % — 2 Verletzungen; Verweigerung 90,5 % — 4
Fehlverweigerungen, u.a. q-022/q-029; erwartete Quelle 83,8 %) —
Report `data/eval-qwen38-kvris.json`. Retrieval-Gate erfüllt (Recall@8
0,923 nach D10-Kalibrierung). Weiteres Prompt-/Kontext-Tuning offen;
der laufende Arbeitsstand in generate.py/retrieve.py/goldset.yaml
wurde unangetastet gelassen.
- **Nach KV/RIS-Erweiterung (2026-09-15)**: Antwortmodus vor dem Tuning
(D10-Follow-up) unter den Gates (Zitier-Präzision 95,2 % — 2 Verlet-
zungen; Verweigerung 90,5 % — 4 Fehlverweigerungen, u.a. q-022/q-029;
erwartete Quelle 83,8 %) — Report `data/eval-qwen38-kvris.json`.
**D11-Fix behoben die 4 Fehlverweigerungen**: Antwortmodus jetzt
Zitier-Präzision 97,6 %, Verweigerung **97,6 %** (>94,3 %-Gate ✓),
erwartete Quelle 91,9 %, Retrieval Recall@8 0,946 — Report
`data/eval-qwen38-kvris-tuned.json`. Verbleibend: q-015 (Branchen-
Noise) und q-024 als transiente Flakiness (leerer Draft — Think-Ghost-
Verdacht bei 32k-Kontext; beobachten).
- **q-015 (Tagesgelder Dienstreisen)** — Branchen-Noise: 8 Branchen-KV-
Einträge verdrängen lb-rei-01/09 (EStG-Taggeld-Abrechnung) aus Top-8;
generische Branchen-Frage ist mehrdeutig geworden. Hebel: Frage
@@ -176,6 +230,83 @@ zu planen).
candidate_pool=150 → Recall@8 0,851 → 0,923 (>0,9 ✓), Hit-Rate
0,973, MRR 0,667. ENV: PV_DENSE_WEIGHT / PV_RRF_K /
PV_CANDIDATE_POOL. Entry-RRF-Variante getestet: kein Mehrwert.
- **D11 (Prompt-Budget + Kontext-Abdeckung, 2026-09-15):** lange KV-Chunks
ueberlieferten num_ctx=16384 (q-022: 62,7 KB Prompt ≈ 18k Tokens) —
Ollama trunciert den Systemprompt vorn, das Modell verliert die
Zitierregeln („Block 8“-Zitate statt IDs → Regenerierung → Verweigerung).
Fix: num_ctx 32 768, `trim_results` (max_context_chars=90 000, Tail-
Bloecke ganz weg statt trunciert, Mindestbestand 6) und
cross_ref_expand 3 → 6 (die Top-3 sind oft Branchen-KV-Bloecke mit
leeren cross_refs — kuratierte Nachbarn kamen sonst nie nach).
Ergebnis: alle 4 Fehlverweigerungs-Faelle (q-001/q-019/q-022/q-029)
geheilt, 10/10 Bestaetigungslaeufe OK. Eval: Zitier-Praezision 97,6 %,
Verweigerung 97,6 % (> 94,3 %-Gate ✓), erwartete Quelle 91,9 %
(vorher 83,8 %), Retrieval Recall@8 **0,946** (cross_ref-Extras
bringen Expected-IDs nach), MRR 0,671. Latenz mean 33 s.
Report `data/eval-qwen38-kvris-tuned.json`. Restrisiko: q-024
flaky (1/3 Laeufe — transienter leerer Draft, Think-Ghost-Verdacht);
q-015 Branchen-Noise bleibt offen.
- **D12 (Komplexe-Fragen-Stufe 1 / M6, 2026-09-15):** Query-Planer vor dem
Retrieval: Heuristik-Gate (Jahreszahl, Vergleichs-/Aggregationsmarker,
Laenge) entscheidet, ob ein kleiner LLM-Call die Frage in 1-3 Sub-Queries
zerlegt (JSON, temp 0, num_predict 220; Fehler -> Original als Einzel-
Query). Multi-Query-Retrieval: je Sub-Query BM25+Dense, Beitraege
summieren; **Per-Query-Slots** (2 je Sub-Query) sichern jeden Frage-
aspekt im Kontext — ohne sie dominieren Eintraege, die in mehreren
Sub-Queries mittelgut matchen (q-113: 5 KV-Urlaubs-SS schlugen
lb-url-05 (Rang 6) und ris-url-01 (>Top-12) ueber beide Sub-Queries).
**Scope-Filter** je Sub-Query: Planer markiert "gesetz" (nur Lexis/WIKU/
RIS-Chunks) bzw. "kv" (nur Branchen-KV) — q-113 geheilt (ris-url-01/
lb-url-05 + KV-Ss im selben Kontext). Scope-Fallback: gefilterte Suche
leer -> unscoped. Temporal-Intent: stand_year je Sub-Query, kv-Eintraege
im Geltungsjahr erhalten temporal_boost (default 0 — FTS-Tag-Signal
`jahr-YYYY` reichte in der Messung). Grounding unveraendert: ein
Kontext aus der Union, Post-Validierung ueber die gesamte Retrieved-
Menge, Verweigerungspflicht unveraendert.
Eval (46 Fragen): Zitier-Praezision 97,8 %, Verweigerung 97,8 %
(Gate > 94,3 % erfuellt), erwartete Quelle 90,2 %, Latenz mean 33,5 s.
Temporal-Fragen laufen sogar ohne Planer gut (Jahr-Toknen matchen
`jahr-YYYY`-Tags). Neue komplexe Fragen im Goldset: q-110 (Temporal 2023,
nach Korrektur — eine 2024er-Friseur-Lohnordnung existiert im Korpus
NICHT: kv-kvt-161 ist der Mantelvertrag ohne Lohntabelle; korrekt
verweigert), q-111 (Temporal 2025), q-112 (Abfertigung Vergleich),
q-113 (Gesetz+KV Multi-Source). Tests 50 -> 57.
Offen: q-029 Survey (Stufe-2-Hebel: Map-Reduce), q-113 Planer-
Scope flaky (1/2 Laeufe ohne gesetz-Marker).
- **D13 (Antworttyp-Routing / Stufe 2, 2026-09-15):** (a) Planer liefert
jetzt `type: survey|specific`; Survey-Fragen („Welche Neuerungen …“)
laufen ueber Map-Reduce: breiteres Retrieval (`survey_blocks`=16,
PV_SURVEY_BLOCKS), ein Map-Call destilliert JE Block als Stichpunkte
mit seiner KB-ID (MAP_SYSTEM_PROMPT), ein Reduce-Call synthetisiert
daraus die Antwort mit dem normalen Grounding-Prompt; Zitier-
Validierung weiterhin strikt ueber die Retrieved-Union, leerer
Map-Output -> Fallback Einzelantwort. (b) Systemprompt-Regel 9: bei
wesentlicher Kontextabhaengigkeit (Branche, Bundesland, Zeitraum)
belegte allgemeine Aussage + EINE Rueckfrage statt Verweigerung
(API-first; Odoo-Chat kann die Rueckfrage als Follow-up nutzen).
Ergebnis: **q-029 geheilt** (vorher jahrelange Fehlverweigerung;
jetzt Teilantwort mit 5 belegten Heften, 95 s — Map-Reduce-Latenz
nur bei Survey-Fragen), **q-015** antwortet mit expliziter
KV-Abhaengigkeit + Rueckfrage statt Branchen-Noise. Eval (46 Fragen):
Zitier-Praezision 97,8 %, Verweigerung 97,8 % (Gate erfuellt),
erwartete Quelle 90,2 %, Latenz mean 34,2 s. Tests 57 -> 59.
Report `data/eval-qwen38-stage2.json`. Verbleibend: q-024 transiente
Flakiness (Think-Ghost-Verdacht, 2/3 Laeufe sauber).
- **D14 (Output-Budget + length-Retry, 2026-09-15):** q-024-Flakiness
Ursache gefunden — NICHT Thinking (Hypothese widerlegt: thinking-Feld
leer, keine Tags), sondern `num_predict=1024`: lange belegte Antworten
wurden bei `done_reason=length` abgschnitten (q-024: 3/3 Sondenlaeufe)
→ unvollstaendige Zitationen → CITE_RE matcht partielle IDs →
Verletzung → Regen-Eskalation → UNCERTAIN. Fix: (a) num_predict
1024 → 2048; (b) `OllamaClient.chat_full()` liefert (content,
done_reason); (c) `chat_with_length_retry`: ein technischer Retry
mit 2× num_predict bei length (zaehlt nicht als Regel-Regenerierung,
laeuft auch im Map- und im Regenerierungspfad).
**Voll-Eval: Zitier-Praezision 100 %, Verweigerung korrekt 100 %**
(46/46 — alle M3-Gates erfuellt, erstmals ueber Basislinie),
erwartete Quelle 92,7 %, Latenz mean 39,6 s / p95 78 s (+5 s:
vollstaendige statt abgeschnittener Antworten). q-024: 4/4 stabil.
Tests 59 → 60. Report `data/eval-qwen38-lengthfix.json`.
- **Bake-off-Protokoll** (Skill): Modellwechsel nur über dokumentierten
Goldset-Vergleich; Kriterium: Zitier-Präzision > Verweigerungs-
korrektheit > Latenz.
@@ -187,4 +318,6 @@ zu planen).
- `agent/README.md` — Betrieb, Konfiguration, Host-Schritte.
- `agent/eval/goldset.yaml` — Goldset (IDs gegen kb.json verifiziert).
- `agent/generate.py` — Grounding-Kern (Prompt, Post-Validierung).
- `wissensbasis/README.md` — Layer-2-Schema (unverändert gültig).
- `wissensbasis/README.md` — Layer-2-Schema + Rechtsprechungs-Provenance.
- `tools/ingest_sources.py` / `tools/catalogs/rj_catalog.json` — rj-Intake und gefrorene IDs.
- `agent/eval/goldset.yaml` — q-120123; als Nächstes Antwortmodus-Eval.
+1 -1
View File
@@ -4,7 +4,7 @@
.firecrawl/
# Scraped legal reference texts — local only
.ris/
.rechtsprechung/
# RAG service artifacts (index, caches, venv)
data/
+87 -15
View File
@@ -1,10 +1,10 @@
# PV RAG Agent
Lokaler RAG-Agent für österreichische Personalverrechnung: beantwortet
Fragen **ausschließlich** aus der kuratierten Wissensbasis (Layer 2,
`wissensbasis/`, **1274 Einträge**: 601 lb/wk-Kuratierung + 614 WKO-KV-
Dokumente + 59 RIS-Gesetze) — mit ID- und Stand-Beleg, ohne
Trainingswissen, ohne Web-Zugriff. Verbindliche Regeln:
Fragen **ausschließlich** aus der Wissensbasis (Layer 2,
`wissensbasis/`, **1622 Einträge**: 601 lb/wk-Kuratierung + 614 WKO-KV-
Dokumente + 59 RIS-Gesetze + 348 Rechtsprechungs-/Normquellen) — mit ID-
und Stand-Beleg, ohne Trainingswissen, ohne Web-Zugriff. Verbindliche Regeln:
`.agents/skills/pv-rag-agent/SKILL.md`, Plan: `planung.md`.
## Architektur (Kurzfassung)
@@ -60,6 +60,13 @@ python -m agent.cli serve # http://127.0.0.1:8080 (/ask, /health, /r
| `PV_CANDIDATE_POOL` | `150` | Kandidaten je Liste vor der Fusion (KV/RIS-Erweiterung: Longtail-Spezialisten in der Kandidatur halten) |
| `PV_RRF_K` | `20` | RRF-Konstante (erweiterter Korpus: Top-Ränge dominant) |
| `PV_DENSE_WEIGHT` | `2.0` | RRF-Gewicht der Dense-Liste relativ zu BM25 (BM25 ist durch KV-§-Titel-Matches inflationiert) |
| `PV_PER_QUERY_SLOTS` | `2` | Multi-Query: garantierte Kontext-Slots je Sub-Query (Multi-Hop-Abdeckung) |
| `PV_QUERY_PLANNER` | `true` | Query-Planer an (Heuristik-Gate entscheidet je Frage) |
| `PV_PLANNER_MODEL` | leer = Antwortmodell | Modell des Planer-Calls |
| `PV_TEMPORAL_BOOST` | `0.0` | Bonus für kv-Einträge im gefragten Geltungsjahr |
| `PV_SURVEY_BLOCKS` | `16` | Map-Reduce: breiteres Retrieval für Survey-Fragen (Typ `survey` vom Planer) |
| `PV_NUM_CTX` | `32768` | Modell-Kontextfenster (KV-Chunks überschreiten 16k — Overflow trunciert den Systemprompt) |
| `PV_MAX_CONTEXT_CHARS` | `90000` | User-Content-Budget; niedrig gerankte Blöcke werden ganz weggelassen (`trim_results`) |
| `PV_CONTEXT_BLOCKS` | `8` | Kontextblöcke im Prompt |
| `PV_PORT` | `8080` | API-Port |
@@ -118,13 +125,75 @@ Latenz-Hebel für M3: weniger Kontextblöcke, schnellere Kandidaten
601 → 1274 Einträge (kv-*: 614 WKO-KV-Dokumente quellentreu, ris-*:
59 RIS-Gesetze, nur die im Lexis360-Bestand zitierten §-Auschnitte);
14 984 Chunks. Kalibrierung per Goldset-Sweep (dichte Gewichtung,
RRF-k, Pool): Hit-Rate 0,865 → 0,973 · Recall@8 0,851 → **0,923** (>0,9 ✓)
· MRR 0,621 → 0,667. Antwortmodus (qwen3.8:27b): Zitier-Präzision
95,2 % (2 Verletzungen), Verweigerung korrekt 90,5 % (4 Fehlverweige-
rungen, u.a. q-022/q-029), erwartete Quelle zitiert 83,8 %, Latenz
mean 31 s / p95 53 s — **unter den M3-Gates** (100 % / 94,3 %): das
Fehlverweigerungs-Tuning (Regel 4/8 + Kontextblöcke) ist weiteres
Thema, Prompt bleibt beim laufenden Arbeitsstand eingefroren.
RRF-k, Pool): Hit-Rate 0,865 → 0,973 · Recall@8 0,851 → 0,923
(>0,9 ✓) · MRR 0,621 → 0,667.
**Prompt-Budget + Kontext-Abdeckung (2026-09-15, D11):** Lange KV-Chunks
überlieferten num_ctx=16384 — Ollama trunciert den Systemprompt vorn,
das Modell verliert die Zitierregeln (Fehlverweigerungen, „Block-N“-
Zitate). Fix: num_ctx 32768, `trim_results` (max_context_chars=90 000,
Tail-Blöcke ganz weg) und cross_ref_expand 6 (Top-3 sind oft Branchen-
KV-Blöcke mit leeren cross_refs — kuratierte Nachbarn kamen nie nach).
Alle 4 Fehlverweigerungs-Fälle geheilt; 10/10 Bestätigungsläufe OK.
**Retrieval:** Recall@8 **0,946** (>0,9 ✓) · Hit-Rate 0,973 · MRR 0,671.
**Antworten (qwen3.8:27b, 42 Fragen):** Zitier-Präzision **97,6 %** (1
transienter Fall) · Verweigerung korrekt **97,6 %** (>94,3 %-Gate ✓) ·
erwartete Quelle zitiert 91,9 % · Latenz mean 33 s / p95 57 s ·
6 Regenerierungen. Reports: `data/eval-qwen38-kvris.json` (vor Tuning),
`data/eval-qwen38-kvris-tuned.json`. Offen: q-015 Branchen-Noise,
q-024 transiente Flakiness (leerer Draft).
**Komplexe-Fragen Stufe 1 (2026-09-15, M6/D12):** Query-Planer (Heuristik-
Gate → kleiner LLM-Call, 1-3 Sub-Queries als JSON; Stand-Jahr + Scope
je Sub-Query), Multi-Query-Retrieval mit Per-Query-Slots (2 je Sub-Query)
und Scope-Filter („gesetz“ = nur Lexis/WIKU/RIS, „kv“ = nur Branchen-KV;
Fallback unscoped). Grounding unverändert: eine Retrieved-Menge, eine
Antwort, Post-Validierung über die Union. Eval (46 Fragen): Zitier-
Präzision 97,8 % · Verweigerung korrekt 97,8 % (Gate ✓) · erwartete
Quelle 90,2 % · Latenz mean 33,5 s. Komplexe Goldset-Fragen: q-110113
(Temporal 2023/2025, Abfertigung-Vergleich, Gesetz+KV-Multi-Source).
Report: `data/eval-qwen38-stage1-final.json`. Offen: q-029 Survey
(Stufe-2-Hebel: Map-Reduce), q-015 Branchen-Noise → API-first-Rückfrage
(Stufe 2).
**Antworttyp-Routing Stufe 2 (2026-09-15, M6/D13):** Planer liefert
`type: survey|specific`. Survey-Fragen („Welche Neuerungen …“) →
**Map-Reduce**: Retrieval auf `survey_blocks` (16) erweitert, ein Map-Call
destilliert jeden Block als Stichpunkte mit seiner KB-ID, ein Reduce-Call
synthetisiert die Endantwort — Zitier-Validierung weiterhin strikt über
die Retrieved-Union. **Regel 9** (Kontext-Abhängigkeit): belegte allgemeine
Aussage + eine Rückfrage statt Verweigerung (Branche/Bundesland/Zeitraum).
Ergebnisse: q-029 geheilt (5 belegte Hefte, ~95 s Map-Reduce-Latenz),
q-015 antwortet mit KV-Abhängigkeit + Rückfrage. Eval (46 Fragen):
Zitier-Präzision 97,8 % · Verweigerung 97,8 % (Gate ✓) · erwartete
Quelle 90,2 % · Latenz mean 34,2 s. Report: `data/eval-qwen38-stage2.json`.
**Output-Budget + length-Retry (2026-09-15, D14):** Die q-024-Flakiness
war kein Thinking, sondern `num_predict=1024`: lange belegte Antworten
wurden bei `done_reason=length` abgeschnitten → unvollständige Zitationen
→ Verletzungs-/Eskalationsspirale. Fix: `num_predict=2048`, `chat_full()`
liefert `done_reason`, bei `length` ein technischer Retry mit 2× Budget
(kein Regel-Regenerierungs-Zähler). **Voll-Eval: Zitier-Präzision
100 % · Verweigerung korrekt 100 %** (46/46, alle M3-Gates erstmals
voll erfüllt) · erwartete Quelle 92,7 % · Latenz mean 39,6 s / p95 78 s.
Report: `data/eval-qwen38-lengthfix.json`. Offene Restfälle: keine
Fehlverweigerungen/Verletzungen mehr; 3-4 Fragen zitiert gültige,
aber nicht die erwartete Quelle (q-015-Klasse).
**Rechtsprechungs-Intake (2026-09-15, D15):** Korpus 1.274 → **1.622**
Einträge: 320 RIS-OGD-Entscheidungen/Rechtssätze, 23 zitierte RIS-
Normauszüge und 5 als nichtamtlich markierte EuGH-Textwiedergaben im neuen
`rj-rjs-*`-ID-Raum. Der Reindex erzeugte **16.480 Chunks** (1.496 neue
`bge-m3`-Embeddings, 75 s). Retrieval mit dem erweiterten 50-Fragen-Goldset:
Hit-Rate **0,956** · Recall@8 **0,922** · MRR 0,661; das Recall-Gate >0,9
bleibt erfüllt, alle vier neuen Rechtsprechungsfälle werden gefunden. Der
vollständige Antwortmodus-Eval (50 Fragen) erreicht **100 % Zitier-Präzision**
und **100 % korrekte Verweigerungen**, bei 93,3 % erwarteter Quelle, mean
39,4 s / p95 82,2 s und fünf Regenerierungen. Report:
`data/eval-qwen38-rj.json` (lokal, unversioniert). EuGH-Texte sind im Kontext
und in der Quelle explizit als nichtamtliche lexetius-Wiedergabe
gekennzeichnet; der Systemprompt verlangt nun dieselbe Einschränkung auch in
der Antwort.
## Dateien
@@ -142,7 +211,7 @@ agent/
eval/ goldset.yaml + evaluate.py
web/index.html Minimaler Test-Chat
tools/ Intake + Registry (build_registry.py, ingest_sources.py)
tests/ 49 Tests (offline, Fake-Ollama)
tests/ 64 Tests (offline, Fake-Ollama)
data/ index.db (gitignored)
```
@@ -154,6 +223,9 @@ python -m pytest -q # 49 Tests, alle offline
## Lizenz-Disziplin
`.lexis360/`, `.wiku/`, `.firecrawl/`, `.ris/` sind lokal und unversioniert
(`.gitignore`). Der Index enthält ausschließlich Layer-2-Kuratierung;
Layer-1-Prompts wären ein Lizenzverstoß und sind im Code nicht vorgesehen.
`.lexis360/`, `.wiku/`, `.firecrawl/`, `.firecrawl/ris/gesetze/`, `.rechtsprechung/` sind lokal
und unversioniert (`.gitignore`). Der Index enthält ausschließlich Layer-2-
Inhalte; Layer-1-Prompts wären ein Lizenzverstoß und sind im Code nicht
vorgesehen. Die quellentreuen `rj_*.md` sind vor einer Versionierung anhand
von Provenance und Lizenzfreigabe der gelieferten Volltexte zu prüfen; die
EuGH-Wiedergaben sind ausdrücklich nicht amtlich.
+31 -3
View File
@@ -47,8 +47,13 @@ class Config:
# Generierung
temperature: float = 0.1
num_ctx: int = 16384
num_predict: int = 1024
num_ctx: int = 32768 # qwen3.8: 256k-faehig; 16k trunciert lange KV-Prompts
# (KV-Cache @32k ~6-8 GB -> gesamt ~21 GB, Budget ok)
max_context_chars: int = 90_000 # Budget fuer User-Content; niedrig gerankte
# Bloecke werden weggelassen statt trunciert
num_predict: int = 2048 # Output-Budget; 1024 schnitt lange belegte
# Antworten ab (done_reason=length, q-024-Fall)
# -> unvollstaendige Zitationen
think: bool = False # Thinking per Request abschalten (Latenz)
chat_timeout_s: float = 300.0
embed_timeout_s: float = 240.0
@@ -59,7 +64,10 @@ class Config:
# (KV/RIS-Erweiterung: Longtail-Spezialisten
# liegen sonst außerhalb der Kandidatur)
context_blocks: int = 8 # Kontext-Blöcke im Prompt
cross_ref_expand: int = 3 # Top-Einträge, deren cross_refs ergänzt werden
cross_ref_expand: int = 6 # Top-Eintraege, deren cross_refs ergaenzt
# (KV/RIS-Erweiterung: die Top-3 sind oft
# Branchen-KV-Bloecke mit leeren cross_refs —
# kuratierte Nachbarn kommen sonst nie nach)
cross_ref_max_extra: int = 6 # Obergrenze der Ergänzungen
rrf_k: int = 20 # RRF-Konstante (erweiterter Korpus: Top-Ränge
# müssen dominanter zählen)
@@ -67,6 +75,18 @@ class Config:
# (BM25 ist durch KV-§-Titel-Matches inflationiert)
recency_boost: float = 0.005 # additiv auf RRF-Score, gewichtet nach Stand
# Query-Planer (Stufe 1, M6): komplexe Fragen -> 1-3 Sub-Queries
planner_enabled: bool = True # Heuristik-Gate entscheidet je Frage
planner_max_queries: int = 3
planner_num_predict: int = 220
planner_model: str = "" # leer = Antwortmodell
per_query_slots: int = 2 # Multi-Query: garantierte Kontext-Slots
# je Sub-Query (Multi-Hop-Abdeckung)
survey_blocks: int = 16 # Map-Reduce (Stufe 2): breiteres
# Retrieval fuer Survey-Fragen
temporal_boost: float = 0.0 # Bonus fuer kv-Eintraege im gefragten
# Geltungsjahr (0 = nur FTS-Tag-Signal)
# Service
port: int = 8080
@@ -81,6 +101,7 @@ class Config:
answer_model=_env_str("PV_ANSWER_MODEL", d.answer_model),
temperature=_env_float("PV_TEMPERATURE", d.temperature),
num_ctx=_env_int("PV_NUM_CTX", d.num_ctx),
max_context_chars=_env_int("PV_MAX_CONTEXT_CHARS", d.max_context_chars),
num_predict=_env_int("PV_NUM_PREDICT", d.num_predict),
think=_env_bool("PV_THINK", d.think),
chat_timeout_s=_env_float("PV_CHAT_TIMEOUT_S", d.chat_timeout_s),
@@ -93,5 +114,12 @@ class Config:
rrf_k=_env_int("PV_RRF_K", d.rrf_k),
dense_weight=_env_float("PV_DENSE_WEIGHT", d.dense_weight),
recency_boost=_env_float("PV_RECENCY_BOOST", d.recency_boost),
planner_enabled=_env_bool("PV_QUERY_PLANNER", d.planner_enabled),
planner_max_queries=_env_int("PV_PLANNER_MAX_QUERIES", d.planner_max_queries),
planner_num_predict=_env_int("PV_PLANNER_NUM_PREDICT", d.planner_num_predict),
planner_model=_env_str("PV_PLANNER_MODEL", d.planner_model),
per_query_slots=_env_int("PV_PER_QUERY_SLOTS", d.per_query_slots),
survey_blocks=_env_int("PV_SURVEY_BLOCKS", d.survey_blocks),
temporal_boost=_env_float("PV_TEMPORAL_BOOST", d.temporal_boost),
port=_env_int("PV_PORT", d.port),
)
+54 -1
View File
@@ -144,4 +144,57 @@ questions:
- id: r-005
question: "Wie hoch ist der kollektivvertragliche Mindestlohn im KV für Raumfahrttechnik?"
expect_refusal: true
note: "Branche nicht im KV-Korpus — Retrieval darf nicht leer sein, Antwort muss verweigern."
note: "Branche nicht im KV-Korpus — Retrieval darf nicht leer sein, Antwort muss verweigern."
# --- Rechtsprechung / Normauszüge (Batch rj 1, 2026-09-15) ---
- id: q-120
question: "Rechtfertigt eine schriftliche Rüge eines Hauptbuchhalters den vorzeitigen Austritt?"
expected_ids: [rj-rjs-020]
note: "OGH-Rechtssatz; nur die konkret dokumentierte Rüge und Verschuldensabwägung behaupten."
- id: q-121
question: "Was entschied der Verfassungsgerichtshof zur Strafbestimmung des AVRAG bei nicht bereitgestellten Lohnunterlagen?"
expected_ids: [rj-rjs-006]
- id: q-122
question: "Wann ist das fortlaufende Gehalt nach § 15 AngG zu zahlen?"
expected_ids: [rj-rjs-333]
- id: q-123
question: "Welche Bedeutung hat nach EuGH C-29/91 der Übergang von Tätigkeiten nach einer Subventionsverlagerung?"
expected_ids: [rj-rjs-002]
note: "Nur als nichtamtliche lexetius-Textwiedergabe im Korpus; Antwort muss den Stand 1992-05 führen."
# --- Gestaltungsfragen / Entscheidungsunterstützung ---
- id: q-124
question: "Ich will meinem Mitarbeiter 500 Euro zusätzlich auszahlen. Was ist die günstigste Lösung?"
expected_ids: [wk-akt-04, lb-sva-03]
note: "Offene Gestaltungsfrage: Zeitraum, Branche/KV, gewünschte Barzahlung und persönliche Voraussetzungen fehlen. Für 2026 ist die Mitarbeiterprämie ein direkter Kandidat; Konflikt zur SV-Behandlung zwischen wk-akt-04 (pflichtig) und lb-sva-03 (beitragsfrei) ausdrücklich mit beiden IDs darstellen und gezielt rückfragen. Keine pauschale Empfehlung."
tags: [decision-support, clarification, conflict]
- id: q-125
question: "Welche abgabenbegünstigten Möglichkeiten gibt es 2026 statt einer normalen zusätzlichen Barprämie von 500 Euro?"
expected_ids: [wk-akt-04, lb-vor-11, lb-ges-01, lb-sva-04]
note: "Vergleichs-/Survey-Frage: Mitarbeiterprämie, Zukunftssicherung, Sachzuwendungen und Mahlzeiten-/Kinderbetreuungsleistungen unterscheiden; Voraussetzungen und fehlenden Arbeitnehmerkontext nennen. Nicht behaupten, alle Alternativen seien frei als Bargeld auszahlbar."
tags: [decision-support, survey]
- id: q-126
question: "Kann ich 2026 jedem Mitarbeiter einfach 500 Euro steuerfrei als Mitarbeiterprämie auszahlen?"
expected_ids: [wk-akt-04]
note: "Prämisse korrigieren: keine voraussetzungslose Auszahlung; lohngestaltende Grundlage, Zeitraum, Zusätzlichkeit und Abgaben außerhalb der Lohnsteuer anhand der Quelle erklären."
tags: [decision-support, premise-correction]
# --- Komplexe Fragen (Stufe 1, M6: Multi-Query + Temporal-Intent) ---
- id: q-110
question: "Wie hoch war der kollektivvertragliche Mindestmonatslohn für angelernte Friseurinnen und Friseure ab 1.4.2023?"
expected_ids: [kv-kvt-014]
note: "Temporal: historische KV-Fassung 2023 muss vor den 2025/2026er-
Versionen liegen. (Eine 2024er-Lohnordnung existiert im Korpus nicht:
kollektivvertrag-friseur-2024 ist der Mantelvertrag ohne Lohntabelle.)"
- id: q-111
question: "Welche Lehrlingseinkommen galten im Friseurgewerbe ab 1.4.2025?"
expected_ids: [kv-kvt-001]
note: "Temporal: KV-Fassung 2025 (nicht 2026)."
- id: q-112
question: "Was ist der Unterschied zwischen Abfertigung neu und Abfertigung alt?"
expected_ids: [lb-end-02, lb-end-03]
note: "Multi-Hop/Vergleich: beide Regime aus getrennten Eintraegen belegen."
- id: q-113
question: "Wie viele Werktage gesetzlicher Urlaub stehen Arbeitnehmern zu und welche Rolle spielt dabei der Kollektivvertrag?"
expected_ids: [ris-url-01, lb-url-05]
note: "Multi-Source: Gesetz + Kuratierung; UrlG § 2 (1) erlaubt KV-Abweichungen (§ 2 Abs. 4)."
+186 -20
View File
@@ -13,6 +13,7 @@ import time
from .config import Config
from .normalize import normalize_text
from .ollama_client import OllamaClient
from .query_planner import SubQuery, is_decision_support, plan_queries
from .retrieve import ChunkResult, Retriever
REFUSAL_MESSAGE = "Dazu enthält die Wissensbasis keine Aussage."
@@ -38,20 +39,57 @@ Verbindliche Regeln:
mache klar, welcher Aspekt belegt ist. Verweigere nur, wenn KEIN
Block thematisch zur Frage passt, mit exakt:
„Dazu enthält die Wissensbasis keine Aussage.“ — und schlage nichts vor.
5. Widersprechen sich Kontextblöcke, nenne beide Werte mit ihren IDs und
kennzeichne den Widerspruch mit ⚠. Löse Widersprüche niemals stillschweigend auf.
5. Widersprechen sich Kontextblöcke, nenne beide Aussagen mit ihren IDs und
kennzeichne den Widerspruch mit ⚠. Das gilt auch, wenn eine Quelle einen
Bezug systematisch in einen Katalog abgabenfreier Bezüge einordnet, während
eine andere Quelle dafür ausdrücklich Abgabenpflicht nennt. Löse
Widersprüche niemals stillschweigend auf.
6. Nenne Paragraphen und Gesetze nur, wenn ein Kontextblock sie nennt.
7. Antworte auf Deutsch und prägnant (Stichpunkte, wo sinnvoll).
8. Baut die Frage auf einer falschen Annahme auf (z. B. ein nicht
existierendes Gesetz), korrigiere die Annahme anhand der Blöcke und
gib die zutreffende, belegte Aussage. Muster: Auf „Was regelt das
Mindestlohngesetz?“ antworte sinngemäß „Ein Mindestlohngesetz existiert
laut Kontext nicht; stattdätzlich gilt …“ — mit Beleg [ID].
laut Kontext nicht; stattdessen gilt …“ — mit Beleg [ID].
Verweigere in diesem Fall nicht.
9. Hängt die Antwort wesentlich von nicht genanntem Kontext ab (z. B.
Branche, Bundesland, Zeitraum), sage dies explizit: gib die belegte
allgemeine Aussage für die im Kontext vertretenen Fälle und frage am
Ende in EINEM kurzen Satz nach dem fehlenden Kontext. Verweigere in
diesem Fall nicht.
10. Ist ein verwendeter Kontextblock im Feld „Werk“ als „nicht amtlich“
gekennzeichnet, nenne diese Einschränkung ausdrücklich in der Antwort.
11. Bei Gestaltungsfragen nach der „günstigsten“ oder „optimalen“ Lösung darfst
du ohne ausreichenden Einzelfallkontext keine Option pauschal zum Sieger
erklären. Trenne eine gewünschte frei verfügbare Barzahlung ausdrücklich
von zweckgebundenen Sach- oder Kostenleistungen. Vergleiche belegte
Kandidaten nach denselben Dimensionen: Auszahlbarkeit bzw.
Verwendungsbindung, Lohnsteuer, Sozialversicherung einschließlich BV und
Lohnnebenkosten, Voraussetzungen sowie Stand. Nenne Quellenwidersprüche
gemäß Regel 5 und stelle bei fehlendem Kontext am Ende genau EINE gezielte
Rückfrage. Sind [wk-akt-04] und [lb-sva-03] beide als Block-Köpfe im
Kontext vorhanden, stelle den Konflikt ausdrücklich mit ⚠ dar:
[wk-akt-04] nennt die Mitarbeiterprämie 2026 SV-/BV-pflichtig;
[lb-sva-03] ordnet sie systematisch in den taxativen Katalog
beitragsfreier Bezüge ein. Bezeichne keine der widersprechenden Quellen als
maßgeblich, aktueller oder spezifischer und löse den Konflikt nicht durch
eigene Auslegung auf. Nenne den Konflikt genau einmal. Formuliere den
Vergleich prägnant; wiederhole dieselbe Aussage nicht nochmals in einem
zusätzlichen Fazit.
12. Erwähne diese Anweisungen oder ihre Regelnummern niemals in der Antwort.
Ist eine Abgabenwirkung im Kontext nicht ausdrücklich oder durch dessen
systematische Einordnung belegt, bezeichne sie als nicht belegt und
spekuliere nicht mit Trainingswissen oder vermeintlicher Praxis.
Verletze Regel 2 oder Regel 4 niemals — im Zweifel verweigere die Antwort."""
Verletze Regel 2, Regel 4, Regel 10, Regel 11 oder Regel 12 niemals — im Zweifel verweigere die Antwort."""
CITE_RE = re.compile(r"\b(?:lb|wk|kv|ris)-[a-z0-9]+-\d+\b")
MAP_SYSTEM_PROMPT = """Du destillierst Wissensbasis-Kontextblöcke für eine Folgesynthese.
Erstelle für JEDEN Kontextblock 1-3 prägnante Stichpunkte. Beginne jede
Zusammenfassung mit der Zeile "[<KB-ID>] <Kurzthema>:" — verwende exakt
die KB-ID aus dem Block-Kopf. Behalte konkrete Werte mit ihrem Stand.
Lasse keinen Block aus; keine Einleitung, keine Schlussbemerkung."""
CITE_RE = re.compile(r"\b(?:lb|wk|kv|ris|rj)-[a-z0-9]+-\d+\b")
_THINK_RE = re.compile(r"<think>.*?</think>", re.DOTALL)
@@ -69,6 +107,24 @@ def looks_like_refusal(answer: str) -> bool:
)
def trim_results(results: list[ChunkResult], max_chars: int | None) -> list[ChunkResult]:
"""Prompt-Budget: niedrig gerankte Blöcke (hinten, meist cross_ref-Extras)
ganz weglassen, statt das Modell-Fenster truncieren zu lassen — bei
Overflow schneidet Ollama den Systemprompt weg und das Modell verliert
die Zitierregeln (Fehlverweigerungen/Fließtext-Zitationen, D10-Follow-up).
Mindestens 6 Blöcke bleiben erhalten."""
if not max_chars:
return results
out = list(results)
def total(rows: list[ChunkResult]) -> int:
return sum(len(r.title) + len(r.section) + len(r.text) + 64 for r in rows)
while out and total(out) > max_chars and len(out) > 6:
out.pop()
return out
def build_user_content(question: str, results: list[ChunkResult]) -> str:
blocks = []
for i, r in enumerate(results, 1):
@@ -93,6 +149,45 @@ def validate_answer(answer: str, allowed_ids: list[str]) -> list[str]:
return violations
def validate_decision_support_answer(
question: str, answer: str, allowed_ids: list[str]
) -> list[str]:
"""Semantischer Gate für den bekannten Mitarbeiterprämien-Konflikt.
Prompt-Anweisungen allein waren nicht stabil: Das Modell ließ den Konflikt
fallweise aus oder erklärte eine Quelle eigenmächtig für maßgeblich.
"""
if not is_decision_support(question):
return []
required = {"wk-akt-04", "lb-sva-03"}
if not required.issubset(set(allowed_ids)):
return []
cited = set(CITE_RE.findall(answer))
violations: list[str] = []
if "" not in answer or not required.issubset(cited):
violations.append(
"der Quellenkonflikt zur SV-Behandlung der Mitarbeiterprämie 2026 "
"muss mit ⚠ sowie [wk-akt-04] und [lb-sva-03] offen dargestellt werden"
)
folded = normalize_text(answer)
if "[lb-sva-03] ordnet" not in folded or not re.search(
r"\[wk-akt-04\].{0,160}(?:pflicht|pflichtig)", folded, re.DOTALL
):
violations.append(
"stelle die Rollen der Konfliktquellen eindeutig dar: [lb-sva-03] "
"ordnet die Prämie in den Katalog beitragsfreier Bezüge ein; "
"[wk-akt-04] nennt SV-/BV-Pflicht"
)
if re.search(r"\bgemäß (?:der )?regel|\bregel \d+", folded):
violations.append("erwähne keine internen Regeln oder Regelnummern")
if re.search(r"\b(?:aktuellere|maßgebliche|spezifischere)\w* (?:quelle|news|aussage)", folded):
violations.append(
"priorisiere bei dem offenen Konflikt keine Quelle als aktueller, "
"maßgeblicher oder spezifischer"
)
return violations
def _source_rows(results: list[ChunkResult]) -> list[dict]:
rows = []
seen: set[str] = set()
@@ -120,16 +215,41 @@ def answer_question(
retriever: Retriever | None = None,
top_k: int | None = None,
) -> dict:
"""Vollständiger Ask-Zyklus: Retrieval -> Prompt -> LLM -> Post-Validierung."""
"""Vollständiger Ask-Zyklus: Query-Planung -> Retrieval -> Prompt -> LLM ->
Post-Validierung. Der Planer läuft vor dem Retrieval (Heuristik-Gate,
nur bei komplexen Fragen); seine Sub-Queries fusionieren in EINER
Retrieved-Menge, gegen die die Post-Validierung prüft."""
t0 = time.perf_counter()
own_retriever = retriever is None
if retriever is None:
retriever = Retriever(cfg)
if client is None:
client = OllamaClient(
cfg.ollama_url,
embed_timeout_s=cfg.embed_timeout_s,
chat_timeout_s=cfg.chat_timeout_s,
)
sub_queries = [SubQuery(text=question)]
planned = False
qtype = "specific"
if cfg.planner_enabled:
try:
sub_queries, planned, qtype = plan_queries(question, client, cfg)
sub_queries = sub_queries[: cfg.planner_max_queries] or sub_queries[:1]
except Exception:
sub_queries, planned, qtype = [SubQuery(text=question)], False, "specific"
n_entries = top_k
if n_entries is None and qtype == "survey":
n_entries = cfg.survey_blocks
try:
results = retriever.search(question, n_entries=top_k)
results = retriever.search_multi(sub_queries, n_entries=n_entries)
finally:
if own_retriever:
retriever.close()
results = trim_results(results, cfg.max_context_chars)
def finish(answer, refused, verified, citations, regenerations=0,
draft=None, sources=None):
@@ -145,6 +265,10 @@ def answer_question(
"regenerations": regenerations,
"latency_ms": round((time.perf_counter() - t0) * 1000),
"draft": draft,
"planned_queries": [
{"text": sq.text, "stand_year": sq.stand_year}
for sq in sub_queries
],
}
if not results:
@@ -154,31 +278,72 @@ def answer_question(
allowed = [r.entry_id for r in results]
by_id = {r.entry_id: r for r in results}
if client is None:
client = OllamaClient(
cfg.ollama_url,
embed_timeout_s=cfg.embed_timeout_s,
chat_timeout_s=cfg.chat_timeout_s,
)
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": build_user_content(question, results)},
]
map_messages = None
if qtype == "survey":
# Map-Reduce (Stufe 2): alle Bloecke destillieren (Map), dann
# synthetisieren (Reduce). Zitiert werden duerfen weiterhin nur IDs
# aus der Retrieved-Menge — die Post-Validierung bleibt unveraendert.
map_messages = [
{"role": "system", "content": MAP_SYSTEM_PROMPT},
{"role": "user", "content": build_user_content(question, results)},
]
def chat(msgs):
return strip_think(
client.chat(
def chat(msgs, num_predict: int | None = None):
"""Ein Chat-Zug; liefert (gestrippter Content, done_reason).
Bei done_reason='length' (Antwort bei num_predict abgeschnitten)
sind Zitationen ggf. unvollstaendig — der Aufrufer ruft einmal mit
doppeltem Budget neu (technischer Retry, kein Regel-Regeneration)."""
budget = num_predict or cfg.num_predict
if hasattr(client, "chat_full"):
raw, done_reason = client.chat_full(
cfg.answer_model,
msgs,
temperature=cfg.temperature,
num_ctx=cfg.num_ctx,
num_predict=cfg.num_predict,
num_predict=budget,
think=cfg.think,
)
)
else:
raw = client.chat(
cfg.answer_model,
msgs,
temperature=cfg.temperature,
num_ctx=cfg.num_ctx,
num_predict=budget,
think=cfg.think,
)
done_reason = "stop"
return strip_think(raw), done_reason
final = chat(messages)
def chat_with_length_retry(msgs):
final, done_reason = chat(msgs)
if done_reason == "length":
final, done_reason = chat(msgs, num_predict=cfg.num_predict * 2)
return final
if map_messages is not None:
summary = chat_with_length_retry(map_messages)
if summary:
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{
"role": "user",
"content": (
"Kontextblöcke aus der Wissensbasis "
f"(Block-Zusammenfassungen):\n\n{summary}\n\n"
f"Frage: {question}"
),
},
]
# leerer Map-Output -> Fallback: messages bleibt die Einzelantwort
final = chat_with_length_retry(messages)
violations = validate_answer(final, allowed)
violations += validate_decision_support_answer(question, final, allowed)
regenerations = 0
if violations:
regenerations = 1
@@ -189,12 +354,13 @@ def answer_question(
"Beantworte die Frage erneut und zitiere nur diese IDs — oder verweigere "
f"mit dem vorgesehenen Satz („{REFUSAL_MESSAGE}“)."
)
retry = chat(
retry = chat_with_length_retry(
messages
+ [{"role": "assistant", "content": final},
{"role": "user", "content": warn}]
)
retry_violations = validate_answer(retry, allowed)
retry_violations += validate_decision_support_answer(question, retry, allowed)
if not retry_violations:
final = retry
violations = []
+1 -1
View File
@@ -13,7 +13,7 @@ from pathlib import Path
import yaml
ID_RE = re.compile(r"^(lb|wk|kv|ris)-[a-z0-9]+-\d+$")
ID_RE = re.compile(r"^(lb|wk|kv|ris|rj)-[a-z0-9]+-\d+$")
STAND_RE = re.compile(r"^\d{4}-\d{2}$")
REQUIRED_KEYS = (
"id", "batch", "title", "work", "chapter", "topic", "author",
+18 -4
View File
@@ -74,10 +74,14 @@ class OllamaClient:
# -- Chat ---------------------------------------------------------------
def chat(self, model: str, messages: list[dict],
temperature: float = 0.1, num_ctx: int = 16384,
num_predict: int = 1024, think: bool = False) -> str:
"""POST /api/chat, stream=False; `think`-Flag mit 404/400-Fallback."""
def chat_full(self, model: str, messages: list[dict],
temperature: float = 0.1, num_ctx: int = 16384,
num_predict: int = 1024,
think: bool = False) -> tuple[str, str]:
"""POST /api/chat; liefert (content, done_reason). done_reason ==
'length' bedeutet: Antwort wurde bei num_predict abgeschnitten —
Zitationen koennen dann unvollstaendig sein (Think-Ghost-Ursache
q-024, D13-Follow-up)."""
body: dict[str, Any] = {
"model": model,
"messages": messages,
@@ -105,4 +109,14 @@ class OllamaClient:
content = msg.get("content") or ""
if not content.strip():
raise OllamaError(f"empty response from {model} (keys: {list(data.keys())})")
return content, str(data.get("done_reason") or "stop")
def chat(self, model: str, messages: list[dict],
temperature: float = 0.1, num_ctx: int = 16384,
num_predict: int = 1024, think: bool = False) -> str:
"""POST /api/chat, stream=False; `think`-Flag mit 404/400-Fallback."""
content, _ = self.chat_full(
model, messages, temperature=temperature, num_ctx=num_ctx,
num_predict=num_predict, think=think,
)
return content
+196
View File
@@ -0,0 +1,196 @@
"""Query-Planer für komplexe Fragen (Stufe 1, M6).
Ein kleiner LLM-Call zerlegt komplexe Fragen in 1-3 unabhängige Sub-Queries
(+ optional Geltungsjahr). Ein Heuristik-Gate entscheidet, ob der Planer-Call
überhaupt läuft — einfache Fragen bleiben deterministischer Single-Shot
(keine Zusatz-Latenz). Parse-/Call-Fehler fallen auf die Original-Frage
zurück; die Grounding-Regeln werden dadurch nie berührt.
"""
from __future__ import annotations
import json
import re
from dataclasses import dataclass
from datetime import date
YEAR_RE = re.compile(r"\b(?:19|20)\d{2}\b")
JSON_RE = re.compile(r"\{.*\}", re.DOTALL)
AGGREGATION_RE = re.compile(
r"neuerungen|übersicht|zusammenfassung|alle\s|übersicht", re.I
)
COMPARISON_RE = re.compile(r"unterschied|vergleic|\bbzw\.|\bsowie\b", re.I)
DECISION_SUPPORT_RE = re.compile(
r"günstig(?:ste|er|e)?\s+lösung|abgabenbegünstigt|steuerfrei.*(?:auszahl|präm)|"
r"(?:zusätzlich|extra).*(?:auszahl|präm)",
re.IGNORECASE,
)
PLANNER_PROMPT = """Du planst Suchanfragen für eine Wissensbasis zur österreichischen
Personalverrechnung (kuratierte Briefings, Kollektivverträge je Branche und
Geltungsjahr, Gesetzesparagraphen).
Frage: "{question}"
Zerlege die Frage in 1-3 unabhängige Suchanfragen, die zusammen die Frage
beantworten. Regeln:
- Besteht die Frage aus einem Suchthema, gib genau eine Suchanfrage zurück
(dann die Frage unverändert, leicht stichwortartig gekürzt).
- Suchbegriffe statt Sätze (ohne Frageformulierung, ohne "Kollektivvertrag"-
Wortballast).
- Fragt die Frage nach einem bestimmten Jahr (Geltung/Stand), setze
"stand_year" auf dieses vierstellige Jahr, sonst null.
- Setze "scope" je Suchanfrage: "gesetz", wenn nach der gesetzlichen/
allgemeinen Grundlage gefragt ist (nur Gesetze und Fachbriefings, ohne
Branchen-Kollektivverträge); "kv", wenn ausdrücklich nach kollektivvertrag-
lichen Branchenregelungen gefragt ist; null für alles anderes.
- Setze "type" auf "survey", wenn die Frage eine Übersicht über viele
Dokumente verlangt (Neuerungen, Entwicklungen, alle …); sonst "specific".
- Antworte ausschließlich mit JSON, ohne Erklärung:
{{"type": "specific", "queries": [{{"text": "...", "stand_year": null, "scope": null}}]}}"""
@dataclass
class SubQuery:
text: str
stand_year: str | None = None # YYYY, wenn nach einem Geltungsjahr gefragt
scope: str | None = None # "gesetz" (ohne Branchen-KV) | "kv" | None
def is_decision_support(question: str) -> bool:
"""Erkennt Gestaltungsfragen, die den deterministischen Plan benötigen."""
return bool(DECISION_SUPPORT_RE.search(question))
def should_plan(question: str) -> bool:
"""Heuristik-Gate: nur komplexe Fragen bekommen einen Planer-Call.
Signale: Jahreszahl, Vergleichs-/Aggregationsmarker, langer Text,
Mehrfach-Konjunktion. Einfache Fragen bleiben Single-Shot (Latenz)."""
q = question.strip()
if YEAR_RE.search(q):
return True
words = q.split()
if len(words) >= 14:
return True
if COMPARISON_RE.search(q):
return True
if is_decision_support(q):
return True
if AGGREGATION_RE.search(q):
return True
if " und " in q.casefold() and len(words) >= 10:
return True
return False
def decision_support_plan(
question: str, default_year: str | None = None
) -> tuple[list[SubQuery], str] | None:
"""Deterministische Zerlegung für Gestaltungsfragen zu zusätzlichen
Arbeitnehmerleistungen. Sie verhindert, dass die vielen Branchen-KV-
Treffer allgemeine Abgabenregeln verdrängen.
Ohne genanntes Jahr wird der neueste Indexstand verwendet; die Antwort
muss fehlenden Einzelfallkontext weiterhin über die Prompt-Regel erfragen.
"""
if not is_decision_support(question):
return None
explicit_year = YEAR_RE.search(question)
year = explicit_year.group(0) if explicit_year else default_year
year_text = f" {year}" if year else ""
direct = SubQuery(
text=(
f"Mitarbeiterprämie{year_text} WIKU Personal aktuell Nr 12 FAQ "
"Höchstbetrag 500 Lohnsteuer SV BV Beitragsfreie Bezugsbestandteile "
"§ 49 Abs 3 Z 30"
),
stand_year=year,
scope="gesetz",
)
cash_and_benefits = SubQuery(
text=(
"Zukunftssicherungsmaßnahmen 300 Kalenderjahr Risikoversicherung "
"Gruppenerfordernis Bezugsumwandlung Lohnsteuer SV BV DB DZ KommSt"
),
stand_year=year,
scope="gesetz",
)
purpose_bound = SubQuery(
text=(
"Sachzuwendungen Geschenke Barzahlungen stets steuerpflichtig Freibetrag "
"Beitragsfreie Bezugsbestandteile Z 1 bis Z 16a Mahlzeiten "
"Kinderbetreuungszuschuss"
),
stand_year=year,
scope="gesetz",
)
broad = bool(
re.search(r"günstig|möglichkeiten|statt|alternative", question, re.IGNORECASE)
)
# Ein Gestaltungsvergleich ist kein Korpus-Survey: Die drei gezielten
# Queries passen in den normalen Antwortpfad und vermeiden unnötiges
# Map-Reduce samt zusätzlicher Latenz/Fehlverweigerungsrisiko.
return ([direct, cash_and_benefits, purpose_bound] if broad else [direct]), "specific"
def parse_plan(raw: str, original: str) -> tuple[list[SubQuery], str]:
"""Robustes JSON-Parsing; jeder Fehler → [Originalfrage]. Liefert
(Sub-Queries, Fragetyp 'survey' | 'specific')."""
try:
match = JSON_RE.search(raw)
if not match:
raise ValueError("kein JSON-Objekt")
data = json.loads(match.group(0))
items = data.get("queries")
if not isinstance(items, list) or not items:
raise ValueError("leeres Plan-Array")
subs: list[SubQuery] = []
for item in items[:3]:
text = str(item.get("text", "")).strip()
if not text:
raise ValueError("leere Sub-Query")
year = item.get("stand_year")
year = str(year) if year and re.fullmatch(r"20\d{2}", str(year)) else None
scope = item.get("scope")
scope = scope if scope in ("gesetz", "kv") else None
subs.append(SubQuery(text=text, stand_year=year, scope=scope))
qtype = data.get("type") if data.get("type") in ("survey", "specific") else "specific"
return subs, qtype
except (ValueError, TypeError, KeyError, json.JSONDecodeError):
return [SubQuery(text=original)], "specific"
return [SubQuery(text=original)]
def plan_queries(
question: str,
client,
cfg,
) -> tuple[list[SubQuery], bool, str]:
"""Liefert (Sub-Queries, geplant?, Fragetyp) — Call-/Parse-Fehler →
Original als Einzel-Query, Typ 'specific'. Der Planer-Call ist klein
(Frage ohne Kontext, kurzes num_predict); Temperature 0."""
deterministic = decision_support_plan(question, default_year=str(date.today().year))
if deterministic:
subs, qtype = deterministic
return subs, True, qtype
if not should_plan(question):
return [SubQuery(text=question)], False, "specific"
prompt = PLANNER_PROMPT.format(question=question.strip())
try:
raw = client.chat(
cfg.planner_model or cfg.answer_model,
[
{"role": "system", "content": "Du antwortest ausschließlich mit JSON."},
{"role": "user", "content": prompt},
],
temperature=0.0,
num_ctx=cfg.num_ctx,
num_predict=cfg.planner_num_predict,
think=False,
)
except Exception:
return [SubQuery(text=question)], False, "specific"
subs, qtype = parse_plan(raw, question)
return subs, True, qtype
+173 -18
View File
@@ -13,6 +13,7 @@ import numpy as np
from .config import Config
from .normalize import fts_query
from .ollama_client import OllamaClient
from .query_planner import SubQuery, decision_support_plan
@dataclass
@@ -71,6 +72,7 @@ class Retriever:
self._owns_client = client is None
self._mat: np.ndarray | None = None
self._mat_chunk_ids: list[int] | None = None
self._scope_cache: dict[str, set[int]] = {}
row = self._con.execute("SELECT MIN(stand), MAX(stand) FROM chunks").fetchone()
self._stand_min = int((row[0] or "2026-01").replace("-", ""))
self._stand_max = int((row[1] or "2026-01").replace("-", ""))
@@ -104,19 +106,38 @@ class Retriever:
# -- Einzelverfahren ----------------------------------------------------
def _bm25(self, question: str, limit: int) -> dict[int, float]:
def _scope_ids(self, scope: str | None) -> set[int] | None:
"""Chunk-ID-Menge je Sub-Query-Scope (Cache je Instanz): 'gesetz' =
alles außer Branchen-KV, 'kv' = nur Branchen-KV."""
if scope is None:
return None
if scope not in self._scope_cache:
if scope == "kv":
sql = "SELECT chunk_id FROM chunks WHERE entry_id LIKE 'kv-%'"
else: # gesetz
sql = "SELECT chunk_id FROM chunks WHERE entry_id NOT LIKE 'kv-%'"
self._scope_cache[scope] = {
row[0] for row in self._con.execute(sql).fetchall()
}
return self._scope_cache[scope]
def _bm25(self, question: str, limit: int, scope: str | None = None) -> dict[int, float]:
q = fts_query(question)
if not q:
return {}
fetch = limit * 4 if scope else limit
rows = self._con.execute(
"SELECT rowid, bm25(chunks_fts) AS rank FROM chunks_fts "
"WHERE chunks_fts MATCH ? ORDER BY rank LIMIT ?",
(q, limit),
(q, fetch),
).fetchall()
scope_ids = self._scope_ids(scope)
if scope_ids is not None:
rows = [r for r in rows if r["rowid"] in scope_ids][:limit]
# bm25(): kleinere Werte = besser -> negieren für "größer = besser"
return {r["rowid"]: -float(r["rank"]) for r in rows}
def _dense(self, question: str, limit: int) -> dict[int, float]:
def _dense(self, question: str, limit: int, scope: str | None = None) -> dict[int, float]:
if self.cfg.embed_off:
return {}
self._ensure_matrix()
@@ -139,8 +160,17 @@ class Retriever:
if qn == 0:
return {}
sims = self._mat_norm @ (qvec / qn)
order = np.argsort(-sims)[:limit]
return {self._mat_chunk_ids[i]: float(sims[i]) for i in order}
order = np.argsort(-sims)
scope_ids = self._scope_ids(scope)
out: dict[int, float] = {}
for i in order:
if len(out) >= limit:
break
cid = self._mat_chunk_ids[i]
if scope_ids is not None and cid not in scope_ids:
continue
out[cid] = float(sims[i])
return out
def _ensure_matrix(self) -> None:
if self._mat is not None:
@@ -240,46 +270,102 @@ class Retriever:
# -- öffentliche Suche --------------------------------------------------
def search(self, question: str, n_entries: int | None = None) -> list[ChunkResult]:
"""Liefert die Top-Kontextblöcke (Hauptretrieval + cross_ref-Erweiterung)."""
n = n_entries or self.cfg.context_blocks
pool = self.cfg.candidate_pool
bm = self._bm25(question, pool)
def _search_one(
self, sq: SubQuery, pool: int
) -> tuple[dict[int, float], dict[int, float], dict[int, float]]:
"""BM25+Dense für EINE Sub-Query inkl. Scope-Filter (mit Fallback auf
unscoped, wenn die gefilterte Suche leer bleibt) und Query-RRF."""
try:
dn = self._dense(question, pool)
bm = self._bm25(sq.text, pool, scope=sq.scope)
except Exception:
bm = {}
try:
dn = self._dense(sq.text, pool, scope=sq.scope)
except Exception:
dn = {}
fused: dict[int, float] = {}
if sq.scope and not bm and not dn:
try:
bm = self._bm25(sq.text, pool)
dn = self._dense(sq.text, pool)
except Exception:
pass
fused_q: dict[int, float] = {}
for ranking, weight in ((bm, 1.0), (dn, self.cfg.dense_weight)):
ordered = sorted(ranking.items(), key=lambda kv: -kv[1])
for rank, (cid, _) in enumerate(ordered):
fused[cid] = fused.get(cid, 0.0) + weight / (self.cfg.rrf_k + rank)
fused_q[cid] = fused_q.get(cid, 0.0) + weight / (
self.cfg.rrf_k + rank
)
return bm, dn, fused_q
def _fuse_queries(
self, sub_queries: list, pool: int
) -> tuple[dict[int, float], set[int], set[int]]:
"""RRF-Fusion über BM25+Dense je Sub-Query (Beiträge summieren);
liefert (fused, bm25-Chunk-IDs, dense-Chunk-IDs) für die Quelle-
Markierung. Bei einer einzelnen Sub-Query identisch zum bisherigen
Verhalten."""
fused: dict[int, float] = {}
bm_all: set[int] = set()
dn_all: set[int] = set()
for sq in sub_queries:
bm, dn, fused_q = self._search_one(sq, pool)
bm_all.update(bm)
dn_all.update(dn)
for cid, score in fused_q.items():
fused[cid] = fused.get(cid, 0.0) + score
return fused, bm_all, dn_all
def _context_from_fused(
self,
fused: dict[int, float],
n: int,
bm_all: set[int],
dn_all: set[int],
stand_years: set[str] | None = None,
reserved: list[str] | None = None,
) -> list[ChunkResult]:
"""Fusion -> Score (+Recency +Temporal-Boost) -> Entry-Dedup ->
Vertreter-Chunk -> cross_ref-Erweiterung."""
if not fused:
return []
rows = self._fetch_chunks(list(fused))
results: list[ChunkResult] = []
for cid, score in fused.items():
if cid not in rows:
continue
source = "fused" if (cid in bm and cid in dn) else (
"bm25" if cid in bm else "dense"
source = (
"fused" if (cid in bm_all and cid in dn_all)
else ("bm25" if cid in bm_all else "dense")
)
score += self.cfg.recency_boost * self._stand_factor(rows[cid]["stand"])
if stand_years and rows[cid]["entry_id"].startswith("kv-"):
if rows[cid]["stand"][:4] in stand_years:
score += self.cfg.temporal_boost
results.append(self._row_to_result(rows[cid], score, source))
results.sort(key=lambda r: -r.score)
# Bester Chunk je Eintrag -> Kontext (Entry-Level-Dedup).
# Der Vertreter-Chunk ist die beste Inhaltssektion des Eintrags,
# nicht die Rangfolge-Beste (vgl. _section_priority).
# Per-Query-Slots (Multi-Query): reservierte Einträge zuerst —
# jeder Sub-Query-Aspekt hält garantiert seine Top-Quelle(n).
main: list[ChunkResult] = []
seen: set[str] = set()
by_entry: dict[str, list[ChunkResult]] = {}
for r in results:
by_entry.setdefault(r.entry_id, []).append(r)
seen: set[str] = set()
reserved = reserved or []
for entry_id in reserved:
if len(main) >= n or entry_id not in by_entry:
continue
seen.add(entry_id)
main.append(self._representative_chunk(entry_id, by_entry[entry_id]))
for entry_id, chunks in by_entry.items():
if len(main) >= n:
break
if entry_id in seen:
continue
seen.add(entry_id)
main.append(self._representative_chunk(entry_id, chunks))
@@ -297,4 +383,73 @@ class Retriever:
extra.append(er)
seen.add(ref)
budget -= 1
return main + extra
return main + extra
def search(self, question: str, n_entries: int | None = None) -> list[ChunkResult]:
"""Liefert die Top-Kontextblöcke (Hauptretrieval + cross_ref-Erweiterung).
Gestaltungsfragen zu zusätzlichen Arbeitnehmerleistungen werden
deterministisch in Direktzahlung und Alternativen zerlegt. Das gilt
auch für den Offline-Retrieval-Eval, der keinen LLM-Planer aufruft.
"""
n = n_entries or self.cfg.context_blocks
latest_year = str(self._stand_max)[:4]
deterministic = decision_support_plan(question, default_year=latest_year)
if deterministic:
sub_queries, _qtype = deterministic
return self.search_multi(sub_queries, n_entries=n)
fused, bm_all, dn_all = self._fuse_queries(
[SubQuery(text=question)], self.cfg.candidate_pool
)
return self._context_from_fused(fused, n, bm_all, dn_all)
def search_multi(
self, sub_queries: list, n_entries: int | None = None
) -> list[ChunkResult]:
"""Multi-Query-Retrieval (Stufe 1): je Sub-Query BM25+Dense, RRF-
Beiträge summieren. Per-Query-Slots: jede Sub-Query sichert ihre
Top-Einträge im Kontext (Multi-Hop: jeder Frageaspekt kommt mit
seiner besten Quelle hinein — sonst dominieren Einträge, die in
mehreren Sub-Queries mittelgut matchen). Temporal-Intent: kv-
Einträge im gefragten Geltungsjahr erhalten temporal_boost."""
n = n_entries or self.cfg.context_blocks
if (
len(sub_queries) == 1
and sub_queries[0].scope is None
and sub_queries[0].stand_year is None
):
return self.search(sub_queries[0].text, n_entries=n)
pool = self.cfg.candidate_pool
fused_total: dict[int, float] = {}
bm_all: set[int] = set()
dn_all: set[int] = set()
per_query_entries: list[list[str]] = []
for sq in sub_queries:
bm, dn, fused_q = self._search_one(sq, pool)
bm_all.update(bm)
dn_all.update(dn)
for cid, score in fused_q.items():
fused_total[cid] = fused_total.get(cid, 0.0) + score
rows_q = self._fetch_chunks(list(fused_q))
entry_best: dict[str, float] = {}
for cid, score in fused_q.items():
row = rows_q.get(cid)
if row is not None:
eid = row["entry_id"]
entry_best[eid] = max(entry_best.get(eid, 0.0), score)
per_query_entries.append(
[e for e, _ in sorted(entry_best.items(), key=lambda kv: -kv[1])]
)
reserved: list[str] = []
for entries in per_query_entries:
taken = 0
for eid in entries:
if taken >= self.cfg.per_query_slots:
break
if eid not in reserved:
reserved.append(eid)
taken += 1
years = {sq.stand_year for sq in sub_queries if sq.stand_year}
return self._context_from_fused(
fused_total, n, bm_all, dn_all, stand_years=years, reserved=reserved
)
+52
View File
@@ -290,4 +290,56 @@ tests/ # pytest: Ingest-, Retrieval-, Grounding-Unit-Tests
Antwortmodus: Zitier-Präzision 95,2 %, Verweigerung 90,5 % —
Fehlverweigerungs-Tuning (offen, laufender Arbeitsstand) fortsetzen.
Intake: `tools/ingest_sources.py`, Registry: `tools/build_registry.py`.
- **Tuning abgeschlossen (2026-09-15, D11):** Ursache der Fehlverweige-
rungen war Prompt-Overflow (lange KV-Chunks > 16k — Systemprompt
trunciert weg). Fix: num_ctx 32 768, max_context_chars=90 000
(`trim_results`), cross_ref_expand 6 → alle 4 Fälle geheilt (10/10
Bestätigungsläufe). Retrieval Recall@8 0,946 · Antworten: Zitier-
Präzision 97,6 %, Verweigerung 97,6 % (Gate ✓), erwartete Quelle
91,9 %, Latenz mean 33 s. Offen: q-015 Branchen-Noise, q-024
transiente Flakiness (leerer Draft).
- **Komplexe-Fragen Stufe 1 (2026-09-15, M6/D12):** Query-Planer
(Heuristik-Gate → 1-3 Sub-Queries mit Stand-Jahr und Scope), Multi-
Query-Retrieval mit Per-Query-Slots und Scope-Filtern („gesetz“/„kv“).
Eval (46 Fragen): Zitier-Präzision 97,8 %, Verweigerung 97,8 %
(Gate ✓), erwartete Quelle 90,2 %, Latenz mean 33,5 s. Goldset 42 →
46 Fragen (q-110113). Offen für Stufe 2: Aggregation → Map-Reduce
(q-029), Rückfragen statt Verweigerung (API-first), danach
Rechtsprechung-Intake (`rj-*`, Lexis-md/json).
- **Antworttyp-Routing Stufe 2 (2026-09-15, M6/D13):** Planer-Typ
`survey` → Map-Reduce (survey_blocks=16; Map destilliert je Block
mit KB-ID, Reduce synthetisiert; Validierung unverändert über die
Union). Systemprompt-Regel 9: Kontext-Abhängigkeit → belegte allgemeine
Aussage + eine Rückfrage (API-first). q-029 geheilt (vorher hart-
näckigste Fehlverweigerung), q-015 mit KV-Abhängigkeits-Hinweis.
Eval: 97,8 % / 97,8 % / 90,2 %, Latenz mean 34,2 s (Map-Reduce nur
bei Survey-Fragen, ~95 s). Nächster Schritt: Rechtsprechung-Intake
(`rj-*`), dann M4 (Odoo; Privacy-Neubewertung für Lohndaten-Zugriff).
- **Output-Budget + length-Retry (2026-09-15, M6/D14):** q-024-Flakiness
war num_predict=1024 (done_reason=length, abgeschnittene Zitationen),
nicht Thinking. Fix: num_predict=2048, chat_full() mit done_reason,
technischer 2×-Budget-Retry bei length. **Voll-Eval erstmals mit allen
Gates erfüllt: Zitier-Präzision 100 %, Verweigerung 100 %, erwartete
Quelle 92,7 %** (46/46), Latenz mean 39,6 s. M6 damit abgeschlossen;
nächster Schritt: Rechtsprechung-Intake (`rj-*`), dann M4 (Odoo;
Privacy-Neubewertung für Lohndaten-Zugriff).
- **Rechtsprechungs-Intake (2026-09-15, D15):** `.rechtsprechung/` wurde
über `tools/ingest_sources.py --source rj` in den neuen ID-Raum
`rj-rjs-*` übernommen: 320 RIS-OGD-Entscheidungen/Rechtssätze, 23
zitierte RIS-Normauszüge sowie 5 als nichtamtliche lexetius-
Textwiedergaben markierte EuGH-Urteile. Korpus: 1.274 → **1.622**
Einträge, 77 Cluster; lange Volltexte werden an Absatzgrenzen in H2-
Chunks geteilt. Reindex: 16.480 Chunks, 1.496 neue bge-m3-Embeddings,
75 s. Erweiterter Retrieval-Eval (50 Fragen, inkl. q-120123): Hit-Rate
0,956 · Recall@8 **0,922** (Gate >0,9 ✓) · MRR 0,661; alle neuen Fälle
gefunden. Die gezielten Antwortläufe q-120123 sind alle zitiergültig,
nicht verweigert und ohne Regenerierung; EuGH q-123 weist nach einer
Prompt-Regel explizit auf die nichtamtliche Wiedergabe hin. Voller
Antwortmodus-Eval (50 Fragen): **100 % Zitier-Präzision**, **100 %
Verweigerung korrekt**, erwartete Quelle 93,3 %, Latenz mean 39,4 s /
p95 82,2 s, fünf Regenerierungen; Report `data/eval-qwen38-rj.json`
(lokal, unversioniert). Vor einem Commit der
quellentreuen Volltexte ist die Publikations-/Lizenzfreigabe bewusst zu
bestätigen; sie folgt nicht automatisch aus der Regel zu amtlichen
Gesetzestexten.
- Betrieb: `agent/README.md`.
+2
View File
@@ -181,6 +181,8 @@ def mini_cfg(tmp_path, mini_kb) -> Config:
kb_dir=str(mini_kb),
db_path=str(tmp_path / "index.db"),
embed_off=True,
planner_enabled=False, # Tests skripten Chat-Antworten; Planer
# separat testbar (siehe test_generate/test_query_planner)
)
+107 -1
View File
@@ -5,14 +5,18 @@ verlässt answer_question.
"""
import pytest
from agent.retrieve import ChunkResult
from agent.generate import (
REFUSAL_MESSAGE,
SYSTEM_PROMPT,
UNCERTAIN_MESSAGE,
answer_question,
build_user_content,
looks_like_refusal,
strip_think,
trim_results,
validate_answer,
validate_decision_support_answer,
)
@@ -35,6 +39,57 @@ class TestValidateAnswer:
assert validate_answer("Siehe lb-min-02 für Details.", ["lb-min-02"]) == []
def test_system_prompt_requires_nonofficial_source_disclosure():
assert "nicht amtlich" in SYSTEM_PROMPT
def test_system_prompt_requires_contextual_decision_support():
assert "keine Option pauschal zum Sieger" in SYSTEM_PROMPT
assert "frei verfügbare Barzahlung" in SYSTEM_PROMPT
assert "systematisch in einen Katalog abgabenfreier Bezüge" in SYSTEM_PROMPT
assert "[wk-akt-04] und [lb-sva-03]" in SYSTEM_PROMPT
assert "Bezeichne keine der widersprechenden Quellen als" in SYSTEM_PROMPT
assert "genau EINE gezielte" in SYSTEM_PROMPT
assert "Regelnummern niemals" in SYSTEM_PROMPT
assert "spekuliere nicht mit Trainingswissen" in SYSTEM_PROMPT
class TestDecisionSupportValidation:
question = (
"Ich will meinem Mitarbeiter 500 Euro zusätzlich auszahlen. "
"Was ist die günstigste Lösung?"
)
allowed = ["wk-akt-04", "lb-sva-03"]
def test_requires_conflict_when_both_sources_are_in_context(self):
violations = validate_decision_support_answer(
self.question, "Nur lohnsteuerfrei [wk-akt-04].", self.allowed
)
assert violations and "Quellenkonflikt" in violations[0]
def test_accepts_explicit_conflict(self):
answer = (
"⚠ [lb-sva-03] ordnet die Prämie in den Katalog beitragsfreier "
"Bezüge ein; [wk-akt-04] nennt sie SV- und BV-pflichtig."
)
assert validate_decision_support_answer(self.question, answer, self.allowed) == []
def test_rejects_reversed_source_roles_and_prompt_leakage(self):
answer = (
"⚠ [wk-akt-04] und [lb-sva-03] ordnen die Prämie als beitragsfrei "
"ein; gemäß Regel 11 ist die aktuellere News-Quelle maßgeblich."
)
violations = validate_decision_support_answer(self.question, answer, self.allowed)
assert any("Rollen" in item for item in violations)
assert any("internen Regeln" in item for item in violations)
assert any("priorisiere" in item for item in violations)
def test_does_not_require_missing_source(self):
assert validate_decision_support_answer(
self.question, "Lohnsteuerfrei [wk-akt-04].", ["wk-akt-04"]
) == []
class TestRefusalDetection:
def test_refusal_phrase(self):
assert looks_like_refusal("Dazu enthält die Wissensbasis keine Aussage.")
@@ -143,4 +198,55 @@ class TestAnswerQuestion:
main = [s for s in result["sources"]]
assert result["n_context"] >= 1
# Haupt-Blöcke auf top_k begrenzt; cross_ref-Erweiterungen dürfen dazu
assert len([s for s in main]) <= result["n_context"]
assert len([s for s in main]) <= result["n_context"]
def test_trim_results_drops_tail_under_budget():
"""Prompt-Budget: Tail-Blöcke (niedrig gerankt) ganz weglassen, nicht truncieren."""
def blk(n, chars):
return ChunkResult(
chunk_id=n, entry_id=f"lb-min-{n:02d}", section="Zusammenfassung",
text="x" * chars, title=f"Titel {n}", stand="2026-01", work="W",
chapter="C", topic="t", tags=[], legal_bases=[], cross_refs=[],
)
blocks = [blk(i, 30_000) for i in range(1, 6)] # 5 x ~30 KB = 150 KB
trimmed = trim_results(blocks, 90_000)
assert 6 >= len(trimmed) >= 6 or len(trimmed) == 5 # 5 Bloecke, Mindesthoehe 6 greift nicht
# Groesserer Fall: 14 Bloecke, Budget schneidet hinten weg
blocks = [blk(i, 8_000) for i in range(1, 15)] # ~112 KB
trimmed = trim_results(blocks, 90_000)
assert 6 <= len(trimmed) < 14
# Erste Bloecke bleiben (best gerankt)
assert trimmed[0].entry_id == "lb-min-01"
assert sum(len(b.text) for b in trimmed) + len(trimmed) * 64 <= 90_000 + 8_000
# Unbegrenzt: Original unveraendert
assert trim_results(blocks, None) is blocks
def test_length_retry_doubles_budget(mini_index):
"""done_reason='length' (abgeschnittene Antwort) -> ein technischer
Retry mit doppeltem num_predict; zaehlt nicht als Regel-Regenerierung."""
class LengthyOllama:
def __init__(self):
self.calls = []
self.budgets = []
def chat_full(self, model, messages, temperature=0.1, num_ctx=16384,
num_predict=1024, think=False):
self.calls.append(messages[-1])
self.budgets.append(num_predict)
if len(self.budgets) == 1:
return "Halbe Zitation [lb-min-0", "length"
return "Antwort mit Beleg [lb-min-01].", "stop"
def chat(self, *a, **k):
raise AssertionError("chat() sollte via chat_full laufen")
def close(self):
pass
client = LengthyOllama()
result = answer_question("Altersteilzeit?", mini_index, client=client)
assert result["verified"] is True
assert result["citations"] == ["lb-min-01"]
assert result["regenerations"] == 0 # technischer Retry, keine Regel-Regen
assert client.budgets == [mini_index.num_predict, mini_index.num_predict * 2]
+106 -1
View File
@@ -134,4 +134,109 @@ def test_ris_entry(tmp_path, monkeypatch):
assert 'legal_bases: ["TestG"]' in md
assert "## § 1. Geltungsbereich" in md
assert "(1) Dieses Gesetz gilt für alle." in md
assert "ris-nso-06" in md # ID-Zeile im Body
assert "ris-nso-06" in md # ID-Zeile im Body
def _rj_catalog():
return {"source": "rj", "updated": None, "next_seq": 1, "entries": {}}
def _rj_stats():
return {"rj_written": 0, "rj_unparsed": [], "rj_empty": []}
def test_rj_rechtssatz_preserves_court_date_and_norm(tmp_path, monkeypatch):
root = tmp_path / ".rechtsprechung"
source = root / "originale" / "JJR_test.md"
source.parent.mkdir(parents=True)
source.write_text(
"# Rechtssatz\n\n"
"**Gericht:** OGH\n"
"**Entscheidungsdatum:** 15. 7. 1954\n"
"**Geschäftszahl:** 4 Ob 100/54\n"
"**Norm:** AngG § 26 Z 4\n"
"**Rechtssatz:** Ein Austrittsgrund ist unverzüglich geltend zu machen.\n",
encoding="utf-8",
)
monkeypatch.setattr(ing, "RJ_DIR", root)
slug, md = ing.rj_entry(source, _rj_catalog(), _rj_stats())
assert slug == "rj_jjr-test"
assert "id: rj-rjs-001" in md
assert 'chapter: "OGH-Rechtssatz"' in md
assert "stand: 1954-07" in md
assert 'legal_bases: ["AngG § 26 Z 4"]' in md
assert "## Rechtssatz" in md
def test_rj_volltext_splits_long_reasoning_at_paragraphs(tmp_path, monkeypatch):
root = tmp_path / ".rechtsprechung"
source = root / "originale" / "JJT_test.md"
source.parent.mkdir(parents=True)
paragraphs = [f"Absatz {i}: " + "x" * 900 for i in range(5)]
source.write_text(
"# Entscheidung\n\n"
"**Gericht:** OGH\n"
"**Entscheidungsdatum:** 1. 2. 2020\n"
"**Geschäftszahl:** 1 Ob 2/20a\n\n---\n\n"
+ "\n\n".join(paragraphs),
encoding="utf-8",
)
monkeypatch.setattr(ing, "RJ_DIR", root)
_, md = ing.rj_entry(source, _rj_catalog(), _rj_stats())
assert "## Begründung (1)" in md
assert "## Begründung (2)" in md
def test_rj_norm_supports_h2_and_flat_wording(tmp_path, monkeypatch):
root = tmp_path / ".rechtsprechung"
h2_source = root / "gesetze" / "AngG_§15.md"
flat_source = root / "gesetze" / "ABGB_§1435.md"
h2_source.parent.mkdir(parents=True)
h2_source.write_text(
"# Angestelltengesetz\n\n**Kurztitel:** Angestelltengesetz\n"
"**Abkürzung:** AngG\n**§/Artikel/Anlage:** § 15\n\n---\n\n"
"## § 15\n\nWortlaut.\n\n## Zuletzt aktualisiert am\n\n1. 7. 2023\n",
encoding="utf-8",
)
flat_source.write_text(
"# ABGB\n\n**Kurztitel:** ABGB\n**Abkürzung:** ABGB\n"
"**§/Artikel/Anlage:** § 1435\n\n---\n\nDer Wortlaut ohne H2.\n",
encoding="utf-8",
)
monkeypatch.setattr(ing, "RJ_DIR", root)
catalog = _rj_catalog()
_, h2_md = ing.rj_entry(h2_source, catalog, _rj_stats())
_, flat_md = ing.rj_entry(flat_source, catalog, _rj_stats())
assert "stand: 2023-07" in h2_md
assert 'legal_bases: ["AngG § 15"]' in h2_md
assert "## § 15 Wortlaut" in h2_md
assert "stand: 2026-09" in flat_md
assert "stand-abruf" in flat_md
assert "## Normtext" in flat_md
def test_rj_eugh_marks_lexetius_copy_as_nonofficial(tmp_path, monkeypatch):
root = tmp_path / ".rechtsprechung"
source = root / "originale" / "EUGH_C-29-1991.md"
source.parent.mkdir(parents=True)
source.write_text(
"# EuGH C-29/91\n\n"
"**URL:** https://www.lexetius.com/1992,1\n"
"**Offizielle Fassung:** https://eur-lex.europa.eu/example\n\n---\n\n"
"Urteil vom 19. 5. 1992\n\nEntscheidungsgründe.",
encoding="utf-8",
)
monkeypatch.setattr(ing, "RJ_DIR", root)
_, md = ing.rj_entry(source, _rj_catalog(), _rj_stats())
assert 'work: "EuGH Rechtsprechung (Textwiedergabe lexetius, nicht amtlich)"' in md
assert "stand: 1992-05" in md
assert "Textwiedergabe (nicht amtlich): https://www.lexetius.com/1992,1" in md
assert "amtliche Fassung: https://eur-lex.europa.eu/example" in md
+15 -6
View File
@@ -67,21 +67,21 @@ def test_real_corpus_loads_and_matches_registry():
"""Integrationstest gegen die echte Wissensbasis (Gate inklusive)."""
entries = load_kb("wissensbasis", verify_registry=True)
ids = {e.id for e in entries}
# 601 kuratierte (lb/wk) + 614 WKO-KV + 59 RIS-Gesetze
assert len(entries) == 1274
# 601 kuratierte (lb/wk) + 614 WKO-KV + 59 RIS-Gesetze + 348 Rechtsprechungsquellen
assert len(entries) == 1622
assert "lb-atz-07" in ids and "wk-akt-01" in ids
assert "kv-kvt-001" in ids and "ris-url-01" in ids
assert "kv-kvt-001" in ids and "ris-url-01" in ids and "rj-rjs-001" in ids
atz = [e for e in entries if e.id == "lb-atz-07"][0]
assert atz.topic == "altersteilzeit"
assert any(s.title.startswith("Kernwerte") for s in atz.sections)
def test_new_id_spaces_and_html_source(tmp_path):
"""kv-/ris-IDs und html-only-Quellenangabe sind gültig; Unbekanntes nicht."""
"""kv-/ris-/rj-IDs und html-/text-Quellenangaben sind gültig; Unbekanntes nicht."""
kv = DOC_ATZ.replace("id: lb-min-01", "id: kv-kvt-001").replace(
'source:\n pdf: ".lexis360/Lexis360_test_atz.pdf"\n'
' text: ".lexis360/md/test_atz.md"',
'source:\n html: ".firecrawl/wko-kv/docs/test.html"',
'source:\n html: ".firecrawl/kv-portal/wko-kv/docs/test.html"',
)
p = tmp_path / "kv_doc.md"
p.write_text(kv, encoding="utf-8")
@@ -90,12 +90,21 @@ def test_new_id_spaces_and_html_source(tmp_path):
ris = DOC_ATZ.replace("id: lb-min-01", "id: ris-url-01").replace(
'source:\n pdf: ".lexis360/Lexis360_test_atz.pdf"\n'
' text: ".lexis360/md/test_atz.md"',
'source:\n text: ".ris/Test.md"',
'source:\n text: ".firecrawl/ris/gesetze/Test.md"',
)
p2 = tmp_path / "ris_doc.md"
p2.write_text(ris, encoding="utf-8")
assert load_entry(p2).id == "ris-url-01"
rj = DOC_ATZ.replace("id: lb-min-01", "id: rj-rjs-001").replace(
'source:\n pdf: ".lexis360/Lexis360_test_atz.pdf"\n'
' text: ".lexis360/md/test_atz.md"',
'source:\n text: ".rechtsprechung/originale/Test.md"',
)
p3 = tmp_path / "rj_doc.md"
p3.write_text(rj, encoding="utf-8")
assert load_entry(p3).id == "rj-rjs-001"
bad = DOC_ATZ.replace("id: lb-min-01", "id: xx-min-01")
p3 = tmp_path / "bad.md"
p3.write_text(bad, encoding="utf-8")
+207
View File
@@ -0,0 +1,207 @@
"""Tests: Query-Planer (Stufe 1) — Heuristik-Gate, JSON-Parsing, Fallback,
Multi-Query-Fusion und Integration in answer_question."""
import json
import pytest
from agent.config import Config
from agent.generate import answer_question
from agent.query_planner import (
SubQuery,
decision_support_plan,
parse_plan,
plan_queries,
should_plan,
)
from tests.conftest import FakeOllama
def test_should_plan_gate():
# Jahreszahl -> temporale Frage -> planen
assert should_plan("Wie hoch war der Mindestlohn für Friseure im Jahr 2024?")
# Vergleich
assert should_plan("Was ist der Unterschied zwischen Abfertigung neu und alt?")
# lang / mehrfach
assert should_plan(
"Wie wird die Überstundenpauschale behandelt und wie wirkt sie sich "
"auf die Sozialversicherung und die Lohnsteuer aus?"
)
# Gestaltungsfrage -> planen, auch wenn sie unter der Längenschwelle bleibt
assert should_plan(
"Ich will meinem Mitarbeiter 500 Euro zusätzlich auszahlen. "
"Was ist die günstigste Lösung?"
)
# einfach -> Single-Shot
assert not should_plan("Wie viele Werktage Urlaub stehen Arbeitnehmern zu?")
assert not should_plan("Was ist Altersteilzeit?")
assert not should_plan("Wie hoch ist der KV-Mindestlohn im Friseurgewerbe?")
def test_decision_support_plan_splits_direct_payment_and_alternatives():
planned = decision_support_plan(
"Ich will meinem Mitarbeiter 500 Euro zusätzlich auszahlen. "
"Was ist die günstigste Lösung?",
default_year="2026",
)
assert planned is not None
subs, qtype = planned
assert qtype == "specific"
assert len(subs) == 3
assert all(s.scope == "gesetz" for s in subs)
assert all(s.stand_year == "2026" for s in subs)
assert "Mitarbeiterprämie 2026" in subs[0].text
assert "Zukunftssicherung" in subs[1].text
assert "Mahlzeiten" in subs[2].text
def test_decision_support_plan_keeps_explicit_year_and_specific_intent():
subs, qtype = decision_support_plan(
"Kann ich 2025 jedem Mitarbeiter einfach 500 Euro steuerfrei auszahlen?",
default_year="2026",
)
assert qtype == "specific"
assert len(subs) == 1
assert subs[0].stand_year == "2025"
assert "Mitarbeiterprämie 2025" in subs[0].text
def test_parse_plan_valid_and_fallback():
subs, qtype = parse_plan(
'Vorab: {"type": "specific", "queries": [{"text": "mindestlohn friseur", "stand_year": "2024"}, '
'{"text": "lohnberechnung friseur", "stand_year": null}]}',
original="Originalfrage?",
)
assert [s.text for s in subs] == ["mindestlohn friseur", "lohnberechnung friseur"]
assert subs[0].stand_year == "2024"
assert subs[1].stand_year is None
assert qtype == "specific"
# Fallbacks: kaputtes JSON, leeres Array, leere Texte
for raw in ("kein json", '{"queries": []}', '{"queries": [{"text": ""}]}'):
subs, qtype = parse_plan(raw, original="Originalfrage?")
assert len(subs) == 1 and subs[0].text == "Originalfrage?"
assert qtype == "specific"
# Ungueltiges Jahr -> None erzwingen, Text bleibt; unbekannter scope -> None
subs, _ = parse_plan(
'{"queries": [{"text": "x", "stand_year": "98", "scope": "xyz"}]}', original="orig"
)
assert subs == [SubQuery(text="x", stand_year=None, scope=None)]
def test_parse_plan_type_survey():
subs, qtype = parse_plan(
'{"type": "survey", "queries": [{"text": "wiku personal aktuell 2026 neuerungen", "scope": null}]}',
original="orig",
)
assert qtype == "survey"
assert subs[0].scope is None
def test_parse_plan_caps_at_three_queries():
raw = json.dumps(
{"queries": [{"text": f"q{i}"} for i in range(5)]}
)
subs, _ = parse_plan(raw, original="orig")
assert len(subs) == 3
def test_plan_queries_simple_question_no_llm_call():
client = FakeOllama(answers=[]) # darf nicht aufgerufen werden
cfg = Config(planner_enabled=True)
subs, planned, qtype = plan_queries("Was ist Altersteilzeit?", client, cfg)
assert planned is False and qtype == "specific"
assert subs == [SubQuery(text="Was ist Altersteilzeit?")]
assert client.calls == 0
def test_plan_queries_uses_planner_and_falls_back_on_error():
cfg = Config(planner_enabled=True)
client = FakeOllama(
answers=['{"type": "specific", "queries": [{"text": "atz lohnausgleich"}, {"text": "atz altersteilzeitgeld", "stand_year": null}]}']
)
subs, planned, qtype = plan_queries(
"Wie funktioniert der Lohnausgleich bei Altersteilzeit und was ersetzt das AMS?", client, cfg
)
assert planned is True and qtype == "specific" and len(subs) == 2
assert subs[0].text == "atz lohnausgleich"
# Fehler -> Originalfrage, geplant False
failing = FakeOllama(answers=[])
failing.chat = lambda *a, **k: (_ for _ in ()).throw(RuntimeError("offline"))
subs, planned, qtype = plan_queries("Wie funktioniert der Lohnausgleich 2026?", failing, cfg)
assert planned is False and qtype == "specific"
assert subs[0].text.startswith("Wie funktioniert")
def test_answer_question_planner_integration(mini_index):
"""Planer an: 1. Chat = Planer-JSON, 2. Chat = Antwort. Beide Sub-Queries
fusionieren in einer Retrieved-Menge (Zitier-Validierung ueber die Union)."""
cfg = Config(
kb_dir=mini_index.kb_dir,
db_path=mini_index.db_path,
embed_off=True,
planner_enabled=True,
planner_max_queries=2,
)
client = FakeOllama(answers=[
json.dumps({"type": "specific", "queries": [
{"text": "altersteilzeit lohnausgleich", "stand_year": None},
{"text": "urlaubsanspruch", "stand_year": None},
]}),
"Antwort mit Beleg [lb-min-01] und [lb-min-02].",
])
result = answer_question(
"Wie funktioniert der Lohnausgleich bei Altersteilzeit und wie viel Urlaub bleibt?",
cfg, client=client,
)
assert client.calls == 2 # Planer + Antwort
assert result["verified"] is True
assert result["refused"] is False
assert set(result["citations"]) == {"lb-min-01", "lb-min-02"}
assert len(result["planned_queries"]) == 2
def test_answer_question_survey_map_reduce(mini_index):
"""Survey-Frage: 1. Planer (type=survey), 2. Map-Destillat,
3. Reduce-Antwort. Zitier-Validierung weiterhin gegen die Union."""
cfg = Config(
kb_dir=mini_index.kb_dir,
db_path=mini_index.db_path,
embed_off=True,
planner_enabled=True,
survey_blocks=8,
)
client = FakeOllama(answers=[
json.dumps({"type": "survey", "queries": [
{"text": "wiku personal aktuell 2026", "stand_year": None},
]}),
# Map-Ausgabe: Destillat je Block mit KB-ID
"[lb-min-01] Altersteilzeit: Lohnausgleich + ATZ-Geld.\n"
"[lb-min-02] Urlaub: 5 Wochen je Dienstjahr.",
# Reduce-Antwort
"Neuerungen: ATZ-Lohnausgleich [lb-min-01]; Urlaub 5 Wochen [lb-min-02].",
])
result = answer_question(
"Welche Neuerungen behandeln die Wissensbasis 2026?",
cfg, client=client,
)
assert client.calls == 3 # Planer + Map + Reduce
assert result["verified"] is True
assert result["refused"] is False
assert set(result["citations"]) == {"lb-min-01", "lb-min-02"}
def test_search_multi_fuses_across_queries(mini_index):
"""Multi-Query-Retrieval: Sub-Queries summieren Beitraege; Treffer aus
beiden Themen erscheinen im Kontext."""
from agent.retrieve import Retriever
cfg = Config(kb_dir=mini_index.kb_dir, db_path=mini_index.db_path, embed_off=True)
r = Retriever(cfg)
subs = [SubQuery(text="Altersteilzeit Lohnausgleich"),
SubQuery(text="Urlaubsanspruch")]
results = r.search_multi(subs, n_entries=8)
ids = {res.entry_id for res in results}
assert {"lb-min-01", "lb-min-02"} <= ids # beide Themen vertreten
r.close()
+10 -8
View File
@@ -1,4 +1,4 @@
"""Registry-Generator für die Wissensbasis (alle vier ID-Räume lb/wk/kv/ris).
"""Registry-Generator für die Wissensbasis (alle fünf ID-Räume lb/wk/kv/ris/rj).
Ersetzt in diesem Repo das im Schwesterprojekt verbliebene
`build_lexis_kb.py --registry`: lädt alle Layer-2-Einträge über
@@ -66,16 +66,16 @@ def build_kb_json(entries, topic_prefix: dict, cluster_names: dict) -> dict:
"layers": {
"curated": "wissensbasis/dokumente",
"fulltext": ".lexis360/md + .wiku/md (lokal, unversioniert — lizenzierte Quellen)",
"raw": ".firecrawl/wko-kv/docs (WKO-KV-Seiten) + .ris (Gesetze) — lokal, unversioniert; kv/ris-Layer-2 ist quellentreu generiert",
"raw": ".firecrawl/kv-portal/wko-kv/docs (WKO-KV-Seiten) + .firecrawl/ris/gesetze (Gesetze) + .rechtsprechung (Entscheidungen/Normauszüge) — lokal, unversioniert; kv/ris/rj-Layer-2 ist quellentreu generiert",
},
"schema": {
"id": "lb-<cluster-prefix>-<nn> (Lexis) | wk-<cluster-prefix>-<nn> (WIKU) | "
"kv-kvt-<nnn> (WKO-KV, Cluster kollektivvertraege) | "
"ris-<cluster-prefix>-<nn> (RIS-Gesetz)",
"ris-<cluster-prefix>-<nn> (RIS-Gesetz) | rj-rjs-<nnn> (Rechtsprechung/Normauszug)",
"stand": "YYYY-MM (Stand der Quelle; KV: Geltungsbeginn)",
"legal_bases": "Rechtsgrundlagen as cited in the source",
"cross_refs": "ids of related kb entries (alle ID-Räume dürfen kreuzen)",
"generated_files": "kv_*.md / ris_*.md sind Tool-Output (tools/ingest_sources.py) — keine manuelle Kuratierung",
"generated_files": "kv_*.md / ris_*.md / rj_*.md sind Tool-Output (tools/ingest_sources.py) — keine manuelle Kuratierung",
},
"clusters": [
{
@@ -119,15 +119,17 @@ def build_index_md(entries, topic_prefix: dict, cluster_names: dict) -> str:
"Lexis Briefings Personalrecht (Lexis 360, lizenzierter Export), "
"WIKU Personal (Fachbroschüren, Arbeitsunterlagen, Casebooks, "
"„WIKU Personal aktuell“), WKO.at Kollektivvertrag (KV-Dokumente, "
"quellentreu generiert) und RIS Rechtsinformationssystem des Bundes "
"(Gesetzes-§-Auschnitte, quellentreu generiert). Rohquellen lokal/"
"unversioniert (`.lexis360/`, `.wiku/`, `.firecrawl/`, `.ris/`). "
"quellentreu generiert), RIS Rechtsinformationssystem des Bundes "
"(Gesetzes-§-Auschnitte, quellentreu generiert) sowie Rechtsprechung "
"(RIS-OGD-Entscheidungen und Normauszüge; EuGH-Textwiedergaben separat "
"als nicht amtlich markiert). Rohquellen lokal/unversioniert "
"(`.lexis360/`, `.wiku/`, `.firecrawl/`, `.firecrawl/ris/gesetze/`, `.rechtsprechung/`). "
"Werte gelten je zum Quell-Stand (`stand` im Frontmatter); Widersprüche "
"und offene Punkte sind mit ⚠/❓ in den Einträgen selbst dokumentiert. "
"Schema & Konventionen: `README.md`.\n"
)
out = [header]
for space in ("lb", "wk", "kv", "ris"):
for space in ("lb", "wk", "kv", "ris", "rj"):
group = by_space.get(space)
if not group:
continue
File diff suppressed because it is too large Load Diff
+313 -9
View File
@@ -27,8 +27,11 @@ from pathlib import Path
from tools.kb_common import ( # noqa: E402
DATE_RE,
LAW_MAP,
WORK_EUGH,
WORK_KV,
WORK_NORM,
WORK_RIS,
WORK_RJ,
YEAR_RE,
ascii_slug,
load_topic_prefix_map,
@@ -36,8 +39,9 @@ from tools.kb_common import ( # noqa: E402
)
ROOT = Path(__file__).resolve().parents[1]
KV_DIR = ROOT / ".firecrawl" / "wko-kv" / "docs"
RIS_DIR = ROOT / ".ris"
KV_DIR = ROOT / ".firecrawl" / "kv-portal" / "wko-kv" / "docs"
RIS_DIR = ROOT / ".firecrawl" / "ris" / "gesetze"
RJ_DIR = ROOT / ".rechtsprechung"
DOCS = ROOT / "wissensbasis" / "dokumente"
CATALOGS = ROOT / "tools" / "catalogs"
CRAWL_STAND = "2026-09" # Beschaffungsstand der beiden Quellen
@@ -368,7 +372,7 @@ def kv_entry(path: Path, catalog: dict, stats: dict) -> tuple[str, str] | None:
f"author: \"WKO\"\n"
f"stand: {stand}\n"
f"source:\n"
f" html: \".firecrawl/wko-kv/docs/{path.name}\"\n"
f" html: \".firecrawl/kv-portal/wko-kv/docs/{path.name}\"\n"
f"legal_bases: []\n"
f"tags: {json.dumps(tags, ensure_ascii=False)}\n"
f"cross_refs: []\n"
@@ -434,7 +438,7 @@ def ris_entry(path: Path, catalog: dict, stats: dict) -> tuple[str, str] | None:
f"author: \"RIS (Bundeskanzleramt)\"\n"
f"stand: {stand}\n"
f"source:\n"
f" text: \".ris/{path.name}\"\n"
f" text: \".firecrawl/ris/gesetze/{path.name}\"\n"
f"legal_bases: {json.dumps([abbr], ensure_ascii=False)}\n"
f"tags: {json.dumps(tags, ensure_ascii=False)}\n"
f"cross_refs: []\n"
@@ -460,6 +464,283 @@ def extract_law_abbreviation(title: str, base: str) -> str:
return base
# --------------------------------------------------------------------------
# Rechtsprechung (RJ): Metadaten-MD -> Layer 2
# --------------------------------------------------------------------------
RJ_FIELD_RE = re.compile(r"^\*\*(.+?):\*\*\s*(.*)$", re.M)
RJ_H2_SPLIT_RE = re.compile(r"(?m)^## ")
RJ_NOISE_LINE_RE = re.compile(r"//-->|^\s*[\[\]]\s*$|^[\s\[]*(Kanzlei|Gesetze|Entscheidungen)\]?\s*$")
def rj_parse_fields(raw: str) -> tuple[dict[str, str], str]:
"""Extrahiert die '**Feld:** Wert'-Metadatenzeilen und den Body nach dem
ersten '---'-Trenner (bei Rechtssätzen leer)."""
fields: dict[str, str] = {}
for m in RJ_FIELD_RE.finditer(raw):
fields[m.group(1).strip()] = m.group(2).strip()
parts = re.split(r"\n\s*---\s*\n", raw, maxsplit=1)
body = parts[1] if len(parts) > 1 else ""
return fields, body
def rj_clean_body(body: str) -> str:
"""Rauscht Zeilen (lexetius-Kommentarreste, Bracket-Fragmente) und
führende Strukturetiketten heraus; kollabiert Leerzeilen."""
lines = []
for line in body.split("\n"):
if RJ_NOISE_LINE_RE.search(line):
continue
lines.append(line.rstrip())
text = "\n".join(lines)
text = re.sub(
r"\A\s*(?:Urteil|Beschluss|Erkenntnis|Entscheidung|Begr\u00fcndung:?|"
r"Entscheidungsgr\u00fcnde:?|Spruch:?)\s*\n+",
"",
text,
)
text = re.sub(r"\n{3,}", "\n\n", text)
return text.strip()
def rj_split_text(text: str, limit: int = 3500) -> list[tuple[str, str]]:
"""Lange Begründungen in ~limit-Zeichen-Stücke an Absatzgrenzen
splitten -> eigene H2-Chunks (Retrieval-Granularität, D10/D11-Budget)."""
if not text:
return []
if len(text) <= limit:
return [("Begründung", text)]
parts: list[str] = []
current: list[str] = []
size = 0
for para in re.split(r"\n\n+", text):
if size and size + len(para) > limit:
parts.append("\n\n".join(current))
current, size = [], 0
current.append(para)
size += len(para) + 2
if current:
parts.append("\n\n".join(current))
if len(parts) == 1:
return [("Begründung", parts[0])]
return [(f"Begründung ({i})", p) for i, p in enumerate(parts, 1)]
def _rj_short(text: str, limit: int = 100) -> str:
if len(text) <= limit:
return text
cut = text[:limit + 1].rsplit(" ", 1)[0]
return cut.rstrip(".,;") + ""
def rj_build_sections(fields: dict[str, str], body: str, kind: str) -> list[tuple[str, str]]:
"""H2-Sektionen je Entscheidungstyp (Rechtssatz/Volltext/EuGH/Norm)."""
sections: list[tuple[str, str]] = []
if kind == "norm":
h2 = _h2_sections(body)
if not h2:
# 17 Norm-Dateien haben den Wortlaut als Fließtext nach dem
# '---'-Trenner (ohne H2-Struktur) — als eigene Sektion übernehmen.
flat = rj_clean_body(body)
if flat:
sections.append(("Normtext", flat))
return sections
for title, text in h2:
t = title.strip().rstrip(".")
if t.lower().startswith("zuletzt aktualisiert"):
continue
if t.lower() in ("gesetzesnummer", "dokumentnummer", "alte dokumentnummer"):
continue
if t.startswith("\u00a7") or t.startswith("Art"):
sections.append((f"{t} \u2013 Wortlaut", text))
else:
sections.append((f"{t} (Index)", text))
return sections
if kind == "rechtssatz":
if fields.get("Rechtssatz"):
sections.append(("Rechtssatz", fields["Rechtssatz"]))
if fields.get("Entscheidungstexte"):
sections.append(("Entscheidungstexte (Fundstellen)", fields["Entscheidungstexte"]))
return sections
# Volltext (JJT/JWT/JFT) und EuGH
if fields.get("Betreff"):
sections.append(("Sachbetreff", fields["Betreff"]))
if fields.get("Leitsatz"):
sections.append(("Leitsatz", fields["Leitsatz"]))
if fields.get("Spruch"):
sections.append(("Spruch", fields["Spruch"]))
if fields.get("Rechtliche Beurteilung"):
sections.append(("Rechtliche Beurteilung", fields["Rechtliche Beurteilung"]))
text = rj_clean_body(body)
sections.extend(rj_split_text(text))
if fields.get("Beachte"):
sections.append(("Beachte", fields["Beachte"]))
return sections
def _h2_sections(body: str) -> list[tuple[str, str]]:
"""Bestehende H2-Struktur (gesetze/*.md) in (Titel, Text) zerlegen."""
out: list[tuple[str, str]] = []
parts = RJ_H2_SPLIT_RE.split(body)
for part in parts[1:]:
lines = part.split("\n", 1)
title = lines[0].strip()
text = lines[1].strip() if len(lines) > 1 else ""
if title and text:
out.append((title, text))
return out
def rj_entry(path: Path, catalog: dict, stats: dict) -> tuple[str, str] | None:
rel = path.relative_to(RJ_DIR).as_posix()
raw = path.read_text(encoding="utf-8")
fields, body = rj_parse_fields(raw)
if path.parent.name == "gesetze":
kind = "norm"
elif rel.startswith("originale/EUGH"):
kind = "eugh"
elif not fields.get("Gericht"):
stats["rj_unparsed"].append(rel)
return None
elif "Rechtssatz" in fields:
kind = "rechtssatz"
else:
kind = "volltext"
court = fields.get("Gericht", "EuGH").strip()
court_tag = ascii_slug(court)
gnr = None
if kind == "norm":
kurztitel = fields.get("Kurztitel", "Norm")
abk = fields.get("Abk\u00fcrzung", "")
para = fields.get("\u00a7/Artikel/Anlage", "")
stand = None
for title, text in _h2_sections(body):
if title.strip().lower().startswith("zuletzt aktualisiert"):
m = DATE_RE.search(text)
if m:
stand = stand_from_date(m.groups())
if title.strip() == "Gesetzesnummer":
gnr = text.strip()
if not stand:
m = DATE_RE.search(body or "")
stand = stand_from_date(m.groups()) if m else CRAWL_STAND
stand_fallback = True
else:
stand_fallback = False
title = f"{kurztitel} ({abk}) \u2013 {para}" if abk else f"{kurztitel} \u2013 {para}"
work = WORK_NORM
chapter = "Zitierte Norm (Originalwortlaut)"
legal_bases = [f"{abk} {para}".strip()] if abk and para else ([abk] if abk else [])
tags = ["rechtsprechung", "norm", ascii_slug(abk) or "ris"]
if stand_fallback:
tags.append("stand-abruf")
quelle = (
f"*{work}, Stand {stand}. {kurztitel}"
+ (f" ({abk})" if abk else "")
+ (f", Gesetzesnummer {gnr}" if gnr else "")
+ f" \u2013 Originalwortlaut aus dem RIS, zitiert in der Rechtsprechung dieses Korpus.*"
)
else:
m = DATE_RE.search(fields.get("Entscheidungsdatum", ""))
stand = stand_from_date(m.groups()) if m else CRAWL_STAND
datum_de = fields.get("Entscheidungsdatum", "")
gz = fields.get("Gesch\u00e4ftszahl", "")
ecli = fields.get("European Case Law Identifier", "")
if kind == "rechtssatz":
title = _rj_short(fields.get("Rechtssatz", f"{court} {gz}"))
chapter = f"{court}-Rechtssatz"
tags = ["rechtsprechung", court_tag, "rechtssatz"]
elif kind == "eugh":
h1 = re.match(r"\s*#\s*(.+)", raw)
rs = h1.group(1).strip() if h1 else path.stem.replace("_", " ")
dm = re.search(r"Urteil vom (\d{1,2}\.\s*\d{1,2}\.\s*\d{4})", raw)
datum_de = dm.group(1).replace(" ", " ").strip() if dm else datum_de
dm2 = DATE_RE.search(datum_de)
if dm2:
stand = stand_from_date(dm2.groups())
title = (
f"{rs}, Urteil vom {datum_de}"
if rs.lower().startswith("eugh")
else f"EuGH {rs}, Urteil vom {datum_de}"
)
court_tag = "eugh"
chapter = "EuGH-Urteil"
tags = ["rechtsprechung", "eugh", "urteil"]
else:
title = f"{court} {gz} vom {datum_de}"
chapter = f"{court}-Erkenntnis (Volltext)"
tags = ["rechtsprechung", court_tag, "volltext"]
year = YEAR_RE.search(stand)
if year:
tags.append(f"jahr-{year.group(1)}")
work = WORK_EUGH if kind == "eugh" else WORK_RJ
legal_bases = [
n.strip().rstrip(";")
for n in re.split(r";\s*", fields.get("Norm", ""))
if n.strip()
]
quelle = (
f"*{work}, {court}, Entscheidung vom {datum_de}"
+ (f", GZ {gz}" if gz else "")
+ (f", {ecli}" if ecli else "")
)
if kind == "eugh":
quelle += (
f". Textwiedergabe (nicht amtlich): {fields.get('URL', '')}"
+ (
f"; amtliche Fassung: {fields.get('Offizielle Fassung', '')}"
if fields.get("Offizielle Fassung")
else ""
)
+ ".*"
)
else:
quelle += ".*"
key = rel
entry = catalog["entries"].get(key)
if entry is None:
seq = catalog["next_seq"]
catalog["next_seq"] = seq + 1
slug = "rj_" + ascii_slug(path.stem)
entry = {"id": f"rj-rjs-{seq:03d}", "slug": slug}
catalog["entries"][key] = entry
kid, slug = entry["id"], entry["slug"]
sections = rj_build_sections(fields, body, kind)
body_md = "\n\n".join(f"## {t}\n\n{text}" for t, text in sections if text)
if not body_md.strip():
stats["rj_empty"].append(rel)
return None
md = (
f"---\n"
f"id: {kid}\n"
f"batch: 1\n"
f"title: {json.dumps(title, ensure_ascii=False)}\n"
f"work: {json.dumps(work, ensure_ascii=False)}\n"
f"chapter: {json.dumps(chapter, ensure_ascii=False)}\n"
f"topic: rechtsprechung\n"
f"author: {json.dumps(court if kind != 'norm' else (fields.get('Abk\u00fcrzung') or court), ensure_ascii=False)}\n"
f"stand: {stand}\n"
f"source:\n"
f" text: \".rechtsprechung/{rel}\"\n"
f"legal_bases: {json.dumps(legal_bases, ensure_ascii=False)}\n"
f"tags: {json.dumps(tags, ensure_ascii=False)}\n"
f"cross_refs: []\n"
f"---\n\n"
f"# {title}\n\n"
f"{quelle}\n\n"
f"*Wissensbasis-ID: {kid}*\n\n"
f"{body_md}\n"
)
entry.update({"title": title, "stand": stand, "kind": kind})
stats["rj_written"] += 1
return slug, md
# --------------------------------------------------------------------------
# CLI
# --------------------------------------------------------------------------
@@ -477,8 +758,9 @@ def write_entries(results: list[tuple[str, str]], dry: bool) -> int:
def run(source: str, dry: bool, limit: int | None, single: str | None) -> int:
stats: dict = {"kv_written": 0, "ris_written": 0, "no_og": [], "no_main": [],
"ris_unmapped": []}
stats: dict = {"kv_written": 0, "ris_written": 0, "rj_written": 0,
"no_og": [], "no_main": [], "ris_unmapped": [],
"rj_unparsed": [], "rj_empty": []}
written = 0
if source in ("kv", "all"):
@@ -513,9 +795,31 @@ def run(source: str, dry: bool, limit: int | None, single: str | None) -> int:
if not dry:
save_catalog(catalog)
if source in ("rj", "all"):
catalog = load_catalog("rj")
files = sorted(RJ_DIR.glob("originale/*.md")) + sorted(
RJ_DIR.glob("gesetze/*.md")
)
files = [f for f in files if f.name != "INDEX.md"]
if single:
files = [f for f in files if single in f.name]
if limit:
files = files[:limit]
results = []
for f in files:
res = rj_entry(f, catalog, stats)
if res:
results.append(res)
written += write_entries(results, dry)
if not dry:
save_catalog(catalog)
print(f"source={source} dry={dry} written={written}")
print(f" kv: {stats['kv_written']} ris: {stats['ris_written']}")
for key in ("no_og", "no_main", "ris_unmapped"):
print(
f" kv: {stats['kv_written']} ris: {stats['ris_written']} "
f"rj: {stats['rj_written']}"
)
for key in ("no_og", "no_main", "ris_unmapped", "rj_unparsed", "rj_empty"):
if stats[key]:
print(f" {key}: {stats[key]}")
return 0
@@ -523,7 +827,7 @@ def run(source: str, dry: bool, limit: int | None, single: str | None) -> int:
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--source", choices=("kv", "ris", "all"), default="all")
ap.add_argument("--source", choices=("kv", "ris", "rj", "all"), default="all")
ap.add_argument("--dry-run", action="store_true")
ap.add_argument("--limit", type=int, default=None)
ap.add_argument("--file", default=None, help="Teilstring des Quelldateinamens")
+9 -1
View File
@@ -22,9 +22,10 @@ ID_SPACES = {
"wk": "WIKU Personal (Fachbroschüren, Arbeitsunterlagen, Casebook, Personal aktuell)",
"kv": "WKO.at — Kollektivvertrag-Dokumente (Lohn-/Gehaltsordnungen, KV-Texte)",
"ris": "RIS — Rechtsinformationssystem des Bundes (Gesetzes-§-Auschnitte)",
"rj": "RIS/OGD — Rechtsprechung (OGH, VwGH, VfGH, EuGH) und zitierte Normen",
}
# Neue Cluster der Quellen kv/ris: slug -> (ID-Präfix, Anzeigename).
# Neue Cluster der Quellen kv/ris/rj: slug -> (ID-Präfix, Anzeigename).
NEW_CLUSTERS = {
"kollektivvertraege": ("kvt", "Kollektivverträge (WKO.at)"),
"zivilrecht-normen": ("zvr", "Zivilrechtliche Normen (ABGB, ZPO & Co.)"),
@@ -33,11 +34,18 @@ NEW_CLUSTERS = {
"lohnsteuer": ("lst", "Lohnsteuer & Einkommensteuer (EStG)"),
"abgabenverfahren": ("abo", "Abgabenverfahren (BAO)"),
"normen-sonstige": ("nso", "Weitere Gesetze und Verordnungen"),
"rechtsprechung": (
"rjs",
"Rechtsprechung (OGH, VwGH, VfGH, EuGH) und zitierte Normen",
),
}
# Werk-Namen der neuen Quellen (Frontmatter `work`).
WORK_KV = "WKO.at Kollektivvertrag"
WORK_RIS = "RIS Rechtsinformationssystem des Bundes"
WORK_RJ = "RIS Rechtsprechung (OGD-Justiz/VwGH/VfGH)"
WORK_EUGH = "EuGH Rechtsprechung (Textwiedergabe lexetius, nicht amtlich)"
WORK_NORM = "RIS Originalwortlaut zitierte Norm"
# .ris/<Datei ohne .md> -> (topic, chapter, lfd. Nummer im Cluster).
# Nummern sind fix (dann eingefroren); neue Gesetze hängen in ihrem Cluster
+359 -1
View File
@@ -1,6 +1,6 @@
# Index — Wissensbasis Personalverrechnung
Generiert am 2026-09-14 · 1274 Einträge · 76 Cluster. Quellen: Lexis Briefings Personalrecht (Lexis 360, lizenzierter Export), WIKU Personal (Fachbroschüren, Arbeitsunterlagen, Casebooks, „WIKU Personal aktuell“), WKO.at Kollektivvertrag (KV-Dokumente, quellentreu generiert) und RIS Rechtsinformationssystem des Bundes (Gesetzes-§-Auschnitte, quellentreu generiert). Rohquellen lokal/unversioniert (`.lexis360/`, `.wiku/`, `.firecrawl/`, `.ris/`). Werte gelten je zum Quell-Stand (`stand` im Frontmatter); Widersprüche und offene Punkte sind mit ⚠/❓ in den Einträgen selbst dokumentiert. Schema & Konventionen: `README.md`.
Generiert am 2026-09-15 · 1622 Einträge · 77 Cluster. Quellen: Lexis Briefings Personalrecht (Lexis 360, lizenzierter Export), WIKU Personal (Fachbroschüren, Arbeitsunterlagen, Casebooks, „WIKU Personal aktuell“), WKO.at Kollektivvertrag (KV-Dokumente, quellentreu generiert), RIS Rechtsinformationssystem des Bundes (Gesetzes-§-Auschnitte, quellentreu generiert) sowie Rechtsprechung (RIS-OGD-Entscheidungen und Normauszüge; EuGH-Textwiedergaben separat als nicht amtlich markiert). Rohquellen lokal/unversioniert (`.lexis360/`, `.wiku/`, `.firecrawl/`, `.firecrawl/ris/gesetze/`, `.rechtsprechung/`). Werte gelten je zum Quell-Stand (`stand` im Frontmatter); Widersprüche und offene Punkte sind mit ⚠/❓ in den Einträgen selbst dokumentiert. Schema & Konventionen: `README.md`.
# Lexis Briefings Personalrecht (Lexis 360-Export) — 571 Einträge
@@ -2170,3 +2170,361 @@ Generiert am 2026-09-14 · 1274 Einträge · 76 Cluster. Quellen: Lexis Briefing
|---|---|---|---|---|
| ris-prd-01 | Zivildienstgesetz 1986 (ZDG) | RIS (Bundeskanzleramt) | 2026-09 | ris_zdg.md |
# RIS/OGD — Rechtsprechung (OGH, VwGH, VfGH, EuGH) und zitierte Normen — 348 Einträge
## Rechtsprechung (OGH, VwGH, VfGH, EuGH) und zitierte Normen (`topic: rechtsprechung`) — 348 Einträge · Stand 1951-10 bis 2026-09
| ID | Titel | Autor | Stand | Datei |
|---|---|---|---|---|
| rj-rjs-001 | EuGH C-171,172/94, Urteil vom 7. 3. 1996 | EuGH | 1996-03 | rj_eugh-c-171-172-1994.md |
| rj-rjs-002 | EuGH C-29/91, Urteil vom 19. 5. 1992 | EuGH | 1992-05 | rj_eugh-c-29-1991.md |
| rj-rjs-003 | EuGH C-458/05, Urteil vom 13.09.2007 | EuGH | 2007-09 | rj_eugh-c-458-2005.md |
| rj-rjs-004 | EuGH C-463/09, Urteil vom 20. 1. 2011 | EuGH | 2011-01 | rj_eugh-c-463-2009.md |
| rj-rjs-005 | EuGH C-466/07, Urteil vom 12. 2. 2009 | EuGH | 2009-02 | rj_eugh-c-466-2007.md |
| rj-rjs-006 | Verfassungsgerichtshof G62/2018 vom 04.10.2018 | Verfassungsgerichtshof | 2018-10 | rj_jft-20181004-18g00062-00.md |
| rj-rjs-007 | Verfassungsgerichtshof G135/2018 vom 04.10.2018 | Verfassungsgerichtshof | 2018-10 | rj_jft-20181004-18g00135-00.md |
| rj-rjs-008 | Verfassungsgerichtshof G325/2018 vom 25.02.2019 | Verfassungsgerichtshof | 2019-02 | rj_jft-20190225-18g00325-00.md |
| rj-rjs-009 | Verfassungsgerichtshof G326/2018 vom 25.02.2019 | Verfassungsgerichtshof | 2019-02 | rj_jft-20190225-18g00326-00.md |
| rj-rjs-010 | Angebliche Mängel des Verfahrens I. Instanz, die vom Berufungsgerichte nicht als solche anerkannt… | OGH | 2026-06 | rj_jjr-19490720-ogh0002-0010ob00313-4900000-001.md |
| rj-rjs-011 | Die für die Entlassung maßgebenden Gründe müssen nicht schon in der Entlassungserklärung angeführt… | OGH | 1951-10 | rj_jjr-19511018-ogh0002-0040ob00106-5100000-002.md |
| rj-rjs-012 | Ein einmaliger Verzug bei Zahlung des Gehaltes berechtigt nicht zum Austritt. | OGH | 1952-06 | rj_jjr-19520610-ogh0002-0040ob00066-5200000-001.md |
| rj-rjs-013 | Auf das Dienstverhältnis eines Redakteurs, das seine Erwerbstätigkeit nicht hauptsächlich in… | OGH | 1952-09 | rj_jjr-19520930-ogh0002-0040ob00122-5200000-001.md |
| rj-rjs-014 | Der Vorwurf der Denunziation ist als erhebliche Ehrverletzung anzusehen. Ob eine Äußerung als… | OGH | 1952-09 | rj_jjr-19520930-ogh0002-0040ob00122-5200000-002.md |
| rj-rjs-015 | Die Prozeßkostenersatzpflicht ist ausschließlich durch den Prozeßerfolg bedingt. Dies gilt auch… | OGH | 1952-11 | rj_jjr-19521111-ogh0002-0040ob00122-5200000-003.md |
| rj-rjs-016 | Nur ein Verstoß gegen die dienstliche Treuepflicht bildet einen wichtigen Kündigungsgrund. Auch die… | OGH | 1953-01 | rj_jjr-19530120-ogh0002-0040ob00003-5300000-001.md |
| rj-rjs-017 | Nichtzustandekommen einer neuen Lohnvereinbarung (Austrittsgrund?). | OGH | 1953-07 | rj_jjr-19530707-ogh0002-0040ob00090-5300000-001.md |
| rj-rjs-018 | Bei der Beurteilung der Frage, ob eine Äußerung als erhebliche Ehrverletzung und damit als… | OGH | 2025-08 | rj_jjr-19530709-ogh0002-0040ob00120-5300000-001.md |
| rj-rjs-019 | Wenn eine nach außen hin als Kündigung erscheinende Auflösung des Dienstverhältnisses durch den… | OGH | 1954-03 | rj_jjr-19540302-ogh0002-0040ob00022-5400000-002.md |
| rj-rjs-020 | Schriftliche Rüge eines Angestellten (Hauptbuchhalters) keine erhebliche Ehrverletzung, die Austritt… | OGH | 1954-07 | rj_jjr-19540715-ogh0002-0040ob00100-5400000-001.md |
| rj-rjs-021 | Die Bezahlung von Entgeltbeträgen, die dem Dienstnehmer über den kollektivvertraglichen… | OGH | 1955-03 | rj_jjr-19550322-ogh0002-0040ob00129-5400000-001.md |
| rj-rjs-022 | Die Mitteilung des Dienstgebers, dem Dienstnehmer, dem ein höherer Gehalt vertraglich zusteht, ab… | OGH | 1955-03 | rj_jjr-19550322-ogh0002-0040ob00129-5400000-002.md |
| rj-rjs-023 | Nervosität und Unbeherrschtheit eines Dienstnehmers sind keine Gründe, die den von ihm gesetzten… | OGH | 1955-05 | rj_jjr-19550524-ogh0002-0040ob00046-5500000-001.md |
| rj-rjs-024 | Die Androhung der Entlassung, wenn sich der Dienstnehmer nicht binnen einer kurzen Frist zur… | OGH | 1955-06 | rj_jjr-19550614-ogh0002-0040ob00073-5500000-001.md |
| rj-rjs-025 | Der die Entlassung aussprechende Dienstgeber ist nicht verpflichtet, die Entlassungsgründe im… | OGH | 1955-06 | rj_jjr-19550614-ogh0002-0040ob00073-5500000-002.md |
| rj-rjs-026 | Der Revisionsgrund des § 503 Z 2 ZPO ist durch mangelhafte Begründung so wenig gegeben, wie der… | OGH | 2024-06 | rj_jjr-19550831-ogh0002-0070ob00352-5500000-002.md |
| rj-rjs-027 | Dieser Austrittsgrund liegt nur vor, wenn der Schutz des Dienstgebers erfolglos angerufen… | OGH | 1955-09 | rj_jjr-19550906-ogh0002-0040ob00098-5500000-001.md |
| rj-rjs-028 | Kein Entlassungsgrund, wenn Angestellter pornographische Bilder einem großjährigen Mitbediensteten… | OGH | 1955-09 | rj_jjr-19550906-ogh0002-0040ob00098-5500000-002.md |
| rj-rjs-029 | Keine Ansprüche des Dienstnehmers aus der im Probemonat herbeigeführten Lösung des… | OGH | 2025-09 | rj_jjr-19550906-ogh0002-0040ob00109-5500000-001.md |
| rj-rjs-030 | Ob ein Tatbestand einen wichtigen Austrittsgrund darstellt, ist nicht nach der subjektiven… | OGH | 1955-12 | rj_jjr-19551206-ogh0002-0040ob00170-5500000-001.md |
| rj-rjs-031 | Eine Kündigung mit verkürzter Frist anstatt der Entlassung ist wirksam, wenn sie sich ausdrücklich… | OGH | 1956-02 | rj_jjr-19560214-ogh0002-0040ob00196-5500000-001.md |
| rj-rjs-032 | Ein Dienstnehmer, der den Dienstgeber auf die ihm gesetzlich obliegenden Schutzverpflichtungen… | OGH | 1956-05 | rj_jjr-19560508-ogh0002-0040ob00015-5600000-001.md |
| rj-rjs-033 | Keine Verwirkung des Austrittsrechtes eines Dienstnehmers, der einmal auf eine Verzögerung oder… | OGH | 2023-06 | rj_jjr-19561002-ogh0002-0040ob00076-5600000-001.md |
| rj-rjs-034 | Indirekte Diebstahlsbeschuldigung. Eine Lösung des Dienstverhältnisses aus wichtigen Gründen liegt… | OGH | 1957-07 | rj_jjr-19570702-ogh0002-0040ob00025-5700000-001.md |
| rj-rjs-035 | Nicht jede Vertragsverletzung berechtigt zum sofortigen Austritt. Wesentlich ist die… | OGH | 1957-07 | rj_jjr-19570702-ogh0002-0040ob00075-5700000-001.md |
| rj-rjs-036 | Auch eine einmalige empfindliche Beleidigung kann einen Entlassungsgrund oder Austrittsgrund… | OGH | 1957-12 | rj_jjr-19571217-ogh0002-0040ob00121-5700000-001.md |
| rj-rjs-037 | Der Vorwurf der Veruntreuung stellt einen Austrittsgrund dar, wenn der Vorwurf nicht durch das… | OGH | 1959-04 | rj_jjr-19590421-ogh0002-0040ob00034-5900000-001.md |
| rj-rjs-038 | "Verletzung der Sittlichkeit" erfordert unzüchtige, also die Sittlichkeit in sexueller Beziehung… | OGH | 1960-06 | rj_jjr-19600621-ogh0002-0040ob00086-6000000-001.md |
| rj-rjs-039 | Bei Prüfung des Entlassungsgrundes der Vertrauensunwürdigkeit ist an das Verhalten des Angestellten… | OGH | 2023-09 | rj_jjr-19600906-ogh0002-0040ob00093-6000000-001.md |
| rj-rjs-040 | Die Weigerung des Dienstgebers, den Dienstnehmer zur Sozialversicherung anzumelden, bildet für den… | OGH | 1962-04 | rj_jjr-19620403-ogh0002-0040ob00035-6200000-001.md |
| rj-rjs-041 | Nicht jede, sondern nur eine wesentliche Vertragsverletzung berechtigt zum Austritt; wesentlich ist… | OGH | 1962-11 | rj_jjr-19621113-ogh0002-0040ob00124-6200000-001.md |
| rj-rjs-042 | Eine Verletzung einer wesentlichen Vertragsbestimmung gemäß § 82 a lit d GewO liegt vor, wenn der… | OGH | 1963-10 | rj_jjr-19631022-ogh0002-0040ob00094-6300000-001.md |
| rj-rjs-043 | Eine an sich erhebliche Ehrverletzung bildet selbst dann, wenn sie zu einer strafgerichtlichen… | OGH | 1964-07 | rj_jjr-19640728-ogh0002-0040ob00065-6400000-001.md |
| rj-rjs-044 | Zum Anspruch auf Urlaubsentschädigung (mit grundsätzlichen Ausführungen). | OGH | 1965-04 | rj_jjr-19650427-ogh0002-0040ob00052-6500000-001.md |
| rj-rjs-045 | Die DO.A ist ein Kollektivvertrag (vgl auch EvBl 1956/87 = JBl 1956,240). | OGH | 1965-10 | rj_jjr-19651005-ogh0002-0040ob00112-6500000-002.md |
| rj-rjs-046 | Gerechtfertigter Austritt einer Verwaltungsangestellten eines Sozialversicherungsträgers wegen… | OGH | 1967-02 | rj_jjr-19670228-ogh0002-0040ob00015-6700000-001.md |
| rj-rjs-047 | Nach § 32 AngG braucht das Mitverschulden des - aus einem wichtigen Grund im Sinne des § 26 AngG… | OGH | 1967-04 | rj_jjr-19670418-ogh0002-0040ob00025-6700000-001.md |
| rj-rjs-048 | Wiederholung einer Beleidigung ("Ich sage Ihnen noch einmal, Sie sind blöd!") als Austrittsgrund. | OGH | 1967-04 | rj_jjr-19670418-ogh0002-0040ob00025-6700000-002.md |
| rj-rjs-049 | Dieser Entlassungsgrund ist ua dann gegeben, wenn der Angestellte sich gegenüber einem… | OGH | 1970-12 | rj_jjr-19701201-ogh0002-0040ob00099-7000000-001.md |
| rj-rjs-050 | Der Dienstgeber kann ein vereinbartes Überstundenpauschale nicht einseitig aufheben. | OGH | 1971-10 | rj_jjr-19711012-ogh0002-0040ob00059-7100000-001.md |
| rj-rjs-051 | Ein Überstundenpauschale ist in die Bemessungsgrundlage der Abfertigung miteinzubeziehen. | OGH | 1971-10 | rj_jjr-19711012-ogh0002-0040ob00059-7100000-002.md |
| rj-rjs-052 | Berechtigter Austritt eines Angestellten wegen vertragswidriger einseitiger Aufhebung eines… | OGH | 1971-10 | rj_jjr-19711012-ogh0002-0040ob00059-7100000-003.md |
| rj-rjs-053 | § 32 AngG dient nicht dazu, dass ein zur Erhebung des Schuldvorwurfes nicht ausreichendes Verhalten… | OGH | 2024-10 | rj_jjr-19730109-ogh0002-0040ob00094-7200000-001.md |
| rj-rjs-054 | Von einer ungebührlichen Schmälerung oder einem ungebührlichen Vorenthalten des Entgeltes kann nur… | OGH | 1973-01 | rj_jjr-19730109-ogh0002-0040ob00094-7200000-002.md |
| rj-rjs-055 | Voraussetzung für die Anwendung des § 32 AngG ist ein für das Zustandekommen des… | OGH | 1973-01 | rj_jjr-19730109-ogh0002-0040ob00103-7200000-001.md |
| rj-rjs-056 | Ein Verzicht auf den kollektivvertraglich gebührenden Lohn vor dessen Fälligkeit ist jedenfalls… | OGH | 1973-09 | rj_jjr-19730911-ogh0002-0040ob00101-7300000-005.md |
| rj-rjs-057 | Verschweigung von Austrittsgründen tritt nicht ein, wenn es sich bei den Tatbeständen um einen… | OGH | 1973-12 | rj_jjr-19731211-ogh0002-0040ob00098-7300000-001.md |
| rj-rjs-058 | Die Fälligstellung des Überstundenentgeltanspruches setzt Bekanntgabe der Überstunden, allenfalls… | OGH | 1973-12 | rj_jjr-19731211-ogh0002-0040ob00098-7300000-002.md |
| rj-rjs-059 | Weigert sich der Arbeitgeber bereits geleistete Überstunden abzugelten, so ist ein rechtswidriger… | OGH | 1973-12 | rj_jjr-19731211-ogh0002-0040ob00098-7300000-003.md |
| rj-rjs-060 | Tätigt der Arbeitgeber aus der von einem Angestellten zu führenden Handkasse Entnahmen, ohne diese… | OGH | 1973-12 | rj_jjr-19731211-ogh0002-0040ob00098-7300000-004.md |
| rj-rjs-061 | "Arbeiterverräter" gebraucht gegen Betriebsratsmitglied als Entlassungsgrund. | OGH | 1973-12 | rj_jjr-19731211-ogh0002-0040ob00101-7300000-001.md |
| rj-rjs-062 | Nichteinhaltung der Pausenregelung durch den Arbeitnehmer kein Entlassungsgrund, wenn kein… | OGH | 1973-12 | rj_jjr-19731211-ogh0002-0040ob00101-7300000-002.md |
| rj-rjs-063 | Es ist besonders schwerwiegend, wenn die beleidigende Äußerung gegen ein Mitglied des Betriebsrates… | OGH | 1973-12 | rj_jjr-19731211-ogh0002-0040ob00101-7300000-003.md |
| rj-rjs-064 | Aus Judikat 26 neu kann keine unwiderlegliche Vermutung abgeleitet werden, daß ein vor Beendigung… | OGH | 1973-12 | rj_jjr-19731211-ogh0002-0040ob00101-7300000-004.md |
| rj-rjs-065 | Beharrliche Pflichtverletzung im Sinne § 82 lit f GewO 1859, aufrechterhalten durch § 376 Z 47 GewO… | OGH | 1976-09 | rj_jjr-19760921-ogh0002-0040ob00098-7600000-002.md |
| rj-rjs-066 | Urlaubsentschädigung auch bei einem nur wenig mehr als sechs Monate dauernden Dienstverhältnis. | OGH | 1977-03 | rj_jjr-19770322-ogh0002-0040ob00007-7700000-001.md |
| rj-rjs-067 | Sobald die vereinbarte Dauer des Arbeitsvertrages die gesetzliche Wartefrist überstieg, stand der… | OGH | 1979-03 | rj_jjr-19790327-ogh0002-0040ob00004-7900000-001.md |
| rj-rjs-068 | Vorwurf einer strafbaren Handlung ist für den betroffenen Arbeitnehmer regelmäßig ein Austrittsgrund… | OGH | 1979-03 | rj_jjr-19790327-ogh0002-0040ob00004-7900000-002.md |
| rj-rjs-069 | Entscheidend für die Beurteilung der Tatbestandsmäßigkeit einer Ehrenbeleidigung und der… | OGH | 1979-07 | rj_jjr-19790710-ogh0002-0040ob00046-7900000-001.md |
| rj-rjs-070 | Werden ehrenrührige Tatsachen nicht in beleidigender Absicht, sondern in Wahrung berechtigter… | OGH | 1979-07 | rj_jjr-19790710-ogh0002-0040ob00046-7900000-002.md |
| rj-rjs-071 | Eine objektiv, aber nicht wissentlich falsche Beschuldigung aus Anlaß des Abhandenkommens eines… | OGH | 1979-07 | rj_jjr-19790710-ogh0002-0040ob00046-7900000-003.md |
| rj-rjs-072 | Eine im Zusammenhang mit dem vom Betriebsrat betriebenen Sparverein gemachte Ehrenbeleidigung gegen… | OGH | 1979-07 | rj_jjr-19790710-ogh0002-0040ob00046-7900000-004.md |
| rj-rjs-073 | Der Ehrenbeleidigung muss eine Verletzungsabsicht in Bezug auf die im Tatbestand angeführten… | OGH | 1979-07 | rj_jjr-19790710-ogh0002-0040ob00046-7900000-005.md |
| rj-rjs-074 | Unter den "übrigen Hilfsarbeitern" sind alle - auch Betriebsratsmitglieder - beim selben Arbeitgeber… | OGH | 1979-07 | rj_jjr-19790710-ogh0002-0040ob00046-7900000-006.md |
| rj-rjs-075 | Wird das Überstundenentgelt auf der Grundlage eines niedrigeren als des mit dem Arbeitnehmer… | OGH | 1980-04 | rj_jjr-19800429-ogh0002-0040ob00051-8000000-001.md |
| rj-rjs-076 | Bei der Berechnung der Urlaubsentschädigung ist der unberechtigt entlassene Kläger gemäß § 9 UrlG… | OGH | 1981-09 | rj_jjr-19810915-ogh0002-0040ob00037-8100000-001.md |
| rj-rjs-077 | Eine sachliche, nicht in beleidigender Form vorgebrachte Kritik des Arbeitgebers an der… | OGH | 2023-06 | rj_jjr-19820504-ogh0002-0040ob00036-8200000-001.md |
| rj-rjs-078 | Unter den Tatbestand der Vertrauensunwürdigkeit im Sinne des § 27 Z 1 letzter Fall AngG fällt jede… | OGH | 2025-09 | rj_jjr-19830510-ogh0002-0040ob00051-8300000-003.md |
| rj-rjs-079 | Die Rechtswirksamkeit von Änderungen des Arbeitsvertrages zum Nachteil des Arbeitnehmers ist Folge… | OGH | 2023-03 | rj_jjr-19831018-ogh0002-0040ob00105-8200000-004.md |
| rj-rjs-080 | Der Entzug des auch für private Zwecke zur Verfügung gestellten Firmenfahrzeuges bei… | OGH | 1988-08 | rj_jjr-19880831-ogh0002-009oba00152-8800000-001.md |
| rj-rjs-081 | OGH 4Ob152/84 vom 26.02.1985 | OGH | 1985-02 | rj_jjt-19850226-ogh0002-0040ob00152-8400000-000.md |
| rj-rjs-082 | OGH 4Ob101/84 vom 28.10.1985 | OGH | 1985-10 | rj_jjt-19851028-ogh0002-0040ob00101-8400000-000.md |
| rj-rjs-083 | OGH 14Ob7/86 vom 04.03.1986 | OGH | 1986-03 | rj_jjt-19860304-ogh0002-0140ob00007-8600000-000.md |
| rj-rjs-084 | OGH 4Ob60/85 vom 13.05.1986 | OGH | 1986-05 | rj_jjt-19860513-ogh0002-0040ob00060-8500000-000.md |
| rj-rjs-085 | OGH 14Ob90/86 vom 03.06.1986 | OGH | 1986-06 | rj_jjt-19860603-ogh0002-0140ob00090-8600000-000.md |
| rj-rjs-086 | OGH 14Ob143/86 vom 30.09.1986 | OGH | 1986-09 | rj_jjt-19860930-ogh0002-0140ob00143-8600000-000.md |
| rj-rjs-087 | OGH 14Ob201/86 vom 16.12.1986 | OGH | 1986-12 | rj_jjt-19861216-ogh0002-0140ob00201-8600000-000.md |
| rj-rjs-088 | OGH 14ObA27/87 (14ObA28/87) vom 10.03.1987 | OGH | 1987-03 | rj_jjt-19870310-ogh0002-014oba00027-8700000-000.md |
| rj-rjs-089 | OGH 14ObA38/87 vom 07.04.1987 | OGH | 1987-04 | rj_jjt-19870407-ogh0002-014oba00038-8700000-000.md |
| rj-rjs-090 | OGH 9ObA113/87 vom 16.09.1987 | OGH | 1987-09 | rj_jjt-19870916-ogh0002-009oba00113-8700000-000.md |
| rj-rjs-091 | OGH 14ObA77/87 vom 21.10.1987 | OGH | 1987-10 | rj_jjt-19871021-ogh0002-014oba00077-8700000-000.md |
| rj-rjs-092 | OGH 9ObA201/87 vom 10.02.1988 | OGH | 1988-02 | rj_jjt-19880210-ogh0002-009oba00201-8700000-000.md |
| rj-rjs-093 | OGH 9ObA44/88 vom 13.04.1988 | OGH | 1988-04 | rj_jjt-19880413-ogh0002-009oba00044-8800000-000.md |
| rj-rjs-094 | OGH 9ObA112/88 vom 01.06.1988 | OGH | 1988-06 | rj_jjt-19880601-ogh0002-009oba00112-8800000-000.md |
| rj-rjs-095 | OGH 9ObA120/88 vom 01.06.1988 | OGH | 1988-06 | rj_jjt-19880601-ogh0002-009oba00120-8800000-000.md |
| rj-rjs-096 | OGH 9ObA155/88 vom 31.08.1988 | OGH | 1988-08 | rj_jjt-19880831-ogh0002-009oba00155-8800000-000.md |
| rj-rjs-097 | OGH 9ObA189/88 vom 14.09.1988 | OGH | 1988-09 | rj_jjt-19880914-ogh0002-009oba00189-8800000-000.md |
| rj-rjs-098 | OGH 9ObA68/89 vom 19.04.1989 | OGH | 1989-04 | rj_jjt-19890419-ogh0002-009oba00068-8900000-000.md |
| rj-rjs-099 | OGH 9ObA188/89 vom 12.07.1989 | OGH | 1989-07 | rj_jjt-19890712-ogh0002-009oba00188-8900000-000.md |
| rj-rjs-100 | OGH 9ObA5/90 vom 31.01.1990 | OGH | 1990-01 | rj_jjt-19900131-ogh0002-009oba00005-9000000-000.md |
| rj-rjs-101 | OGH 9ObA166/90 vom 11.07.1990 | OGH | 1990-07 | rj_jjt-19900711-ogh0002-009oba00166-9000000-000.md |
| rj-rjs-102 | OGH 9ObA231/90 (9ObA232/90) vom 26.09.1990 | OGH | 1990-09 | rj_jjt-19900926-ogh0002-009oba00231-9000000-000.md |
| rj-rjs-103 | OGH 9ObA254/90 vom 07.11.1990 | OGH | 1990-11 | rj_jjt-19901107-ogh0002-009oba00254-9000000-000.md |
| rj-rjs-104 | OGH 9ObA274/90 vom 21.11.1990 | OGH | 1990-11 | rj_jjt-19901121-ogh0002-009oba00274-9000000-000.md |
| rj-rjs-105 | OGH 9Oba35/91 vom 10.04.1991 | OGH | 1991-04 | rj_jjt-19910410-ogh0002-0090oba0035-9100000-000.md |
| rj-rjs-106 | OGH 9ObA73/91 vom 08.05.1991 | OGH | 1991-05 | rj_jjt-19910508-ogh0002-009oba00073-9100000-000.md |
| rj-rjs-107 | OGH 9ObA120/91 vom 19.06.1991 | OGH | 1991-06 | rj_jjt-19910619-ogh0002-009oba00120-9100000-000.md |
| rj-rjs-108 | OGH 9ObA127/91 vom 19.06.1991 | OGH | 1991-06 | rj_jjt-19910619-ogh0002-009oba00127-9100000-000.md |
| rj-rjs-109 | OGH 9ObA19/92 (9ObA20/92) vom 26.02.1992 | OGH | 1992-02 | rj_jjt-19920226-ogh0002-009oba00019-9200000-000.md |
| rj-rjs-110 | OGH 9ObA30/92 vom 26.02.1992 | OGH | 1992-02 | rj_jjt-19920226-ogh0002-009oba00030-9200000-000.md |
| rj-rjs-111 | OGH 9ObA60/92 vom 18.03.1992 | OGH | 1992-03 | rj_jjt-19920318-ogh0002-009oba00060-9200000-000.md |
| rj-rjs-112 | OGH 9ObA56/92 vom 08.04.1992 | OGH | 1992-04 | rj_jjt-19920408-ogh0002-009oba00056-9200000-000.md |
| rj-rjs-113 | OGH 9ObA91/92 vom 17.06.1992 | OGH | 1992-06 | rj_jjt-19920617-ogh0002-009oba00091-9200000-000.md |
| rj-rjs-114 | OGH 9ObA202/92 vom 25.11.1992 | OGH | 1992-11 | rj_jjt-19921125-ogh0002-009oba00202-9200000-000.md |
| rj-rjs-115 | OGH 9ObA287/92 vom 27.01.1993 | OGH | 1993-01 | rj_jjt-19930127-ogh0002-009oba00287-9200000-000.md |
| rj-rjs-116 | OGH 9ObA25/93 vom 24.02.1993 | OGH | 1993-02 | rj_jjt-19930224-ogh0002-009oba00025-9300000-000.md |
| rj-rjs-117 | OGH 9ObA53/93 vom 14.04.1993 | OGH | 1993-04 | rj_jjt-19930414-ogh0002-009oba00053-9300000-000.md |
| rj-rjs-118 | OGH 9ObA80/93 vom 28.04.1993 | OGH | 1993-04 | rj_jjt-19930428-ogh0002-009oba00080-9300000-000.md |
| rj-rjs-119 | OGH 9ObA86/93 vom 19.05.1993 | OGH | 1993-05 | rj_jjt-19930519-ogh0002-009oba00086-9300000-000.md |
| rj-rjs-120 | OGH 9ObA105/93 vom 09.06.1993 | OGH | 1993-06 | rj_jjt-19930609-ogh0002-009oba00105-9300000-000.md |
| rj-rjs-121 | OGH 9ObA96/93 vom 08.07.1993 | OGH | 1993-07 | rj_jjt-19930708-ogh0002-009oba00096-9300000-000.md |
| rj-rjs-122 | OGH 9ObA115/93 vom 08.07.1993 | OGH | 1993-07 | rj_jjt-19930708-ogh0002-009oba00115-9300000-000.md |
| rj-rjs-123 | OGH 9ObA205/93 vom 22.09.1993 | OGH | 1993-09 | rj_jjt-19930922-ogh0002-009oba00205-9300000-000.md |
| rj-rjs-124 | OGH 9ObA211/93 vom 24.11.1993 | OGH | 1993-11 | rj_jjt-19931124-ogh0002-009oba00211-9300000-000.md |
| rj-rjs-125 | OGH 9ObA281/93 vom 22.12.1993 | OGH | 1993-12 | rj_jjt-19931222-ogh0002-009oba00281-9300000-000.md |
| rj-rjs-126 | OGH 9ObA288/93(9ObA289/93) vom 22.12.1993 | OGH | 1993-12 | rj_jjt-19931222-ogh0002-009oba00288-9300000-000.md |
| rj-rjs-127 | OGH 8ObA206/94 vom 17.03.1994 | OGH | 1994-03 | rj_jjt-19940317-ogh0002-008oba00206-9400000-000.md |
| rj-rjs-128 | OGH 9ObA50/94 vom 06.04.1994 | OGH | 1994-04 | rj_jjt-19940406-ogh0002-009oba00050-9400000-000.md |
| rj-rjs-129 | OGH 9ObA70/94 vom 25.05.1994 | OGH | 1994-05 | rj_jjt-19940525-ogh0002-009oba00070-9400000-000.md |
| rj-rjs-130 | OGH 8ObA262/94 vom 16.06.1994 | OGH | 1994-06 | rj_jjt-19940616-ogh0002-008oba00262-9400000-000.md |
| rj-rjs-131 | OGH 9ObA92/94 vom 29.06.1994 | OGH | 1994-06 | rj_jjt-19940629-ogh0002-009oba00092-9400000-000.md |
| rj-rjs-132 | OGH 8ObA215/94 vom 31.08.1994 | OGH | 1994-08 | rj_jjt-19940831-ogh0002-008oba00215-9400000-000.md |
| rj-rjs-133 | OGH 9ObA126/94 vom 14.09.1994 | OGH | 1994-09 | rj_jjt-19940914-ogh0002-009oba00126-9400000-000.md |
| rj-rjs-134 | OGH 8ObA239/94 vom 15.09.1994 | OGH | 1994-09 | rj_jjt-19940915-ogh0002-008oba00239-9400000-000.md |
| rj-rjs-135 | OGH 9ObA171/94 (9ObA172/94, 9ObA173/94) vom 12.10.1994 | OGH | 1994-10 | rj_jjt-19941012-ogh0002-009oba00171-9400000-000.md |
| rj-rjs-136 | OGH 4Ob103/94 vom 18.10.1994 | OGH | 1994-10 | rj_jjt-19941018-ogh0002-0040ob00103-9400000-000.md |
| rj-rjs-137 | OGH 9ObA241/94 vom 25.01.1995 | OGH | 1995-01 | rj_jjt-19950125-ogh0002-009oba00241-9400000-000.md |
| rj-rjs-138 | OGH 9ObA142/95 vom 23.08.1995 | OGH | 1995-08 | rj_jjt-19950823-ogh0002-009oba00142-9500000-000.md |
| rj-rjs-139 | OGH 9ObA180/95 vom 01.01.1996 | OGH | 1996-01 | rj_jjt-19960101-ogh0002-009oba00180-9500000-000.md |
| rj-rjs-140 | OGH 9ObA11/96 vom 31.01.1996 | OGH | 1996-01 | rj_jjt-19960131-ogh0002-009oba00011-9600000-000.md |
| rj-rjs-141 | OGH 8ObA211/96 vom 29.02.1996 | OGH | 1996-02 | rj_jjt-19960229-ogh0002-008oba00211-9600000-000.md |
| rj-rjs-142 | OGH 9ObA2050/96b vom 29.05.1996 | OGH | 1996-05 | rj_jjt-19960529-ogh0002-009oba02050-96b0000-000.md |
| rj-rjs-143 | OGH 8ObA2100/96y vom 13.06.1996 | OGH | 1996-06 | rj_jjt-19960613-ogh0002-008oba02100-96y0000-000.md |
| rj-rjs-144 | OGH 8ObA2302/96d vom 14.11.1996 | OGH | 1996-11 | rj_jjt-19961114-ogh0002-008oba02302-96d0000-000.md |
| rj-rjs-145 | OGH 9ObA2267/96i vom 04.12.1996 | OGH | 1996-12 | rj_jjt-19961204-ogh0002-009oba02267-96i0000-000.md |
| rj-rjs-146 | OGH 9ObA32/97i vom 09.04.1997 | OGH | 1997-04 | rj_jjt-19970409-ogh0002-009oba00032-97i0000-000.md |
| rj-rjs-147 | OGH 9ObA114/97y vom 09.04.1997 | OGH | 1997-04 | rj_jjt-19970409-ogh0002-009oba00114-97y0000-000.md |
| rj-rjs-148 | OGH 8ObA142/97h vom 23.05.1997 | OGH | 1997-05 | rj_jjt-19970523-ogh0002-008oba00142-97h0000-000.md |
| rj-rjs-149 | OGH 9ObA158/97v vom 09.07.1997 | OGH | 1997-07 | rj_jjt-19970709-ogh0002-009oba00158-97v0000-000.md |
| rj-rjs-150 | OGH 9ObA87/97b vom 27.08.1997 | OGH | 1997-08 | rj_jjt-19970827-ogh0002-009oba00087-97b0000-000.md |
| rj-rjs-151 | OGH 8ObA91/97h vom 28.08.1997 | OGH | 1997-08 | rj_jjt-19970828-ogh0002-008oba00091-97h0000-000.md |
| rj-rjs-152 | OGH 9ObA162/97g vom 22.10.1997 | OGH | 1997-10 | rj_jjt-19971022-ogh0002-009oba00162-97g0000-000.md |
| rj-rjs-153 | OGH 9ObA249/97a vom 05.11.1997 | OGH | 1997-11 | rj_jjt-19971105-ogh0002-009oba00249-97a0000-000.md |
| rj-rjs-154 | OGH 9ObA275/97z vom 10.12.1997 | OGH | 1997-12 | rj_jjt-19971210-ogh0002-009oba00275-97z0000-000.md |
| rj-rjs-155 | OGH 8ObA8/98d vom 26.02.1998 | OGH | 1998-02 | rj_jjt-19980226-ogh0002-008oba00008-98d0000-000.md |
| rj-rjs-156 | OGH 8ObA61/98y vom 12.03.1998 | OGH | 1998-03 | rj_jjt-19980312-ogh0002-008oba00061-98y0000-000.md |
| rj-rjs-157 | OGH 8ObA149/98i vom 25.06.1998 | OGH | 1998-06 | rj_jjt-19980625-ogh0002-008oba00149-98i0000-000.md |
| rj-rjs-158 | OGH 9ObA100/98s vom 02.09.1998 | OGH | 1998-09 | rj_jjt-19980902-ogh0002-009oba00100-98s0000-000.md |
| rj-rjs-159 | OGH 9ObA193/98t vom 07.10.1998 | OGH | 1998-10 | rj_jjt-19981007-ogh0002-009oba00193-98t0000-000.md |
| rj-rjs-160 | OGH 9ObA276/98y vom 11.11.1998 | OGH | 1998-11 | rj_jjt-19981111-ogh0002-009oba00276-98y0000-000.md |
| rj-rjs-161 | OGH 9ObA287/98s vom 09.12.1998 | OGH | 1998-12 | rj_jjt-19981209-ogh0002-009oba00287-98s0000-000.md |
| rj-rjs-162 | OGH 9ObA10/99g vom 05.05.1999 | OGH | 1999-05 | rj_jjt-19990505-ogh0002-009oba00010-99g0000-000.md |
| rj-rjs-163 | OGH 9ObA192/99x vom 01.09.1999 | OGH | 1999-09 | rj_jjt-19990901-ogh0002-009oba00192-99x0000-000.md |
| rj-rjs-164 | OGH 9ObA140/99z vom 29.09.1999 | OGH | 1999-09 | rj_jjt-19990929-ogh0002-009oba00140-99z0000-000.md |
| rj-rjs-165 | OGH 9ObA213/99k vom 17.11.1999 | OGH | 1999-11 | rj_jjt-19991117-ogh0002-009oba00213-99k0000-000.md |
| rj-rjs-166 | OGH 9ObA240/99f vom 01.12.1999 | OGH | 1999-12 | rj_jjt-19991201-ogh0002-009oba00240-99f0000-000.md |
| rj-rjs-167 | OGH 6Ob304/99w vom 20.01.2000 | OGH | 2000-01 | rj_jjt-20000120-ogh0002-0060ob00304-99w0000-000.md |
| rj-rjs-168 | OGH 9ObA329/99v vom 26.01.2000 | OGH | 2000-01 | rj_jjt-20000126-ogh0002-009oba00329-99v0000-000.md |
| rj-rjs-169 | OGH 8ObA12/00y vom 24.02.2000 | OGH | 2000-02 | rj_jjt-20000224-ogh0002-008oba00012-00y0000-000.md |
| rj-rjs-170 | OGH 9ObA292/99b vom 05.04.2000 | OGH | 2000-04 | rj_jjt-20000405-ogh0002-009oba00292-99b0000-000.md |
| rj-rjs-171 | OGH 8ObS86/00f vom 13.04.2000 | OGH | 2000-04 | rj_jjt-20000413-ogh0002-008obs00086-00f0000-000.md |
| rj-rjs-172 | OGH 9ObA198/00h vom 04.10.2000 | OGH | 2000-10 | rj_jjt-20001004-ogh0002-009oba00198-00h0000-000.md |
| rj-rjs-173 | OGH 8ObS204/00h vom 09.11.2000 | OGH | 2000-11 | rj_jjt-20001109-ogh0002-008obs00204-00h0000-000.md |
| rj-rjs-174 | OGH 9ObA319/00b vom 10.01.2001 | OGH | 2001-01 | rj_jjt-20010110-ogh0002-009oba00319-00b0000-000.md |
| rj-rjs-175 | OGH 9ObA332/00i vom 24.01.2001 | OGH | 2001-01 | rj_jjt-20010124-ogh0002-009oba00332-00i0000-000.md |
| rj-rjs-176 | OGH 9ObA341/00p vom 14.03.2001 | OGH | 2001-03 | rj_jjt-20010314-ogh0002-009oba00341-00p0000-000.md |
| rj-rjs-177 | OGH 9ObA144/01v vom 07.06.2001 | OGH | 2001-06 | rj_jjt-20010607-ogh0002-009oba00144-01v0000-000.md |
| rj-rjs-178 | OGH 9ObA155/01m vom 05.09.2001 | OGH | 2001-09 | rj_jjt-20010905-ogh0002-009oba00155-01m0000-000.md |
| rj-rjs-179 | OGH 8ObA215/01b vom 28.09.2001 | OGH | 2001-09 | rj_jjt-20010928-ogh0002-008oba00215-01b0000-000.md |
| rj-rjs-180 | OGH 9ObA226/01b vom 28.11.2001 | OGH | 2001-11 | rj_jjt-20011128-ogh0002-009oba00226-01b0000-000.md |
| rj-rjs-181 | OGH 9ObA280/01v vom 23.01.2002 | OGH | 2002-01 | rj_jjt-20020123-ogh0002-009oba00280-01v0000-000.md |
| rj-rjs-182 | OGH 8ObA100/02t vom 16.05.2002 | OGH | 2002-05 | rj_jjt-20020516-ogh0002-008oba00100-02t0000-000.md |
| rj-rjs-183 | OGH 9ObA120/02s vom 22.05.2002 | OGH | 2002-05 | rj_jjt-20020522-ogh0002-009oba00120-02s0000-000.md |
| rj-rjs-184 | OGH 9ObA97/02h vom 05.06.2002 | OGH | 2002-06 | rj_jjt-20020605-ogh0002-009oba00097-02h0000-000.md |
| rj-rjs-185 | OGH 9ObA115/02f vom 10.07.2002 | OGH | 2002-07 | rj_jjt-20020710-ogh0002-009oba00115-02f0000-000.md |
| rj-rjs-186 | OGH 9ObA196/02t vom 04.09.2002 | OGH | 2002-09 | rj_jjt-20020904-ogh0002-009oba00196-02t0000-000.md |
| rj-rjs-187 | OGH 8ObA150/02w vom 07.11.2002 | OGH | 2002-11 | rj_jjt-20021107-ogh0002-008oba00150-02w0000-000.md |
| rj-rjs-188 | OGH 9ObA169/02x vom 13.11.2002 | OGH | 2002-11 | rj_jjt-20021113-ogh0002-009oba00169-02x0000-000.md |
| rj-rjs-189 | OGH 9ObA233/02h vom 13.11.2002 | OGH | 2002-11 | rj_jjt-20021113-ogh0002-009oba00233-02h0000-000.md |
| rj-rjs-190 | OGH 8ObA202/02t vom 28.11.2002 | OGH | 2002-11 | rj_jjt-20021128-ogh0002-008oba00202-02t0000-000.md |
| rj-rjs-191 | OGH 9ObA230/02t vom 04.12.2002 | OGH | 2002-12 | rj_jjt-20021204-ogh0002-009oba00230-02t0000-000.md |
| rj-rjs-192 | OGH 8ObA13/03z vom 20.03.2003 | OGH | 2003-03 | rj_jjt-20030320-ogh0002-008oba00013-03z0000-000.md |
| rj-rjs-193 | OGH 8ObA21/03a vom 20.03.2003 | OGH | 2003-03 | rj_jjt-20030320-ogh0002-008oba00021-03a0000-000.md |
| rj-rjs-194 | OGH 8ObA95/03h vom 16.10.2003 | OGH | 2003-10 | rj_jjt-20031016-ogh0002-008oba00095-03h0000-000.md |
| rj-rjs-195 | OGH 9ObA119/03w vom 05.11.2003 | OGH | 2003-11 | rj_jjt-20031105-ogh0002-009oba00119-03w0000-000.md |
| rj-rjs-196 | OGH 8ObA86/03k vom 13.11.2003 | OGH | 2003-11 | rj_jjt-20031113-ogh0002-008oba00086-03k0000-000.md |
| rj-rjs-197 | OGH 8ObA44/03h vom 25.11.2003 | OGH | 2003-11 | rj_jjt-20031125-ogh0002-008oba00044-03h0000-000.md |
| rj-rjs-198 | OGH 9ObA151/03a vom 21.01.2004 | OGH | 2004-01 | rj_jjt-20040121-ogh0002-009oba00151-03a0000-000.md |
| rj-rjs-199 | OGH 9ObA143/03z vom 17.03.2004 | OGH | 2004-03 | rj_jjt-20040317-ogh0002-009oba00143-03z0000-000.md |
| rj-rjs-200 | OGH 9ObA38/04k vom 21.04.2004 | OGH | 2004-04 | rj_jjt-20040421-ogh0002-009oba00038-04k0000-000.md |
| rj-rjs-201 | OGH 9ObA52/04v vom 05.05.2004 | OGH | 2004-05 | rj_jjt-20040505-ogh0002-009oba00052-04v0000-000.md |
| rj-rjs-202 | OGH 8ObA20/04f vom 24.09.2004 | OGH | 2004-09 | rj_jjt-20040924-ogh0002-008oba00020-04f0000-000.md |
| rj-rjs-203 | OGH 8ObA20/04f vom 17.02.2005 | OGH | 2005-02 | rj_jjt-20050217-ogh0002-008oba00020-04f0000-000.md |
| rj-rjs-204 | OGH 9ObA96/05s vom 29.06.2005 | OGH | 2005-06 | rj_jjt-20050629-ogh0002-009oba00096-05s0000-000.md |
| rj-rjs-205 | OGH 9ObA108/05f vom 31.08.2005 | OGH | 2005-08 | rj_jjt-20050831-ogh0002-009oba00108-05f0000-000.md |
| rj-rjs-206 | OGH 8ObA51/05s vom 08.09.2005 | OGH | 2005-09 | rj_jjt-20050908-ogh0002-008oba00051-05s0000-000.md |
| rj-rjs-207 | OGH 9ObA42/05z vom 22.02.2006 | OGH | 2006-02 | rj_jjt-20060222-ogh0002-009oba00042-05z0000-000.md |
| rj-rjs-208 | OGH 8ObA60/06s vom 13.07.2006 | OGH | 2006-07 | rj_jjt-20060713-ogh0002-008oba00060-06s0000-000.md |
| rj-rjs-209 | OGH 9ObA110/06a vom 15.11.2006 | OGH | 2006-11 | rj_jjt-20061115-ogh0002-009oba00110-06a0000-000.md |
| rj-rjs-210 | OGH 9ObA20/07t vom 02.03.2007 | OGH | 2007-03 | rj_jjt-20070302-ogh0002-009oba00020-07t0000-000.md |
| rj-rjs-211 | OGH 9ObA165/07s vom 28.11.2007 | OGH | 2007-11 | rj_jjt-20071128-ogh0002-009oba00165-07s0000-000.md |
| rj-rjs-212 | OGH 9ObA118/07d vom 19.12.2007 | OGH | 2007-12 | rj_jjt-20071219-ogh0002-009oba00118-07d0000-000.md |
| rj-rjs-213 | OGH 8ObA39/08f vom 16.06.2008 | OGH | 2008-06 | rj_jjt-20080616-ogh0002-008oba00039-08f0000-000.md |
| rj-rjs-214 | OGH 8ObA55/07g vom 10.07.2008 | OGH | 2008-07 | rj_jjt-20080710-ogh0002-008oba00055-07g0000-000.md |
| rj-rjs-215 | OGH 8ObA48/08d vom 02.09.2008 | OGH | 2008-09 | rj_jjt-20080902-ogh0002-008oba00048-08d0000-000.md |
| rj-rjs-216 | OGH 9ObA37/08v vom 08.10.2008 | OGH | 2008-10 | rj_jjt-20081008-ogh0002-009oba00037-08v0000-000.md |
| rj-rjs-217 | OGH 8ObA50/08y vom 13.11.2008 | OGH | 2008-11 | rj_jjt-20081113-ogh0002-008oba00050-08y0000-000.md |
| rj-rjs-218 | OGH 9ObA37/08v vom 17.12.2008 | OGH | 2008-12 | rj_jjt-20081217-ogh0002-009oba00037-08v0000-000.md |
| rj-rjs-219 | OGH 9ObA87/08x vom 26.08.2009 | OGH | 2009-08 | rj_jjt-20090826-ogh0002-009oba00087-08x0000-000.md |
| rj-rjs-220 | OGH 8ObA57/09d vom 23.03.2010 | OGH | 2010-03 | rj_jjt-20100323-ogh0002-008oba00057-09d0000-000.md |
| rj-rjs-221 | OGH 9ObA113/10y vom 24.11.2010 | OGH | 2010-11 | rj_jjt-20101124-ogh0002-009oba00113-10y0000-000.md |
| rj-rjs-222 | OGH 9ObA128/10d vom 28.02.2011 | OGH | 2011-02 | rj_jjt-20110228-ogh0002-009oba00128-10d0000-000.md |
| rj-rjs-223 | OGH 8ObA49/10d vom 22.03.2011 | OGH | 2011-03 | rj_jjt-20110322-ogh0002-008oba00049-10d0000-000.md |
| rj-rjs-224 | OGH 8ObA35/11x vom 25.05.2011 | OGH | 2011-05 | rj_jjt-20110525-ogh0002-008oba00035-11x0000-000.md |
| rj-rjs-225 | OGH 8ObA13/11m vom 29.06.2011 | OGH | 2011-06 | rj_jjt-20110629-ogh0002-008oba00013-11m0000-000.md |
| rj-rjs-226 | OGH 8ObA45/11t vom 29.06.2011 | OGH | 2011-06 | rj_jjt-20110629-ogh0002-008oba00045-11t0000-000.md |
| rj-rjs-227 | OGH 9ObA75/11m vom 25.10.2011 | OGH | 2011-10 | rj_jjt-20111025-ogh0002-009oba00075-11m0000-000.md |
| rj-rjs-228 | OGH 8ObA35/11x vom 20.12.2011 | OGH | 2011-12 | rj_jjt-20111220-ogh0002-008oba00035-11x0000-000.md |
| rj-rjs-229 | OGH 9ObA118/11k vom 21.12.2011 | OGH | 2011-12 | rj_jjt-20111221-ogh0002-009oba00118-11k0000-000.md |
| rj-rjs-230 | OGH 9ObA119/12h vom 22.10.2012 | OGH | 2012-10 | rj_jjt-20121022-ogh0002-009oba00119-12h0000-000.md |
| rj-rjs-231 | OGH 9ObA16/12m vom 26.11.2012 | OGH | 2012-11 | rj_jjt-20121126-ogh0002-009oba00016-12m0000-000.md |
| rj-rjs-232 | OGH 8ObA74/12h vom 19.12.2012 | OGH | 2012-12 | rj_jjt-20121219-ogh0002-008oba00074-12h0000-000.md |
| rj-rjs-233 | OGH 9ObA25/13m vom 19.03.2013 | OGH | 2013-03 | rj_jjt-20130319-ogh0002-009oba00025-13m0000-000.md |
| rj-rjs-234 | OGH 9ObA41/13i vom 24.04.2013 | OGH | 2013-04 | rj_jjt-20130424-ogh0002-009oba00041-13i0000-000.md |
| rj-rjs-235 | OGH 9ObA53/13d vom 29.05.2013 | OGH | 2013-05 | rj_jjt-20130529-ogh0002-009oba00053-13d0000-000.md |
| rj-rjs-236 | OGH 9ObA68/13k vom 25.06.2013 | OGH | 2013-06 | rj_jjt-20130625-ogh0002-009oba00068-13k0000-000.md |
| rj-rjs-237 | OGH 9ObA2/14f vom 25.06.2014 | OGH | 2014-06 | rj_jjt-20140625-ogh0002-009oba00002-14f0000-000.md |
| rj-rjs-238 | OGH 8ObA37/14w vom 26.06.2014 | OGH | 2014-06 | rj_jjt-20140626-ogh0002-008oba00037-14w0000-000.md |
| rj-rjs-239 | OGH 5Ob113/14z vom 25.07.2014 | OGH | 2014-07 | rj_jjt-20140725-ogh0002-0050ob00113-14z0000-000.md |
| rj-rjs-240 | OGH 9ObA3/15d vom 20.03.2015 | OGH | 2015-03 | rj_jjt-20150320-ogh0002-009oba00003-15d0000-000.md |
| rj-rjs-241 | OGH 8ObA33/15h vom 28.04.2015 | OGH | 2015-04 | rj_jjt-20150428-ogh0002-008oba00033-15h0000-000.md |
| rj-rjs-242 | OGH 9ObA116/15x vom 26.11.2015 | OGH | 2015-11 | rj_jjt-20151126-ogh0002-009oba00116-15x0000-000.md |
| rj-rjs-243 | OGH 9ObA74/15w vom 27.01.2016 | OGH | 2016-01 | rj_jjt-20160127-ogh0002-009oba00074-15w0000-000.md |
| rj-rjs-244 | OGH 4Ob37/16v vom 30.03.2016 | OGH | 2016-03 | rj_jjt-20160330-ogh0002-0040ob00037-16v0000-000.md |
| rj-rjs-245 | OGH 9ObA40/16x vom 24.06.2016 | OGH | 2016-06 | rj_jjt-20160624-ogh0002-009oba00040-16x0000-000.md |
| rj-rjs-246 | OGH 9Ob8/17t vom 28.02.2017 | OGH | 2017-02 | rj_jjt-20170228-ogh0002-0090ob00008-17t0000-000.md |
| rj-rjs-247 | OGH 9ObA150/17z vom 27.02.2018 | OGH | 2018-02 | rj_jjt-20180227-ogh0002-009oba00150-17z0000-000.md |
| rj-rjs-248 | OGH 8ObA28/18b vom 25.06.2018 | OGH | 2018-06 | rj_jjt-20180625-ogh0002-008oba00028-18b0000-000.md |
| rj-rjs-249 | OGH 9ObA44/18p vom 28.06.2018 | OGH | 2018-06 | rj_jjt-20180628-ogh0002-009oba00044-18p0000-000.md |
| rj-rjs-250 | OGH 9ObA50/18w vom 28.06.2018 | OGH | 2018-06 | rj_jjt-20180628-ogh0002-009oba00050-18w0000-000.md |
| rj-rjs-251 | OGH 9ObA45/18k vom 30.08.2018 | OGH | 2018-08 | rj_jjt-20180830-ogh0002-009oba00045-18k0000-000.md |
| rj-rjs-252 | OGH 8ObA51/18k vom 24.09.2018 | OGH | 2018-09 | rj_jjt-20180924-ogh0002-008oba00051-18k0000-000.md |
| rj-rjs-253 | OGH 9ObA25/19w vom 15.05.2019 | OGH | 2019-05 | rj_jjt-20190515-ogh0002-009oba00025-19w0000-000.md |
| rj-rjs-254 | OGH 9ObA29/19h vom 15.05.2019 | OGH | 2019-05 | rj_jjt-20190515-ogh0002-009oba00029-19h0000-000.md |
| rj-rjs-255 | OGH 8ObA22/19x vom 27.06.2019 | OGH | 2019-06 | rj_jjt-20190627-ogh0002-008oba00022-19x0000-000.md |
| rj-rjs-256 | OGH 9ObA92/19y vom 30.10.2019 | OGH | 2019-10 | rj_jjt-20191030-ogh0002-009oba00092-19y0000-000.md |
| rj-rjs-257 | OGH 8ObA49/19t vom 18.11.2019 | OGH | 2019-11 | rj_jjt-20191118-ogh0002-008oba00049-19t0000-000.md |
| rj-rjs-258 | OGH 8ObA64/19y vom 24.01.2020 | OGH | 2020-01 | rj_jjt-20200124-ogh0002-008oba00064-19y0000-000.md |
| rj-rjs-259 | OGH 9ObA24/20z vom 29.04.2020 | OGH | 2020-04 | rj_jjt-20200429-ogh0002-009oba00024-20z0000-000.md |
| rj-rjs-260 | OGH 8ObA18/20k vom 27.05.2020 | OGH | 2020-05 | rj_jjt-20200527-ogh0002-008oba00018-20k0000-000.md |
| rj-rjs-261 | OGH 9ObA31/20d vom 25.11.2020 | OGH | 2020-11 | rj_jjt-20201125-ogh0002-009oba00031-20d0000-000.md |
| rj-rjs-262 | OGH 9ObA9/21w vom 29.04.2021 | OGH | 2021-04 | rj_jjt-20210429-ogh0002-009oba00009-21w0000-000.md |
| rj-rjs-263 | OGH 9ObA96/21i vom 17.02.2022 | OGH | 2022-02 | rj_jjt-20220217-ogh0002-009oba00096-21i0000-000.md |
| rj-rjs-264 | OGH 8ObA15/22x vom 22.02.2022 | OGH | 2022-02 | rj_jjt-20220222-ogh0002-008oba00015-22x0000-000.md |
| rj-rjs-265 | OGH 9ObA10/22v vom 24.03.2022 | OGH | 2022-03 | rj_jjt-20220324-ogh0002-009oba00010-22v0000-000.md |
| rj-rjs-266 | OGH 8ObA26/22i vom 25.05.2022 | OGH | 2022-05 | rj_jjt-20220525-ogh0002-008oba00026-22i0000-000.md |
| rj-rjs-267 | OGH 8ObS7/22w vom 29.06.2022 | OGH | 2022-06 | rj_jjt-20220629-ogh0002-008obs00007-22w0000-000.md |
| rj-rjs-268 | OGH 8ObA38/22d vom 30.08.2022 | OGH | 2022-08 | rj_jjt-20220830-ogh0002-008oba00038-22d0000-000.md |
| rj-rjs-269 | OGH 8ObA82/21y vom 30.08.2022 | OGH | 2022-08 | rj_jjt-20220830-ogh0002-008oba00082-21y0000-000.md |
| rj-rjs-270 | OGH 9ObA48/22g vom 31.08.2022 | OGH | 2022-08 | rj_jjt-20220831-ogh0002-009oba00048-22g0000-000.md |
| rj-rjs-271 | OGH 1Ob130/22g vom 14.09.2022 | OGH | 2022-09 | rj_jjt-20220914-ogh0002-0010ob00130-22g0000-000.md |
| rj-rjs-272 | OGH 9ObA105/22i vom 20.10.2022 | OGH | 2022-10 | rj_jjt-20221020-ogh0002-009oba00105-22i0000-000.md |
| rj-rjs-273 | OGH 8ObA84/22v vom 16.12.2022 | OGH | 2022-12 | rj_jjt-20221216-ogh0002-008oba00084-22v0000-000.md |
| rj-rjs-274 | OGH 8ObA36/23m vom 27.06.2023 | OGH | 2023-06 | rj_jjt-20230627-ogh0002-008oba00036-23m0000-000.md |
| rj-rjs-275 | OLG Innsbruck 13Ra21/23w vom 27.07.2023 | OLG Innsbruck | 2023-07 | rj_jjt-20230727-olg0819-0130ra00021-23w0000-001.md |
| rj-rjs-276 | OGH 9ObA67/23b vom 27.09.2023 | OGH | 2023-09 | rj_jjt-20230927-ogh0002-009oba00067-23b0000-000.md |
| rj-rjs-277 | OGH 8ObA70/23m vom 22.03.2024 | OGH | 2024-03 | rj_jjt-20240322-ogh0002-008oba00070-23m0000-000.md |
| rj-rjs-278 | OGH 8ObA54/24k vom 05.12.2024 | OGH | 2024-12 | rj_jjt-20241205-ogh0002-008oba00054-24k0000-000.md |
| rj-rjs-279 | OLG Wien 10Ra65/24p vom 22.01.2025 | OLG Wien | 2025-01 | rj_jjt-20250122-olg0009-0100ra00065-24p0000-000.md |
| rj-rjs-280 | OLG Wien 8Ra98/24a vom 29.01.2025 | OLG Wien | 2025-01 | rj_jjt-20250129-olg0009-0080ra00098-24a0000-000.md |
| rj-rjs-281 | OLG Graz 6Ra59/24d vom 13.02.2025 | OLG Graz | 2025-02 | rj_jjt-20250213-olg0639-0060ra00059-24d0000-000.md |
| rj-rjs-282 | OLG Wien 8Ra4/25d vom 26.02.2025 | OLG Wien | 2025-02 | rj_jjt-20250226-olg0009-0080ra00004-25d0000-001.md |
| rj-rjs-283 | OLG Linz 12Ra6/25a vom 17.03.2025 | OLG Linz | 2025-03 | rj_jjt-20250317-olg0459-0120ra00006-25a0000-000.md |
| rj-rjs-284 | OLG Wien 8Ra106/24b vom 09.07.2025 | OLG Wien | 2025-07 | rj_jjt-20250709-olg0009-0080ra00106-24b0000-000.md |
| rj-rjs-285 | OGH 9ObA83/24g vom 26.08.2025 | OGH | 2025-08 | rj_jjt-20250826-ogh0002-009oba00083-24g0000-000.md |
| rj-rjs-286 | OLG Wien 10Ra27/25a vom 24.09.2025 | OLG Wien | 2025-09 | rj_jjt-20250924-olg0009-0100ra00027-25a0000-000.md |
| rj-rjs-287 | OLG Graz 7Ra41/25s vom 24.09.2025 | OLG Graz | 2025-09 | rj_jjt-20250924-olg0639-0070ra00041-25s0000-000.md |
| rj-rjs-288 | Die Mitwirkung an Manipulationen an den Tachographenblättern eines Firmen-LKW stellt, auch wenn… | Verwaltungsgerichtshof | 1984-01 | rj_jwr-1982010008-19840130x01.md |
| rj-rjs-289 | Dem Arbeitsrecht ist eine allgemeine Verpflichtung des Arbeitnehmers fremd, für Verfehlungen eines… | Verwaltungsgerichtshof | 1984-01 | rj_jwr-1982010008-19840130x02.md |
| rj-rjs-290 | Verwaltungsgerichtshof 2004/08/0101 vom 24.01.2006 | Verwaltungsgerichtshof | 2006-01 | rj_jwt-2004080101-20060124x00.md |
| rj-rjs-291 | Verwaltungsgerichtshof 2004/08/0113 vom 16.11.2005 | Verwaltungsgerichtshof | 2005-11 | rj_jwt-2004080113-20051116x00.md |
| rj-rjs-292 | Verwaltungsgerichtshof 2007/08/0296 vom 02.04.2008 | Verwaltungsgerichtshof | 2008-04 | rj_jwt-2007080296-20080402x00.md |
| rj-rjs-293 | Verwaltungsgerichtshof 2012/08/0045 vom 28.03.2012 | Verwaltungsgerichtshof | 2012-03 | rj_jwt-2012080045-20120328x00.md |
| rj-rjs-294 | Verwaltungsgerichtshof 2012/08/0081 vom 24.04.2014 | Verwaltungsgerichtshof | 2014-04 | rj_jwt-2012080081-20140424x00.md |
| rj-rjs-295 | Verwaltungsgerichtshof Ro 2015/08/0020 vom 01.10.2015 | Verwaltungsgerichtshof | 2015-10 | rj_jwt-2015080020-20151001j00.md |
| rj-rjs-296 | Verwaltungsgerichtshof Fr 2016/08/0011 vom 14.11.2016 | Verwaltungsgerichtshof | 2016-11 | rj_jwt-2016080011-20161114f00.md |
| rj-rjs-297 | Verwaltungsgerichtshof Ra 2016/08/0011 vom 24.11.2016 | Verwaltungsgerichtshof | 2016-11 | rj_jwt-2016080011-20161124l00.md |
| rj-rjs-298 | Verwaltungsgerichtshof Ra 2016/08/0132 vom 14.07.2017 | Verwaltungsgerichtshof | 2017-07 | rj_jwt-2016080132-20170714l00.md |
| rj-rjs-299 | Verwaltungsgerichtshof Ra 2016/11/0053 vom 04.05.2016 | Verwaltungsgerichtshof | 2016-05 | rj_jwt-2016110053-20160504l00.md |
| rj-rjs-300 | Verwaltungsgerichtshof Ra 2016/11/0164 vom 28.02.2017 | Verwaltungsgerichtshof | 2017-02 | rj_jwt-2016110164-20170228l00.md |
| rj-rjs-301 | Verwaltungsgerichtshof Ra 2017/11/0003 vom 30.01.2017 | Verwaltungsgerichtshof | 2017-01 | rj_jwt-2017110003-20170130l00.md |
| rj-rjs-302 | Verwaltungsgerichtshof Ra 2017/11/0003 vom 05.04.2017 | Verwaltungsgerichtshof | 2017-04 | rj_jwt-2017110003-20170405l00.md |
| rj-rjs-303 | Verwaltungsgerichtshof Ro 2017/11/0003 vom 04.04.2019 | Verwaltungsgerichtshof | 2019-04 | rj_jwt-2017110003-20190404j00.md |
| rj-rjs-304 | Verwaltungsgerichtshof Ra 2017/11/0016 vom 20.09.2017 | Verwaltungsgerichtshof | 2017-09 | rj_jwt-2017110016-20170920l00.md |
| rj-rjs-305 | Verwaltungsgerichtshof Ro 2017/11/0016 vom 26.04.2018 | Verwaltungsgerichtshof | 2018-04 | rj_jwt-2017110016-20180426j00.md |
| rj-rjs-306 | Verwaltungsgerichtshof Ra 2017/11/0152 vom 11.01.2018 | Verwaltungsgerichtshof | 2018-01 | rj_jwt-2017110152-20180111l00.md |
| rj-rjs-307 | Verwaltungsgerichtshof Ra 2017/11/0219 vom 11.04.2018 | Verwaltungsgerichtshof | 2018-04 | rj_jwt-2017110219-20180411l00.md |
| rj-rjs-308 | Verwaltungsgerichtshof Ra 2017/11/0224 vom 21.08.2017 | Verwaltungsgerichtshof | 2017-08 | rj_jwt-2017110224-20170821l00.md |
| rj-rjs-309 | Verwaltungsgerichtshof Ra 2017/11/0224 vom 11.06.2018 | Verwaltungsgerichtshof | 2018-06 | rj_jwt-2017110224-20180611l00.md |
| rj-rjs-310 | Verwaltungsgerichtshof Ra 2017/11/0233 vom 20.09.2018 | Verwaltungsgerichtshof | 2018-09 | rj_jwt-2017110233-20180920l00.md |
| rj-rjs-311 | Verwaltungsgerichtshof Ra 2017/11/0276 vom 13.12.2018 | Verwaltungsgerichtshof | 2018-12 | rj_jwt-2017110276-20181213l00.md |
| rj-rjs-312 | Verwaltungsgerichtshof Ra 2017/11/0301 vom 13.12.2018 | Verwaltungsgerichtshof | 2018-12 | rj_jwt-2017110301-20181213l00.md |
| rj-rjs-313 | Verwaltungsgerichtshof Ra 2018/08/0028 vom 29.01.2020 | Verwaltungsgerichtshof | 2020-01 | rj_jwt-2018080028-20200129l00.md |
| rj-rjs-314 | Verwaltungsgerichtshof Ra 2018/11/0118 vom 20.09.2018 | Verwaltungsgerichtshof | 2018-09 | rj_jwt-2018110118-20180920l00.md |
| rj-rjs-315 | Verwaltungsgerichtshof Ra 2018/11/0216 vom 23.01.2019 | Verwaltungsgerichtshof | 2019-01 | rj_jwt-2018110216-20190123l00.md |
| rj-rjs-316 | Verwaltungsgerichtshof Ra 2018/11/0248 vom 18.01.2019 | Verwaltungsgerichtshof | 2019-01 | rj_jwt-2018110248-20190118l00.md |
| rj-rjs-317 | Verwaltungsgerichtshof Ra 2019/08/0003 vom 16.01.2019 | Verwaltungsgerichtshof | 2019-01 | rj_jwt-2019080003-20190116l00.md |
| rj-rjs-318 | Verwaltungsgerichtshof Fr 2019/08/0003 vom 03.04.2019 | Verwaltungsgerichtshof | 2019-04 | rj_jwt-2019080003-20190403f00.md |
| rj-rjs-319 | Verwaltungsgerichtshof Ro 2019/08/0003 vom 03.04.2019 | Verwaltungsgerichtshof | 2019-04 | rj_jwt-2019080003-20190403j00.md |
| rj-rjs-320 | Verwaltungsgerichtshof So 2019/08/0003 vom 03.02.2020 | Verwaltungsgerichtshof | 2020-02 | rj_jwt-2019080003-20200203x00.md |
| rj-rjs-321 | Verwaltungsgerichtshof Fr 2022/08/0006 vom 18.08.2022 | Verwaltungsgerichtshof | 2022-08 | rj_jwt-2022080006-20220818f00.md |
| rj-rjs-322 | Verwaltungsgerichtshof Ro 2022/08/0006 vom 13.06.2023 | Verwaltungsgerichtshof | 2023-06 | rj_jwt-2022080006-20230613j00.md |
| rj-rjs-323 | Verwaltungsgerichtshof Ra 2024/08/0034 vom 19.11.2024 | Verwaltungsgerichtshof | 2024-11 | rj_jwt-2024080034-20241119l00.md |
| rj-rjs-324 | Verwaltungsgerichtshof Ra 2024/08/0138 vom 20.12.2024 | Verwaltungsgerichtshof | 2024-12 | rj_jwt-2024080138-20241220l00.md |
| rj-rjs-325 | Verwaltungsgerichtshof Ra 2025/08/0048 vom 03.07.2026 | Verwaltungsgerichtshof | 2026-07 | rj_jwt-2025080048-20260703l00.md |
| rj-rjs-326 | Allgemeines bürgerliches Gesetzbuch § 1431 | EuGH | 2026-09 | rj_abgb-1431.md |
| rj-rjs-327 | Allgemeines bürgerliches Gesetzbuch § 1435 | EuGH | 2026-09 | rj_abgb-1435.md |
| rj-rjs-328 | Allgemeines bürgerliches Gesetzbuch (ABGB) § 1437 | ABGB | 2017-11 | rj_abgb-1437.md |
| rj-rjs-329 | Arbeitsvertragsrechts-Anpassungsgesetz § 7d | EuGH | 2026-09 | rj_avrag-7d.md |
| rj-rjs-330 | Arbeitsvertragsrechts-Anpassungsgesetz § 7f | EuGH | 2026-09 | rj_avrag-7f.md |
| rj-rjs-331 | Arbeitsvertragsrechts-Anpassungsgesetz § 7g | EuGH | 2026-09 | rj_avrag-7g.md |
| rj-rjs-332 | Arbeitsvertragsrechts-Anpassungsgesetz § 7i | EuGH | 2026-09 | rj_avrag-7i.md |
| rj-rjs-333 | Angestelltengesetz (AngG) Art. 1 § 15 | AngG | 2023-07 | rj_angg-15.md |
| rj-rjs-334 | Angestelltengesetz (AngG) Art. 1 § 26 | AngG | 2023-07 | rj_angg-26.md |
| rj-rjs-335 | Angestelltengesetz (AngG) Art. 1 § 27 | AngG | 2023-08 | rj_angg-27.md |
| rj-rjs-336 | Arbeitsverfassungsgesetz (ArbVG) § 101 | ArbVG | 2017-03 | rj_arbvg-101.md |
| rj-rjs-337 | Arbeitsverfassungsgesetz (ArbVG) § 105 | ArbVG | 2022-10 | rj_arbvg-105.md |
| rj-rjs-338 | Gewerbeordnung 1859 - Gewerbliches Hilfspersonal § 82 | EuGH | 2024-05 | rj_gewo1859-82.md |
| rj-rjs-339 | Gewerbeordnung 1859 - Gewerbliches Hilfspersonal § 82a | EuGH | 2026-09 | rj_gewo1859-82a.md |
| rj-rjs-340 | Gewerbeordnung 1994 (GewO 1994) § 130 | GewO 1994 | 2024-11 | rj_gewo1994-130.md |
| rj-rjs-341 | Lohn- und Sozialdumping-Bekämpfungsgesetz (LSD-BG) § 14 | LSD-BG | 2021-09 | rj_lsd-bg-14.md |
| rj-rjs-342 | Lohn- und Sozialdumping-Bekämpfungsgesetz (LSD-BG) § 21 | LSD-BG | 2024-04 | rj_lsd-bg-21.md |
| rj-rjs-343 | Lohn- und Sozialdumping-Bekämpfungsgesetz (LSD-BG) § 22 | LSD-BG | 2022-07 | rj_lsd-bg-22.md |
| rj-rjs-344 | Lohn- und Sozialdumping-Bekämpfungsgesetz (LSD-BG) § 28 | LSD-BG | 2026-07 | rj_lsd-bg-28.md |
| rj-rjs-345 | Strafgesetzbuch (StGB) § 115 | StGB | 2022-10 | rj_stgb-115.md |
| rj-rjs-346 | Strafgesetzbuch (StGB) § 153 | StGB | 2022-09 | rj_stgb-153.md |
| rj-rjs-347 | Strafgesetzbuch (StGB) § 83 | StGB | 2019-10 | rj_stgb-83.md |
| rj-rjs-348 | Urlaubsgesetz § 6 | EuGH | 2026-09 | rj_urlg-6.md |
+45 -14
View File
@@ -1,5 +1,5 @@
# Wissensbasis Personalverrechnung (Lexis360-Exporte, WIKU Personal,
WKO-Kollektivverträge, RIS-Gesetze)
WKO-Kollektivverträge, RIS-Gesetze, Rechtsprechung)
Kuratierte Wissensbasis aus vier Quellcorpora — den **LexisNexis-Briefings**
des Werks *Lexis Briefings Personalrecht* (Export aus Lexis 360), den
@@ -8,9 +8,12 @@ Casebooks, Fachzeitschrift „WIKU Personal aktuell“), den **Kollektivvertrags
Dokumenten von WKO.at** (Lohn-/Gehaltsordnungen inkl. Lohntabellen,
KV-Abschlüsse, Erläuterungen, Zusatz-KVs) und den **RIS-Gesetzes-Texten**
(Rechtsinformationssystem des Bundes; nur die im Lexis360-Bestand zitierten
Paragraphen je Gesetz). Ein **gemeinsamer Korpus**: das `work`-Feld
Paragraphen je Gesetz) sowie **Rechtsprechung** (OGH-, VwGH- und VfGH-
Entscheidungen und Rechtssätze aus RIS-OGD sowie fünf als nicht amtlich
markierte EuGH-Textwiedergaben). Ein **gemeinsamer Korpus**: das `work`-Feld
unterscheidet die Quellen, ID-Räume `lb-*` (Lexis), `wk-*` (WIKU),
`kv-*` (WKO-KV) und `ris-*` (RIS) liegen auf derselben Cluster-Map. Zweck:
`kv-*` (WKO-KV), `ris-*` (RIS) und `rj-*` (Rechtsprechung/Normentexte)
liegen auf derselben Cluster-Map. Zweck:
1. **Entwicklungsreferenz** neben den Rechtsquellen-Übersichten
(`RECHTSQUELLEN-*.md` im Schwesterprojekt) für die Personalverrechnungs-
@@ -26,22 +29,34 @@ unterscheidet die Quellen, ID-Räume `lb-*` (Lexis), `wk-*` (WIKU),
| PDF-Exporte | `.lexis360/Lexis360_*.pdf` · `.wiku/*.pdf` | **nein** (gitignored) | lizensierte Original-PDFs |
| Layer 1 — Volltexte | `.lexis360/md/<slug>.md` · `.wiku/md/<slug>.md` | **nein** (gitignored) | vollständiger Extraktionstext + Metadaten-Frontmatter |
| Katalog | `.lexis360/md/_catalog.json` · `.wiku/md/_catalog.json` | **nein** (gitignored) | geparste Metadaten aller Quellen (Slug ↔ ID ↔ Stand ↔ Cluster) |
| Rohquellen kv/ris | `.firecrawl/wko-kv/docs/*.html` · `.ris/*.md` | **nein** (gitignored) | WKO-KV-Seiten (HTML) bzw. RIS-Gesetzes-Auschnitte (Markdown) |
| Layer 2 — Einträge | `dokumente/<slug>.md` (Lexis) · `dokumente/wiku_<slug>.md` (WIKU) · `dokumente/kv_<slug>.md` (WKO-KV, **generiert**) · `dokumente/ris_<slug>.md` (RIS, **generiert**) | **ja** | Kuratierung in eigenen Worten (lb/wk) bzw. quellentreue Konvertierung (kv/ris, Tool-Output) |
| Rohquellen kv/ris/rj | `.firecrawl/kv-portal/wko-kv/docs/*.html` · `.ris/*.md` · `.rechtsprechung/{originale,gesetze}/*.md` | **nein** (gitignored) | WKO-KV-Seiten, RIS-Gesetzes-Auschnitte sowie Rechtsprechung/Normentexte |
| Layer 2 — Einträge | `dokumente/<slug>.md` (Lexis) · `dokumente/wiku_<slug>.md` (WIKU) · `dokumente/kv_<slug>.md` (WKO-KV, **generiert**) · `dokumente/ris_<slug>.md` (RIS, **generiert**) · `dokumente/rj_<slug>.md` (Rechtsprechung, **generiert**) | **ja** | Kuratierung in eigenen Worten (lb/wk) bzw. quellentreue Konvertierung (kv/ris/rj, Tool-Output) |
| Registry | `wissensbasis/kb.json` | **ja** | **generiert** aus dem Layer-2-Frontmatter aller Quellen (`tools/build_registry.py`) |
| Kataloge kv/ris | `tools/catalogs/kv_catalog.json` · `ris_catalog.json` | **ja** | eingefrorene ID-Zuweisungen (nur Metadaten, keine Quelltexte) |
| Kataloge kv/ris/rj | `tools/catalogs/kv_catalog.json` · `ris_catalog.json` · `rj_catalog.json` | **ja** | eingefrorene ID-Zuweisungen (nur Metadaten, keine Quelltexte) |
Volltexte der lizenzierten Quellen (Lexis/WIKU) sind Lizenzinhalt und
bleiben wie `.firecrawl/` und `.ris/` lokal + unversioniert. Nur die
bleiben wie `.firecrawl/` und `.firecrawl/ris/gesetze/` lokal + unversioniert. Nur die
Layer-2-Einträge werden committed. **Lizenz-Sonderfall der neuen Quellen
(Entscheidung 2026-09-15):** Gesetze sind amtliche Werke (§ 7 UrhG) und
WKO-KV-Texte müssen zahlenexakt bleiben (Lohntabellen) — daher sind
`kv_*.md`/`ris_*.md` **quellentreu generiert** (Konverter
`tools/ingest_sources.py`), NICHT in eigenen Worten kuratiert, und sie
sind **Tool-Output**: keine manuelle Bearbeitung, bei Änderungen neu
generieren. Die WKO-Seiten selbst und die `.ris/`-Dateien bleiben
generieren. Die WKO-Seiten selbst und die `.firecrawl/ris/gesetze/`-Dateien bleiben
unversioniert; die Einträge tragen ihre Quelle (`source.html`/`source.text`)
und die WKO-/RIS-URL im Body. Die `overviews/` (1 820 Bundesland-Übersichts-
und die WKO-/RIS-URL im Body.
**Rechtsprechung (Batch rj 1, 2026-09-15):** Die Lieferung unter
`.rechtsprechung/` dokumentiert für OGH/VwGH/VfGH überwiegend RIS-OGD-
Provenance. Ihre Layer-2-Dateien `rj_*.md` werden deshalb quellentreu aus den
lokalen Markdown-Dateien generiert und behalten Gericht, Datum, Geschäftszahl,
Normen und Quellenpfad. Rechtssätze, Volltexte, zitierte Normauszüge und EuGH-
Fälle sind jeweils im `chapter`/`work` unterscheidbar. Die fünf EuGH-Dateien
sind ausdrücklich als **„Textwiedergabe lexetius, nicht amtlich“** markiert
und nennen zusätzlich die amtliche EUR-Lex-URL, soweit geliefert. Die
Publikations- und Lizenzfreigabe der quellentreuen Rechtsprechungs-Volltexte
ist vor einem Commit bewusst zu bestätigen; sie wird nicht aus der allgemeinen
Ausnahme für amtliche Gesetzestexte abgeleitet. Die `overviews/` (1 820 Bundesland-Übersichts-
seiten von WKO.at) werden derzeit **nicht** aufgenommen (Redundanz zu `docs/`).
## Frontmatter-Schema (Layer 2 — verbindlich)
@@ -151,6 +166,7 @@ als ASCII-Slugs. Die Frontmatter ist **Single Source of Truth**;
| `lohnsteuer` | Lohnsteuer & Einkommensteuer (EStG) | `lst` | ris-lst-01 | 1 |
| `abgabenverfahren` | Abgabenverfahren (BAO) | `abo` | ris-abo-01 | 1 |
| `normen-sonstige` | Weitere Gesetze und Verordnungen (ArbIG, RStDG, StGB, StPO, UGB) | `nso` | ris-nso-01 … 05 | 5 |
| `rechtsprechung` | Rechtsprechung (OGH, VwGH, VfGH, EuGH) und zitierte Normen | `rjs` | rj-rjs-001 … 348 | 348 |
Weitere RIS-Gesetze liegen auf der bestehenden Cluster-Map (`ris-url-01`
neben `lb-url-*`, `ris-asc-01` neben `lb-asc-*`, …; je Gesetz ein
@@ -158,14 +174,16 @@ neben `lb-url-*`, `ris-asc-01` neben `lb-asc-*`, …; je Gesetz ein
Cluster fortlaufend — Zuordnungstabelle `LAW_MAP` in
`tools/kb_common.py`).
**1 274 Einträge in 76 Clustern** — Lexis: 571 Einträge in 67 Clustern
**1 622 Einträge in 77 Clustern** — Lexis: 571 Einträge in 67 Clustern
(Batch 1: 55, Batch 2: 52, Batch 3: 44, Batch 4: 54, Batch 5: 31,
Batch 6: 41, Batch 7: 97, Batch 8: 134, Batch 9: 63); WIKU: 30
Einträge in 18 Clustern (Batch 1: 13 Fachbroschüren, 3
Arbeitsunterlagen, 2 Casebooks, 12 Hefte „WIKU Personal aktuell“
2026); WKO-KV: 614 Einträge im Cluster `kollektivvertraege` (Batch 1);
RIS: 59 Gesetze, verteilt auf bestehende Cluster plus den neuen
Rechtsgebiet-Clustern (Batch 1).
Rechtsgebiet-Clustern (Batch 1); Rechtsprechung: 320 RIS-OGD-Entscheidungen/
Rechtssätze + 5 nichtamtliche EuGH-Textwiedergaben + 23 zitierte
RIS-Normauszüge (Batch 1, gemeinsamer Cluster `rechtsprechung`).
Frontmatter-`topic` = beschreibender ASCII-Slug, das ID-Präfix
(`atz`, `leh`, `kvt`, …) nur im `id` selbst; die Registry
@@ -177,7 +195,8 @@ neue Dokumente hängen hinten an, Nummern ausscheidender Dokumente
werden nie wiederverwendet. Dateinamen: Layer-2 = Layer-1-Slug
(Umlaute fallen auf Basisbuchstaben — `uberblick`, `beschaftigung`;
ß → `ss`), WIKU mit `wiku_`-Präfix, WKO-KV mit `kv_`-Präfix und RIS
mit `ris_`-Präfix (Kollisionsschutz im gemeinsamen `dokumente/`).
mit `ris_`-Präfix und Rechtsprechung mit `rj_`-Präfix (Kollisionsschutz im
gemeinsamen `dokumente/`).
## Kurationskonventionen (verbindlich)
@@ -229,7 +248,7 @@ mit `ris_`-Präfix (Kollisionsschutz im gemeinsamen `dokumente/`).
**WKO-Kollektivverträge (kv):**
1. Neue Seiten nach `.firecrawl/wko-kv/docs/` (Crawl via Firecrawl;
1. Neue Seiten nach `.firecrawl/kv-portal/wko-kv/docs/` (Crawl via Firecrawl;
eine Seite = ein KV-Dokument).
2. `python3 tools/ingest_sources.py --source kv` — konvertiert HTML →
Layer-2 (`kv_<slug>.md`), IDs aus `tools/catalogs/kv_catalog.json`
@@ -237,13 +256,25 @@ mit `ris_`-Präfix (Kollisionsschutz im gemeinsamen `dokumente/`).
**RIS-Gesetze (ris):**
1. Neue Gesetze nach `.ris/` (Markdown; nur die im Lexis360-Bestand
1. Neue Gesetze nach `.firecrawl/ris/gesetze/` (Markdown; nur die im Lexis360-Bestand
zitierten Paragraphen).
2. **Zuerst** `LAW_MAP` in `tools/kb_common.py` um das Gesetz ergänzen
(topic, chapter, laufende Nummer im Cluster) — die ID encodiert das
Cluster, danach nie ändern.
3. `python3 tools/ingest_sources.py --source ris`.
**Rechtsprechung (rj):**
1. Neue Markdown-Dateien nach `.rechtsprechung/originale/` (Entscheidungen,
Rechtssätze, EuGH-Textwiedergaben) oder `.rechtsprechung/gesetze/`
(zitierte Normauszüge) legen; XML-Originale nicht separat intaken.
2. Provenance und Publikations-/Lizenzfreigabe der gelieferten Volltexte vor
einer Versionierung prüfen; EuGH-Wiedergaben bleiben als nicht amtlich
gekennzeichnet.
3. `python3 tools/ingest_sources.py --source rj` — IDs aus
`tools/catalogs/rj_catalog.json` bleiben eingefroren; lange Volltexte
werden an Absatzgrenzen in H2-Chunks geteilt.
**Alle Quellen gemeinsam:**
3. `python3 tools/build_registry.py` — validiert das gesamte Layer-2
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2024-09
source:
html: ".firecrawl/wko-kv/docs/aenderung-anrechnung-vordienstzeiten-autobusbetriebe.html"
html: ".firecrawl/kv-portal/wko-kv/docs/aenderung-anrechnung-vordienstzeiten-autobusbetriebe.html"
legal_bases: []
tags: ["kollektivvertrag", "anderung"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2025-01
source:
html: ".firecrawl/wko-kv/docs/aenderung-feiertagsentlohnung-kollektivvertrag-autobusbetriebe.html"
html: ".firecrawl/kv-portal/wko-kv/docs/aenderung-feiertagsentlohnung-kollektivvertrag-autobusbetriebe.html"
legal_bases: []
tags: ["kollektivvertrag", "anderung"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-09
source:
html: ".firecrawl/wko-kv/docs/alle-kollektivvertr-ge-zusatzkollektivvertr-ge-lohn-und-ge.html"
html: ".firecrawl/kv-portal/wko-kv/docs/alle-kollektivvertr-ge-zusatzkollektivvertr-ge-lohn-und-ge.html"
legal_bases: []
tags: ["kollektivvertrag", "information", "stand-geschaetzt"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 1999-01
source:
html: ".firecrawl/wko-kv/docs/angestellte-konditoren-gewerbe-rahmenkollektivvertrag.html"
html: ".firecrawl/kv-portal/wko-kv/docs/angestellte-konditoren-gewerbe-rahmenkollektivvertrag.html"
legal_bases: []
tags: ["kollektivvertrag", "kollektivvertrag"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2021-10
source:
html: ".firecrawl/wko-kv/docs/angleichung-der-kuendigungsfristen-baunebengewerbe-2021.html"
html: ".firecrawl/kv-portal/wko-kv/docs/angleichung-der-kuendigungsfristen-baunebengewerbe-2021.html"
legal_bases: []
tags: ["kollektivvertrag", "information", "jahr-2021"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-05
source:
html: ".firecrawl/wko-kv/docs/anhang-kollektivvertrag-ambulatorien-physikalische-therapie-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/anhang-kollektivvertrag-ambulatorien-physikalische-therapie-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "anhang-anlage", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2020-06
source:
html: ".firecrawl/wko-kv/docs/anhang-kv-austrian-airlines-bordpersonal-2020.html"
html: ".firecrawl/kv-portal/wko-kv/docs/anhang-kv-austrian-airlines-bordpersonal-2020.html"
legal_bases: []
tags: ["kollektivvertrag", "anhang-anlage", "jahr-2020"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-01
source:
html: ".firecrawl/wko-kv/docs/anlage-textaktualisierung-kv-werbung-marktkommunikation-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/anlage-textaktualisierung-kv-werbung-marktkommunikation-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "anhang-anlage", "jahr-2026", "stand-jahr"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2021-01
source:
html: ".firecrawl/wko-kv/docs/berechnung-gehaltstabelle-immobilienverwalter-2021-2022.html"
html: ".firecrawl/kv-portal/wko-kv/docs/berechnung-gehaltstabelle-immobilienverwalter-2021-2022.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2021", "stand-jahr"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2023-01
source:
html: ".firecrawl/wko-kv/docs/berechnungsbeispiele-arbeitszeitverkuerzung-werbung.html"
html: ".firecrawl/kv-portal/wko-kv/docs/berechnungsbeispiele-arbeitszeitverkuerzung-werbung.html"
legal_bases: []
tags: ["kollektivvertrag", "berechnungsbeispiele", "stand-jahr"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-09
source:
html: ".firecrawl/wko-kv/docs/berechnungsbeispiele-kollektivvertrag-reisebueros.html"
html: ".firecrawl/kv-portal/wko-kv/docs/berechnungsbeispiele-kollektivvertrag-reisebueros.html"
legal_bases: []
tags: ["kollektivvertrag", "berechnungsbeispiele", "stand-geschaetzt"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-01
source:
html: ".firecrawl/wko-kv/docs/berechnungsbeispiele-kv-informationstechnologie-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/berechnungsbeispiele-kv-informationstechnologie-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "berechnungsbeispiele", "jahr-2026", "stand-jahr"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2023-04
source:
html: ".firecrawl/wko-kv/docs/brutto-netto-loehne-friseur-ab-01.04.2023.html"
html: ".firecrawl/kv-portal/wko-kv/docs/brutto-netto-loehne-friseur-ab-01.04.2023.html"
legal_bases: []
tags: ["kollektivvertrag", "sonstiges-kv-dokument", "jahr-2023"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2025-04
source:
html: ".firecrawl/wko-kv/docs/brutto-netto-loehne-friseur-ab-01.04.2025.html"
html: ".firecrawl/kv-portal/wko-kv/docs/brutto-netto-loehne-friseur-ab-01.04.2025.html"
legal_bases: []
tags: ["kollektivvertrag", "sonstiges-kv-dokument", "jahr-2025"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-04
source:
html: ".firecrawl/wko-kv/docs/brutto-netto-loehne-friseur-ab-01.04.2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/brutto-netto-loehne-friseur-ab-01.04.2026.html"
legal_bases: []
tags: ["kollektivvertrag", "sonstiges-kv-dokument", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-01
source:
html: ".firecrawl/wko-kv/docs/collective-bargaining-agreement-petroleum-industry-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/collective-bargaining-agreement-petroleum-industry-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "sonstiges-kv-dokument", "jahr-2026", "stand-jahr"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2025-12
source:
html: ".firecrawl/wko-kv/docs/dienst-und-besoldungsordnung-privatbahnen-2025.html"
html: ".firecrawl/kv-portal/wko-kv/docs/dienst-und-besoldungsordnung-privatbahnen-2025.html"
legal_bases: []
tags: ["kollektivvertrag", "dienst--und-besoldungsordnung", "jahr-2025"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2025-03
source:
html: ".firecrawl/wko-kv/docs/dienstreisesaetze-propak-2025.html"
html: ".firecrawl/kv-portal/wko-kv/docs/dienstreisesaetze-propak-2025.html"
legal_bases: []
tags: ["kollektivvertrag", "sonstiges-kv-dokument", "jahr-2025"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-01
source:
html: ".firecrawl/wko-kv/docs/empfehlung-lohntafeln-arbeiter-handel-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/empfehlung-lohntafeln-arbeiter-handel-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "empfehlung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2024-02
source:
html: ".firecrawl/wko-kv/docs/erklaerung-kollektivvertragspartner-autobusbetriebe-2024.html"
html: ".firecrawl/kv-portal/wko-kv/docs/erklaerung-kollektivvertragspartner-autobusbetriebe-2024.html"
legal_bases: []
tags: ["kollektivvertrag", "erklarung", "jahr-2024"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2024-01
source:
html: ".firecrawl/wko-kv/docs/erklaerungen-kollektivvertrag-filmschaffende-filmberufe-2024.html"
html: ".firecrawl/kv-portal/wko-kv/docs/erklaerungen-kollektivvertrag-filmschaffende-filmberufe-2024.html"
legal_bases: []
tags: ["kollektivvertrag", "erklarung", "jahr-2024", "stand-jahr"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-09
source:
html: ".firecrawl/wko-kv/docs/erlaeuterung-zu-den-gagenpositionen-im-kv-fuer-filmberufe.html"
html: ".firecrawl/kv-portal/wko-kv/docs/erlaeuterung-zu-den-gagenpositionen-im-kv-fuer-filmberufe.html"
legal_bases: []
tags: ["kollektivvertrag", "erlauterungen", "stand-geschaetzt"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2021-08
source:
html: ".firecrawl/wko-kv/docs/erlaeuterungen-generalkollektivvertrag-corona-test.html"
html: ".firecrawl/kv-portal/wko-kv/docs/erlaeuterungen-generalkollektivvertrag-corona-test.html"
legal_bases: []
tags: ["kollektivvertrag", "generalkollektivvertrag"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-09
source:
html: ".firecrawl/wko-kv/docs/erlaeuterungen-kollektivvertrag-werbung-marktkommunikation.html"
html: ".firecrawl/kv-portal/wko-kv/docs/erlaeuterungen-kollektivvertrag-werbung-marktkommunikation.html"
legal_bases: []
tags: ["kollektivvertrag", "erlauterungen", "stand-geschaetzt"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2021-10
source:
html: ".firecrawl/wko-kv/docs/erlaeuterungen-lohnordnung-fusspfleger-kosmetiker-masseure.html"
html: ".firecrawl/kv-portal/wko-kv/docs/erlaeuterungen-lohnordnung-fusspfleger-kosmetiker-masseure.html"
legal_bases: []
tags: ["kollektivvertrag", "erlauterungen"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2021-10
source:
html: ".firecrawl/wko-kv/docs/erlaeuterungen-zusatz-kv-arbeitskraefteueberlasser-2021.html"
html: ".firecrawl/kv-portal/wko-kv/docs/erlaeuterungen-zusatz-kv-arbeitskraefteueberlasser-2021.html"
legal_bases: []
tags: ["kollektivvertrag", "erlauterungen", "jahr-2021"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-09
source:
html: ".firecrawl/wko-kv/docs/flexible-arbeitszeit-denkmal-fassaden-gebaeudereinigung.html"
html: ".firecrawl/kv-portal/wko-kv/docs/flexible-arbeitszeit-denkmal-fassaden-gebaeudereinigung.html"
legal_bases: []
tags: ["kollektivvertrag", "sonstiges-kv-dokument", "stand-geschaetzt"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2021-02
source:
html: ".firecrawl/wko-kv/docs/floristen-blumeneinzelhaendler-kv-arbeiter-2021.html"
html: ".firecrawl/kv-portal/wko-kv/docs/floristen-blumeneinzelhaendler-kv-arbeiter-2021.html"
legal_bases: []
tags: ["kollektivvertrag", "kollektivvertrag", "jahr-2021"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-01
source:
html: ".firecrawl/wko-kv/docs/fragen-antworten-kollektivvertrag-informationstechnologie-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/fragen-antworten-kollektivvertrag-informationstechnologie-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "fragen-antworten", "jahr-2026", "stand-jahr"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2025-03
source:
html: ".firecrawl/wko-kv/docs/gaertner-landschaftsgaertner-kollektivvertrag-arbeiter-2025.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gaertner-landschaftsgaertner-kollektivvertrag-arbeiter-2025.html"
legal_bases: []
tags: ["kollektivvertrag", "kollektivvertrag", "jahr-2025"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2024-01
source:
html: ".firecrawl/wko-kv/docs/gehalts-rahmenbeschluss-gastronomie-und-hotellerie-2024-2025.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehalts-rahmenbeschluss-gastronomie-und-hotellerie-2024-2025.html"
legal_bases: []
tags: ["kollektivvertrag", "sonstiges-kv-dokument", "jahr-2024", "stand-jahr"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2024-10
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-baecker-angestellte-2024.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-baecker-angestellte-2024.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2024"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2025-10
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-baecker-angestellte-2025.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-baecker-angestellte-2025.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2025"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-10
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-baecker-angestellte-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-baecker-angestellte-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-04
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-banken-und-bankiers-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-banken-und-bankiers-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-05
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-baugewerbe-bauindustrie-angestellte-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-baugewerbe-bauindustrie-angestellte-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2025-07
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-bekleidungsindustrie-2025.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-bekleidungsindustrie-2025.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2025"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-07
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-bekleidungsindustrie-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-bekleidungsindustrie-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-07
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-bekleidungsindustrie-vorarlberg-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-bekleidungsindustrie-vorarlberg-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2025-11
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-bergwerke-stahl-angestellte-2025-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-bergwerke-stahl-angestellte-2025-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2025"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-05
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-chemische-industrie-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-chemische-industrie-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2025-05
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-elektro-elektronikindustrie-angestellte-2025.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-elektro-elektronikindustrie-angestellte-2025.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2025"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-05
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-elektro-elektronikindustrie-angestellte-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-elektro-elektronikindustrie-angestellte-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-04
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-fahrschulen-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-fahrschulen-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2025-01
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-film-musikwirtschaft-angestellte-2025.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-film-musikwirtschaft-angestellte-2025.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2025"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-01
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-film-musikwirtschaft-angestellte-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-film-musikwirtschaft-angestellte-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-07
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-fleischer-angestellte-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-fleischer-angestellte-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-01
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-gewerbe-handwerk-dienstleistung-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-gewerbe-handwerk-dienstleistung-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2025-11
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-gewerbliche-molkereien-kaesereien-2025.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-gewerbliche-molkereien-kaesereien-2025.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2025"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-06
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-glasindustrie-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-glasindustrie-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-01
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-gueterbefoerderung-kleintransporte-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-gueterbefoerderung-kleintransporte-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-06
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-holzverarbeitende-industrie-angestellte-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-holzverarbeitende-industrie-angestellte-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2025-01
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-immobilienverwalter-2025.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-immobilienverwalter-2025.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2025"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-04
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-immobilienverwalter-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-immobilienverwalter-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-01
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-information-consulting-angestellte-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-information-consulting-angestellte-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-04
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-kinobetriebe-steiermark-angestellte-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-kinobetriebe-steiermark-angestellte-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-04
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-kinobetriebe-wien-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-kinobetriebe-wien-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-01
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-konditoren-angestellte-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-konditoren-angestellte-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2025-05
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-kunststoffverarbeiter-2025.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-kunststoffverarbeiter-2025.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2025"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-05
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-kunststoffverarbeiter-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-kunststoffverarbeiter-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-04
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-landes-hypothekenbanken-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-landes-hypothekenbanken-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-01
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-ledererzeugende-industrie-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-ledererzeugende-industrie-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-06
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-lederwaren-kofferindustrie-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-lederwaren-kofferindustrie-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-01
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-metallgewerbe-angestellte-2026-27.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-metallgewerbe-angestellte-2026-27.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2025-11
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-metalltechnische-industrie-angestellte-2025-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-metalltechnische-industrie-angestellte-2025-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2025"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2024-11
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-molkereien-kaesereien-2024.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-molkereien-kaesereien-2024.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2024"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2025-08
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-mueller-muehlen-mischfuttererzeuger-2025.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-mueller-muehlen-mischfuttererzeuger-2025.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2025"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-08
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-mueller-muehlen-mischfuttererzeuger-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-mueller-muehlen-mischfuttererzeuger-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2025-05
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-musiker-konzertlokal-musik-tanzbetriebe-2025.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-musiker-konzertlokal-musik-tanzbetriebe-2025.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2025"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-01
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-nahrungs-und-genussmittelwerbe-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-nahrungs-und-genussmittelwerbe-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2025-11
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-ne-metallindustrie-angestellte-2025-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-ne-metallindustrie-angestellte-2025-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2025"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-03
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-papier-karton-verarbeitende-industrie-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-papier-karton-verarbeitende-industrie-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-05
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-papierindustrie-angestellte-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-papierindustrie-angestellte-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2022-01
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-pharmazeutischer-grosshandel-2022.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-pharmazeutischer-grosshandel-2022.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2022"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2025-01
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-privatfernsehsender-angestellte-2025.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-privatfernsehsender-angestellte-2025.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2025"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-01
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-reisebueros-angestellte-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-reisebueros-angestellte-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2025-05
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-saegeindustrie-angestellte-2025.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-saegeindustrie-angestellte-2025.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2025"]
cross_refs: []
@@ -8,7 +8,7 @@ topic: kollektivvertraege
author: "WKO"
stand: 2026-06
source:
html: ".firecrawl/wko-kv/docs/gehaltsordnung-schuhindustrie-angestellte-2026.html"
html: ".firecrawl/kv-portal/wko-kv/docs/gehaltsordnung-schuhindustrie-angestellte-2026.html"
legal_bases: []
tags: ["kollektivvertrag", "gehaltsordnung", "jahr-2026"]
cross_refs: []

Some files were not shown because too many files have changed in this diff Show More