Compare commits

...

3 commits

Author SHA1 Message Date
34c325e419 feat(flow): port flows to the chain-0.16 YAML syntax + add bytes-input variant
The earlier durchstich-gewo-14.yaml used the templated
${{ inputs.x }} syntax and rich input declarations
(type / required / description). chain 0.16 expects the
simpler $inputs.x form and flat 'inputs: { name: type }'
maps — see flows/hello.yaml in fai/chain. Rewritten to match.

Adds two siblings:

  - test-cohort.yaml: trivial stats.cohort_size-only smoke
    test. Confirms a Recl∆Im module loads, instantiates against
    the chain:platform/invoke@1.0.0 ABI, and writes a clean
    flow.completed event. Useful as the first test you run
    when wiring a fresh hub.

  - durchstich-from-file.yaml: end-to-end demo without the
    http.request permission dance. Takes the source XML as a
    bytes input (--input content=@./norm.xml), runs through
    text.akoma_normalize, stats.cohort_size and the
    system.approval gate, and writes the norm + cohort bag to
    the audit log. First end-to-end Recl∆Im flow that survives
    a hub restart with audit-trail intact.

Verified locally: chain run flows/test-cohort.yaml → flow.completed
in 5 ms; chain run flows/durchstich-from-file.yaml + approve →
flow.completed in 6 s, output bag carries normalized GewO §14
with paragraphs + source_sha256.

Signed-off-by: flemming-it <sf@flemming.it>
2026-06-18 15:38:23 +02:00
1be2ad075d fix(ui): chip wrap below the heatmap shows the norm title too
The compact chips under the heatmap showed only [Tier-Badge]
jurabk paragraph (e.g. 'T4 GewO § 14'), which was enough for a
methodologically oriented reader but not for someone scanning
'what was that pilot norm about?'. Adds a muted mid-dot + the
norm.title after the paragraph, so each chip now reads:

  T4 · amtlich  GewO § 14  ·  Anzeigepflicht

The chips stay clickable, navigation behaviour unchanged.

Signed-off-by: flemming-it <sf@flemming.it>
2026-06-18 15:38:23 +02:00
cf8c7bf1fd feat(brand): add Recl∆Im lockup + mark as standalone SVG assets
Two scalable artefacts under assets/branding/ so the brand can
travel outside the Flutter app — favicon source, slide footers,
press kit, GitHub social-preview, README hero. Both files use
prefers-color-scheme so a single asset inverts cleanly on
ink/paper backdrops, with the petrol signal (#2E8F9E) staying
constant on the ∆-eye and the ∆I letters in the wordmark.

  reclaim-lockup.svg — 720×200 viewBox, mark on the left, the
                       wordmark 'Recl∆Im' on the right, ∆I in
                       petrol. The single artefact a stage or
                       browser tab wants to show.

  reclaim-mark.svg   — 160×160 viewBox, just the triangle and
                       the eye. Favicon source, social cards,
                       any tight-box context.

Signed-off-by: flemming-it <sf@flemming.it>
2026-06-18 15:38:23 +02:00
6 changed files with 187 additions and 106 deletions

View file

@ -112,6 +112,19 @@ class _HeatmapPageState extends State<HeatmapPage> {
'${e.norm.jurabk} ${e.norm.paragraph}', '${e.norm.jurabk} ${e.norm.paragraph}',
style: t.labelLarge, style: t.labelLarge,
), ),
Text(
' · ',
style: t.labelLarge?.copyWith(
color: ReclaimColors.mute,
),
),
Text(
e.norm.title,
style: t.labelLarge?.copyWith(
color: ReclaimColors.mute,
fontWeight: FontWeight.w400,
),
),
], ],
), ),
), ),

