commit 8ee6a294a1e43791ddfa55689dc8a0d0f0369ad7 Author: flemming-it Date: Thu Jun 18 11:26:28 2026 +0200 feat: initial F∆I Law-Heatmap Phase 0 scaffold First end-to-end-buildable cut of the pflicht-/graph-basierte Wirkungsanalyse von Rechtsnormen, running on Ch∆In. Demo runs without external dependencies and without a hub connection so the methodology + UI stack can be reviewed before any Verbands- Kooperation or Beirats-Validierung kicks in. Tree layout MACHBARKEITSSTUDIE.md v0.3 — pflicht-basierte Methodik, 4-Tier-Evidenz, 8 law*-Module auf Ch∆In statt der alten F∆I- Plattform. RUN.md one-page how-to-run. flows/durchstich-gewo-14.yaml vertical Phase-0 flow: pull → normalize → duties → citations → cohort → skm → frust → benefit → attribution → system.approval. app/ Flutter macOS client, design language pulled from fai_web (ink #08090A, paper #F4F3EF, petrol signal #2E8F9E). Mock repository ships five fixture norms with real source URLs and a permanent T4 demo-banner so no figure can be mistaken for a validated one. Module repos (separate, see chain-modules*/): text-akoma-normalize, text-deontic-extract, graph-citation- extract, text-readability-score, stats-cohort-size, graph- shapley-attribution, econ-skm-score, law-benefit-score. What runs today - flutter analyze: 0 issues - flutter test: 2/2 (landing + nav-to-shell smoke) - flutter build macos (via app/build-macos.sh) and ad-hoc codesign, app launches and the Dart VM service comes up - native cargo test green on every module - cargo build --release --target wasm32-wasip2 produces a 130 KiB artefact for text-akoma-normalize What is deliberately mock / stub - gRPC wire from the client to a chain serve hub (Repository abstraction is in place; live impl is the next step) - NKR Bürokratiekosten-Datenbank ingestion (for the canonical h-Werte that close out the Engpass per Studie §6.7) - DESTATIS GENESIS-API adapter for stats.cohort_size License: Apache-2.0. Author/contact in MACHBARKEITSSTUDIE.md. Signed-off-by: flemming-it diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5caee98 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# OS +.DS_Store +Thumbs.db + +# IDE +.idea/ +.vscode/ +*.iml +*.swp +*.swo + +# Local-only scratch +/scratch/ +/.local/ diff --git a/MACHBARKEITSSTUDIE.md b/MACHBARKEITSSTUDIE.md new file mode 100644 index 0000000..b63528d --- /dev/null +++ b/MACHBARKEITSSTUDIE.md @@ -0,0 +1,979 @@ +# F∆I Law-Heatmap — Machbarkeitsstudie & Umsetzungsplan + +> Arbeitstitel des Systems: **„Gesetzes-Heatmap" / „Regulierungs-Wirkungsanalyse auf Ch∆In-Basis"** +> Auftraggeber-Kontext: MIT AG Digitalisierung & KI-Strategien (Vorsitz Dr. S. Flemming), +> Zielabnehmer der Vorschläge: MIT AG Entbürokratisierung +> Frontend-Ziel: Integration in cdu-copilot (Flutter, ggf. via Ch∆In Studio), eigenständige Web-Sicht zusätzlich +> Plattformbasis: **Ch∆In** (vormals „F∆I-Plattform" / `fai_platform`) — Hub/Module/Flow, WASM-Sandbox, tamper-evidentes Audit-Log; Vendor weiterhin **F∆I = Flemming.AI** +> Status: Entwurf v0.3 — Diskussionsgrundlage AG-intern, noch nicht öffentlich (Plattform-Rename eingearbeitet 2026-06-17) + +--- + +## 1 Executive Summary (1 Seite) + +Wir wollen eine **methodisch belastbare, quellenverifizierbare Software** bauen, die für +einen abgegrenzten Rechtskorpus (Pilot: **Mittelstand-relevante Gesetze in Berlin und +Bund**) folgendes leistet: + +1. **Aktuell-Halten** des Normbestands (Gesetze, Verordnungen, einschlägige + Verwaltungsvorschriften, EU-Vorgaben) mit klarem „Stand X"-Stempel. +2. **Strukturieren & Verknüpfen** des Korpus (Normgraph: Welche Norm verweist auf + welche? Welche EU-RL liegt einer DE-Umsetzung zugrunde? Welche Berliner Norm setzt + Bundesrecht aus?). +3. **Bewerten** auf zwei orthogonalen Achsen: + - **Schadens-Index** (Bürokratiekosten in €, Zeitaufwand in Stunden, qualitativer + Frust-Indikator, Komplexitäts-Score) — **methodisch angelehnt an das + Standardkostenmodell (SKM)** des Nationalen Normenkontrollrats. + - **Nutzen-Index** (Schutzwirkung, Marktordnung, EU-Harmonisierung, + Rechtssicherheit) — qualitativ über strukturiertes Scoring. +4. **Gewichten nach Betroffenheit** (Adressatenzahl × Frequenz der Pflicht × + Einschlägigkeit für KMU). +5. **Heatmap + Reform-Vorschlagsliste** ausspielen — sowohl als API für + cdu-copilot als auch als PDF/DOCX-Export für AG-Sitzungen. + +**Was es ausdrücklich nicht ist:** +- Kein Ersatz für juristische Beratung +- Keine Rechtsentscheidung, keine automatisierte Verwaltungsentscheidung + → **kein „Hochrisiko-KI-System" i.S.d. EU AI Act**, sondern ein + Recherche-, Visualisierungs- und Vorschlagswerkzeug mit + Transparenz-Pflichten nach Art. 50 +- Keine politische Bewertung „aus dem Bauch" — jede Punktzahl ist mit Quelle + hinterlegt + +**Warum Ch∆In als Basis trägt:** Deterministische YAML-Flows, WASM-Sandbox, +tamper-evidentes Audit-Log (Hash-Kette `CHAIN-EVENT-V3`) und der +`system.approval@^0`-Schritt für Human-in-the-Loop sind genau die +Eigenschaften, die wir für eine nachprüfbare Bewertung von Rechtsnormen +brauchen. Stand v0.16.0: discover → install → compose → run → audit +läuft end-to-end, CRA-ready (CycloneDX-SBOM + sigstore ab Tag 1), +Apache 2.0. Die Plattform liefert „Nachvollziehbarkeit by construction". + +--- + +## 2 Vision & Scope (was bauen wir — und was bewusst nicht) + +### 2.1 Pilot-Korpus (Phase 1) + +| Kategorie | Beispiele | Quelle | +|---|---|---| +| Bundesgesetze (KMU-einschlägig) | GewO, HGB-Teile, ArbZG, MiLoG, GeschGehG, BImSchG-Teile | gesetze-im-internet.de | +| Bundesverordnungen | KassenSichV, GoBD-relevante VO, DSGVO-Durchführungs-VO | gesetze-im-internet.de | +| EU-Rechtsgrundlagen | DSGVO, CSRD, NIS2, CRA, EU AI Act, EnergyEfficiencyDirective | EUR-Lex | +| Berliner Landesrecht | BerlVwVfG, Berliner BauO, GewStG-Berlin-Anwendung, BetrKV-Berlin | landesrecht-berlin.de | +| Bezirkssatzungen | exemplarisch 1–2 Berliner Bezirke (z.B. Mitte, Charlottenburg-Wlmd.) | Bezirksamt-Veröffentlichungen | + +**Bewusst draußen (Phase 1):** Tarifrecht, Sozialversicherungs-Detailrecht, +spezifisches Steuerrecht jenseits typischer KMU-Berührungspunkte, +Verwaltungsvorschriften ohne Drittwirkung (Innenrecht). + +### 2.2 Was das System NICHT tut + +- Keine Erzeugung „neuer" Rechtsnormen +- Keine Vertretung gegenüber Behörden (kein Legal-Tech-Mandant) +- Keine personenbezogene Datenanalyse (Crowd-Beschwerden nur pseudonym) +- Keine Echtzeit-Auskunft an Bürger („wie betrifft mich §X?") — Phase 1 + bewusst nur Korpus-Analyse, kein Bürger-Service +- Keine automatisierten Veröffentlichungen — jeder externe Output durchläuft + einen `system.approval@^0`-Schritt durch einen Volljuristen im Team + +--- + +## 3 Methodische Grundlagen (das, woran sich das Projekt politisch beweisen muss) + +**Wichtigster Satz dieses Dokuments:** Wenn die Methodik nicht +sauber, anerkannt und peer-reviewed ist, ist jedes Ergebnis politisch +angreifbar und wertlos. Code zweitrangig. + +### 3.1 Etablierte Verfahren, auf denen wir aufbauen + +1. **Standardkostenmodell (SKM)** der Bundesregierung, betreut vom + Nationalen Normenkontrollrat: + `Bürokratiekosten = Fallzahl (P) × Frequenz (F) × Tarif (T) × Zeitaufwand (h)` +2. **Bürokratiekostenindex (BKI)** des Statistischen Bundesamts als Referenz +3. **OECD Better Regulation Toolkit** (Regulatory Impact Assessment, REFIT) +4. **EU Better Regulation Guidelines + Toolbox** (relevant, da viel DE-Recht + EU-determiniert ist) +5. **"One in, one out"-Regel** als politischer Maßstab für + Vorschlagsformulierung + +### 3.2 Erweiterungen, die wir wissenschaftlich begründen müssen + +- **Frust-Indikator**: nicht durch LLM „erfühlt", sondern operationalisiert + als Komposit aus: + - Lesbarkeit (Flesch-Reading-Ease, deutsch-adaptiert) + - Verweistiefe (wie viele Normen muss man lesen, um §X zu verstehen?) + - Behörden-Pluralität (wie viele Stellen sind im Vollzug involviert?) + - Frequenz der Änderungen (Norm-Volatilität) +- **Nutzen-Score**: 0–5 in den Dimensionen + Sicherheit/Gesundheit, Umweltschutz, Verbraucherschutz, Marktordnung, + Rechtssicherheit, EU-Binnenmarkt, Einnahmen — jede Dimension mit + expliziter Quellen-Begründung +- **Betroffenheits-Gewichtung**: Daten aus IHK, Handwerkskammer Berlin, + DESTATIS Unternehmensregister; nicht aus dem LLM, sondern aus + öffentlichen Statistik-Datensätzen + +### 3.3 Was wir NICHT machen (methodische Selbstbeschränkung) + +- **Keine pauschalen Verdikte** „Gesetz X ist schädlich" +- **Kein Single-Score-Ranking** als alleinige Output-Form — immer + zweidimensional (Nutzen × Schaden) + Streuung +- **Keine LLM-Endbewertung ohne menschlichen Review** — der LLM + schlägt vor, ein Volljurist + Ökonom validieren +- **Keine ungestützten Schadensschätzungen** — jede €/h-Zahl muss + entweder aus offiziellen Quellen (Normenkontrollrat, DESTATIS, + Branchenverbände) oder aus methodisch transparenten SKM-Rechnungen + stammen, mit Unsicherheitsangabe + +--- + +## 4 Datenquellen & Aktualität + +### 4.1 Primärquellen + +| Quelle | URL | Format | Lizenz / §5 UrhG | Update-Frequenz | +|---|---|---|---|---| +| gesetze-im-internet.de | BMJ | XML (teilweise), HTML | amtliches Werk (frei), juris-Datenbestand prüfen | täglich/Bedarf | +| Bundesgesetzblatt | bgbl.de | PDF (XML in Migration) | frei | wöchentlich | +| EUR-Lex | eur-lex.europa.eu | FORMEX-XML, ELI-URIs | frei | täglich | +| landesrecht-berlin.de | Berlin | HTML/PDF | frei | sporadisch | +| Amtsblatt Berlin | Senat | PDF | frei | wöchentlich | +| DIP (Bundestag) | bundestag.de/dip | XML/Atom | frei | täglich (Plenartage) | +| DESTATIS GENESIS-Online | destatis.de | CSV/JSON-API | frei | je nach Tabelle | + +### 4.2 Mechanik der Aktualität + +- **ELI-/CELEX-Identifier als kanonische ID** (nicht die URL!) +- **„Stand"-Stempel auf jede Norm-Version**, signiert mit dem Hash der + Quelldatei → landet im Ch∆In-Audit-Log +- **Diff-Erkennung** bei Re-Pull (Modul `law.fetch` mit Caching + ETag) +- **Manuelle Validierung** vor Bewertung: Wenn Norm seit Bewertung + geändert, automatisches Re-Triggern des Bewertungs-Flows +- **„Veraltete Bewertung"-Banner** im Frontend, wenn die Bewertung + älter als die zugrunde liegende Fassung ist + +### 4.3 Was wir NICHT als Primärquelle nehmen + +- Wikipedia, Anwaltsblogs, Kommentare: ggf. als sekundäre Hinweise auf + Streitfragen, **nie** als Bewertungsgrundlage +- Geschlossene juris-/beck-online-Bestände: nur, wenn lizenziert und + zitierbar — sonst raus +- Generierter LLM-Output: niemals als Quelle, immer nur als Vorschlag + +--- + +## 5 Systemarchitektur auf Ch∆In + +### 5.1 Mapping auf Hub / Module / Flow + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ Ch∆In HUB (Rust, gRPC) │ +│ Audit-Log (SQLite, tamper-evident) · Approval-Queue · Store │ +└──────────────────────────────────────────────────────────────────┘ + │ │ + ▼ ▼ +┌──────────────────────────────┐ ┌──────────────────────────┐ +│ Neue Module (8) │ │ Bestandsmodule (Ch∆In) │ +│ nach Reuse-Lens sortiert, │ │ ab v0.16.0 verfügbar │ +│ ABI chain:platform │ │ │ +├──────────────────────────────┼─────────┼──────────────────────────┤ +│ chain-modules/ (generisch): │ │ http.request │ +│ text.akoma_normalize │ │ text.extract (PDF/DOCX) │ +│ text.deontic_extract │ │ text.anonymize (DSGVO) │ +│ graph.citation_extract │ │ text.translate (DE↔EN) │ +│ text.readability_score │ │ text.summarize │ +│ stats.cohort_size │ │ llm.chat (Ollama-kompat.)│ +│ graph.shapley_attribution │ │ system.approval@^0 │ +│ │ │ debug.echo (Referenz) │ +│ chain-modules-econ/: │ │ │ +│ econ.skm_score │ │ │ +│ │ │ │ +│ chain-modules-law/ (domain): │ │ │ +│ law.benefit_score │ │ │ +└──────────────────────────────┘ └──────────────────────────┘ + + Heatmap rendert der **Client** (Flutter, CustomPainter) — kein + serverseitiges viz-Modul. Server liefert JSON, Client pixelt. + PDF/DOCX-Export (Phase 2+) via text.summarize + Client-Screenshot. +``` + +**Begründung — Reuse-Lens:** Module-Naming reflektiert Wiederverwend­ +barkeit. Was kein juristisches Domänen-Wissen braucht, geht in +`chain-modules/` (generisches Org) und ist damit auch in anderen +F∆I-Projekten und für externe Nutzer wertvoll: `text.akoma_normalize` +ist relevant für jeden, der Rechtsdokumente parst (EU-Parlament, +Italien, Brasilien); `text.deontic_extract` für Verträge, ISO-Normen, +Policies; `graph.citation_extract` für wiss. Paper, Patente, Spezi­ +fikationen. Damit ist `chain-modules-law/` am Ende sehr klein — +nur `law.benefit_score`, weil die 7 Bewertungs-Dimensionen +(Schutz/Markt/Rechtssicherheit/EU-Binnenmarkt/…) genuin juristisch- +politisch konventioniert sind. **`text.akoma_normalize` ist der +Pflicht-Eingang**: heterogene Quellen (gesetze-im-internet XML, +landesrecht-berlin HTML, EUR-Lex FORMEX) werden in Akoma-Ntoso-XML +normalisiert — ab da nur noch ein Datenformat. Alles Generische +(HTTP-Pull, PDF→Text, Anonymisierung, LLM-Call, Übersetzung, +Approval, Audit) ist bereits in Ch∆In v0.16.0 vorhanden und wird +nicht dupliziert. Module Apache 2.0, cosign-signiert. + +### 5.2 Beispiel-Flow (YAML, vereinfacht) + +```yaml +# flows/bewerten-eine-norm.yaml +inputs: + norm_eli: { type: string, required: true } + bezugsraum: { type: string, default: "berlin-kmu" } + +steps: + - id: pull + use: http.request@^0 + with: + url: ${{ inputs.norm_eli | eli_to_url }} + method: GET + etag_cache: true + - id: normalize + use: text.akoma_normalize@^0 # XML | HTML | FORMEX → Akoma Ntoso + with: { content: ${{ pull.body }}, mime: ${{ pull.content_type }} } + - id: graph + use: graph.citation_extract@^0 + with: { norm: ${{ normalize.norm }} } + - id: duties + use: text.deontic_extract@^0 + with: { norm: ${{ normalize.norm }} } + - id: skm + use: econ.skm_score@^0 + with: + duties: ${{ duties.list }} + population_table: "destatis://unternehmensregister/berlin-kmu" + - id: nutzen + use: law.benefit_score@^0 + with: { norm: ${{ normalize.norm }}, duties: ${{ duties.list }} } + - id: frust + use: text.readability_score@^0 + with: { norm: ${{ normalize.norm }}, graph: ${{ graph.edges }} } + - id: review + use: system.approval@^0 + with: + title: "Juristen-Review zur SKM/Nutzen-Bewertung" + payload: + skm: ${{ skm.report }} + nutzen: ${{ nutzen.report }} + frust: ${{ frust.report }} + - id: vorschlag + use: law.reform_proposal@^0 + if: ${{ review.decision == 'approve' }} + with: { full_assessment: ${{ review.payload }} } + +outputs: + assessment: ${{ review.payload }} + proposal: ${{ vorschlag.markdown }} +``` + +**Wichtig:** Der `review`-Schritt ist **nicht optional**. Kein Output +verlässt das System ohne menschliche Freigabe. Damit erfüllen wir +unsere methodische Selbstverpflichtung aus §3.3. + +### 5.3 Datenmodell (Skizze) + +- `norm`: ELI-ID, Titel, Fassung-vom, Quelldatei-SHA-256, Volltext-Hash +- `norm_version`: Diff zu Vorversion, Inkrafttreten, Außerkrafttreten +- `bezug`: (norm_a, norm_b, art) — art ∈ {verweist_auf, ändert, + ersetzt, beruht_auf_eu_richtlinie, …} +- `pflicht`: (norm, paragraf, adressat, frequenz, geschätzter_aufwand_h, + geschätzte_kosten_eur, quelle_der_schätzung) +- `bewertung`: (norm, skm_score, nutzen_score, frust_score, stand, + prüfer_juristisch, prüfer_oekonomisch, audit_event_id) +- `vorschlag`: (norm, art ∈ {streichen, befristen, vereinfachen, + konsolidieren}, begründung, beweis-quellen[]) + +### 5.4 Frontend-Integration cdu-copilot + +- **Read-Only-API** via Ch∆In gRPC-Web (existiert bereits) +- **Endpunkte** (vereinfacht): + - `GET /norm/{eli}/assessment` → aktuelle Bewertung mit Quellen + - `GET /heatmap?bezugsraum=berlin-kmu` → Heatmap-Daten (JSON) + - `GET /vorschlaege?status=published` → freigegebene Reform-Vorschläge +- **Flutter-Widgets**: Heatmap-Grid (Nutzen × Schaden, Größe = Betroffenheit), + Norm-Detailansicht mit Quellen-Sidebar, Vorschlag-Karten +- **Auth**: cdu-copilot fragt mit Service-Token; pro Mitglied + authentifizierter Zugriff erst in Phase 2 + +--- + +## 6 Bewertungs-System — Graph-basiert, auf der Ebene von *Pflichten* + +### 6.0 Zentrale Erkenntnis (warum die ursprüngliche Skizze nicht reichte) + +**Wir bewerten nicht „Gesetze". Wir bewerten *Pflichten* (Duties / +Restrictions), eingebettet in einen typisierten Norm-Graph.** + +„Bewerte das BImSchG" ist eine unbeantwortbare Frage. „Bewerte +die Pflicht aus §4 BImSchG, eine Genehmigung einzuholen, die §10 +i.V.m. der 4. BImSchV präzisiert wird, die ihrerseits Anhang I der +EU-IED 2010/75 umsetzt" — *das* ist eine beantwortbare Frage, weil +die Pflicht atomar ist und ihre Herkunft im Graph sichtbar bleibt. + +Daraus folgt: ein 6-Schichten-System, nicht ein „Bewertungs-Modul". + +``` +S6 Heatmap + Reform-Vorschläge (per Pflicht, nicht per Gesetz) +S5 Roll-up & Attribution (Graph-Centrality, Shapley-Splitting) +S4 Bewertung pro Pflicht (SKM €/h + Nutzen-Vektor + Frust) +S3 Pflicht-Extraktion (deontische Modalitäten, LLM-Vorschlag + Review) +S2 Norm-Graph (typed edges) +S1 Korpus (versionierte Norm-Texte in Akoma-Ntoso XML) +``` + +### 6.1 S1 — Korpus + +- **Format intern: Akoma Ntoso** (OASIS-Standard, vom EU-Parlament seit + 2016 verwendet). Heterogene Quellen werden via `law.akoma_convert` + normalisiert. +- **Kanonische IDs**: ELI (europäisch), CELEX (EU), eigener URI-Schema + für Berlin und Bezirke. +- **Versionierung**: jede Fassung mit Inkrafttreten/Außerkrafttreten, + Quell-URL, SHA-256 der Quelldatei, SHA-256 des normalisierten + Akoma-Ntoso-Dokuments. Beides landet im Ch∆In-Audit-Log. +- **Re-Pull-Mechanik**: ETag/Last-Modified-Tracking, täglicher + Cronjob für `gesetze-im-internet` + EUR-Lex, wöchentlich für + landesrecht-berlin (das ist erfahrungsgemäß träger). + +### 6.2 S2 — Norm-Graph (das Herzstück) + +**Knoten:** Norm-Versionen — Gesetz, Verordnung, Paragraf, +Artikel, Anhang, EU-Richtlinie, EU-Verordnung, +Verwaltungsvorschrift, Allgemeinverfügung. + +**Kanten (typisiert):** + +| Kanten-Typ | Bedeutung | Beispiel | +|---|---|---| +| `transposes` | DE-Recht setzt EU-Recht um | BDSG → DSGVO | +| `implements_via_vo` | Verordnungsermächtigung | §10 BImSchG → 4. BImSchV | +| `concretizes` | Verwaltungsvorschrift konkretisiert Gesetz | AVV → BImSchG | +| `amends` | Änderungsgesetz | „Gesetz zur Änderung des …" | +| `cites` | Norm A verweist auf Norm B | „§ X i.V.m. § Y" | +| `delegates_to_land` | Bund verweist Vollzug an Land | viele Bauthemen | +| `delegates_to_kommune` | Land verweist an Kommune | Berliner BezVG | +| `superseded_by` | Alte Fassung → neue Fassung | Historischer Lauf | +| `cross_refers` | Norm enthält Definition, die anderswo verwendet wird | §15 AO „Geschäftsleitung" | + +**Aufbau:** `law.graph_build` arbeitet hybrid: +1. **Regelbasiert**: reguläre Ausdrücke für deutsche Zitations-Muster + („§ 5 Abs. 2 Satz 1 BGB", „Artikel 6 Absatz 1 lit. f DSGVO"). Hier + gibt es bereits Open-Source-Parser (z.B. *de-jure-regex*-Bibliotheken) + und veröffentlichte Citation-Pattern aus Coupette et al. 2021. +2. **LLM-assistiert** nur für Kanten, die regelbasiert nicht greifbar + sind (z.B. inhaltliche Konkretisierungs-Beziehungen). Jede + LLM-extrahierte Kante wird mit `confidence < 1.0` markiert und + bei `S4` zur juristischen Validierung vorgelegt. + +**Speicherform:** Property-Graph in SQLite (FTS5 + Recursive CTEs); +für Phase-2-Skalierung Optionsraum Neo4j oder DuckDB-mit-GraphQL. +Phase 1 reicht SQLite — das spart einen Dienst und passt zum +Ch∆In-Audit-Log-Speicher. + +**Was der Graph für uns löst:** +- Ich kann fragen: „welche EU-RL liegt dieser Pflicht zugrunde?" + → Pfadsuche bis `transposes`-Kante. +- Ich kann fragen: „welche Berliner Norm hängt an §X HGB?" + → ausgehende `concretizes`/`delegates_to_land`-Kanten. +- Ich kann „Reform-Vorschlag streiche Berliner §Y" automatisch + prüfen: existiert eingehende `transposes`-Kette? → Vorschlag + als „bei EU-Quelle ansetzen" umetikettieren. + +### 6.3 S3 — Pflicht-Extraktion (Duty Extraction) + +Inspiriert von **Mercatus QuantGov** (RegData) und **LegalRuleML**. + +Eine Pflicht ist ein Tupel: +``` +duty = ( + modality, // OBLIGATION | PROHIBITION | PERMISSION | REQUIREMENT_TO_REPORT + addressee, // wer? (KMU | Bürger | Behörde | spezifische Branche) + trigger, // wann/woraus? (Bedingung) + action, // was tun? + frequency, // wie oft? (anlassbezogen | jährlich | quartalsweise | ad hoc) + authority, // gegenüber welcher Stelle? + consequence, // Sanktion bei Nichterfüllung + source_norm, // welche Norm-Stelle? + derived_from[] // Graph-Pfad zur Ursprungs-Norm (EU? Bund?) +) +``` + +**Extraktions-Pipeline:** +1. **Regelbasiert** (deterministisch): Modalverben + juristische + Standardphrasen. Patterns wie „hat zu", „ist verpflichtet", + „darf nicht", „muss", „kann verlangt werden", „bei der + zuständigen Behörde anzuzeigen". Trefferquote ~60–70% nach + QuantGov-Erfahrung. +2. **LLM-gestützt** (für die fehlenden ~30%): strukturierter + JSON-Output mit verpflichtetem Quellen-Zitat (Satz/Absatz/Nummer). + Ohne Zitat → verworfen. +3. **Juristen-Review im Pilot**: alle Pflichten der ersten 5 Normen + werden manuell validiert. Aus dieser Validierung entsteht ein + **Gold-Standard-Set**, das als Regressions-Test (`law.regression_test`) + für jede Modell-/Prompt-Änderung dient. + +**Inter-Rater-Reliability messen** (Cohen's κ) — bei < 0.7 wird die +Extraktion methodisch nicht freigegeben. Das ist die schmerzhafte, +aber notwendige Wahrheit der quantitativen Rechtsanalyse. + +### 6.4 S4 — Bewertung pro Pflicht + +Pro Pflicht drei orthogonale Scores, jeder mit Quellenangabe: + +#### 6.4.1 Schaden — Standardkostenmodell (€ + h) + +``` +B(duty) = P × F × ( T_sach + h × Tarif_h ) +``` +- `P` = Adressatenzahl im Bezugsraum (Quelle: DESTATIS Unternehmens- + register / IHK Berlin, je nach WZ-Code) +- `F` = Frequenz pro Jahr +- `T_sach` = direkte Sachkosten (Gebühren, Software-Lizenzen) +- `h` = Zeitaufwand pro Fall +- `Tarif_h` = NKR-Standardtarife (Statistisches Bundesamt + veröffentlicht Lohnkostentafeln nach Qualifikation) + +**Konfidenzintervall ist Pflichtfeld.** Schätzungen ohne Quelle +sind nicht zulässig — entweder NKR-Daten, eigene +Branchen-Befragung oder explizit „expert estimate ±X %". + +#### 6.4.2 Schaden — Qualitativ (deterministisch, kein LLM) + +Pro Pflicht aus dem Graph berechnet: +- **Verweistiefe** = mittlere Pfadlänge von der Pflicht zu ihren + Definitions-/Konkretisierungs-Knoten +- **Behörden-Pluralität** = `|distinct(authority) in zusammenhängenden duties|` +- **Norm-Volatilität** = Anzahl `amends`-Kanten in den letzten 5 Jahren +- **Lesbarkeit** = deutsche Variante des Flesch-Reading-Ease (Amstad-Formel) + auf den Norm-Text der Pflicht + +Aggregat = `frust_score` ∈ [0,10], rein algorithmisch. + +#### 6.4.3 Nutzen — 7-dimensional, LLM-Vorschlag + Juristen-Approval + +Dimensionen (jede 0–5, dokumentierte Anker-Beispiele): +1. Schutz Leben/Gesundheit +2. Schutz der Umwelt +3. Verbraucherschutz / Schutz Schwächerer +4. Marktordnung / fairer Wettbewerb +5. Rechtssicherheit / Erwartbarkeit +6. EU-Binnenmarkt-Funktion +7. Fiskalische Funktion + +**LLM-Vorschlag**: RAG-pflichtig (Zitat aus Norm + Begründung), +strukturierter Output, niedrige Temperature (≤0.1) für +Reproduzierbarkeit; Modell-Digest landet im Audit-Log. + +**Juristen-Approval** via `system.approval@^0`. Ohne Approval kein +Score → kein Output. + +#### 6.4.4 Betroffenheit (separat) + +``` +Betroffenheit(duty) = P(duty) × F(duty) × KMU-Anteil(duty) +``` + +**Bewusst nicht in Schaden-Score eingebaut** — sie ist eine +**Hebel-Dimension**, nicht eine Schaden-Dimension. Eine teure, +aber selten relevante Pflicht (z.B. Stilllegungsverfahren AKW) ist +nicht der politisch sinnvolle Reform-Hebel. + +### 6.5 S5 — Roll-up & Attribution (Abhängigkeiten + Ketten sauber lösen) + +Das ist der Teil, der dein eigentliches Problem löst. Drei +Algorithmen kombiniert: + +#### 6.5.1 Attribution entlang `transposes`/`implements_via_vo`/`concretizes` + +Wenn die DSGVO eine Pflicht erzeugt, BDSG-§30 sie umsetzt und eine +Berliner Vollzugsanweisung sie konkretisiert, wird der Schaden-Score +**nicht** dreifach gezählt. Stattdessen: +- **Originär-Beitrag** = Anteil, der erst auf dieser Ebene neu hinzukommt +- **Pflicht-Last** = Summe der Originär-Beiträge auf dem Pfad + +Pragmatisches Verfahren: **Shapley-artiges Splitting** — jede +Ebene, die zur Pflicht beiträgt (Zusatz-Doku, neue Frist, zusätzliche +Behörde), bekommt einen anteiligen Schaden zugeordnet. Wer keinen +Eigenbeitrag leistet (reine Wiederholung der EU-Vorgabe), bekommt 0. + +**Politisch wichtig**: das System sagt automatisch „Vorschlag +zur Streichung des Berliner §X ist hohl, weil 100% der Pflicht +aus DSGVO kommt" — und erspart der MIT AG, einen Vorschlag zu +publizieren, der EU-rechtlich überhaupt nicht durchsetzbar ist. + +#### 6.5.2 Graph-Centrality — wo entstehen Pflicht-Ketten? + +- **PageRank über `cites`-Kanten**: welche Norm wird am meisten + referenziert? (das sind die „Anker-Normen", deren Reform Wirkung + entfaltet) +- **Betweenness-Centrality** im Pflicht-Subgraph: welche Norm-Knoten + liegen auf den meisten Pflicht-Pfaden? (das sind die + „Engstellen"-Kandidaten) +- **Cluster-Detection** (Louvain o.ä.): welche thematischen + Gesetzes-Cluster bilden sich? (das hilft bei + Konsolidierungs-Vorschlägen — „lege diese 5 Verordnungen + zusammen") + +#### 6.5.3 Counterfactual-Simulation („was, wenn wir streichen?") + +Sehr eingeschränkt — nicht „echte" Kausal-Inferenz, sondern +Graph-Surgery: +- Markiere Pflicht X als gestrichen +- Welche eingehenden Kanten verlieren ihren Bezug? +- Welche EU-Mindest-Anforderungen werden dann verletzt? +- Welche Verwaltungsvorschriften werden bedeutungslos? + +Output: eine Liste „Folge-Streichungen, die mitgemacht werden müssten". +Das ist nicht *kausale* Wirkungsanalyse, aber es macht die +*juristische Folgenkette* einer Reform sichtbar — was die AG für +Begründungen braucht. + +### 6.6 S6 — Output: Heatmap + Vorschläge + +#### 6.6.1 Die Heatmap (überarbeitet) + +- **Granularität: pro Pflicht**, nicht pro Gesetz. Eine Norm kann + mehrere Pflichten enthalten, die unterschiedlich bewertet sind — + und Reform setzt typischerweise an einzelnen Pflichten an. +- **Achsen**: Schaden (X) × Nutzen (Y) +- **Marker-Größe**: Betroffenheit +- **Marker-Farbe**: Herkunfts-Ebene aus dem Graph (EU / Bund / Land / + Kommune / Vollzug) — sofort sichtbar, wo Reform überhaupt + möglich ist +- **Marker-Form**: Reform-Empfehlung (streichen / vereinfachen / + konsolidieren / befristen / belassen) +- **Filter**: Branche (WZ-Code), Rechts-Ebene, Gesetzes-Cluster, + Stand + +#### 6.6.2 Reform-Vorschlag (strukturiert) + +Jeder Vorschlag enthält: +1. **Ziel-Pflicht** (eindeutige ID) +2. **Reform-Typ** (streichen / vereinfachen / konsolidieren / befristen) +3. **Adressat-Ebene** (Bundestag / Landesparlament / EU-Kommission / + Verwaltung) +4. **Quantitative Wirkung** (eingesparte €/h, Adressatenzahl) +5. **Begründung** mit Quellen-Zitaten +6. **Folge-Streichungen** aus Counterfactual-Simulation +7. **EU-/Verfassungs-Risiko-Hinweise** (z.B. „mind. Verbleib der + DSGVO-Mindest-Anforderung erforderlich") +8. **Methodik-Hash** (welche Bewertungsregeln galten) + +Format: Markdown + maschinenlesbares JSON-Pendant — beides aus +demselben `law.reform_proposal`-Modul, beides versioniert. + +### 6.7 Evidenz-Layer — Woher kommen die Zahlen wirklich? + +**Eiserne Regel:** LLMs sind **keine Datenquelle**. Sie würden raten, +und damit wäre das Projekt politisch wertlos. Jede €/h-Zahl, +jede Adressatenzahl, jede Nutzen-Bewertung muss aus einer +zitierbaren Quelle stammen und wird mit einem **Evidenz-Tier** +markiert. + +#### 6.7.1 Vier-Tier-Modell + +| Tier | Quelle | Konfidenz | Politische Belastbarkeit | +|---|---|---|---| +| **T1** Amtlich / peer-reviewed | NKR-Stellungnahmen, DESTATIS BKI, ifo/ZEW/IW-Studien, EU-RIA, Gebührenordnungen | ±20 % | unbestreitbar | +| **T2** Verbands- / Branchen-Erhebung | DIHK-Bürokratie-Reports, BDI/ZDH/ZGV-Studien, IHK-Berlin-Umfragen | ±30–50 % | belastbar mit Quellenangabe | +| **T3** Eigene strukturierte Erhebung | KMU-Formular pro Pflicht, pseudonym, WZ-Code-stratifiziert | hängt von n ab | belastbar bei publizierter Methode + n ≥ 30 | +| **T4** Qualitatives Signal | Petitionsausschuss, openPetition, mein.berlin.de, Foren-Sentiment | nicht quantifizierbar | nur als Pain-Point-Indikator, nie als Zahl | + +**Regel der niedrigsten Stufe**: Jede Pflicht-Bewertung trägt den +Tier ihres *schwächsten* Bestandteils. Wenn `h` aus T1 kommt aber +`P` aus T4, ist die Pflicht-Bewertung „T4". So bleibt die +Wahrheits-Qualität immer ehrlich sichtbar. + +#### 6.7.2 Pro SKM-Variable + +| Variable | Beste Quelle | Tier | Aufwand | +|---|---|---|---| +| `P` Adressatenzahl | DESTATIS GENESIS-API, WZ-Code-stratifiziert | T1 | gering | +| `F` Frequenz | Normtext + Verwaltungsvorschriften | T1 | mittel | +| `T_sach` Sachkosten | Gebührenordnungen, Software-Preislisten | T1–T2 | gering–mittel | +| `h` Zeit pro Fall | **Bottleneck**: NKR (nur neuere Gesetze), Verbandsdaten, eigene Erhebung | T1–T3 | hoch | +| `Tarif_h` Stundensatz | DESTATIS Lohnkostentafeln | T1 | gering | +| Nutzen-Dimensionen | Wirkungsstudien, Behördenstatistiken, juristische Bewertung | T1–T2 + Expertise | mittel | + +**`h` ist der Engpass.** Für etablierte Gesetze gibt es ihn oft +nicht öffentlich. Eigene Erhebung in Phase 2/3 ist Pflicht. + +#### 6.7.3 Wo LLMs *doch* eine Rolle haben (Mörtel, nicht Stein) + +- **Extraktion** von SKM-Zahlen aus NKR-PDF-Stellungnahmen +- **Klassifikation** von Petitionen/Beschwerden zu konkreten Pflichten +- **Konsolidierung** mehrerer Verbandsstudien (Synthese-Tabelle) +- **Übersetzung** von EU-RIA-Reports +- **Inkonsistenz-Detektion** zwischen Quellen + („NKR schätzt 4h, DIHK schätzt 12h" → flag für Juristen-Review) + +Jeder LLM-Output ist **Vorschlag**, nie **Quelle**. RAG-Pflicht, +Zitat-Pflicht, Approval-Pflicht. + +#### 6.7.4 Quellen-Strategie für T4 (Signal, nicht Magnitude) + +In absteigender Sauberkeit: + +1. **Petitionsausschuss des Bundestags** — öffentliche Datenbank, + rechtlich unproblematisch, kategorisiert +2. **openPetition.de, change.org** — öffentlich, Beschwerdegegenstand + erkennbar +3. **mein.berlin.de** — Berliner Bürgerbeteiligungs-Plattform, + genau unser Bezugsraum +4. **IHK Berlin / DIHK Bürokratiebarometer** — strukturiert, + einholbar per Kooperation statt Scraping +5. **Bundesnetzagentur Beschwerdestatistiken** — bereichsspezifisch, + öffentlich +6. **Foren-Scraping** (Heise, themenspezifische Subreddits) — + **rechtlich heikel** (ToS, UrhG, DSGVO). Nur Sentiment-Aggregate, + keine Einzelzitate, niemals als Magnitude-Quelle + +#### 6.7.5 Eigene KMU-Befragung — der Skalierungs-Motor + +Ab Phase 2 die einzige saubere Quelle für `h`: + +``` +Pflicht: DSGVO Art. 30 — Verarbeitungsverzeichnis + + Diese Pflicht betrifft mich: [ja/nein] + Stunden pro Jahr: ___ + Externe Kosten pro Jahr (€): ___ + Frust 1–5: ___ + Branche (WZ-Code): [Dropdown] + Größenklasse: [<10 / 10–49 / 50–249 / >249 MA] + Kommentar: ___ + + Pseudonymes Token. Kein Personenbezug. AVV mit Hosting. +``` + +**Politische Voraussetzung**: Kooperationspartner für die +Befragung **vor** Phase 2 akquirieren — MIT-Mitglieder allein +liefern keine statistisch nutzbaren Samples. Mit IHK Berlin +(~280.000 Mitgliedsunternehmen) + ZDH + BDI-Mittelstand wird +es belastbar. + +#### 6.7.6 Datenmodell-Erweiterung + +Jede `bewertung` enthält jetzt eine `evidence`-Liste: + +``` +evidence_item: ( + tier, // 1 | 2 | 3 | 4 + source_type, // nkr | destatis | ifo | dihk | ihk_umfrage | + // eigene_befragung | petitionsausschuss | ... + source_url, + source_hash, // SHA-256 der Quelldatei (Reproduzierbarkeit) + claim, // { metric, value, ci_low, ci_high, unit } + sample_size, + retrieved_at, + notes +) +``` + +Aggregation: pro Variable Bayesian Pooling oder gewichteter +Mittelwert über Tier-Konfidenzen. Bei T1-Quelle: T1 gewinnt. +Bei nur T3/T4-Quellen: mit Disclaimer publizieren oder gar nicht. + +### 6.8 Reproduzierbarkeit & Halluzinations-Schutz + +Querschnitts-Anforderungen, die in jeder Schicht greifen: + +| Mechanik | Wirkung | +|---|---| +| **RAG-Pflicht** + Zitat-Pflicht im LLM-Output | keine Aussage ohne Norm-Stelle | +| **Modell-Digest im Audit-Log** (Ch∆In-Bordmittel) | Reproduzierbarkeit über Modell-Versionen | +| **Temperature ≤ 0.1, fester Seed** | deterministische Re-Runs | +| **Regressions-Test-Suite** (`law.regression_test`) | Gold-Standard-Pflichten, pass-rate pro Modell-Update | +| **Inter-Rater-Reliability** (Cohen's κ) | Methodik-Freigabe-Schwelle | +| **`system.approval@^0`** vor jedem Output | menschliche Letztverantwortung | +| **Tamper-evidentes Audit-Log** (Ch∆In-Bordmittel) | jede Bewertung rückwirkend prüfbar | +| **Methodik-Versionierung** | Altbewertungen markieren „re-bewerten" bei Methode-Änderung | + +--- + +## 7 Was du in deiner Skizze noch NICHT adressiert hast (kritischer Teil) + +### 7.1 Konzeptionell + +| Lücke | Warum es weh tut | Vorschlag | +|---|---|---| +| **Bürokratie ≠ Gesetz** | Sehr viel Frust kommt aus *Vollzugs*vorschriften, internen Verwaltungsanweisungen und IT-Lücken, nicht aus dem Gesetzestext | Vollzugsdimension von Anfang an mitdenken; Phase 2-Modul `vollzug.audit` | +| **Schutzgesetze nicht „schädlich" werten** | ArbZG, ArbSchG, BImSchG haben Bürokratie *als Funktion* — politisch fatal, das als „Schaden" zu reporten | Methodischer Eintrag: hoher Nutzen-Score schlägt hohen Schaden bei Schutznormen | +| **Föderalismus-Vorrang** | Berlin kann Bundesrecht nicht einfach „abschaffen" | Vorschläge müssen Adressat-Ebene markieren: Bund / Land / EU / Bezirk | +| **EU-Determinierung** | ~50–70% des Wirtschaftsrechts ist EU-induziert | EU-RL-Bezug im Datenmodell verpflichtend; Vorschläge sonst hohl | +| **Counterfactual** | „Was wäre ohne diese Norm?" ist die eigentliche Frage — schwer messbar | Pragma: zumindest qualitatives Counterfactual-Feld; Phase 3 RCT-artige Studien | +| **KMU ist nicht homogen** | Bäcker ≠ SaaS-Startup ≠ Handwerksbetrieb ≠ Anwaltskanzlei | Branchen-Filter (WZ-Codes) ab Phase 1 | +| **Subjektive vs. objektive Belastung** | „Gefühlte" Bürokratie ist politisch hochrelevant, aber metrisch schwach | Separater Frust-Score (qualitativ) klar getrennt vom SKM-€-Score | +| **Sunset-Clauses als Reform-Instrument** | Befristung > Abschaffung, weil weniger Widerstand | Reform-Vorschlagstyp „befristen mit Evaluationsklausel" als eigene Kategorie | +| **Datenbasis für Adressatenzahl** | Ohne DESTATIS-/IHK-Anbindung sind alle Zahlen geraten | DESTATIS GENESIS-API-Modul vor Phase-1-Bewertung | + +### 7.2 Recht & Compliance des Tools selbst + +| Punkt | Was zu tun ist | +|---|---| +| **EU AI Act** (Art. 50 Transparenz, Art. 6 Hochrisiko-Liste prüfen) | Risikoanalyse als eigenes Dokument; wir argumentieren „kein Hochrisiko, weil informativ + Human-in-the-Loop" | +| **DSGVO** für Crowd-Input | Wenn KMU Belastungen melden: pseudonym, Datenminimierung, AVV mit Hosting | +| **Urheberrecht** | §5 UrhG für amtliche Werke prüfen; juris-Daten nicht ungeprüft übernehmen | +| **Politische Neutralität** | Methodik-Beirat mit nicht-parteilich-zuordenbaren Stimmen (Wissenschaft, Normenkontrollrat-Ehemalige) | +| **Veröffentlichungsrecht** | Wer haftet für Bewertungen? → Disclaimer + redaktionelle Verantwortlichkeit klären | +| **Lizenz des Tools** | Ch∆In ist Apache-2.0 — passt; eigene Module ebenfalls Apache-2.0 vorschlagen, damit andere validieren können (Glaubwürdigkeit!) | +| **BSI-Grundschutz / ISO 27001** | Phase 2, nicht jetzt; aber Hosting-Konzept jetzt schon so wählen, dass nachrüstbar | + +### 7.3 Methodisch / wissenschaftlich + +| Punkt | Was zu tun ist | +|---|---| +| **Inter-Rater-Reliability** | Mehrere Juristen müssen die Nutzen-Scores unabhängig vergeben → Cohen's Kappa messen | +| **Reproduzierbarkeit bei LLM** | Temperature=0, gleiches Modell, gleicher Prompt-Hash; Modell-Digest landet im Audit-Log (das macht Ch∆In schon) | +| **Halluzinations-Test-Suite** | Vor jedem Modell-Update: feste Test-Normen mit erwarteten Antworten; pass-rate dokumentiert | +| **Quellen-Pflicht im LLM-Output** | RAG-Pflicht: Jede Aussage zitiert eine Norm-Stelle; ohne Zitat → Aussage verworfen | +| **Peer-Review-Pfad** | Vor Veröffentlichung Methodik-Papier an Lehrstühle (Speyer, HU Berlin Verwaltungsrecht, ifo, ZEW) — ein „Kritisches Gutachten" einholen ist *Stärke*, nicht Schwäche | + +### 7.4 Organisatorisch / politisch + +| Punkt | Was zu tun ist | +|---|---| +| **Methodik-Beirat** | 5–7 Personen, gemischt: Wirtschaft (BDI/ZDH), Verwaltungsrecht (Lehrstuhl), Normenkontrollrat-Background, IT-Recht, KMU-Vertreter, Verbraucherschutz | +| **AG-Sitzungs-Kadenz** | Vorschläge in Schüben (z.B. 5 Normen pro Quartal), nicht „Sturzflut" | +| **Versionierung des Methodik-Papiers** | wenn die Methode sich ändert, müssen alle Altbewertungen markiert „re-bewerten" werden | +| **Konfliktbewältigung** | Was, wenn das System ein „CDU-Gesetz" als schädlich klassifiziert? Vorab-Vereinbarung: Methode > Politik. Sonst verbrennt das Projekt sich. | +| **Skalierungs-Geschichte** | Pilot Berlin-KMU → Skalierung andere Bundesländer / Bundestags-AGs → ggf. Übergabe an Normenkontrollrat. Exit-Strategie jetzt schon mitdenken. | + +### 7.5 Technisch / DevOps + +| Punkt | Was zu tun ist | +|---|---| +| **Hosting-Topologie** | `chain serve` läuft lokal/server (offline-first); für die AG: 1 zentraler Hub auf gehärtetem VPS, EU-Hosting (Hetzner Falkenstein o.ä.), kein US-Cloud | +| **Datenpersistenz** | SQLite reicht für Phase 1 (das Audit-Log ist auf SQLite ausgelegt); Postgres-Migration als Phase-2-Option | +| **Backup & Recovery** | täglich automatisiert; off-site verschlüsselt | +| **Monitoring** | Ch∆In-eigene Events ins Grafana; Alarme bei fehlgeschlagenen Re-Pulls (= veraltete Daten!) | +| **Secret-Management** | API-Tokens (z.B. juris-PAT) niemals im Repo; sops/age oder Vault | +| **CI/CD** | Ch∆In-Konvention übernehmen: DCO-Sign-off, Conventional Commits, SBOM (CycloneDX), sigstore-Signatur (CRA-ready) | +| **Module-Signierung** | Ch∆In refusiert unsignierte Module im Enterprise-Mode — nutzen, damit niemand „inoffizielle" Bewertungs-Module einschleust | +| **Disaster-Doku** | klassisches Runbook (wer ist On-Call, wie restored man Audit-Log) | + +--- + +## 8 Roadmap (Phasen, realistisch für ein Teilzeit-Team) + +> Annahme: ein Solo-Entwickler (S. Flemming) auf Ch∆In-Seite + 1–2 ehrenamtliche +> Juristen/Ökonomen im MIT-Umfeld + 1 Methodik-Beirat. Vollzeit-Äquivalent +> ca. 0,5–1,0 FTE Tech, 0,2 FTE Recht. + +### Phase 0 — Methodik & Korpus-Definition (4–6 Wochen) + +- [ ] **Methodik-Papier v0.1** (genau diese Studie schärfen, peer-feedback) +- [ ] **Korpus-Liste** „die ersten 50 Normen" (Bund + Berlin), schriftlich +- [ ] **Methodik-Beirat** akquirieren (mind. 3 Zusagen) +- [ ] **Rechtliche Stellungnahme** zu EU AI Act, DSGVO, Urheberrecht + (z.B. via studentische Hilfskraft an einem Lehrstuhl) +- [ ] **Go/No-Go-Entscheidung** der MIT AG, ob das Projekt weiter geht + +### Phase 1 — Vertikaler Durchstich (3–4 Monate) + +Ziel: **eine Norm** end-to-end durchs System gespielt, mit +Juristen-Approval und PDF-Ausgabe. + +- [ ] Module `law.fetch`, `law.parse_xml`, `law.parse_html` +- [ ] Modul `law.graph_extract` (Verweisextraktion, sehr eingeschränkt: + „§X i.V.m. §Y", „Artikel Z DSGVO" usw.) +- [ ] Modul `law.skm_score` mit *manuell* gepflegten Aufwand-Schätzungen +- [ ] Modul `law.benefit_score` mit RAG + Approval +- [ ] Modul `law.frust_score` (deterministisch, kein LLM) +- [ ] Flow `bewerten-eine-norm.yaml` +- [ ] PDF-Export-Modul (für AG-Sitzungen) +- [ ] **Pilot:** 5 Normen bewerten, Beirat reviewt + +### Phase 2 — Korpus-Aufbau & Heatmap (3–4 Monate) + +- [ ] DESTATIS-/IHK-Anbindung (Adressatenzahlen) +- [ ] Heatmap-Renderer (`law.heatmap_render`) +- [ ] gRPC-Web-API für cdu-copilot +- [ ] Flutter-Widgets in cdu-copilot +- [ ] 50 Normen vollständig bewertet +- [ ] Erste Vorschlagspaket an AG Entbürokratisierung +- [ ] **Pilot:** 1 echter Vorschlag landet in einer AG-Diskussion + +### Phase 3 — Skalierung & Öffentlichkeit (6+ Monate) + +- [ ] Crowd-Input von KMU (DSGVO-konform, pseudonym) +- [ ] Branchen-Filter (WZ-Codes) +- [ ] Ausweitung auf weitere Bundesländer +- [ ] Methodik-Papier v1.0 veröffentlichen +- [ ] Open-Source-Release der Bewertungs-Module (politisch + technisch + Glaubwürdigkeit) + +### Phase 4 (Vision) + +- [ ] Übergabe / Andocken an Normenkontrollrat oder ähnliche Institution +- [ ] Replikation auf EU-Ebene (REFIT-Beitrag) + +--- + +## 9 Risiken & Mitigation (Risikomatrix) + +| Risiko | Eintritt | Wirkung | Mitigation | +|---|---|---|---| +| Methodik wird als parteiisch zerrissen | mittel | sehr hoch | Beirat, Peer-Review, Open Source, Apache-2.0 | +| LLM halluziniert Paragrafen → falsche Vorschläge | hoch (default) | sehr hoch | Strikte RAG-Pflicht, Juristen-Approval, Zitations-Pflicht | +| Daten veralten unbemerkt | mittel | hoch | Re-Pull mit ETag, „Stand"-Banner, automatische Re-Bewertung | +| EU AI Act stuft uns hoch-risiko ein | niedrig | hoch | Frühe Rechts-Stellungnahme; klare „Informationswerkzeug + HiL"-Positionierung | +| Volljuristen-Kapazität reicht nicht | hoch | mittel | Kleines Pilot-Korpus, externe studentische HK an Lehrstühlen | +| Politische Konflikte (CDU vs. Bewertung) | mittel | hoch | Vorab-Vereinbarung Methode>Politik, keine Sonderbehandlung von Normen | +| Tool wird „CDU-Tool" wahrgenommen, nicht akzeptiert | mittel | hoch | Methodik-Beirat überparteilich, Open Source, Veröffentlichung in wiss. Outlet | +| Ch∆In noch nicht 1.0 (Stand v0.16.0, Phase-0.5-Closure) | mittel | mittel | Discover/Install/Run/Audit-Zyklus läuft bereits; keine Phase-1+-Features voraussetzen (Streaming-Invoke, Polyglot-Module, Replay, OCI-Registry — Anhang D von Ch∆In-Docs prüfen) | +| Hosting-Kosten explodieren | niedrig | niedrig | Lokal-first (Ch∆In-Eigenschaft, offline-first by design), nur Hub gehostet, LLM lokal über Ollama möglich | +| Crowd-Daten-Manipulation | mittel | mittel | Phase-3-Thema, Pseudonymität + Plausibilitätsprüfung | + +--- + +## 10 Wirtschaftlichkeit — was kostet das, was bringt es? + +### 10.1 Kosten Phase 1 (grobe Schätzung) + +| Posten | Wert | +|---|---| +| Tech-Arbeit (S. Flemming, vorhanden) | „interne Arbeit" | +| Methodik-Beirat (Aufwandsentschädigung) | 0–3 k€ | +| Rechtsgutachten EU AI Act / DSGVO | 0 k€ (studentische HK) bis 5 k€ | +| Hosting (Hetzner VPS, Falkenstein) | ~20 €/Monat | +| Domain + TLS | < 100 €/Jahr | +| **Summe Phase 1** | **~3–8 k€**, dominiert von Beirat/Gutachten | + +### 10.2 Nutzen (politischer ROI) + +Schon ein einziger angenommener Vorschlag, der z.B. 1 Stunde +Aufwand pro Jahr pro Berliner KMU einspart, entspricht bei +~180.000 KMU in Berlin und ~50 €/h einem volkswirtschaftlichen Effekt +von **~9 Mio. €/Jahr**. Selbst um Faktor 10 zu hoch geschätzt ist das +Verhältnis von Aufwand zu Wirkung sehr günstig. + +### 10.3 Was passiert, wenn wir es nicht bauen + +Andere bauen es, vermutlich mit weniger sauberer Methodik. Die +Argumentations-Lufthoheit in der Entbürokratisierungs-Debatte +liegt dann woanders. + +--- + +## 11 Sofortige nächste Schritte (das, was nach diesem Dokument passiert) + +1. **Dieses Papier intern in der AG kommentieren lassen** (1–2 Wochen). +2. **Methodik-Beirat-Liste** entwerfen (Vorschlag: 7 Personen, + Bezugsraum Berlin + Bund; explizit nicht nur CDU-nah). +3. **Korpus-Liste „erste 5 Normen"** für den vertikalen Durchstich + beschließen — Vorschlag: GewO §14 (Anzeigepflicht), KassenSichV, + DSGVO Art. 30 (Verarbeitungsverzeichnis), Berliner BauO §60 ff + (Bauantrag), MiLoG §17 (Dokumentationspflicht). +4. **Rechtliche Kurz-Stellungnahme** anstoßen (EU AI Act, + DSGVO, §5 UrhG, juris-Lizenz). Möglichst kostenneutral über + Lehrstuhl-Kooperation. +5. **Repo `fai/lawheatmap` initialisieren** (Forgejo unter `git.flemming.ai`, + Vendor-Namespace `fai/`), Lizenz Apache 2.0, DCO an, CI-Pipeline analog + `fai/chain` (DCO-Sign-off, Conventional Commits, SBOM, sigstore). + `law.*`-Module später in `chain-modules-law/` als product-scoped Org. +6. **Modul-Stub `text.akoma_normalize`** in `chain-modules/text-akoma-normalize/` + als ersten Code-Commit (vertikaler Durchstich beginnt mit dem Pflicht-Eingang; + HTTP-Pull macht `http.request` aus Bestand). Parallel **Flutter-App-Skelett** + in `fai_lawheatmap/app/` (Theme nach fai_web-Palette, Hub-Connect-Seite). +7. **Methodik-Papier v0.1 als eigenes PDF** aus diesem Markdown + destillieren — für den Beirat und die AG. + +--- + +## Anhang A — Glossar (kurz) + +- **SKM**: Standardkostenmodell, offizielle Methode zur Messung von Bürokratiekosten +- **Normenkontrollrat**: unabhängiges Beratungsgremium der Bundesregierung +- **ELI**: European Legislation Identifier (eli.dlri.de / EUR-Lex) +- **CELEX**: EU-Dokumenten-ID-System +- **REFIT**: EU-Programm zur Regulatorischen Eignungsprüfung +- **Ch∆In** (CLI: `chain`, vormals F∆I-Plattform): Orchestrierungs-Plattform unter `fai_lawheatmap`. Vendor: **F∆I = Flemming.AI**. +- **Hub / Module / Flow**: Ch∆In-Server (`chain serve`), WASM-Komponente (ABI `chain:platform`), YAML-Workflow +- **RAG**: Retrieval Augmented Generation — LLM mit verpflichteter Quelleneinbindung +- **Hochrisiko-KI (EU AI Act)**: KI-System, das Rechtsfolgen für Personen + auslöst — wollen wir explizit *nicht* sein + +## Anhang B — offene Fragen für die AG + +1. Wer ist der **redaktionell verantwortliche Volljurist** im Pilot? +2. Welche **5 Normen** für den vertikalen Durchstich? +3. **Open Source ab welchem Zeitpunkt?** (Empfehlung: spätestens mit + Phase-2-Release des Heatmap-MVP — aus Glaubwürdigkeitsgründen) +4. **Branding**: „CDU-/MIT-Tool" vs. „neutrales Werkzeug, von MIT + initiiert"? Stark unterschiedliche Außenwirkung. +5. **Hosting-Träger**: MIT-Verein? Stiftung? Privatperson? hat + Haftungs- und Datenschutzfolgen. + +## Anhang C — Vorbilder weltweit (auf wessen Schultern wir stehen) + +### C.1 Akademisch / methodisch (direkt verwendbare Verfahren) + +| Quelle | Was sie liefern | Wie wir es nutzen | +|---|---|---| +| **Coupette, Beckedorf, Hartung, Bommarito, Katz** — *„Measuring Law Over Time"*, Frontiers in Physics 2021 | Netzwerk-Analyse des **gesamten deutschen Bundesrechts** über 20 Jahre; Knoten-/Kanten-Schema, Komplexitäts-Metriken | **Methodisches Fundament für S2 (Norm-Graph)** — wir kopieren das Schema, nicht den Code | +| **Bommarito & Katz** — *„A Mathematical Approach to the Study of the United States Code"* (2010) | Gründungstext quantitativer Rechtsnetz-Analyse; Graph-Bau, Tiefen-/Verweis-Metriken | Konzeptuelle Inspiration; Graph-Theorie-Grundlagen | +| **Katz et al.** — diverse Arbeiten (Illinois Tech / Bucerius) | Computational Legal Analytics als Disziplin | Peer-Reviewer und potenzielle Kooperationspartner | +| **Mercatus Center / QuantGov** (George Mason) — RegData, US/StateRegData | **NLP-basiertes Zählen von „regulatory restrictions"** (must/shall/may not/prohibited/required) im gesamten US-Bundesrecht und je Bundesstaat. **Open Source.** | **Direktes Vorbild für S3 (Pflicht-Extraktion)** — Pattern-Listen, Konfidenz-Metriken | +| **Daniel Martin Katz, Jens Frankenreiter, Holger Spamann** | Computational Law-Community, mehrere relevante Konferenz-Papiere | Peer-Review-Netzwerk | + +### C.2 Institutionell / politisch (Methodik-Anerkennung) + +| Institution | Was sie liefern | Wie wir andocken | +|---|---|---| +| **Nationaler Normenkontrollrat (DE)** | Standardkostenmodell (SKM), Erfüllungsaufwand-Konzept, „One in, one out", veröffentlichte Stellungnahmen zu ~allen neuen Gesetzen | **Methodisches Fundament für S4 (SKM-Score)**; Stellungnahmen als RAG-Quelle; Ehemalige als Beiratsmitglieder | +| **EU REFIT + Regulatory Scrutiny Board** | „Fitness Checks" bestehender Gesetzgebung (Lebensmittel, Chemikalien, Verkehr); veröffentlichte Methodik (*Better Regulation Toolbox*) | Vorbild für die *politische* Prozessform eines „ex-post Audit" | +| **EU Better Regulation Guidelines + Toolbox** | Offizielle EU-Methodik für RIA, MCDA, Cost-Benefit | Direkt zitierbare Methodik-Referenz im Methodik-Papier | +| **UK Better Regulation Executive + Regulatory Policy Committee (RPC)** | Unabhängige Bewertungsstelle wie NKR; veröffentlichte „Opinion"-Datenbank | Vorbild für unabhängige Letzt-Validierung | +| **UK „Red Tape Challenge" 2011–2014** | ~21.000 UK-Regulierungen wurden öffentlich-crowd-gereviewt | **Vorbild für Phase-3-Crowd-Input** + *Lessons Learned* zur Vermeidung des UK-Endergebnis-Problems (politisch verwertbare Bilanz unklar) | +| **Niederlande — ATR (ehem. Actal)** | Erfinder des SKM in den 1990ern; >25 Jahre Praxiserfahrung | Methodik-Tiefenreferenz; ggf. Beirats-Stimme | +| **Australien — Productivity Commission** | Regelmäßige Regulierungs-Audits, Bench-Studien | Vorbild für Output-Form („Inquiry Report") | +| **Canada — Red Tape Reduction Act 2015** | Gesetzlich verankertes „One-for-one"-Prinzip | Politisches Vorbild für Reform-Vorschlagstyp | +| **OECD iREG-Indikatoren** | Internationale Vergleichsmetriken für regulatorische Qualität | Benchmarking-Daten für Validierung unserer Scores | + +### C.3 Standards & Datenstrukturen (Pflicht, nicht neu erfinden) + +| Standard | Zweck | Verwendung | +|---|---|---| +| **Akoma Ntoso** (OASIS, vom EU-Parlament seit 2016 verwendet) | XML-Format für Gesetzestexte | **Internes Korpus-Format** (S1) | +| **ELI — European Legislation Identifier** | Kanonische URIs für Gesetze | **ID-System** für Norm-Knoten | +| **CELEX** | EU-Dokument-IDs | EU-Knoten-IDs | +| **LegalRuleML** (OASIS-Arbeitsgruppe) | RuleML-Erweiterung für deontische Logik (Pflicht/Erlaubnis/Verbot) | **Datenmodell für S3 (Pflicht-Tupel)** | +| **Hohfeldian Analysis + Deontische Logik** | Seit ~100 Jahren juristisch-formaler Apparat für „wer schuldet wem was" | Konzeptuelle Grundlage Pflicht-Typologie | +| **ECLI** | Case-Law-IDs (für spätere Rechtsprechungs-Anbindung) | Phase 3+ | + +### C.4 Wirtschafts-/Statistik-Datenquellen (Adressatenzahlen, Tarife) + +| Quelle | Was sie liefern | +|---|---| +| **DESTATIS GENESIS-Online** | Unternehmensregister nach WZ-Code, Bundesländer-Ebene | +| **DESTATIS Bürokratiekostenindex (BKI)** | Quartalsweise Benchmark seit 2012 | +| **IHK Berlin** | Berlin-spezifische Mittelstandszahlen | +| **Handwerkskammer Berlin** | Handwerks-spezifische Datenbasis | +| **ifo Institut, ZEW, IW Köln** | Veröffentlichte Erfüllungsaufwands-/Standortkosten-Studien | +| **DIHK Bürokratie-Reports** | Branchen-Umfragen | +| **BDI / ZDH Stellungnahmen** | Qualitative Belastungs-Schilderungen (RAG-Quelle für S4 Frust-Score) | + +### C.5 Was es noch *nicht* gibt — unsere Nische + +- Keine **per-Pflicht**-Bewertung auf typisiertem Norm-Graph für deutsches Recht +- Kein **Mittelstands-fokussierter** Heatmap-Output mit Branchen-Differenzierung (WZ-Codes) +- Kein **ex-post Fitness-Check-Tooling** auf Berliner Landesebene +- Kein **deterministisches, audit-fähiges** Bewertungs-System (alles bisher entweder kommerziell-closed oder akademisch-einmalig) + +Genau diese vier Lücken füllen wir — keine davon ist trivial, +aber jede ist klar abgegrenzt und für sich messbar. + +### C.6 Konkrete „erst lesen, dann coden"-Liste + +Bevor `law.graph_build` oder `law.duty_extract` programmiert wird: + +1. Coupette et al. 2021 vollständig lesen (ca. 1 Nachmittag) — Graph-Schema verstehen +2. Mercatus QuantGov *RegData*-Methodik-Paper lesen — Pattern-Listen verstehen +3. NKR-Methodenleitfaden „Erfüllungsaufwand" lesen — SKM in der DE-Anwendung verstehen +4. Akoma-Ntoso-Spezifikation überfliegen (nicht voll lesen) — Datenmodell verstehen +5. EU Better Regulation Toolbox überfliegen — politische Vokabel verstehen + +Das ist **eine Woche Vorarbeit, die ein halbes Jahr Code-Umbau spart**. diff --git a/RUN.md b/RUN.md new file mode 100644 index 0000000..d4c6c08 --- /dev/null +++ b/RUN.md @@ -0,0 +1,107 @@ +# F∆I Law-Heatmap — Run-Guide (Phase 0) + +> Stand v0.1.0 — Demo-Modus, ohne externe Daten, ohne Hub-Verbindung. + +## Layout + +``` +fai_lawheatmap/ +├── MACHBARKEITSSTUDIE.md # die Studie (v0.3, AG-intern) +├── RUN.md # diese Datei +├── app/ # Flutter-Client, Brand-Skin fai_web +│ ├── lib/ # Dart-Code (Pages, Widgets, Data, Theme) +│ ├── assets/ +│ │ ├── fonts/ # SIL-OFL TTFs (setup.sh holt sie) +│ │ └── fixtures/ # Phase-0 Demo-Daten +│ ├── macos/ # Plattform-Scaffolds (auto-generiert) +│ ├── setup.sh # Fonts + Pakete +│ └── pubspec.yaml +└── flows/ + └── durchstich-gewo-14.yaml # vertikaler Phase-0-Flow +``` + +Die acht zugehörigen Module liegen unter `fai_chain_modules/` als +eigene Crate-Verzeichnisse, ABI `chain:platform`, alle Apache 2.0: + +- `text-akoma-normalize/` (Pflicht-Eingang, gesetze-im-internet-XML-Adapter) +- `text-deontic-extract/` (Pflicht-Tupel via Regex + LLM-Vorschlag im Flow) +- `graph-citation-extract/` (`cites`, `verweist_iVm`) +- `text-readability-score/` (Flesch-DE + Verweistiefe) +- `stats-cohort-size/` (DESTATIS-/IHK-Kohorten, Phase 0 mit Stub-Tabelle) +- `graph-shapley-attribution/` (Pfad-anteilige Splittung) +- `econ-skm-score/` (Standardkostenmodell P×F×T×h) +- `law-benefit-score/` (7-dim. Nutzen-Aggregation — das einzige genuin + domänen-spezifische Modul, gehört in `chain-modules-law/`) + +## Sofort starten (Demo-UI, kein Hub nötig) + +```bash +cd app +./setup.sh # Fonts + flutter pub get +./build-macos.sh # Robust-Build (strippt macOS-xattrs vor CodeSign) +open build/macos/Build/Products/Debug/lawheatmap_app.app +``` + +Direkter `flutter run -d macos` funktioniert auch, kann aber bei +einem frischen Checkout an `com.apple.provenance`-xattrs scheitern +(macOS-Eigenheit bei via curl/iCloud erstellten Dateien) — dann +einmal `./build-macos.sh` und danach `flutter run -d macos`. + +Erwartet: Brand-Landing → Klick auf „App öffnen (Demo-Modus)" → +NavigationRail (Heatmap / Normen / Hub / Methodik) mit fünf +Pilot-Norm-Punkten und einem permanenten T4-Demo-Banner. + +Test-Suite (smoke): +```bash +flutter test +``` + +## Modul-Tests (lokal grün ohne Hub) + +```bash +for m in text-akoma-normalize text-deontic-extract \ + graph-citation-extract text-readability-score \ + stats-cohort-size graph-shapley-attribution \ + econ-skm-score law-benefit-score; do + (cd ../fai_chain_modules/$m && cargo test) || break +done +``` + +## WASM-Build (für `chain install`) + +```bash +cd ../fai_chain_modules/text-akoma-normalize +cargo build --release --target wasm32-wasip2 +# Artefakt: target/wasm32-wasip2/release/text_akoma_normalize.wasm +``` + +Selber Aufruf für die anderen sieben Verzeichnisse. + +## Live-Flow (sobald `chain serve` läuft — Phase 0 Woche 1) + +```bash +chain serve & +chain install text.akoma_normalize text.deontic_extract \ + graph.citation_extract text.readability_score \ + stats.cohort_size graph.shapley_attribution \ + econ.skm_score law.benefit_score \ + http.request system.approval +chain run flows/durchstich-gewo-14.yaml \ + --input norm_url=https://www.gesetze-im-internet.de/gewo/__14.xml \ + --input cohort_id=berlin-kmu +chain admin events --limit 10 +chain admin approvals list # Juristen-Gate +``` + +Im Flutter-Client den `MockRepository` durch eine `HubRepository` +(gRPC gegen `localhost:50051`) ersetzen — Interface bleibt gleich, +keine UI-Änderung nötig. + +## Was Phase 0 NICHT enthält (bewusst) + +- gRPC-Anbindung Flutter ↔ Hub (Woche 1) +- Echte NKR-Datenbank-Anbindung (Woche 2-3) +- DESTATIS GENESIS-API-Adapter (Woche 4) +- Juristen-Approval-Backend-Anbindung (Woche 5) +- Volle Akoma-Ntoso-Konvertierung (v0.2) +- HTML- und FORMEX-Adapter für `text.akoma_normalize` (v0.2/0.3) diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/app/.metadata b/app/.metadata new file mode 100644 index 0000000..32850fc --- /dev/null +++ b/app/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "559ffa3f75e7402d65a8def9c28389a9b2e6fe42" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + - platform: macos + create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..241dc2b --- /dev/null +++ b/app/README.md @@ -0,0 +1,17 @@ +# lawheatmap_app + +F∆I Law-Heatmap client + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter) +- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/app/analysis_options.yaml b/app/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/app/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/app/assets/fonts/Inter[opsz,wght].ttf b/app/assets/fonts/Inter[opsz,wght].ttf new file mode 100644 index 0000000..047c92f Binary files /dev/null and b/app/assets/fonts/Inter[opsz,wght].ttf differ diff --git a/app/assets/fonts/JetBrainsMono[wght].ttf b/app/assets/fonts/JetBrainsMono[wght].ttf new file mode 100644 index 0000000..aa310be Binary files /dev/null and b/app/assets/fonts/JetBrainsMono[wght].ttf differ diff --git a/app/assets/fonts/SpaceGrotesk[wght].ttf b/app/assets/fonts/SpaceGrotesk[wght].ttf new file mode 100644 index 0000000..a1b2e6c Binary files /dev/null and b/app/assets/fonts/SpaceGrotesk[wght].ttf differ diff --git a/app/build-macos.sh b/app/build-macos.sh new file mode 100755 index 0000000..87938f6 --- /dev/null +++ b/app/build-macos.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Robust macOS build for the F∆I Law-Heatmap client. +# +# macOS xattrs (especially `com.apple.provenance` on files that +# touched iCloud or were downloaded by curl) make Xcode's +# codesign step reject the .app. This wrapper strips xattrs +# before AND after the build, then re-signs ad-hoc so the +# binary launches without Gatekeeper friction. +# +# Use this instead of bare `flutter build macos --debug` for +# local dev builds. CI on a clean machine doesn't need it. + +set -euo pipefail + +cd "$(dirname "$0")" + +echo "[1/4] strip xattrs (assets, macos, build)" +xattr -cr assets/ 2>/dev/null || true +xattr -cr macos/ 2>/dev/null || true +xattr -cr build/ 2>/dev/null || true + +# The Xcode CodeSign step runs ON its own; if it sees a stale +# xattr it aborts. We disable its built-in signing via env and +# resign afterwards. +echo "[2/4] flutter build macos --debug (codesigning disabled)" +flutter build macos --debug \ + --dart-define=CHAIN_HUB_MODE=mock \ + || { + rc=$? + echo " flutter exited $rc — continuing to xattr+resign step in case the binary linked" + } + +APP=build/macos/Build/Products/Debug/lawheatmap_app.app +if [ ! -d "$APP" ]; then + echo "ERROR: $APP does not exist — flutter build failed before linking" + exit 1 +fi + +echo "[3/4] strip xattrs from built .app" +xattr -cr "$APP" + +echo "[4/4] ad-hoc resign" +/usr/bin/codesign --force --sign - --timestamp=none --deep "$APP" + +echo +echo "done. Launch with:" +echo " open $APP" +echo " # or directly: $APP/Contents/MacOS/lawheatmap_app" diff --git a/app/lib/data/fixtures.dart b/app/lib/data/fixtures.dart new file mode 100644 index 0000000..57fcd25 --- /dev/null +++ b/app/lib/data/fixtures.dart @@ -0,0 +1,274 @@ +import 'models.dart'; + +/// Phase-0 fixture data for the five pilot norms. +/// +/// **THIS IS DEMO DATA.** The numbers are plausible (drawn from +/// public NKR/DIHK/DESTATIS reporting where available), but the +/// composed scores have NOT been peer-reviewed. The UI displays a +/// permanent "Demo / nicht freigegeben" banner whenever this data +/// is on screen — see [HeatmapPage] and the `tierLowest = T4` cap +/// applied to every fixture below. +/// +/// Source pointers per Norm are real and clickable so the +/// methodological audit story stays intact even in demo mode. +class Fixtures { + static final List evaluations = [ + _gewo14, + _kassenSichV, + _dsgvoArt30, + _berlinBauO60, + _miLoG17, + ]; + + static final _gewo14 = Evaluation( + norm: Norm( + eli: 'eli/bund/gewo/14', + title: 'Anzeigepflicht', + jurabk: 'GewO', + paragraph: '§ 14', + standDate: DateTime.utc(2025, 12, 1), + sourceUrl: 'https://www.gesetze-im-internet.de/gewo/__14.html', + sourceSha256: 'demo-${'a' * 60}', + ), + skmEurPerYear: 2_800_000, + benefitScore: 2.5, + affectedCount: 180_000, + frustScore: 4.5, + tierLowest: EvidenceTier.t4, + duties: const [ + Duty( + modality: 'OBLIGATION', + addressee: 'Gewerbetreibende', + action: 'Beginn, Wechsel, Aufgabe eines stehenden Gewerbes anzeigen', + frequency: 'anlassbezogen', + sourceNorm: 'GewO § 14', + authority: 'zuständige Behörde am Niederlassungsort', + ), + ], + sources: const [ + Source( + label: 'BMJ — gesetze-im-internet.de', + url: 'https://www.gesetze-im-internet.de/gewo/__14.html', + tier: EvidenceTier.t1, + ), + Source( + label: 'NKR Bürokratiekosten-Datenbank', + url: 'https://www.normenkontrollrat.bund.de/', + tier: EvidenceTier.t1, + note: 'h-Wert noch nicht NKR-validiert', + ), + Source( + label: 'IHK Berlin — Mittelstandsumfrage 2025', + url: 'https://www.ihk.de/berlin', + tier: EvidenceTier.t2, + ), + ], + auditEventId: 'demo-evt-gewo-14', + notes: + 'Demo-Schätzung. h aus IHK-Beispielwert, P aus DESTATIS Unternehmensregister Berlin.', + ); + + static final _kassenSichV = Evaluation( + norm: Norm( + eli: 'eli/bund/kassensichv', + title: 'Kassensicherungsverordnung', + jurabk: 'KassenSichV', + paragraph: 'gesamt', + standDate: DateTime.utc(2025, 11, 15), + sourceUrl: + 'https://www.gesetze-im-internet.de/kassensichv/index.html', + sourceSha256: 'demo-${'b' * 60}', + ), + skmEurPerYear: 25_000_000, + benefitScore: 3.0, + affectedCount: 500_000, + frustScore: 7.5, + tierLowest: EvidenceTier.t4, + duties: const [ + Duty( + modality: 'OBLIGATION', + addressee: 'Betreiber elektronischer Kassen', + action: 'TSE-Zertifizierte Sicherheitseinrichtung einsetzen', + frequency: 'kontinuierlich', + sourceNorm: 'KassenSichV § 1–4', + authority: 'Finanzamt / BSI', + consequence: 'Bußgeld bis 25.000 €', + ), + Duty( + modality: 'OBLIGATION', + addressee: 'Betreiber', + action: 'TSE-Ausfall an Finanzamt melden', + frequency: 'anlassbezogen', + sourceNorm: 'KassenSichV § 3', + authority: 'Finanzamt', + ), + ], + sources: const [ + Source( + label: 'BMJ — gesetze-im-internet.de', + url: + 'https://www.gesetze-im-internet.de/kassensichv/index.html', + tier: EvidenceTier.t1, + ), + Source( + label: 'DIHK — Bürokratie-Index 2025', + url: 'https://www.dihk.de/buerokratie', + tier: EvidenceTier.t2, + ), + Source( + label: 'DEHOGA Berlin Mittelstandsumfrage', + url: 'https://www.dehoga-berlin.de/', + tier: EvidenceTier.t2, + note: 'h-Wert aus Gastronomie-Stichprobe', + ), + ], + auditEventId: 'demo-evt-kassensichv', + notes: + 'Demo. Hoher Frust-Score wegen TSE-Ausfallmeldung + Hardware-Folgekosten.', + ); + + static final _dsgvoArt30 = Evaluation( + norm: Norm( + eli: 'eli/eu/reg/2016/679/art30', + title: 'Verzeichnis von Verarbeitungstätigkeiten', + jurabk: 'DSGVO', + paragraph: 'Art. 30', + standDate: DateTime.utc(2025, 10, 1), + sourceUrl: 'https://eur-lex.europa.eu/eli/reg/2016/679', + sourceSha256: 'demo-${'c' * 60}', + ), + skmEurPerYear: 450_000_000, + benefitScore: 3.8, + affectedCount: 3_000_000, + frustScore: 8.5, + tierLowest: EvidenceTier.t4, + duties: const [ + Duty( + modality: 'OBLIGATION', + addressee: 'Verantwortliche + Auftragsverarbeiter', + action: 'Verzeichnis aller Verarbeitungstätigkeiten führen', + frequency: 'jährlich überprüfen', + sourceNorm: 'DSGVO Art. 30 Abs. 1+2', + authority: 'Aufsichtsbehörde (auf Verlangen)', + consequence: 'Bußgeld bis 10 Mio € / 2 % Jahresumsatz', + ), + ], + sources: const [ + Source( + label: 'EUR-Lex — DSGVO Art. 30', + url: 'https://eur-lex.europa.eu/eli/reg/2016/679', + tier: EvidenceTier.t1, + ), + Source( + label: 'DIHK — DSGVO-Belastung der Wirtschaft 2024', + url: 'https://www.dihk.de/dsgvo', + tier: EvidenceTier.t2, + ), + Source( + label: 'BfDI — Tätigkeitsbericht', + url: 'https://www.bfdi.bund.de/', + tier: EvidenceTier.t1, + ), + ], + auditEventId: 'demo-evt-dsgvo-30', + notes: + 'Demo. EU-determiniert → Reform-Vorschlag muss Brüssel adressieren, nicht DE-Streichung.', + ); + + static final _berlinBauO60 = Evaluation( + norm: Norm( + eli: 'eli/land/be/bauo/60', + title: 'Bauantrag', + jurabk: 'BauO Bln', + paragraph: '§ 60 ff', + standDate: DateTime.utc(2025, 9, 1), + sourceUrl: + 'https://gesetze.berlin.de/bsbe/document/jlr-BauOBE2005V25P60', + sourceSha256: 'demo-${'d' * 60}', + ), + skmEurPerYear: 72_000_000, + benefitScore: 3.5, + affectedCount: 15_000, + frustScore: 9.0, + tierLowest: EvidenceTier.t4, + duties: const [ + Duty( + modality: 'OBLIGATION', + addressee: 'Bauherr', + action: 'Bauantrag mit Bauvorlagen einreichen', + frequency: 'pro Bauvorhaben', + sourceNorm: 'BauO Bln § 60–69', + authority: 'Bauaufsichtsbehörde Bezirk', + ), + ], + sources: const [ + Source( + label: 'landesrecht-berlin.de', + url: + 'https://gesetze.berlin.de/bsbe/document/jlr-BauOBE2005V25P60', + tier: EvidenceTier.t1, + ), + Source( + label: 'Handwerkskammer Berlin — Bauantragsdauer 2025', + url: 'https://www.hwk-berlin.de/', + tier: EvidenceTier.t2, + ), + Source( + label: 'mein.berlin.de — Bürgerbeteiligung Bauverfahren', + url: 'https://mein.berlin.de/', + tier: EvidenceTier.t4, + ), + ], + auditEventId: 'demo-evt-berlinbauo-60', + notes: + 'Demo. Höchster Frust-Score (Verweistiefe + Behörden-Pluralität Bezirk×Senat).', + ); + + static final _miLoG17 = Evaluation( + norm: Norm( + eli: 'eli/bund/milog/17', + title: 'Dokumentationspflichten', + jurabk: 'MiLoG', + paragraph: '§ 17', + standDate: DateTime.utc(2025, 11, 1), + sourceUrl: 'https://www.gesetze-im-internet.de/milog/__17.html', + sourceSha256: 'demo-${'e' * 60}', + ), + skmEurPerYear: 270_000_000, + benefitScore: 3.2, + affectedCount: 3_000_000, + frustScore: 7.0, + tierLowest: EvidenceTier.t4, + duties: const [ + Duty( + modality: 'OBLIGATION', + addressee: 'Arbeitgeber in MiLoG-relevanten Branchen', + action: 'Beginn, Ende, Dauer der täglichen Arbeitszeit aufzeichnen', + frequency: 'arbeitstäglich', + sourceNorm: 'MiLoG § 17 Abs. 1', + authority: 'Zoll (FKS)', + consequence: 'Bußgeld bis 30.000 €', + ), + ], + sources: const [ + Source( + label: 'BMJ — gesetze-im-internet.de', + url: 'https://www.gesetze-im-internet.de/milog/__17.html', + tier: EvidenceTier.t1, + ), + Source( + label: 'NKR — Stellungnahme Mindestlohngesetz', + url: 'https://www.normenkontrollrat.bund.de/', + tier: EvidenceTier.t1, + ), + Source( + label: 'ZDH — Bürokratiekosten Handwerk', + url: 'https://www.zdh.de/', + tier: EvidenceTier.t2, + ), + ], + auditEventId: 'demo-evt-milog-17', + notes: + 'Demo. P × F × T × h: 3 Mio Betroffene × 220 Arbeitstage × 0,1h × 30€/h.', + ); +} diff --git a/app/lib/data/models.dart b/app/lib/data/models.dart new file mode 100644 index 0000000..d439635 --- /dev/null +++ b/app/lib/data/models.dart @@ -0,0 +1,144 @@ +// Domain model for the F∆I Law-Heatmap client. +// +// These types are the lingua franca between the (eventually +// gRPC-fed) repository and the UI. Names mirror the Studie's +// §5.3 data model. + +/// Evidence tier of the lowest-tier ingredient in a value. +/// Per Studie §6.7: "Regel der niedrigsten Stufe" — any composed +/// figure carries the tier of its weakest input. +enum EvidenceTier { + /// Official / peer-reviewed: NKR, DESTATIS, ifo, ZEW. + t1, + + /// Verbands-Erhebung: DIHK, BDI, ZDH, IHK. + t2, + + /// Eigene strukturierte Erhebung (Phase 2/3). + t3, + + /// Qualitatives Signal — pain indicator only, never magnitude. + t4, +} + +extension EvidenceTierLabel on EvidenceTier { + String get short => switch (this) { + EvidenceTier.t1 => 'T1', + EvidenceTier.t2 => 'T2', + EvidenceTier.t3 => 'T3', + EvidenceTier.t4 => 'T4', + }; + + String get description => switch (this) { + EvidenceTier.t1 => 'amtlich / peer-reviewed', + EvidenceTier.t2 => 'Verband', + EvidenceTier.t3 => 'eigene Erhebung', + EvidenceTier.t4 => 'qualitatives Signal', + }; +} + +/// A citable source — never an LLM, always a public document or +/// dataset reference. UI surfaces these next to every figure. +class Source { + const Source({ + required this.label, + required this.url, + required this.tier, + this.note, + }); + + final String label; + final String url; + final EvidenceTier tier; + final String? note; +} + +/// One atomic obligation (Pflicht) extracted from a norm. +/// Tuple shape matches Studie §6.3. +class Duty { + const Duty({ + required this.modality, + required this.addressee, + required this.action, + required this.frequency, + required this.sourceNorm, + this.authority, + this.consequence, + }); + + final String modality; // OBLIGATION | PROHIBITION | … + final String addressee; + final String action; + final String frequency; + final String sourceNorm; + final String? authority; + final String? consequence; +} + +/// One norm version — a versioned legal text identified by its +/// (synthetic for now) ELI/CELEX. +class Norm { + const Norm({ + required this.eli, + required this.title, + required this.jurabk, + required this.paragraph, + required this.standDate, + required this.sourceUrl, + required this.sourceSha256, + }); + + final String eli; + final String title; + final String jurabk; + final String paragraph; + final DateTime standDate; + final String sourceUrl; + final String sourceSha256; +} + +/// A heatmap point: one norm with its computed scores. +/// Axes mirror §6 in the study. +class Evaluation { + const Evaluation({ + required this.norm, + required this.skmEurPerYear, + required this.benefitScore, + required this.affectedCount, + required this.frustScore, + required this.tierLowest, + required this.duties, + required this.sources, + required this.auditEventId, + this.notes, + }); + + final Norm norm; + + /// Yearly bureaucracy cost across the addressee population. + /// €/year, computed via SKM (`P × F × T × h`). + final double skmEurPerYear; + + /// 0–5, composite of seven dimensions + /// (safety / market / legal-certainty / EU-harmony / …). + final double benefitScore; + + /// Number of addressees (KMU, Bürger, etc.). UI uses this for + /// heatmap point size. + final int affectedCount; + + /// 0–10, composite of readability + Verweistiefe + Akteurs- + /// pluralität + Norm-Volatilität. + final double frustScore; + + /// Lowest tier among all ingredients of any displayed figure. + final EvidenceTier tierLowest; + + final List duties; + final List sources; + + /// Pointer into the Ch∆In audit log (SQLite event_log row). + final String auditEventId; + + final String? notes; +} diff --git a/app/lib/data/repository.dart b/app/lib/data/repository.dart new file mode 100644 index 0000000..1ef272b --- /dev/null +++ b/app/lib/data/repository.dart @@ -0,0 +1,38 @@ +import 'models.dart'; +import 'fixtures.dart'; + +/// Repository abstraction for evaluations. +/// +/// Phase 0 ships exactly one implementation: [MockRepository], +/// backed by [Fixtures]. The live implementation (gRPC against a +/// running `chain serve`) lands in Phase 0 week 1 — its API will +/// match this interface so no UI change is needed. +abstract class EvaluationRepository { + Future> list(); + Future byEli(String eli); + String get mode; + String get hubLabel; +} + +/// In-memory, no-network implementation. Marks itself clearly so +/// the UI shows a "demo mode" badge. +class MockRepository implements EvaluationRepository { + const MockRepository(); + + @override + Future> list() async => Fixtures.evaluations; + + @override + Future byEli(String eli) async { + for (final e in Fixtures.evaluations) { + if (e.norm.eli == eli) return e; + } + return null; + } + + @override + String get mode => 'demo'; + + @override + String get hubLabel => 'in-memory fixtures (kein Hub)'; +} diff --git a/app/lib/main.dart b/app/lib/main.dart new file mode 100644 index 0000000..3488de0 --- /dev/null +++ b/app/lib/main.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; + +import 'data/repository.dart'; +import 'pages/landing_page.dart'; +import 'theme/lawheatmap_theme.dart'; + +void main() { + runApp(const LawHeatmapApp()); +} + +class LawHeatmapApp extends StatelessWidget { + const LawHeatmapApp({super.key}); + + @override + Widget build(BuildContext context) { + // Phase 0: in-memory fixtures only. Swap to a HubRepository + // (gRPC against `chain serve`) without changing the UI. + const EvaluationRepository repository = MockRepository(); + + return MaterialApp( + title: 'F∆I Law-Heatmap', + debugShowCheckedModeBanner: false, + theme: LawHeatmapTheme.light(), + darkTheme: LawHeatmapTheme.dark(), + themeMode: ThemeMode.dark, + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('de'), Locale('en')], + home: const LandingPage(repository: repository), + ); + } +} diff --git a/app/lib/pages/about_page.dart b/app/lib/pages/about_page.dart new file mode 100644 index 0000000..c217db6 --- /dev/null +++ b/app/lib/pages/about_page.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; + +import '../theme/lawheatmap_tokens.dart'; +import '../widgets/lawheatmap_card.dart'; + +class AboutPage extends StatelessWidget { + const AboutPage({super.key}); + + @override + Widget build(BuildContext context) { + final t = Theme.of(context).textTheme; + return SingleChildScrollView( + padding: const EdgeInsets.all(LawHeatmapSpace.xl), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Methodik & Quellen', style: t.displaySmall), + const SizedBox(height: LawHeatmapSpace.xs), + Text( + 'Wie das System bewertet, woran es sich messen lässt.', + style: t.bodyLarge?.copyWith(color: LawHeatmapColors.mute), + ), + const SizedBox(height: LawHeatmapSpace.lg), + LawHeatmapCard( + accent: true, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Drei nicht verhandelbare Sätze', + style: t.headlineSmall), + const SizedBox(height: LawHeatmapSpace.md), + Text( + '1. Wir bewerten Pflichten, nicht Gesetze.\n' + '2. Jede Zahl trägt die Tier-Stufe ihres schwächsten ' + 'Bestandteils. Kein figürlich-runder Wert ohne Quelle.\n' + '3. Kein Output verlässt das System ohne Juristen-Gate ' + '(`system.approval@^0`).', + style: t.bodyLarge?.copyWith(height: 1.7), + ), + ], + ), + ), + const SizedBox(height: LawHeatmapSpace.lg), + LawHeatmapCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Methodische Vorbilder', style: t.headlineSmall), + const SizedBox(height: LawHeatmapSpace.sm), + _bullet(t, + 'Standardkostenmodell (SKM) — Nationaler Normenkontrollrat. P × F × T × h.'), + _bullet(t, + 'Coupette / Katz / Bommarito 2021 — Netzwerkanalyse deutsches Bundesrecht (Graph-Schema).'), + _bullet(t, + 'Mercatus QuantGov / RegData — NLP-basierte Pflicht-Klassifikation (must/shall/may).'), + _bullet(t, + 'EU Better Regulation Toolbox + REFIT — Cost-Benefit + Evaluations-Methodik.'), + _bullet(t, + 'Akoma Ntoso 1.0 (OASIS) + LegalRuleML — XML-Format und Deontik-Datenmodell.'), + _bullet(t, + 'UK Better Regulation Framework — Regulatory Policy Committee als unabhängige Letzt-Validierung.'), + ], + ), + ), + const SizedBox(height: LawHeatmapSpace.lg), + LawHeatmapCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('4-Tier-Evidenz-Modell', style: t.headlineSmall), + const SizedBox(height: LawHeatmapSpace.sm), + _bullet(t, + 'T1 — amtlich/peer-reviewed: NKR, DESTATIS, ifo, ZEW, BMWK, SVR, IfM Bonn.'), + _bullet(t, + 'T2 — Verbands-Erhebung: DIHK, IHK Berlin, ZDH, BDI Mittelstand, BVMW, HDE.'), + _bullet(t, + 'T3 — eigene strukturierte Erhebung (Phase 2/3, DSGVO-anonymisiert).'), + _bullet(t, + 'T4 — qualitatives Signal: Petitionen, openPetition, mein.berlin.de, Foren — nur Pain-Indikator.'), + ], + ), + ), + const SizedBox(height: LawHeatmapSpace.lg), + LawHeatmapCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Plattform', style: t.headlineSmall), + const SizedBox(height: LawHeatmapSpace.sm), + Text( + 'Ch∆In v0.16.0 (vormals F∆I-Plattform) — Vendor: ' + 'F∆I = Flemming.AI. Hub. Module. Flow. Deterministisch, ' + 'WASM-sandboxed, tamper-evident Audit-Log ' + '(CHAIN-EVENT-V3), Apache 2.0, CRA-ready.', + style: t.bodyLarge?.copyWith(height: 1.6), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _bullet(TextTheme t, String text) { + return Padding( + padding: const EdgeInsets.only(bottom: LawHeatmapSpace.sm), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 6, right: 10), + child: Container( + width: 5, + height: 5, + decoration: const BoxDecoration( + color: LawHeatmapColors.signal, + shape: BoxShape.circle, + ), + ), + ), + Expanded( + child: Text(text, style: t.bodyLarge?.copyWith(height: 1.6)), + ), + ], + ), + ); + } +} diff --git a/app/lib/pages/heatmap_page.dart b/app/lib/pages/heatmap_page.dart new file mode 100644 index 0000000..37dc7f1 --- /dev/null +++ b/app/lib/pages/heatmap_page.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; + +import '../data/models.dart'; +import '../data/repository.dart'; +import '../theme/lawheatmap_tokens.dart'; +import '../widgets/demo_banner.dart'; +import '../widgets/heatmap_grid.dart'; +import '../widgets/lawheatmap_card.dart'; +import '../widgets/tier_badge.dart'; +import 'norm_detail_page.dart'; + +class HeatmapPage extends StatefulWidget { + const HeatmapPage({super.key, required this.repository}); + + final EvaluationRepository repository; + + @override + State createState() => _HeatmapPageState(); +} + +class _HeatmapPageState extends State { + late Future> _future; + + @override + void initState() { + super.initState(); + _future = widget.repository.list(); + } + + @override + Widget build(BuildContext context) { + final t = Theme.of(context).textTheme; + return Column( + children: [ + const DemoBanner(), + Expanded( + child: Padding( + padding: const EdgeInsets.all(LawHeatmapSpace.xl), + child: FutureBuilder>( + future: _future, + builder: (context, snap) { + if (!snap.hasData) { + return const Center( + child: CircularProgressIndicator()); + } + final items = snap.data!; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Heatmap', style: t.displaySmall), + const SizedBox(height: LawHeatmapSpace.xs), + Text( + 'X: jährliche Bürokratiekosten (€, log10). ' + 'Y: Nutzen-Score 0–5. Punktgröße: Betroffenheit. ' + 'Klick auf einen Punkt → Detail mit Pflichten + Quellen.', + style: t.bodyLarge?.copyWith( + color: LawHeatmapColors.mute, + ), + ), + const SizedBox(height: LawHeatmapSpace.lg), + Expanded( + child: LawHeatmapCard( + accent: true, + expand: true, + child: HeatmapGrid( + evaluations: items, + onTap: (e) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => NormDetailPage( + repository: widget.repository, + evaluation: e, + ), + ), + ); + }, + ), + ), + ), + const SizedBox(height: LawHeatmapSpace.lg), + Wrap( + spacing: LawHeatmapSpace.md, + runSpacing: LawHeatmapSpace.sm, + children: [ + for (final e in items) + InkWell( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => NormDetailPage( + repository: widget.repository, + evaluation: e, + ), + ), + ); + }, + child: LawHeatmapCard( + padding: const EdgeInsets.symmetric( + horizontal: LawHeatmapSpace.md, + vertical: LawHeatmapSpace.sm, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + TierBadge( + tier: e.tierLowest, compact: true), + const SizedBox( + width: LawHeatmapSpace.sm), + Text( + '${e.norm.jurabk} ${e.norm.paragraph}', + style: t.labelLarge, + ), + ], + ), + ), + ), + ], + ), + ], + ); + }, + ), + ), + ), + ], + ); + } +} diff --git a/app/lib/pages/hub_status_page.dart b/app/lib/pages/hub_status_page.dart new file mode 100644 index 0000000..49a1c47 --- /dev/null +++ b/app/lib/pages/hub_status_page.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; + +import '../data/repository.dart'; +import '../theme/lawheatmap_tokens.dart'; +import '../widgets/lawheatmap_card.dart'; + +class HubStatusPage extends StatelessWidget { + const HubStatusPage({super.key, required this.repository}); + + final EvaluationRepository repository; + + @override + Widget build(BuildContext context) { + final t = Theme.of(context).textTheme; + return SingleChildScrollView( + padding: const EdgeInsets.all(LawHeatmapSpace.xl), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Ch∆In Hub', style: t.displaySmall), + const SizedBox(height: LawHeatmapSpace.xs), + Text( + 'Status der Verbindung zum lokalen Hub und des Audit-Logs.', + style: t.bodyLarge?.copyWith(color: LawHeatmapColors.mute), + ), + const SizedBox(height: LawHeatmapSpace.lg), + LawHeatmapCard( + accent: true, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _row('Modus', repository.mode.toUpperCase(), t), + _row('Quelle', repository.hubLabel, t), + _row('gRPC-Endpunkt', '— (Phase-0-Stub)', t), + _row('Audit-DB', '~/.chain/chain.db (geplant)', t), + _row('Hash-Tag', 'CHAIN-EVENT-V3', t), + ], + ), + ), + const SizedBox(height: LawHeatmapSpace.lg), + LawHeatmapCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Geplante Live-Anbindung', style: t.headlineSmall), + const SizedBox(height: LawHeatmapSpace.sm), + Text( + '1. `chain serve` startet lokal auf Port 50051.\n' + '2. Diese App liest `~/.chain/hub-auth-token` und ' + 'verbindet via gRPC.\n' + '3. Capability `text.akoma_normalize@^0` muss ' + 'installiert sein (`chain install text.akoma_normalize`).\n' + '4. Pilot-Flow `flows/durchstich-gewo-14.yaml` läuft ' + 'gegen GewO §14.\n' + '5. Jedes step.completed-Event landet im Audit-Log; ' + 'die Repository-Implementierung tauscht von MockRepository ' + 'auf HubRepository (gleiches Interface, kein UI-Wechsel).', + style: t.bodyLarge?.copyWith( + height: 1.6, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _row(String label, String value, TextTheme t) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + SizedBox( + width: 160, + child: Text(label, + style: t.labelLarge + ?.copyWith(color: LawHeatmapColors.mute)), + ), + Expanded( + child: Text(value, + style: t.bodyLarge + ?.copyWith(fontFamily: LawHeatmapTypography.mono)), + ), + ], + ), + ); + } +} diff --git a/app/lib/pages/landing_page.dart b/app/lib/pages/landing_page.dart new file mode 100644 index 0000000..a10745d --- /dev/null +++ b/app/lib/pages/landing_page.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; + +import '../data/repository.dart'; +import '../theme/lawheatmap_theme.dart'; +import '../theme/lawheatmap_tokens.dart'; +import '../widgets/delta_mark.dart'; +import 'shell_page.dart'; + +/// First-run landing: brand mark, one-line claim, "enter app" CTA. +/// Mock-mode bypasses Hub connect; the live wire lands in week 1. +class LandingPage extends StatelessWidget { + const LandingPage({super.key, required this.repository}); + + final EvaluationRepository repository; + + @override + Widget build(BuildContext context) { + final t = Theme.of(context).textTheme; + return Scaffold( + body: SafeArea( + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 720), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: LawHeatmapSpace.xxl, + vertical: LawHeatmapSpace.xxxl, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const DeltaMark(size: 96), + const SizedBox(height: LawHeatmapSpace.xxl), + Text('F∆I Law-Heatmap', style: t.displaySmall), + const SizedBox(height: LawHeatmapSpace.sm), + Text( + 'Pflicht-basierte Wirkungsanalyse von Rechtsnormen — ' + 'auf Ch∆In, mit Quellen-Audit by construction.', + style: t.bodyLarge + ?.copyWith(color: LawHeatmapColors.mute), + ), + const SizedBox(height: LawHeatmapSpace.xxl), + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: LawHeatmapSpace.md, + runSpacing: LawHeatmapSpace.sm, + children: [ + FilledButton.icon( + onPressed: () { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (_) => + ShellPage(repository: repository), + ), + ); + }, + icon: const Icon(Icons.east), + label: const Text('App öffnen (Demo-Modus)'), + ), + Text( + 'Demo: 5 Pilot-Normen, ohne Hub-Verbindung', + style: t.labelSmall?.copyWith( + color: LawHeatmapColors.mute, + ), + ), + ], + ), + const SizedBox(height: LawHeatmapSpace.xl), + Text( + 'v0.1.0 · Phase 0 · Mock-Modus · ' + 'Live-Hub-Anbindung folgt in Woche 1', + style: LawHeatmapTheme.mono( + t.labelSmall ?? const TextStyle(), + ).copyWith(color: LawHeatmapColors.mute), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/app/lib/pages/norm_detail_page.dart b/app/lib/pages/norm_detail_page.dart new file mode 100644 index 0000000..e94accb --- /dev/null +++ b/app/lib/pages/norm_detail_page.dart @@ -0,0 +1,271 @@ +import 'package:flutter/material.dart'; + +import '../data/models.dart'; +import '../data/repository.dart'; +import '../theme/lawheatmap_tokens.dart'; +import '../widgets/demo_banner.dart'; +import '../widgets/evidence_sidebar.dart'; +import '../widgets/lawheatmap_card.dart'; +import '../widgets/tier_badge.dart'; + +class NormDetailPage extends StatelessWidget { + const NormDetailPage({ + super.key, + required this.repository, + required this.evaluation, + }); + + final EvaluationRepository repository; + final Evaluation evaluation; + + @override + Widget build(BuildContext context) { + final t = Theme.of(context).textTheme; + final e = evaluation; + return Scaffold( + appBar: AppBar( + title: Text('${e.norm.jurabk} ${e.norm.paragraph}'), + ), + body: Column( + children: [ + const DemoBanner(), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(LawHeatmapSpace.xl), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text(e.norm.title, + style: t.displaySmall), + const SizedBox(height: 4), + SelectableText( + e.norm.eli, + style: t.bodyLarge?.copyWith( + fontFamily: + LawHeatmapTypography.mono, + color: LawHeatmapColors.mute, + ), + ), + ], + ), + ), + TierBadge(tier: e.tierLowest), + ], + ), + const SizedBox(height: LawHeatmapSpace.lg), + _MetricsRow(e: e), + const SizedBox(height: LawHeatmapSpace.lg), + LawHeatmapCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Pflichten', + style: t.headlineSmall), + const SizedBox( + height: LawHeatmapSpace.sm), + Text( + 'Atomare deontische Aussagen, extrahiert ' + 'aus dem Norm-Text (Phase 1: regex + LLM-' + 'Vorschlag, Juristen-Review obligatorisch).', + style: t.labelSmall?.copyWith( + color: LawHeatmapColors.mute), + ), + const SizedBox( + height: LawHeatmapSpace.md), + for (final d in e.duties) + _DutyTile(duty: d), + ], + ), + ), + if (e.notes != null) ...[ + const SizedBox(height: LawHeatmapSpace.lg), + LawHeatmapCard( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text('Methodik-Notiz', + style: t.headlineSmall), + const SizedBox( + height: LawHeatmapSpace.sm), + Text(e.notes!, style: t.bodyLarge), + ], + ), + ), + ], + ], + ), + ), + const SizedBox(width: LawHeatmapSpace.xl), + SizedBox( + width: 340, + child: EvidenceSidebar(evaluation: e), + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _MetricsRow extends StatelessWidget { + const _MetricsRow({required this.e}); + final Evaluation e; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + _MetricBox( + label: 'Schaden', + value: _eur(e.skmEurPerYear), + sub: 'SKM P×F×T×h', + ), + const SizedBox(width: LawHeatmapSpace.md), + _MetricBox( + label: 'Nutzen', + value: '${e.benefitScore.toStringAsFixed(1)} / 5', + sub: '7-dim. Score', + ), + const SizedBox(width: LawHeatmapSpace.md), + _MetricBox( + label: 'Betroffene', + value: _kmu(e.affectedCount), + sub: 'DESTATIS', + ), + const SizedBox(width: LawHeatmapSpace.md), + _MetricBox( + label: 'Frust', + value: '${e.frustScore.toStringAsFixed(1)} / 10', + sub: 'Lesbarkeit + Tiefe', + ), + ], + ); + } + + static String _eur(double v) { + if (v >= 1e9) return '${(v / 1e9).toStringAsFixed(2)} Mrd €'; + if (v >= 1e6) return '${(v / 1e6).toStringAsFixed(1)} Mio €'; + if (v >= 1e3) return '${(v / 1e3).toStringAsFixed(0)} k €'; + return '${v.toStringAsFixed(0)} €'; + } + + static String _kmu(int n) { + if (n >= 1_000_000) return '${(n / 1e6).toStringAsFixed(1)} Mio'; + if (n >= 1_000) return '${(n / 1e3).toStringAsFixed(0)} k'; + return n.toString(); + } +} + +class _MetricBox extends StatelessWidget { + const _MetricBox({ + required this.label, + required this.value, + required this.sub, + }); + final String label; + final String value; + final String sub; + + @override + Widget build(BuildContext context) { + final t = Theme.of(context).textTheme; + return Expanded( + child: LawHeatmapCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, + style: t.labelSmall + ?.copyWith(color: LawHeatmapColors.mute)), + const SizedBox(height: 4), + Text(value, style: t.displaySmall), + const SizedBox(height: 4), + Text(sub, + style: t.labelSmall + ?.copyWith(color: LawHeatmapColors.mute)), + ], + ), + ), + ); + } +} + +class _DutyTile extends StatelessWidget { + const _DutyTile({required this.duty}); + final Duty duty; + + @override + Widget build(BuildContext context) { + final t = Theme.of(context).textTheme; + return Padding( + padding: const EdgeInsets.only(bottom: LawHeatmapSpace.md), + child: Container( + padding: const EdgeInsets.all(LawHeatmapSpace.md), + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.12)), + borderRadius: const BorderRadius.all(LawHeatmapRadius.sm), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: LawHeatmapColors.signal.withValues(alpha: 0.18), + borderRadius: + const BorderRadius.all(LawHeatmapRadius.sm), + ), + child: Text( + duty.modality, + style: t.labelSmall?.copyWith( + color: LawHeatmapColors.signal, + fontWeight: FontWeight.w600, + ), + ), + ), + const SizedBox(width: LawHeatmapSpace.sm), + Text(duty.sourceNorm, + style: t.labelSmall?.copyWith( + fontFamily: LawHeatmapTypography.mono, + color: LawHeatmapColors.mute, + )), + ]), + const SizedBox(height: LawHeatmapSpace.sm), + Text(duty.action, style: t.bodyLarge), + const SizedBox(height: 4), + Text( + 'Adressat: ${duty.addressee} · ' + 'Frequenz: ${duty.frequency}' + '${duty.authority != null ? " · Behörde: ${duty.authority}" : ""}' + '${duty.consequence != null ? " · Sanktion: ${duty.consequence}" : ""}', + style: t.labelSmall?.copyWith(color: LawHeatmapColors.mute), + ), + ], + ), + ), + ); + } +} diff --git a/app/lib/pages/norms_list_page.dart b/app/lib/pages/norms_list_page.dart new file mode 100644 index 0000000..9f0d9b2 --- /dev/null +++ b/app/lib/pages/norms_list_page.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; + +import '../data/models.dart'; +import '../data/repository.dart'; +import '../theme/lawheatmap_tokens.dart'; +import '../widgets/demo_banner.dart'; +import '../widgets/lawheatmap_card.dart'; +import '../widgets/tier_badge.dart'; +import 'norm_detail_page.dart'; + +class NormsListPage extends StatelessWidget { + const NormsListPage({super.key, required this.repository}); + + final EvaluationRepository repository; + + @override + Widget build(BuildContext context) { + final t = Theme.of(context).textTheme; + return Column( + children: [ + const DemoBanner(), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(LawHeatmapSpace.xl), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Normen-Pilotkorpus', style: t.displaySmall), + const SizedBox(height: LawHeatmapSpace.xs), + Text( + '5 Normen für den vertikalen Durchstich. ' + 'Korpus erweiterbar in Phase 1.', + style: t.bodyLarge?.copyWith( + color: LawHeatmapColors.mute, + ), + ), + const SizedBox(height: LawHeatmapSpace.lg), + FutureBuilder>( + future: repository.list(), + builder: (context, snap) { + if (!snap.hasData) { + return const Center( + child: CircularProgressIndicator()); + } + return Column( + children: [ + for (final e in snap.data!) + Padding( + padding: const EdgeInsets.only( + bottom: LawHeatmapSpace.md), + child: _NormRow( + evaluation: e, + repository: repository, + ), + ), + ], + ); + }, + ), + ], + ), + ), + ), + ], + ); + } +} + +class _NormRow extends StatelessWidget { + const _NormRow({required this.evaluation, required this.repository}); + + final Evaluation evaluation; + final EvaluationRepository repository; + + @override + Widget build(BuildContext context) { + final t = Theme.of(context).textTheme; + return InkWell( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => NormDetailPage( + repository: repository, + evaluation: evaluation, + ), + ), + ); + }, + child: LawHeatmapCard( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 4, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${evaluation.norm.jurabk} ${evaluation.norm.paragraph} — ${evaluation.norm.title}', + style: t.titleMedium, + ), + const SizedBox(height: 4), + Text( + evaluation.norm.eli, + style: t.labelSmall?.copyWith( + fontFamily: LawHeatmapTypography.mono, + color: LawHeatmapColors.mute, + ), + ), + ], + ), + ), + const SizedBox(width: LawHeatmapSpace.lg), + Expanded( + child: _MetricColumn( + label: 'Schaden / Jahr', + value: _eur(evaluation.skmEurPerYear), + ), + ), + Expanded( + child: _MetricColumn( + label: 'Nutzen', + value: + '${evaluation.benefitScore.toStringAsFixed(1)} / 5', + ), + ), + Expanded( + child: _MetricColumn( + label: 'Betroffene', + value: _kmu(evaluation.affectedCount), + ), + ), + const SizedBox(width: LawHeatmapSpace.md), + TierBadge(tier: evaluation.tierLowest), + ], + ), + ), + ); + } + + String _eur(double v) { + if (v >= 1e9) return '${(v / 1e9).toStringAsFixed(2)} Mrd €'; + if (v >= 1e6) return '${(v / 1e6).toStringAsFixed(1)} Mio €'; + if (v >= 1e3) return '${(v / 1e3).toStringAsFixed(0)} k €'; + return '${v.toStringAsFixed(0)} €'; + } + + String _kmu(int n) { + if (n >= 1_000_000) return '${(n / 1e6).toStringAsFixed(1)} Mio'; + if (n >= 1_000) return '${(n / 1e3).toStringAsFixed(0)} k'; + return n.toString(); + } +} + +class _MetricColumn extends StatelessWidget { + const _MetricColumn({required this.label, required this.value}); + + final String label; + final String value; + + @override + Widget build(BuildContext context) { + final t = Theme.of(context).textTheme; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: t.labelSmall?.copyWith(color: LawHeatmapColors.mute), + ), + const SizedBox(height: 2), + Text(value, style: t.titleMedium), + ], + ); + } +} diff --git a/app/lib/pages/shell_page.dart b/app/lib/pages/shell_page.dart new file mode 100644 index 0000000..75dc952 --- /dev/null +++ b/app/lib/pages/shell_page.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; + +import '../data/repository.dart'; +import '../theme/lawheatmap_tokens.dart'; +import '../widgets/delta_mark.dart'; +import 'about_page.dart'; +import 'heatmap_page.dart'; +import 'hub_status_page.dart'; +import 'norms_list_page.dart'; + +/// App shell with a NavigationRail. Each rail destination is one +/// page; pages do their own scrolling. +class ShellPage extends StatefulWidget { + const ShellPage({super.key, required this.repository}); + + final EvaluationRepository repository; + + @override + State createState() => _ShellPageState(); +} + +class _ShellPageState extends State { + int _index = 0; + + @override + Widget build(BuildContext context) { + final t = Theme.of(context).textTheme; + final pages = [ + HeatmapPage(repository: widget.repository), + NormsListPage(repository: widget.repository), + HubStatusPage(repository: widget.repository), + const AboutPage(), + ]; + + return Scaffold( + body: Row( + children: [ + NavigationRail( + backgroundColor: Theme.of(context).colorScheme.surface, + extended: MediaQuery.of(context).size.width > 1100, + selectedIndex: _index, + onDestinationSelected: (i) => setState(() => _index = i), + leading: Padding( + padding: const EdgeInsets.symmetric( + vertical: LawHeatmapSpace.lg, + horizontal: LawHeatmapSpace.md, + ), + child: Column( + children: [ + const DeltaMark(size: 36), + const SizedBox(height: LawHeatmapSpace.sm), + Text( + 'F∆I', + style: t.labelLarge?.copyWith( + letterSpacing: 2, + color: LawHeatmapColors.signal, + ), + ), + ], + ), + ), + destinations: const [ + NavigationRailDestination( + icon: Icon(Icons.grid_view_outlined), + selectedIcon: Icon(Icons.grid_view), + label: Text('Heatmap'), + ), + NavigationRailDestination( + icon: Icon(Icons.list_alt_outlined), + selectedIcon: Icon(Icons.list_alt), + label: Text('Normen'), + ), + NavigationRailDestination( + icon: Icon(Icons.hub_outlined), + selectedIcon: Icon(Icons.hub), + label: Text('Hub'), + ), + NavigationRailDestination( + icon: Icon(Icons.info_outline), + selectedIcon: Icon(Icons.info), + label: Text('Methodik'), + ), + ], + ), + const VerticalDivider(width: 1), + Expanded(child: pages[_index]), + ], + ), + ); + } +} diff --git a/app/lib/theme/lawheatmap_theme.dart b/app/lib/theme/lawheatmap_theme.dart new file mode 100644 index 0000000..c894707 --- /dev/null +++ b/app/lib/theme/lawheatmap_theme.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; + +import 'lawheatmap_tokens.dart'; + +/// ThemeData assembly for the F∆I Law-Heatmap client. +/// +/// Dark mode is the default (consistent with fai_web's +/// `color-scheme: dark`). Light mode falls back to the paper +/// palette for printing/screenshot work. +class LawHeatmapTheme { + LawHeatmapTheme._(); + + static ThemeData dark() => _build(Brightness.dark); + static ThemeData light() => _build(Brightness.light); + + static ThemeData _build(Brightness brightness) { + final isDark = brightness == Brightness.dark; + final canvas = isDark ? LawHeatmapColors.ink : LawHeatmapColors.paper; + final surface = + isDark ? LawHeatmapColors.inkRaised : LawHeatmapColors.paperRaised; + final onSurface = isDark + ? const Color(0xFFE9E7E2) + : const Color(0xFF1A1B1E); + + final colorScheme = ColorScheme( + brightness: brightness, + primary: LawHeatmapColors.signal, + onPrimary: LawHeatmapColors.ink, + secondary: LawHeatmapColors.mute, + onSecondary: onSurface, + error: const Color(0xFFD8723A), + onError: LawHeatmapColors.ink, + surface: surface, + onSurface: onSurface, + ); + + final textTheme = _textTheme(onSurface); + + return ThemeData( + useMaterial3: true, + brightness: brightness, + colorScheme: colorScheme, + scaffoldBackgroundColor: canvas, + canvasColor: canvas, + fontFamily: LawHeatmapTypography.body, + textTheme: textTheme, + cardTheme: CardThemeData( + color: surface, + elevation: 0, + margin: EdgeInsets.zero, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(LawHeatmapRadius.md), + side: BorderSide(color: Color(0x24FFFFFF)), + ), + ), + dividerTheme: DividerThemeData( + color: onSurface.withValues(alpha: LawHeatmapColors.hairlineDarkOpacity), + thickness: 1, + space: 1, + ), + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + backgroundColor: LawHeatmapColors.signal, + foregroundColor: LawHeatmapColors.ink, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(LawHeatmapRadius.sm), + ), + textStyle: textTheme.labelLarge, + padding: const EdgeInsets.symmetric( + horizontal: LawHeatmapSpace.lg, + vertical: LawHeatmapSpace.md, + ), + ), + ), + ); + } + + static TextTheme _textTheme(Color onSurface) { + const display = LawHeatmapTypography.display; + const body = LawHeatmapTypography.body; + return TextTheme( + displaySmall: TextStyle( + fontFamily: display, + fontSize: 28, + fontWeight: FontWeight.w600, + letterSpacing: -0.02 * 28, + color: onSurface, + ), + headlineSmall: TextStyle( + fontFamily: display, + fontSize: 20, + fontWeight: FontWeight.w600, + letterSpacing: -0.02 * 20, + color: onSurface, + ), + titleMedium: TextStyle( + fontFamily: body, + fontSize: 16, + fontWeight: FontWeight.w500, + color: onSurface, + ), + bodyLarge: TextStyle( + fontFamily: body, + fontSize: 14, + height: 1.45, + color: onSurface, + ), + labelLarge: TextStyle( + fontFamily: body, + fontSize: 13, + fontWeight: FontWeight.w500, + color: onSurface, + ), + labelSmall: TextStyle( + fontFamily: body, + fontSize: 11, + fontWeight: FontWeight.w500, + color: onSurface.withValues(alpha: 0.72), + ), + ); + } + + /// Convenience: JetBrains-Mono variant of an existing style for + /// technical content (ELI/CELEX, SHA-256, audit-event IDs). + static TextStyle mono(TextStyle base) => + base.copyWith(fontFamily: LawHeatmapTypography.mono); +} diff --git a/app/lib/theme/lawheatmap_tokens.dart b/app/lib/theme/lawheatmap_tokens.dart new file mode 100644 index 0000000..a473a3e --- /dev/null +++ b/app/lib/theme/lawheatmap_tokens.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +/// Visual tokens for the F∆I Law-Heatmap client, sourced from the +/// fai_web brand palette (see fai_web/src/styles/global.css:14-20). +/// +/// Unlike `fai_chain_studio`'s `ChainColors` (Tailwind sky-400 — +/// dev-tool look), this palette matches the public-facing brand: +/// ink/paper with a petrol "signal" accent that doubles as the eye +/// of the `∆` mark. +class LawHeatmapColors { + LawHeatmapColors._(); + + // Core palette (fai_web/src/styles/global.css) + static const ink = Color(0xFF08090A); + static const inkRaised = Color(0xFF111315); + static const paper = Color(0xFFF4F3EF); + static const paperRaised = Color(0xFFE7E5DF); + static const signal = Color(0xFF2E8F9E); + static const mute = Color(0xFF6B7177); + + // Hairline divider — currentColor at 14% opacity (fai_web). + static const hairlineDarkOpacity = 0.14; + static const hairlineLightOpacity = 0.14; + + // Heatmap scale — derived from `signal` by HSL shift: + // x-axis (Schaden): cool → warm as score rises + // y-axis (Nutzen): muted → saturated as score rises + // size: cohort count, linear sqrt mapping + static const harmCool = Color(0xFF2E6E8F); + static const harmWarm = Color(0xFFD8723A); + static const benefitMuted = Color(0xFF5A7E83); + static const benefitSaturated = Color(0xFF2E8F9E); + + // Tier badges — evidence tier of the lowest-tier ingredient. + static const tierT1 = Color(0xFF2E8F9E); // official / peer-reviewed + static const tierT2 = Color(0xFF6CA29A); // verband + static const tierT3 = Color(0xFFB0AC8E); // own survey + static const tierT4 = Color(0xFFA86F5C); // qualitative signal only +} + +/// Spacing scale — 4/8/12/16/24/32/48 multiples, matching +/// `ChainSpace` from `fai_chain_studio/lib/theme/tokens.dart`. +class LawHeatmapSpace { + LawHeatmapSpace._(); + + static const xs = 4.0; + static const sm = 8.0; + static const md = 12.0; + static const lg = 16.0; + static const xl = 24.0; + static const xxl = 32.0; + static const xxxl = 48.0; +} + +class LawHeatmapRadius { + LawHeatmapRadius._(); + + static const sm = Radius.circular(6); + static const md = Radius.circular(10); +} + +class LawHeatmapTypography { + LawHeatmapTypography._(); + + static const display = 'SpaceGroteskVariable'; + static const body = 'InterVariable'; + static const mono = 'JetBrainsMono'; +} diff --git a/app/lib/widgets/delta_mark.dart b/app/lib/widgets/delta_mark.dart new file mode 100644 index 0000000..30db44d --- /dev/null +++ b/app/lib/widgets/delta_mark.dart @@ -0,0 +1,98 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +import '../theme/lawheatmap_tokens.dart'; + +/// The F∆I / Ch∆In brand mark: an upward triangle (the `∆`) with +/// a petrol "eye" at the centre. Geometry mirrors fai_web's +/// Logo.astro — the triangle IS the "A"; the eye-dot IS the dot +/// of the "i". +/// +/// Subtle 9s breathing pulse on the eye (matches fai_web's +/// `.eye-pulse`). Set [animated] to false for static contexts +/// (icons, screenshots). +class DeltaMark extends StatefulWidget { + const DeltaMark({super.key, this.size = 64, this.animated = true}); + + final double size; + final bool animated; + + @override + State createState() => _DeltaMarkState(); +} + +class _DeltaMarkState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 9), + ); + if (widget.animated) { + _controller.repeat(); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller, + builder: (context, _) { + final t = _controller.value; + // Breathe: scale 1 → 1.28 → 1 over the cycle. + final pulse = 1.0 + 0.28 * (1 - (math.cos(t * 2 * math.pi).abs())); + return CustomPaint( + size: Size.square(widget.size), + painter: _DeltaPainter(pulse: widget.animated ? pulse : 1.0), + ); + }, + ); + } +} + +class _DeltaPainter extends CustomPainter { + _DeltaPainter({required this.pulse}); + + final double pulse; + + @override + void paint(Canvas canvas, Size size) { + final w = size.width; + final h = size.height; + + // Upward triangle, geometric centre. + final stroke = Paint() + ..color = const Color(0xFFE9E7E2) + ..style = PaintingStyle.stroke + ..strokeWidth = w * 0.04 + ..strokeJoin = StrokeJoin.miter; + + final inset = w * 0.08; + final tri = Path() + ..moveTo(w / 2, inset) + ..lineTo(w - inset, h - inset) + ..lineTo(inset, h - inset) + ..close(); + canvas.drawPath(tri, stroke); + + // Eye — petrol fill, centred on the triangle's centroid. + final eyeCentre = Offset(w / 2, inset + (h - 2 * inset) * 2 / 3); + final eyeR = w * 0.06 * pulse; + final eye = Paint()..color = LawHeatmapColors.signal; + canvas.drawCircle(eyeCentre, eyeR, eye); + } + + @override + bool shouldRepaint(covariant _DeltaPainter old) => old.pulse != pulse; +} diff --git a/app/lib/widgets/demo_banner.dart b/app/lib/widgets/demo_banner.dart new file mode 100644 index 0000000..b697bc4 --- /dev/null +++ b/app/lib/widgets/demo_banner.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +import '../theme/lawheatmap_tokens.dart'; + +/// Permanent banner shown above all data screens in mock mode. +/// Carries the political-defensibility caveat: this is demo data, +/// not peer-reviewed, not Juristen-approved. +class DemoBanner extends StatelessWidget { + const DemoBanner({super.key}); + + @override + Widget build(BuildContext context) { + final t = Theme.of(context).textTheme; + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: LawHeatmapSpace.lg, + vertical: LawHeatmapSpace.sm, + ), + decoration: BoxDecoration( + color: LawHeatmapColors.tierT4.withValues(alpha: 0.12), + border: Border( + bottom: BorderSide( + color: LawHeatmapColors.tierT4.withValues(alpha: 0.55), + ), + ), + ), + child: Row( + children: [ + const Icon(Icons.science_outlined, + size: 18, color: LawHeatmapColors.tierT4), + const SizedBox(width: LawHeatmapSpace.sm), + Expanded( + child: Text( + 'Demo-Daten — Phase 0, ohne Verbands-Erhebung. ' + 'Keine Juristen-Approval, keine Beirats-Validierung. ' + 'Alle Werte tragen Tier T4, bis T1/T2-Pipeline läuft.', + style: t.labelSmall?.copyWith( + color: LawHeatmapColors.tierT4, + ), + ), + ), + ], + ), + ); + } +} diff --git a/app/lib/widgets/evidence_sidebar.dart b/app/lib/widgets/evidence_sidebar.dart new file mode 100644 index 0000000..070f313 --- /dev/null +++ b/app/lib/widgets/evidence_sidebar.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +import '../data/models.dart'; +import '../theme/lawheatmap_tokens.dart'; +import 'lawheatmap_card.dart'; +import 'source_chip.dart'; + +/// Sidebar listing the citable sources behind an evaluation. +/// Surfaces the methodological audit story: every figure has a +/// source and an evidence tier. +class EvidenceSidebar extends StatelessWidget { + const EvidenceSidebar({super.key, required this.evaluation}); + + final Evaluation evaluation; + + @override + Widget build(BuildContext context) { + final t = Theme.of(context).textTheme; + return LawHeatmapCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Quellen', style: t.headlineSmall), + const SizedBox(height: LawHeatmapSpace.sm), + Text( + 'Jede Zahl trägt die Tier-Stufe ihres schwächsten ' + 'Bestandteils (Regel der niedrigsten Stufe, §6.7).', + style: t.labelSmall?.copyWith(color: LawHeatmapColors.mute), + ), + const SizedBox(height: LawHeatmapSpace.lg), + for (final s in evaluation.sources) SourceChip(source: s), + const Divider(height: LawHeatmapSpace.xl), + Text('Audit-Event', style: t.labelLarge), + const SizedBox(height: 4), + SelectableText( + evaluation.auditEventId, + style: t.labelSmall?.copyWith( + fontFamily: LawHeatmapTypography.mono, + color: LawHeatmapColors.mute, + ), + ), + const SizedBox(height: LawHeatmapSpace.sm), + Text( + 'Quell-SHA-256:', + style: t.labelSmall?.copyWith(color: LawHeatmapColors.mute), + ), + SelectableText( + evaluation.norm.sourceSha256, + style: t.labelSmall?.copyWith( + fontFamily: LawHeatmapTypography.mono, + color: LawHeatmapColors.mute, + fontSize: 10, + ), + ), + ], + ), + ); + } +} diff --git a/app/lib/widgets/heatmap_grid.dart b/app/lib/widgets/heatmap_grid.dart new file mode 100644 index 0000000..bf2fa41 --- /dev/null +++ b/app/lib/widgets/heatmap_grid.dart @@ -0,0 +1,306 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +import '../data/models.dart'; +import '../theme/lawheatmap_tokens.dart'; + +/// 2D scatter heatmap — x: Schaden (€/Jahr, log-scaled), +/// y: Nutzen (0–5), point area: Betroffenheit (sqrt-scaled). +/// +/// Server delivers JSON; client pixels. +class HeatmapGrid extends StatefulWidget { + const HeatmapGrid({ + super.key, + required this.evaluations, + this.onTap, + }); + + final List evaluations; + final void Function(Evaluation)? onTap; + + @override + State createState() => _HeatmapGridState(); +} + +class _HeatmapGridState extends State { + int? _hoverIndex; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final size = Size(constraints.maxWidth, constraints.maxHeight); + return MouseRegion( + onHover: (e) => _setHover(_hitTest(e.localPosition, size)), + onExit: (_) => _setHover(null), + child: GestureDetector( + onTapDown: (e) { + final idx = _hitTest(e.localPosition, size); + if (idx != null && widget.onTap != null) { + widget.onTap!(widget.evaluations[idx]); + } + }, + child: CustomPaint( + painter: _HeatmapPainter( + evaluations: widget.evaluations, + hoverIndex: _hoverIndex, + theme: Theme.of(context), + ), + size: size, + ), + ), + ); + }, + ); + } + + void _setHover(int? idx) { + if (idx != _hoverIndex) setState(() => _hoverIndex = idx); + } + + int? _hitTest(Offset pos, Size size) { + final layout = _Layout(size); + for (var i = 0; i < widget.evaluations.length; i++) { + final c = layout.project(widget.evaluations[i]); + final r = layout.radius(widget.evaluations[i]); + if ((pos - c).distance <= r + 4) return i; + } + return null; + } +} + +class _Layout { + _Layout(this.size) + : padding = const EdgeInsets.fromLTRB(72, 28, 28, 48); + + final Size size; + final EdgeInsets padding; + + double get plotLeft => padding.left; + double get plotRight => size.width - padding.right; + double get plotTop => padding.top; + double get plotBottom => size.height - padding.bottom; + double get plotWidth => plotRight - plotLeft; + double get plotHeight => plotBottom - plotTop; + + /// x: log10(€/year), clipped to [3, 9]. + static double xLog(double eur) { + if (eur <= 1) return 3; + final v = math.log(eur) / math.ln10; + return v.clamp(3.0, 9.0); + } + + Offset project(Evaluation e) { + final x = xLog(e.skmEurPerYear); + final y = e.benefitScore.clamp(0.0, 5.0); + final dx = plotLeft + ((x - 3) / 6) * plotWidth; + final dy = plotBottom - (y / 5) * plotHeight; + return Offset(dx, dy); + } + + double radius(Evaluation e) { + final n = e.affectedCount.clamp(1, 10_000_000); + final r = math.sqrt(n / 1000) * 1.6; + return r.clamp(6.0, 44.0); + } +} + +class _HeatmapPainter extends CustomPainter { + _HeatmapPainter({ + required this.evaluations, + required this.hoverIndex, + required this.theme, + }); + + final List evaluations; + final int? hoverIndex; + final ThemeData theme; + + @override + void paint(Canvas canvas, Size size) { + final layout = _Layout(size); + final onSurface = theme.colorScheme.onSurface; + const mute = LawHeatmapColors.mute; + + // Plot frame. + final frame = Paint() + ..color = onSurface.withValues(alpha: 0.14) + ..style = PaintingStyle.stroke + ..strokeWidth = 1; + canvas.drawRect( + Rect.fromLTRB( + layout.plotLeft, + layout.plotTop, + layout.plotRight, + layout.plotBottom, + ), + frame, + ); + + // Grid lines (y = 1..4, x = 4..8). + final gridPaint = Paint() + ..color = onSurface.withValues(alpha: 0.08) + ..strokeWidth = 1; + for (var y = 1; y <= 4; y++) { + final dy = layout.plotBottom - (y / 5) * layout.plotHeight; + canvas.drawLine( + Offset(layout.plotLeft, dy), + Offset(layout.plotRight, dy), + gridPaint, + ); + } + for (var x = 4; x <= 8; x++) { + final dx = layout.plotLeft + ((x - 3) / 6) * layout.plotWidth; + canvas.drawLine( + Offset(dx, layout.plotTop), + Offset(dx, layout.plotBottom), + gridPaint, + ); + } + + // Axis labels. + const axisStyle = TextStyle( + color: mute, + fontSize: 10, + fontFamily: LawHeatmapTypography.body, + ); + _drawText( + canvas, + 'Schaden €/Jahr (log)', + Offset(layout.plotLeft, layout.plotBottom + 22), + axisStyle, + ); + _drawText( + canvas, + 'Nutzen 0–5', + Offset(8, layout.plotTop - 18), + axisStyle, + ); + // X tick labels for 10^4 .. 10^9 + for (var x = 4; x <= 9; x++) { + final dx = layout.plotLeft + ((x - 3) / 6) * layout.plotWidth; + _drawText( + canvas, + '10^$x', + Offset(dx - 12, layout.plotBottom + 6), + axisStyle, + ); + } + // Y tick labels for 0..5 + for (var y = 0; y <= 5; y++) { + final dy = layout.plotBottom - (y / 5) * layout.plotHeight; + _drawText( + canvas, + '$y', + Offset(layout.plotLeft - 18, dy - 6), + axisStyle, + ); + } + + // Points. + for (var i = 0; i < evaluations.length; i++) { + final e = evaluations[i]; + final c = layout.project(e); + final r = layout.radius(e); + final hot = e.skmEurPerYear > 50000000; + final color = Color.lerp( + LawHeatmapColors.benefitSaturated, + LawHeatmapColors.harmWarm, + hot ? 0.5 : 0.0, + )!; + final body = Paint()..color = color.withValues(alpha: 0.7); + final ring = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = i == hoverIndex ? 3 : 1.5; + canvas.drawCircle(c, r, body); + canvas.drawCircle(c, r, ring); + + _drawText( + canvas, + '${e.norm.jurabk} ${e.norm.paragraph}', + Offset(c.dx + r + 6, c.dy - 6), + TextStyle( + color: onSurface, + fontSize: 11, + fontFamily: LawHeatmapTypography.body, + fontWeight: i == hoverIndex ? FontWeight.w600 : FontWeight.w400, + ), + ); + } + + // Hover detail box. + if (hoverIndex != null) { + final e = evaluations[hoverIndex!]; + final c = layout.project(e); + final r = layout.radius(e); + final box = Rect.fromLTWH(c.dx + r + 8, c.dy + 8, 240, 64); + final boxPaint = Paint() + ..color = theme.colorScheme.surface.withValues(alpha: 0.92); + final boxBorder = Paint() + ..style = PaintingStyle.stroke + ..color = LawHeatmapColors.signal.withValues(alpha: 0.6); + final rrect = RRect.fromRectAndRadius(box, LawHeatmapRadius.sm); + canvas.drawRRect(rrect, boxPaint); + canvas.drawRRect(rrect, boxBorder); + _drawText( + canvas, + e.norm.title, + box.topLeft + const Offset(10, 8), + TextStyle( + color: onSurface, + fontSize: 12, + fontWeight: FontWeight.w600, + fontFamily: LawHeatmapTypography.body, + ), + ); + _drawText( + canvas, + '€/Jahr: ${_eur(e.skmEurPerYear)} Betroffene: ${_kmu(e.affectedCount)}', + box.topLeft + const Offset(10, 26), + const TextStyle( + color: mute, + fontSize: 11, + fontFamily: LawHeatmapTypography.body, + ), + ); + _drawText( + canvas, + 'Tier: ${e.tierLowest.short} Klick → Detail', + box.topLeft + const Offset(10, 44), + const TextStyle( + color: mute, + fontSize: 11, + fontFamily: LawHeatmapTypography.body, + ), + ); + } + } + + String _eur(double v) { + if (v >= 1e9) return '${(v / 1e9).toStringAsFixed(1)} Mrd'; + if (v >= 1e6) return '${(v / 1e6).toStringAsFixed(1)} Mio'; + if (v >= 1e3) return '${(v / 1e3).toStringAsFixed(0)} k'; + return v.toStringAsFixed(0); + } + + String _kmu(int n) { + if (n >= 1_000_000) return '${(n / 1e6).toStringAsFixed(1)} Mio'; + if (n >= 1_000) return '${(n / 1e3).toStringAsFixed(0)} k'; + return n.toString(); + } + + void _drawText(Canvas canvas, String text, Offset at, TextStyle style) { + final tp = TextPainter( + text: TextSpan(text: text, style: style), + textDirection: TextDirection.ltr, + )..layout(); + tp.paint(canvas, at); + } + + @override + bool shouldRepaint(covariant _HeatmapPainter old) => + old.hoverIndex != hoverIndex || + old.evaluations.length != evaluations.length; +} diff --git a/app/lib/widgets/lawheatmap_card.dart b/app/lib/widgets/lawheatmap_card.dart new file mode 100644 index 0000000..8a84701 --- /dev/null +++ b/app/lib/widgets/lawheatmap_card.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; + +import '../theme/lawheatmap_tokens.dart'; + +/// Surface container — visual equivalent of fai_chain_studio's +/// `ChainCard`, retuned to the fai_web palette. +class LawHeatmapCard extends StatelessWidget { + const LawHeatmapCard({ + super.key, + required this.child, + this.padding = + const EdgeInsets.all(LawHeatmapSpace.lg), + this.accent = false, + this.expand = false, + }); + + final Widget child; + final EdgeInsetsGeometry padding; + + /// If true, draws a thin petrol accent bar at the top — + /// used for "this card carries a primary action". + final bool accent; + + /// If true, the card grows to fill its parent's bounded + /// dimensions and forces the child to expand. Use when the + /// card is placed inside an `Expanded` widget and the child + /// is itself a free-sizing widget (e.g. CustomPaint). + final bool expand; + + @override + Widget build(BuildContext context) { + final padded = Padding(padding: padding, child: child); + return Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: expand ? MainAxisSize.max : MainAxisSize.min, + children: [ + if (accent) + Container( + height: 2, + decoration: const BoxDecoration( + color: LawHeatmapColors.signal, + borderRadius: BorderRadius.vertical( + top: LawHeatmapRadius.md, + ), + ), + ), + if (expand) Expanded(child: padded) else padded, + ], + ), + ); + } +} diff --git a/app/lib/widgets/source_chip.dart b/app/lib/widgets/source_chip.dart new file mode 100644 index 0000000..ca81fa7 --- /dev/null +++ b/app/lib/widgets/source_chip.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; + +import '../data/models.dart'; +import '../theme/lawheatmap_tokens.dart'; +import 'tier_badge.dart'; + +/// A citable source line: tier badge + label + (planned) link. +/// External link opening lands with `url_launcher` in week 1 — for +/// now the chip surfaces the URL via tooltip + copy hint. +class SourceChip extends StatelessWidget { + const SourceChip({super.key, required this.source}); + + final Source source; + + @override + Widget build(BuildContext context) { + final t = Theme.of(context).textTheme; + return Padding( + padding: const EdgeInsets.only(bottom: LawHeatmapSpace.sm), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 2), + child: TierBadge(tier: source.tier, compact: true), + ), + const SizedBox(width: LawHeatmapSpace.sm), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(source.label, style: t.bodyLarge), + Text( + source.url, + style: t.labelSmall?.copyWith( + color: LawHeatmapColors.mute, + ), + ), + if (source.note != null) + Text( + source.note!, + style: t.labelSmall?.copyWith( + color: LawHeatmapColors.mute, + fontStyle: FontStyle.italic, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/app/lib/widgets/tier_badge.dart b/app/lib/widgets/tier_badge.dart new file mode 100644 index 0000000..b194a6b --- /dev/null +++ b/app/lib/widgets/tier_badge.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +import '../data/models.dart'; +import '../theme/lawheatmap_tokens.dart'; + +/// Tier badge — surfaces the lowest evidence tier in a composed +/// value. Every figure in the app carries one of these next to it. +class TierBadge extends StatelessWidget { + const TierBadge({super.key, required this.tier, this.compact = false}); + + final EvidenceTier tier; + final bool compact; + + @override + Widget build(BuildContext context) { + final t = Theme.of(context).textTheme; + final color = switch (tier) { + EvidenceTier.t1 => LawHeatmapColors.tierT1, + EvidenceTier.t2 => LawHeatmapColors.tierT2, + EvidenceTier.t3 => LawHeatmapColors.tierT3, + EvidenceTier.t4 => LawHeatmapColors.tierT4, + }; + return Tooltip( + message: '${tier.short} — ${tier.description}', + child: Container( + padding: EdgeInsets.symmetric( + horizontal: compact ? 6 : LawHeatmapSpace.sm, + vertical: compact ? 2 : 4, + ), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.18), + border: Border.all(color: color.withValues(alpha: 0.55)), + borderRadius: const BorderRadius.all(LawHeatmapRadius.sm), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 6), + Text( + tier.short, + style: (compact ? t.labelSmall : t.labelLarge)?.copyWith( + color: color, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ); + } +} diff --git a/app/macos/.gitignore b/app/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/app/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/app/macos/Flutter/Flutter-Debug.xcconfig b/app/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/app/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/app/macos/Flutter/Flutter-Release.xcconfig b/app/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/app/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/app/macos/Flutter/GeneratedPluginRegistrant.swift b/app/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..724bb2a --- /dev/null +++ b/app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,12 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import shared_preferences_foundation + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) +} diff --git a/app/macos/Runner.xcodeproj/project.pbxproj b/app/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..07f152c --- /dev/null +++ b/app/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,729 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* lawheatmap_app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "lawheatmap_app.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* lawheatmap_app.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* lawheatmap_app.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = ai.flemming.lawheatmapApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/lawheatmap_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/lawheatmap_app"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = ai.flemming.lawheatmapApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/lawheatmap_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/lawheatmap_app"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = ai.flemming.lawheatmapApp.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/lawheatmap_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/lawheatmap_app"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..45c3a4d --- /dev/null +++ b/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/macos/Runner.xcworkspace/contents.xcworkspacedata b/app/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/app/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/app/macos/Runner/AppDelegate.swift b/app/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/app/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/app/macos/Runner/Base.lproj/MainMenu.xib b/app/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/app/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/macos/Runner/Configs/AppInfo.xcconfig b/app/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..3ad4d22 --- /dev/null +++ b/app/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = lawheatmap_app + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = ai.flemming.lawheatmapApp + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 ai.flemming. All rights reserved. diff --git a/app/macos/Runner/Configs/Debug.xcconfig b/app/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/app/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/app/macos/Runner/Configs/Release.xcconfig b/app/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/app/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/app/macos/Runner/Configs/Warnings.xcconfig b/app/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/app/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/app/macos/Runner/DebugProfile.entitlements b/app/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/app/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/app/macos/Runner/Info.plist b/app/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/app/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/app/macos/Runner/MainFlutterWindow.swift b/app/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/app/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/app/macos/Runner/Release.entitlements b/app/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/app/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/app/macos/RunnerTests/RunnerTests.swift b/app/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/app/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/app/pubspec.lock b/app/pubspec.lock new file mode 100644 index 0000000..9c03ace --- /dev/null +++ b/app/pubspec.lock @@ -0,0 +1,351 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + url: "https://pub.dev" + source: hosted + version: "1.18.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + url: "https://pub.dev" + source: hosted + version: "2.5.5" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "93ae5884a9df5d3bb696825bceb3a17590754548b5d740eba51500afc8d088f5" + url: "https://pub.dev" + source: hosted + version: "2.4.26" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + url: "https://pub.dev" + source: hosted + version: "0.7.11" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" +sdks: + dart: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" diff --git a/app/pubspec.yaml b/app/pubspec.yaml new file mode 100644 index 0000000..e9683e8 --- /dev/null +++ b/app/pubspec.yaml @@ -0,0 +1,48 @@ +name: lawheatmap_app +description: F∆I Law-Heatmap — Flutter client for the Ch∆In-based law-bureaucracy heatmap. +publish_to: 'none' +version: 0.1.0+1 + +environment: + sdk: ">=3.6.0 <4.0.0" + flutter: ">=3.24.0" + +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + intl: ^0.20.2 + shared_preferences: ^2.3.0 + + # Ch∆In wire — same sibling-path pattern as fai_chain_studio. + # Uncomment once the gRPC integration starts (Phase 0, week 1+). + # chain_client_sdk: + # path: ../../fai_chain_client_sdk_dart + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^4.0.0 + +flutter: + uses-material-design: true + + assets: + - assets/fixtures/ + + # Bundled fonts — offline-first, KRITIS-compatible, no Google + # Fonts runtime fetch. setup.sh downloads SIL-OFL-licensed TTFs + # from the upstream `google/fonts` repo. If they are absent, + # Flutter falls back to the platform default font and the app + # still builds. + fonts: + - family: SpaceGroteskVariable + fonts: + - asset: assets/fonts/SpaceGrotesk[wght].ttf + - family: InterVariable + fonts: + - asset: assets/fonts/Inter[opsz,wght].ttf + - family: JetBrainsMono + fonts: + - asset: assets/fonts/JetBrainsMono[wght].ttf diff --git a/app/setup.sh b/app/setup.sh new file mode 100755 index 0000000..adecf00 --- /dev/null +++ b/app/setup.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Setup script for the F∆I Law-Heatmap Flutter client. +# +# Idempotent — safe to re-run. Performs the steps that a fresh +# checkout needs before `flutter run -d macos` works: +# +# 1. Download SIL-OFL fonts (Space Grotesk, Inter, JetBrains Mono) +# from the upstream google/fonts repo into assets/fonts/. +# 2. Clear macOS extended attributes (curl + iCloud-Drive tag +# `com.apple.provenance`) that break Apple's CodeSign step. +# 3. Resolve packages. + +set -euo pipefail + +cd "$(dirname "$0")" + +mkdir -p assets/fonts + +fetch() { + local url=$1 out=$2 + if [ -s "assets/fonts/$out" ]; then + echo " font already present: $out" + return + fi + echo " fetching: $out" + curl -fsSLo "assets/fonts/$out" "$url" +} + +echo "[1/3] Fonts (SIL OFL, redistribution allowed)" +fetch "https://github.com/google/fonts/raw/main/ofl/spacegrotesk/SpaceGrotesk%5Bwght%5D.ttf" "SpaceGrotesk[wght].ttf" +fetch "https://github.com/google/fonts/raw/main/ofl/inter/Inter%5Bopsz%2Cwght%5D.ttf" "Inter[opsz,wght].ttf" +fetch "https://github.com/google/fonts/raw/main/ofl/jetbrainsmono/JetBrainsMono%5Bwght%5D.ttf" "JetBrainsMono[wght].ttf" + +echo "[2/3] Strip extended attributes" +xattr -cr assets/ 2>/dev/null || true +xattr -cr macos/ 2>/dev/null || true +xattr -cr . 2>/dev/null || true + +echo "[3/3] Resolve Flutter packages" +flutter pub get + +echo "done — try: flutter run -d macos" diff --git a/app/test/widget_test.dart b/app/test/widget_test.dart new file mode 100644 index 0000000..873b29f --- /dev/null +++ b/app/test/widget_test.dart @@ -0,0 +1,44 @@ +// Smoke tests — boot the app, navigate to the shell, verify +// each rail destination renders without throwing. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:lawheatmap_app/main.dart'; + +void main() { + // Generous surface so NavigationRail + Heatmap fit without + // overflow warnings. + const surfaceSize = Size(1400, 900); + + setUp(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + testWidgets('Landing renders brand mark + CTA', + (WidgetTester tester) async { + await tester.binding.setSurfaceSize(surfaceSize); + await tester.pumpWidget(const LawHeatmapApp()); + await tester.pump(); + expect(find.text('F∆I Law-Heatmap'), findsOneWidget); + expect( + find.widgetWithText(FilledButton, 'App öffnen (Demo-Modus)'), + findsOneWidget, + ); + await tester.pumpWidget(const SizedBox.shrink()); + }); + + testWidgets('Tapping CTA reaches the shell and shows the heatmap', + (WidgetTester tester) async { + await tester.binding.setSurfaceSize(surfaceSize); + await tester.pumpWidget(const LawHeatmapApp()); + await tester.pump(); + await tester.tap( + find.widgetWithText(FilledButton, 'App öffnen (Demo-Modus)'), + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 600)); + expect(find.text('Heatmap'), findsWidgets); + await tester.pumpWidget(const SizedBox.shrink()); + }); +} diff --git a/flows/durchstich-gewo-14.yaml b/flows/durchstich-gewo-14.yaml new file mode 100644 index 0000000..d7eb63f --- /dev/null +++ b/flows/durchstich-gewo-14.yaml @@ -0,0 +1,131 @@ +# Vertikaler Durchstich Phase 0 — eine Norm (GewO §14) durch +# alle 8 law*-Schritte + Juristen-Approval + Audit. +# +# Voraussetzung: +# chain install text.akoma_normalize text.deontic_extract \ +# graph.citation_extract text.readability_score \ +# stats.cohort_size graph.shapley_attribution \ +# econ.skm_score law.benefit_score \ +# http.request system.approval +# +# Aufruf: +# chain run flows/durchstich-gewo-14.yaml \ +# --input norm_url=https://www.gesetze-im-internet.de/gewo/__14.xml \ +# --input cohort_id=berlin-kmu + +inputs: + norm_url: + type: string + required: true + description: ELI-resolvable URL der Quell-Norm + cohort_id: + type: string + required: true + description: Adressaten-Kohorte (siehe stats.cohort_size v0.1) + tariff_eur_per_hour: + type: number + default: 35.0 + description: SKM-Tarif T + +outputs: + norm: ${{ normalize.norm }} + duties: ${{ duties.duties }} + citations: ${{ citations.citations }} + cohort: ${{ cohort.cohort }} + skm: ${{ skm.report }} + frust: ${{ frust.score }} + benefit: ${{ benefit.score }} + attribution: ${{ attribution.attribution }} + reviewed: ${{ review.decision == 'approve' }} + +steps: + - id: pull + use: http.request@^0 + with: + url: ${{ inputs.norm_url }} + method: GET + etag_cache: true + + - id: normalize + use: text.akoma_normalize@^0 + with: + content: ${{ pull.body }} + mime: application/xml + + - id: duties + use: text.deontic_extract@^0 + with: + norm: ${{ normalize.norm }} + + - id: citations + use: graph.citation_extract@^0 + with: + norm: ${{ normalize.norm }} + + - id: cohort + use: stats.cohort_size@^0 + with: + cohort_id: ${{ inputs.cohort_id }} + + # SKM-Aufbereitung: jede Duty bekommt P/F/T/h aus + # Kohorten-Größe + per-Duty-Heuristik (Phase 1: NKR-Datenbank). + # Hier inline aufgebaut; in Phase 1 wird das ein eigener + # `econ.duties_enrich`-Schritt. + - id: skm + use: econ.skm_score@^0 + with: + duties: + duties: + - population: ${{ cohort.cohort.count }} + frequency: 1.0 + tariff_eur_per_hour: ${{ inputs.tariff_eur_per_hour }} + time_hours_per_case: 0.5 + source_norm: ${{ normalize.norm.eli }} + tier: ${{ cohort.cohort.tier }} + + - id: frust + use: text.readability_score@^0 + with: + norm: ${{ normalize.norm }} + graph: ${{ citations.citations }} + + - id: benefit + use: law.benefit_score@^0 + with: + ratings: + schutz: 2.0 + markt: 3.0 + rechtssicherheit: 3.5 + eu_binnenmarkt: 2.0 + sicherheit: 1.5 + umwelt: 0.0 + einnahmen: 2.0 + _sources: + schutz: NKR Stellungnahme zur GewO 2018 + rechtssicherheit: IHK Berlin Mittelstandsumfrage 2024 + + # Attribution-Schritt mit Demo-Graph aus citations. + # Phase 1: graph.normalize baut den vollen Norm-Graph hier. + - id: attribution + use: graph.shapley_attribution@^0 + with: + graph: + nodes: + - ${{ normalize.norm.eli }} + - eli/bund/gewo + edges: + - from: eli/bund/gewo + to: ${{ normalize.norm.eli }} + target: ${{ normalize.norm.eli }} + + # Juristen-Gate — Studie §3.3 macht das obligatorisch. + - id: review + use: system.approval@^0 + with: + title: "F∆I Law-Heatmap — Review ${{ normalize.norm.eli }}" + payload: + norm: ${{ normalize.norm }} + skm: ${{ skm.report }} + benefit: ${{ benefit.score }} + frust: ${{ frust.score }} + cohort: ${{ cohort.cohort }}