Lage.Bonn

OpenSpec

Spezifikationsgetriebene Entwicklung: Changes sind vorgeschlagene Vorhaben (Proposal, Design, Tasks, Delta-Specs); Specifications sind die daraus abgeleiteten aktiven Anforderungen. Diese Seite ist statisch aus dem openspec/-Verzeichnis generiert — die Inhalte liegen auf Codeberg.

01 Changes (proposed) 6

Change 0/15 Tasks

Add Civic Kpi Tier1 ↗

add-civic-kpi-tier1

Lage has two KPI families today and neither measures the mission. The 49 Prometheus metrics (cycles, errors, latency, freshness — `web/src/lib/metrics.json`) prove *the machine runs*. The Spec Quality Index (`docs/kpi/spec-quality-index.md`) proves *specs implement cheaply*. Nothing answers the question VISION.md actually poses: **is Lage delivering the three civic moments?**

  • Add read-only KPI accessors to the store that compute Tier-1 metrics over a time
  • Add a GET /api/kpi/product endpoint returning the computed Tier-1 KPI snapshot as
  • Add a prerendered SvelteKit page at web/src/routes/kpi/product/ that renders the
  • Document the exact definitions in docs/kpi/product-kpis.md.
Change 0/26 Tasks

Add Event Identification UI ↗

add-event-identification-ui

The Correlator (`add-event-correlation`) groups LageItems into Event rows automatically — but per ADR-0003, an auto-extracted event is a **candidate**, not a public fact: "every Event in the broker is confirmed to exist." Nothing today lets a human identify candidates (confirm, merge phantom duplicates, reject noise) or assign the closed `evtcat:` SKOS category (ADR-0005), and nothing presents Events to the public in vocabulary terms (`civic:Event` thin referent with German labels from vocab.machdenstaat.de). Without the gate, the UI would serve ghost events; without the list, correlation is invisible.

  • Split Event lifecycle into **candidate** (correlator output, staging-only) and **confirmed** (human-identified, public) per ADR-0003 D-2. Candidates never appear on public surfaces.
  • Mint stable public URNs urn:ngsi-ld:Event:bonn-<year>-<seq> at confirm time (ADR-0003 D-3); keep a persisted many-to-one resolution mapping from correlator events to public URNs.
  • Add event_category (one of the 8 evtcat: SKOS concepts) to Events; rule-based default from item evidence, human-overridable at review.
  • New curation REST endpoints: confirm, recategorize, merge, reject — token-gated (curator secret), since add-event-correlation excluded auth.
  • New **curator review queue** UI (/review/ereignisse): candidate cards with match evidence (contributing items, Jaccard score, time/geo anchors) and the four actions; SKOS category picker with German prefLabels.
  • New **public Events page** (/ereignisse): confirmed Events as grouped cards — evtcat badge (German label), thin what/where/when referent, contributing Reports (source links), live via SSE.
  • Vocab labels bundled at build time from docs/future/civic-categories.jsonld (same pattern as existing vocab-sync generated assets) — no runtime dependency on the vocab server.
Change 0/35 Tasks

Add Instagram Collector Ocr ↗

add-instagram-collector-ocr

Some Bonn-relevant organisations are most active on Instagram, where a meaningful share of the information lives **inside the graphics** — road closures, maps, event flyers — as text burned into images, not in the post caption. The existing `feed-collector` cannot read Instagram, and nothing in the pipeline extracts text from images. The motivating example is `@radiobonnrheinsieg`, but the capability is **per-handle and registry-driven**: any number of Instagram accounts can be added as registry sources without code changes.

  • Add a new instagram source **kind** to the registry, parameterized by a handle.
  • Extend LageItem with a media: list[str] field (post image / video URLs) and an
  • Add a **separate, asynchronous enrichment consumer** (its own deployment) that
  • **Provenance for scraped + derived content** (the "no source, no trust" invariant):
  • *Acquisition trail* — each scraped item records that it arrived via Apify and which
  • *Derivation envelope* — ocr_text is machine-inferred by Claude vision, **never**
  • *Enforcement* — the provenance-gate accessor is extended so derived content without a
  • Add the Apify scraper as the only new acquisition dependency and APIFY_TOKEN /
Change 20/20 Tasks

Add Verkehr Road Geometry ↗

add-verkehr-road-geometry