View file

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Recl∆Im — primary brand lockup
Mark (the ∆ triangle with petrol eye) + wordmark on a single row.
Single-colour palette so it inverts cleanly on dark/light ink/paper
backdrops. ASCII spelling: reclaim.
Geometry mirrors fai_web's Logo.astro: the eye sits at the geometric
centroid of the triangle (a third up from the base). Stroke widths
are tuned for legibility down to 24 px tall.
Author: Dr. Stefan Flemming, F∆I (Flemming.AI). Apache-2.0.
-->
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 720 200"
role="img"
aria-label="Recl∆Im">
<defs>
<style>
:root { color-scheme: light dark; }
.ink { fill: #08090A; }
.paper { fill: #F4F3EF; }
.signal { fill: #2E8F9E; }
.stroke-ink { stroke: #08090A; }
.stroke-paper { stroke: #F4F3EF; }
@media (prefers-color-scheme: dark) {
.auto-ink { fill: #F4F3EF; }
.auto-stroke { stroke: #F4F3EF; }
}
@media (prefers-color-scheme: light) {
.auto-ink { fill: #08090A; }
.auto-stroke { stroke: #08090A; }
}
</style>
</defs>
<!--
Mark: 160 x 160 box at left, triangle inset by 12 px.
Stroke 6 px, no fill, petrol eye at (80, 110) radius 10.
-->
<g transform="translate(20,20)">
<polygon class="auto-stroke"
points="80,12 148,148 12,148"
fill="none"
stroke-width="6"
stroke-linejoin="miter" />
<circle class="signal" cx="80" cy="110" r="10" />
</g>
<!--
Wordmark "Recl∆Im" starting at x=210. Hand-fitted glyph
positions so the ∆ matches the mark's stroke weight and the I
above the ∆ becomes the dot/eye of "i". Petrol on the ∆I block.
-->
<g font-family="'Space Grotesk', 'Inter', system-ui, sans-serif"
font-weight="600"
font-size="120"
letter-spacing="-4">
<text x="210" y="138" class="auto-ink">Recl</text>
<text x="430" y="138" class="signal">∆I</text>
<text x="546" y="138" class="auto-ink">m</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Recl∆Im — standalone mark (just the ∆ + eye).
Use as favicon source, social-share preview, and any context where
the wordmark would be too wide.
-->
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 160 160"
role="img"
aria-label="Recl∆Im — Mark">
<defs>
<style>
.signal { fill: #2E8F9E; }
@media (prefers-color-scheme: dark) {
.auto-stroke { stroke: #F4F3EF; }
}
@media (prefers-color-scheme: light) {
.auto-stroke { stroke: #08090A; }
}
</style>
</defs>
<polygon class="auto-stroke"
points="80,12 148,148 12,148"
fill="none"
stroke-width="6"
stroke-linejoin="miter" />
<circle class="signal" cx="80" cy="110" r="10" />
</svg>

After

Width:  |  Height:  |  Size: 812 B

View file

@ -0,0 +1,39 @@
# Vertikaler Durchstich, Variante "ohne http-Permission".
#
# Nimmt das Norm-XML als bytes-Input direkt entgegen (statt es
# über http.request zu ziehen). Sinnvoll für den ersten end-to-
# end-Test gegen den lokal laufenden Hub, wenn dessen Operator-
# Policy noch keine Allowlist für gesetze-im-internet.de hat.
#
# Aufruf:
# chain run flows/durchstich-from-file.yaml \
# --input content=@/tmp/gewo-14-mini.xml \
# --input cohort_id=berlin-kmu
name: durchstich-from-file
inputs:
content: bytes
cohort_id: text
steps:
- id: normalize
use: text.akoma_normalize@^0
with:
content: $inputs.content
mime: "application/xml"
- id: cohort
use: stats.cohort_size@^0
with:
cohort_id: $inputs.cohort_id
- id: review
use: system.approval@^0
with:
title: "Recl∆Im — Review GewO §14"
payload: $normalize.norm
outputs:
norm: $normalize.norm
cohort: $cohort.cohort

View file

@ -1,131 +1,53 @@
# Vertikaler Durchstich Phase 0 — eine Norm (GewO §14) durch # Phase-0 vertikaler Durchstich für GewO §14.
# alle 8 law*-Schritte + Juristen-Approval + Audit.
# #
# Voraussetzung: # Minimaler Pfad zum ersten echten flow.completed-Event im
# chain install text.akoma_normalize text.deontic_extract \ # Audit-Log — http.request → text.akoma_normalize →
# graph.citation_extract text.readability_score \ # stats.cohort_size → system.approval. Beweist, dass die
# stats.cohort_size graph.shapley_attribution \ # Recl∆Im-Pipeline live gegen den Ch∆In-Hub läuft. Die
# econ.skm_score law.benefit_score \ # komplexen Bewertungs-Schritte (econ.skm_score,
# http.request system.approval # law.benefit_score, graph.shapley_attribution) bündeln wir in
# einem zweiten Flow, wenn dieser hier durchläuft.
#
# Voraussetzung — alle Module installieren:
# chain install --link <fai_chain_modules>/<modul>
# für: http-request, text-akoma-normalize, stats-cohort-size
# #
# Aufruf: # Aufruf:
# chain run flows/durchstich-gewo-14.yaml \ # chain run flows/durchstich-gewo-14.yaml \
# --input norm_url=https://www.gesetze-im-internet.de/gewo/__14.xml \ # --input norm_url=https://www.gesetze-im-internet.de/gewo/__14.xml \
# --input cohort_id=berlin-kmu # --input cohort_id=berlin-kmu
inputs: name: durchstich-gewo-14
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: inputs:
norm: ${{ normalize.norm }} norm_url: text
duties: ${{ duties.duties }} cohort_id: text
citations: ${{ citations.citations }}
cohort: ${{ cohort.cohort }}
skm: ${{ skm.report }}
frust: ${{ frust.score }}
benefit: ${{ benefit.score }}
attribution: ${{ attribution.attribution }}
reviewed: ${{ review.decision == 'approve' }}
steps: steps:
- id: pull - id: pull
use: http.request@^0 use: http.request@^0
with: with:
url: ${{ inputs.norm_url }} url: $inputs.norm_url
method: GET method: "GET"
etag_cache: true
- id: normalize - id: normalize
use: text.akoma_normalize@^0 use: text.akoma_normalize@^0
with: with:
content: ${{ pull.body }} content: $pull.body
mime: application/xml 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 - id: cohort
use: stats.cohort_size@^0 use: stats.cohort_size@^0
with: with:
cohort_id: ${{ inputs.cohort_id }} 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 - id: review
use: system.approval@^0 use: system.approval@^0
with: with:
title: "Recl∆Im (F∆I) — Review ${{ normalize.norm.eli }}" title: "Recl∆Im — Review GewO §14"
payload: payload: $normalize.norm
norm: ${{ normalize.norm }}
skm: ${{ skm.report }} outputs:
benefit: ${{ benefit.score }} norm: $normalize.norm
frust: ${{ frust.score }} cohort: $cohort.cohort
cohort: ${{ cohort.cohort }} decision: $review.decision

16
flows/test-cohort.yaml Normal file
View file

@ -0,0 +1,16 @@
# Minimal-Test: nur stats.cohort_size, ohne externe Calls.
# Beweist, dass meine Module mit dem Hub-ABI kompatibel sind.
name: test-cohort
inputs:
cohort_id: text
steps:
- id: cohort
use: stats.cohort_size@^0
with:
cohort_id: $inputs.cohort_id
outputs:
cohort: $cohort.cohort