Verkehr (traffic) events on `/karte` currently render as a single point marker, but an Autobahn roadworks or warning affects a *stretch* of road, not a point. The Autobahn API (`verkehr.autobahn.de`) already returns a GeoJSON `LineString` `geometry` for every item describing exactly which segment is affected — and `fetch.py` discards it today. Surfacing that geometry lets the map highlight the affected street, which reads far more accurately than a lone pin floating on a motorway.

  • Capture the Autobahn API's geometry (GeoJSON LineString) field in the
  • Persist geometry through the store so it survives the collector → DB → panel
  • Expose geometry on the verkehr panel's entries only (feed/other entries keep
  • On /karte, draw a polyline for any entry carrying geometry, styled by the
  • Geometry is additive and nullable end-to-end: items without it (all
Change 0/12 Tasks

Add Vision Readiness Kpi ↗

add-vision-readiness-kpi

VISION.md commits to a civic graph where `Ereignis ≠ Bericht ≠ Behauptung`, every `Anliegen` chains to an `Akteur` and a `Beschluss`, every `Indikator` carries the `Angebot`s that address it, and every entity carries provenance and a verification level. Today's schema is a flat `LageItem`; none of those entities exist yet.

  • Add a docs/kpi/vision-readiness-kpis.md catalog: each Tier-2 KPI with its
  • Add a harness module that exposes each KPI through a uniform contract returning
  • Add GET /api/kpi/vision-readiness returning the harness snapshot, plus a Prometheus
  • The prerendered /kpi/vision-readiness burndown page is **deferred** until ≥2 KPIs can
  • As each backing change archives, a follow-up task here flips that KPI's provider from
Change 0/22 Tasks

Harden Openapi Contract ↗

harden-openapi-contract

The `api` component is a FastAPI app, so it already emits an auto-generated OpenAPI document — but the contract is effectively useless to consumers: interactive docs are switched off (`docs_url=None, redoc_url=None`), and nearly every one of the ~20 routes returns a bare `JSONResponse`, so the schema advertises untyped `200` responses with no shapes, tags, or summaries. There is no machine-readable contract the web frontend, operators, or external integrators can rely on. This change makes the OpenAPI surface a real, typed, browsable contract.

  • Declare a Pydantic response_model (and explicit error responses where a route can return 4xx/503) for every /api/* endpoint, replacing bare JSONResponse returns with typed models so the generated schema carries real response shapes.
  • Add OpenAPI tags and human-readable summary/description to every route, grouping them (e.g. panels, signals, events, meta, feeds).
  • Set explicit OpenAPI document metadata on the FastAPI(...) app: title, version (sourced from the existing _API_VERSION), description, and a servers entry.
  • Re-enable the interactive docs **publicly**: restore /docs (Swagger UI), /redoc, and /openapi.json so the contract is reachable by anyone in production.
  • Keep operational/internal routes (/metrics, /healthz, the atom-feed HEAD duplicates) explicitly out of the public schema via include_in_schema=False where they are noise rather than contract.
  • Pin and gate the contract: export the generated document to a committed openapi.json artifact and add a check (pre-commit / CI) that fails when the live schema drifts from the committed file, so the contract cannot silently rot.
Capabilities openapi-contract8

02 Specifications (active) 19

03 Changes (archived) 16

04 Spec Quality Index

SQI misst wie billig eine Spec implementiert wurde — keine Stakeholder-Fragen, keine Rework-Runden, keine falschen Behauptungen. Zwei Teilscores: I (Implementability) und F (Fidelity); Blend 50/50 → SQI. Methodologie ↗

A 90–100 Straight-throughB 80–89 Minor gapsC 70–79 CorrectionsD 60–69 EscalationF <60 Unusable
Change SQI I F Reqs Conf. Graded
add-event-correlation 95 95 16 2026-06-16
S3 S5 S3 S3
open311-bonn-collector 75 75 5 2026-06-15
S3 S3 S3 S1 S3 S2
add-vorlagen-party-filter 95 95 5 2026-06-15
S3 Spec is internally inconsistent on the exact aria-live status string: the scenario/example text reads "Grüne · 7 von 23 Vorlagen" while design.md (D4) and tasks.md 2.4 show "{selected} · {visible.length} von {data.count}" with no trailing 'Vorlagen'. The implementer silently chose the shorter form (no 'Vorlagen' word) with no human input. Under-specified detail filled alone.
add-radzaehlung-source 94 A 100 88 6 2026-06-15
S3 GeoJSON location fallback property names (bezeichnung, name) — spec said join on lage; impl added undocumented alternatives when lage is absentS3 CSV column name case variants (Lage, Wann_Datum, Anzahl_Raeder) — spec referenced lowercase column names; impl silently handled mixed-case CSV headersS3 Summary/title format strings — spec said conveys the hourly profile; impl chose format independently
add-provenance-invariant-gate 44 F 0 88 3 2026-06-14T15:30:00Z
S7 task.blocked: original violation definition unimplementable — spec halted until lage-tlb shipped + full re-specS6 decision: user consulted to choose scope fork A/B/CS3 metrics-collection path unspecified; impl chose /api/status hookS1 alert for: window not specified; impl resolved to 5m
oepnv-per-bezirk-signals 94 A 100 88 4 2026-06-13
S4 Spec asserted UI tests for the suppression scenarios; no component test framework exists in web/ and the scope wall forbids adding one. Impl substituted svelte-check type-verification of the HEALTHY_SUPPRESSED map structure, journalled the decision, and rewrote the task. Spec claim was oversimplified; resolved without a human.
add-issue-category-scheme 90 A 100 81 4 2026-06-13
S3 Spec required byte-identical deterministic sync but was silent on how to model each mapping resource. Impl found blank nodes get fresh rdflib IDs each parse (non-deterministic .nt) and chose named IRIs (bonn:1.1 .. bonn:retired-gruenschnitt) alone, without a human. Gap the spec should have named.S4 Serving task asserted that registering both resources in registry.ts (plus verifying the index/content-negotiation) was the serving step. In this repo routing is per-resource: each served resource also needs its own SvelteKit +server.ts route handler, or the path 404s on every representation while still listing on the /lage index. Spec/task omitted the route-handler step; the build-only verifier never made an HTTP request so it false-passed. Corrected without escalation by adding the two route handlers and re-verifying over live HTTP.
add-allris-watcher-signal 88 B 88 1 2026-06-12
S1 HTTP-200 HTML maintenance page parses as well-formed empty feed (bozo=False) -> watcher returns 'up'. Spec explicitly flagged this as verify-and-branch (design D2 'verified by the fixture, not assumed'; Risks documents residual false-up as accepted; tasks 5.2 conditional bead). Impl confirmed via fixture, xfail'd the test, filed lage-0i4. Resolved by introspection, no human, no rework loop.
add-allris-atom-feed 68 D 81 56 4 2026-06-11
S4 /feed.atom renamed to /feed/current.atom — spec said combined /feed.atom route 'SHALL be unaffected' and scenario 'Combined feed is unchanged'; /atom alias kept, /feed.atom URL goneS4 proposal asserted 'k8s: none — no new workload, ingress' change; manifest/base/ingress.yaml required routing for /feed/*.atom (399eedf)S5 deploy fail->fix->fail->fix: first fix routed /feed/*.atom to api (399eedf), still broken because OpenShift drops pathType Exact, second fix to Prefix (f03c254) — 1 extra roundS3 HEAD handlers added for all feed routes — spec silentS3 /feed web index page (+page.svelte) + nav/footer links — spec silent on any web UIS3 scripts/check-feeds.py + scripts/openspec-journal.py — spec silent on operational helpers
nina-warn-signal 92 A 92 6 2026-06-10
S4 Delta spec asserted the window cut-off 'SHALL be evaluated in the database query (NOW()-relative) for single-clock comparison'. Impl computes the cutoff client-side (effective_now - timedelta) and compares first_seen >= cutoff string. Built without a human; verifier passed. The single-clock/DB-side intent (lens F6) was the spec's assertion; impl simplified to a passed-now. Fidelity divergence the spec should be corrected to match (or impl hardened).
migrate-feed-pipeline-to-kafka 97 A 97 9 2026-06-10
S1 design.md left 'one collector for all sources vs one Deployment per source-type' open. Impl resolved it via introspection — one feed-collector run_once iterating all due sources (collectors/feed.py). No human, no rework. The spec did its job by naming the unknown.S1 design.md D-3 left 'consumer re-tags vs trusts the producer tag' to decide in tasks. Impl resolved it — geo.tag_item + bonn_only applied at the producer (collectors/feed.py). Self-resolved, no human.
nina-collector 88 B 100 75 9 2026-06-09
S4 Spec asserted wrong NINA API field names: payload.data[] (not info[]), languageCode (not language); effective treated as reliable (absent in practice). Impl discovered real shape from live API, self-corrected. Spec updated in-place during apply (write-back done).S4 Spec listed wrong geocodes: 051110000000 (Düsseldorf) and 053160000000 (Leverkusen) instead of 053140000000 (Bonn) and 053820000000 (Rhein-Sieg-Kreis). Impl fixed silently in commit e04ca4d. Archived spec still contains the wrong values — write-back NOT done.S3 Spec stated poll interval=60s default but was silent on CLI configurability. Impl added --interval flag and --once mode (needed for CronJob pattern). No human consulted.S3 Spec silent on observability. Impl added Prometheus /metrics HTTP endpoint and Kubernetes ServiceMonitor (commits 5338d0e, ba3239b). No mention in spec or tasks.S3 Spec silent on liveness mechanism. Impl added --heartbeat-path file-touch for Kubernetes livenessProbe (commit 7b6768f). Infra requirement not stated.S3 Spec silent on Kafka producer durability config. Impl explicitly sets acks=all on Producer to prevent silent drops on Cancel messages. Derived from operational reasoning, not the spec.S3 Spec said provision a KafkaTopic but was silent on the Strimzi bootstrap service name. Impl required lage-kafka-bootstrap (Strimzi naming convention), corrected in commit 7db82b3 after initial failure.

scripts/gen-specs.py (just update-specs) · scripts/gen-sqi.py (just update-sqi)