Zum Inhalt springen
ZVV
techstack

Tech-Stack

Der ZVV-Stack als Vertrag statt als Steckbrief. Jede Regel nennt Datei und Zeile; wo Doku und Referenz-Repo auseinanderlaufen, steht das da — mit beiden Fundstellen.

AI-Agent: hol dir https://atlas.zvv.dev/llms.txt und arbeite den Vertrag von R0 bis R9 ab.

Manueller Abgleich der Atlas-Doku gegen dieses Repo, kein Scanner. Misst keine fremden App-Repos. Stand des Abgleichs: 2026-08-05. Atlas v0.3.

62
Regeln
8
Widersprüche entschieden
2
Lücken benannt
Prompt — neue ZVV-Apptext
Lies https://atlas.zvv.dev/llms.txt und baue danach eine neue ZVV-App nach Atlas v0.3.
Arbeite die Abschnitte 0 bis 9 der Reihe nach ab und halte jede Regel-ID ein.
[SPEC] setzt du 1:1 um. [GUIDELINE] ist dein Default. [VORBILD] ist nicht normiert.
Was dort [UNGEDECKT] heisst, erfindest du nicht — leg mir die Optionen mit Quelle vor und frag.
Jede Zahl, die du in Code schreibst, hat in llms.txt eine Quelle. Steht sie dort nicht,
ist sie nicht belegt — dann schreibst du sie nicht.
Melde am Schluss jede Abweichung mit Regel-ID, Begründung und Datum.
Prompt — bestehende App angleichentext
Lies https://atlas.zvv.dev/llms.txt und prüfe dieses Repo Regel für Regel dagegen.
Gib mir je Abschnitt eine Liste: erfüllt / verletzt / nicht anwendbar.
Bei "verletzt" nennst du die Regel-ID, die Quelle aus llms.txt und den Ort im Repo.
Regeln der Klasse [ENTSCHIEDEN] oder [UNGEDECKT] sind kein App-Bug: Atlas versteht Drift
als eigenen Fehler — melde sie als Atlas-Issue, statt die App glattzubügeln.
Aendere nichts, bevor ich die Liste freigegeben habe.
Abrufbash
curl -s https://atlas.zvv.dev/llms.txt

Kurzantwort

Die Ist-Spalte kommt für vier der fünf Zeilen aus der package.json dieses Projekts — sie kann darum nicht veralten.

BausteinAtlas-SollReferenz-IstBeleg
Next.js15.5+ App Router, 16+ erlaubt16.2.6docs/07-tech-stack.md:28·example/package.json:36
React19.2 (neueste LTS)19.2.7docs/07-tech-stack.md:46·example/package.json:39
TypeScript5.x/6.x, strict^6docs/07-tech-stack.md:15·example/package.json:58
Node22+ (Vercel auto-managed)22 (.nvmrc)docs/07-tech-stack.md:16·.nvmrc:1
Tailwind4.x — steht in Kapitel 04, nicht in 07^4docs/04-ui-stack.md:16·example/package.json:57

Wie du das liest

Verbindlichkeit — was Atlas sagt

spec
Aus 01 Identity, 02 Auth, 06 Mailing, 10 Security oder patterns/login-page.md. MUSS.
guideline
Aus einem Guideline-Kapitel. SOLL — Abweichung begründungspflichtig.
vorbild
Steht in keinem Kapitel; nur das Referenz-Repo tut es. KANN.

Nur spec und guideline sind Atlas-Kategorien (CLAUDE.md:33-34, docs/index.md:17-18). vorbild ist ein Etikett dieser Seite — ebenso alle drei Klassen rechts.

Klasse — Doku gegen Referenz-Repo

belegt
Doku und Repo stimmen überein. Befolge sie.
entschieden
Sie widersprechen sich. Der Vertrag entscheidet und nennt beide Fundstellen.
ungedeckt
Kein Vorbild oder keine Doku-Deckung. Erfinde nichts. Frag nach.

Tie-Break

Widersprechen sich Doku und Referenz-Repo, folgst du dem Repo — es baut, es ist getestet, und die nächste App verhält sich dann konsistent dazu. Danach meldest du die Abweichung als Atlas-Issue.

Atlas regelt diesen Fall nicht. Der Vorrang des Repos ist eine Entscheidung dieses Vertrags, damit die Anweisung eindeutig bleibt — nicht eine Fundstelle. Sinngemäss dazu, aber ausdrücklich nicht als Fundstelle für den Vorrang: CLAUDE.md:63 — die Zeile handelt von Drift zwischen Apps und schreibt die Gegenrichtung vor: erst Atlas schärfen, dann Apps angleichen.

§0 Geltung

Du weisst, was MUSS und was SOLL ist — und wo eine Abweichung hingehört.

Fertig, wenn: README-Zeile und package.json-Feld stehen, die Abweichungstabelle existiert.

  • G01specbelegt

    Setze 01 Identity, 02 Auth, 06 Mailing und 10 Security 1:1 um — diese vier Kapitel sind Spec, jede ZVV-App MUSS sich daran halten.

    Beleg: CLAUDE.md:33·docs/index.md:17

  • G02guidelinebelegt

    Behandle die acht übrigen Kapitel (03, 04, 05, 07, 08, 09, 11, 12) als Guideline: starker Default, Abweichung erlaubt, aber begründungspflichtig.

    Beleg: CLAUDE.md:34·docs/index.md:18

  • G03specbelegt

    Behandle patterns/login-page.md ebenfalls als verbindlich — es ist das einzige Pattern mit Status Spec, und es steht ausserhalb der Kapitelzählung.

    Beleg: patterns/login-page.md:4·docs/index.md:63

  • G04guidelinebelegt

    Lies dieses Kapitel — 07 Tech-Stack — als Guideline und nicht als Spec; es sagt das im eigenen Kopf. Wer abweicht, muss App-spezifisch begründen.

    Beleg: docs/07-tech-stack.md:3·docs/07-tech-stack.md:5

  • G05guidelinebelegt

    Deklariere Conformance an beiden Orten: die README-Zeile «Diese App folgt ZVV Atlas v0.3» und das Custom-Field "zvvAtlas": "0.3" in der package.json. Konsumiert die App das Package, kommt der Pin "@zvvch/atlas": "^0.3.0" dazu.

    Beleg: docs/08-engineering.md:199-219·CLAUDE.md:51

    Welcher der beiden Mechanismen im Zweifel gilt, sagt keine der Stellen. Setze beide.

  • G06vorbildungedeckt

    Halte jede Abweichung als Tabelle in der App-Doku fest — Regel-ID, Abweichung, Begründung, Datum —, damit die Begründung zitierfähig wird.

    Beleg: CLAUDE.md:33·docs/01-identity.md:98·docs/10-security.md:313

    Atlas fordert die Begründung an mehreren Stellen, legt aber weder Ort noch Form fest. Die Tabelle ist ein Vorschlag dieses Vertrags, keine Atlas-Regel.

§1 Toolchain & Projekt

Das Projekt startet auf der richtigen Runtime, mit genau einem Lockfile.

Fertig, wenn: `node --version` passt zur .nvmrc, ein Lockfile ist committet, `npm run dev` läuft.

  • T01guidelinebelegt

    Pinne Node auf 22 in einer .nvmrc und lass die CI die Version aus dieser Datei lesen statt sie im Workflow zu hardcoden — dann wandert die Runtime an genau einer Stelle.

    Beleg: docs/07-tech-stack.md:16·.nvmrc:1·.github/workflows/example-ci.yml:37

  • T02vorbildentschieden

    Benutze npm, nicht die von Kapitel 07 empfohlene pnpm — CI und Quickstart des Referenz-Repos fahren npm, und Atlas lässt npm ausdrücklich zu.

    Doku sagt
    pnpm 9+ (Empfehlung), CI-Beispiel in Kapitel 08 mit pnpm
    Repo tut
    npm ci in der CI, npm install im Quickstart, zwei package-lock.json

    Beleg: .github/workflows/example-ci.yml:52·docs/00-quickstart.md:28·docs/07-tech-stack.md:93

    Dagegen: docs/07-tech-stack.md:91

    Drei Doku-Stellen, zwei Manager: docs/08-engineering.md:155 zeigt eine pnpm-CI. Die Wahl npm ist keine Abweichung, sondern die tolerierte Variante — melde die widersprüchliche pnpm-CI als Atlas-Issue.

  • T03guidelinebelegt

    Committe genau ein Lockfile pro Install-Root und niemals pnpm-lock.yaml und package-lock.json nebeneinander.

    Beleg: docs/07-tech-stack.md:95

    Zum realen Fall dieses Repos — zwei package-lock.json in getrennten Install-Roots, weil example/ nicht in den Workspaces steht — sagt die Regel nichts.

  • T04vorbildungedeckt

    Setze kein packageManager-Feld, wenn du dem Vorbild folgst: keine der drei package.json des Referenz-Repos führt eins, und Kapitel 07 verlangt keins.

    Beleg: package.json·example/package.json·packages/atlas/package.json

    Die Manager-Wahl ist damit nirgends maschinenlesbar. Wer das Feld setzt, verbessert bewusst — es ist keine Atlas-Regel.

  • T05vorbildbelegt

    Lege die Scripts lint, format:check, typecheck und test an — genau diese vier Namen ruft die CI auf.

    Beleg: example/package.json:12-20·.github/workflows/example-ci.yml:54-77

    Kapitel 07 nennt keine Script-Namen. Ein lint:fix-Script existiert im Vorbild nicht; format schreibt, format:check prüft nur.

  • T07guidelinebelegt

    Übernimm example/.prettierrc unverändert — singleQuote, semi: false, printWidth 100 — statt eigene Werte zu wählen.

    Beleg: docs/07-tech-stack.md:22·docs/07-tech-stack.md:145·example/.prettierrc:2-4

    Genau diese drei Werte, kein vierter: Die Config ist die kopierbare Vorlage für App-Repos und darf nicht von ihrer eigenen Doku abweichen. Zeilenenden regelt .gitattributes (LF), nicht Prettier — sonst wäre endOfLine der vierte Wert.

  • T06vorbildungedeckt

    Verdrahte commitlint mit dem 12er type-enum und husky, wenn du die Commit-Konvention durchsetzen willst — das Referenz-Repo tut es, Kapitel 07 erwähnt es mit keinem Wort.

    Beleg: example/.commitlintrc.json:4-21·package.json:10

    Achtung: subject-case steht dort auf 0. Die von docs/08-engineering.md:56 verlangte Kleinschreibung des Subjects wird also nicht geprüft.

§2 Versionen & Pinning

Die package.json trägt die Atlas-Versionen.

Fertig, wenn: Die fünf Zeilen der Kurzantwort stimmen mit deiner package.json überein.

§3 TypeScript

Die tsconfig steht, bevor die erste Route entsteht.

Fertig, wenn: `npm run typecheck` läuft sauber durch.

  • TS01guidelinebelegt

    Setze strict: true — die Doku-Spec und beide realen tsconfigs sind sich hier einig.

    Beleg: docs/07-tech-stack.md:66·example/tsconfig.json:7·packages/atlas/tsconfig.json:5

  • TS02guidelinebelegt

    Setze moduleResolution auf "bundler".

    Beleg: docs/07-tech-stack.md:70·example/tsconfig.json:11

  • TS03guidelinebelegt

    Setze in einer App den Alias "@/*": ["./*"]. Ein publiziertes Package lässt paths bewusst weg und importiert relativ, weil es rohe Source ausliefert und der Consumer die Auflösung übernimmt.

    Beleg: docs/07-tech-stack.md:71-73·example/tsconfig.json:21-23·packages/atlas/tsconfig.json

  • TS04vorbildentschieden

    Setze target auf ES2017 wie beide realen tsconfigs — nicht auf das von der Doku-Spec geforderte ES2022.

    Doku sagt
    "target": "ES2022"
    Repo tut
    "target": "ES2017" in beiden realen tsconfigs

    Beleg: example/tsconfig.json:3·packages/atlas/tsconfig.json:3

    Dagegen: docs/07-tech-stack.md:69

    Welcher Wert sachlich richtig ist, entscheidet Atlas — nicht diese Seite. Der Vertrag legt nur die Handlung fest (Vorbild-Treue), damit alle ZVV-Apps dasselbe Ausgabeziel haben, und verlangt die Meldung als Atlas-Issue.

  • TS05vorbildentschieden

    Lass noUncheckedIndexedAccess und noImplicitOverride weg, solange das Referenz-Repo sie nicht führt — beide fehlen in beiden realen tsconfigs, obwohl die Doku-Spec sie verlangt.

    Doku sagt
    beide Flags true
    Repo tut
    in beiden realen tsconfigs nicht gesetzt

    Beleg: example/tsconfig.json·packages/atlas/tsconfig.json

    Dagegen: docs/07-tech-stack.md:67-68

    Das Vorbild-Repo erfüllt hier seine eigene Spec nicht. Wer die Flags einschaltet, bekommt Typfehler, die im Vorbild niemand sieht — zulässig, gehört aber in die Abweichungstabelle (G06).

  • TS06guidelinebelegt

    Verboten sind any ohne Begründungs-Kommentar, as-unknown-as-Doppel-Casts und @ts-ignore ohne Issue-Link; erlaubt sind as nach einem Runtime-Check und // @ts-expect-error mit Kommentar.

    Beleg: docs/07-tech-stack.md:78-85

    Die durchsetzende Lint-Regel ist im Referenz-Repo nicht aktiv (siehe Q02). Das Verbot gilt trotzdem — es hängt nur an der Disziplin statt am Tor.

§4 App Router — die 500er-Vermeidung

Routen sind server-first und bleiben ohne JavaScript lesbar. Hier entstehen die meisten Laufzeitfehler einer neuen App.

Fertig, wenn: Keine Route trägt ohne Not "use client"; `npm run build` läuft durch.

§5 Atlas-Package konsumieren

@zvvch/atlas ist installiert, gestylt und importierbar — ohne Auflösungs-Fehler.

Fertig, wenn: Ein Button aus @zvvch/atlas/ui/button rendert in einer Server-Komponente.

§6 Qualitätstore

Lint, Typecheck und Tests greifen wirklich — statt nur zu existieren.

Fertig, wenn: `npm run lint`, `npm run typecheck` und `npm test` sind grün.

  • Q01guidelineentschieden

    Schreibe die ESLint-Konfiguration als Flat Config (eslint.config.mjs) und übernimm das .eslintrc.json-Snippet der Doku NICHT — ESLint 9 liest es ohne ESLINT_USE_FLAT_CONFIG=false gar nicht.

    Doku sagt
    .eslintrc.json mit extends next/core-web-vitals
    Repo tut
    eslint.config.mjs (Flat Config) mit nextVitals + nextTs

    Beleg: docs/07-tech-stack.md:21·example/eslint.config.mjs:1-7

    Dagegen: docs/07-tech-stack.md:132

    Dasselbe Kapitel verlangt ESLint 9+ und zeigt eine Legacy-Config. Ein Agent, der das Snippet kopiert, baut eine Datei, die nie gelesen wird.

  • Q02guidelineungedeckt

    Entscheide bewusst über die beiden Atlas-Lint-Regeln no-explicit-any und no-unused-vars mit argsIgnorePattern ^_ — im Referenz-Repo sind sie nicht aktiv, die eslint.config.mjs hat gar keinen rules-Block.

    Beleg: docs/07-tech-stack.md:139-140·example/eslint.config.mjs

    Ob eslint-config-next die Regeln in Flat Config bereits mitbringt, ist hier nicht geprüft — prüfe es mit `npx eslint --print-config` in deiner App, bevor du sie doppelt setzt.

  • Q03vorbildentschieden

    Lege Tests flach unter tests/<name>.test.ts ab. Co-located Tests laufen stillschweigend nicht mit — beide Vitest-Configs sammeln ausschliesslich tests/**/*.test.ts ein.

    Doku sagt
    co-located <file>.test.ts oder tests/unit/
    Repo tut
    include: ['tests/**/*.test.ts'] in beiden Configs

    Beleg: example/vitest.config.ts:9·packages/atlas/vitest.config.ts:11

    Dagegen: docs/07-tech-stack.md:119

    Drei Varianten dokumentiert oder impliziert, eine funktioniert. Ein tests/unit/-Verzeichnis existiert im Repo nicht.

  • Q04guidelinebelegt

    Halte die CI-Reihenfolge Install → Lint → Typecheck → Test → Build ein und lass tsc --noEmit als eigenen Schritt laufen, zusätzlich zur Typprüfung in next build.

    Beleg: docs/08-engineering.md:161-164·.github/workflows/example-ci.yml:51-80

§7 next.config.ts & Security

Die Pflicht-Header stehen. Als einziger Block dieses Vertrags kommt er aus einem Spec-Kapitel.

Fertig, wenn: `curl -I` auf eine beliebige Route zeigt alle fünf Header.

  • N01specbelegt

    Setze die fünf Pflicht-Header X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy strict-origin-when-cross-origin, Permissions-Policy und Strict-Transport-Security — sie gehören in dieselbe next.config.ts wie das Build-Tooling aus Kapitel 07.

    Beleg: docs/10-security.md:254-258·example/next.config.ts:6-18

  • N02specentschieden

    Uebernimm bei Permissions-Policy und HSTS die Werte der Referenz-Implementierung und melde die Differenz — die Spec-Liste nennt andere Werte, und keine der beiden Stellen sagt, welcher Wert gilt.

    Doku sagt
    camera=(), microphone=(), geolocation=() · max-age=31536000; includeSubDomains
    Repo tut
    … zusätzlich browsing-topics=() · max-age=63072000; includeSubDomains; preload

    Beleg: example/next.config.ts:10-17·docs/10-security.md:315

    Dagegen: docs/10-security.md:257-258

    Ein Spec-Kapitel, das seine eigene Referenz-Implementierung nicht deckt: hier ist die Meldung an Atlas wichtiger als die Wahl.

  • N03specbelegt

    Lockere X-Frame-Options DENY ausschliesslich über Content-Security-Policy: frame-ancestors 'self' und nur unter den vier Bedingungen: Pfade einzeln aufzählen, Liste neben dem einbettenden Code, alle übrigen Routen behalten DENY, Ausnahme in der App-Doku begründet.

    Beleg: docs/10-security.md:308-313·example/next.config.ts:34

  • N04vorbildungedeckt

    Trage Barrel-Libs in experimental.optimizePackageImports ein, wenn du dem Vorbild folgst — Next 16 optimiert lucide-react und recharts bereits per Default, der Rest ist Handarbeit.

    Beleg: example/next.config.ts:39-49

    Kapitel 07 nennt unter Build-Tooling nur Turbopack und den Bundle-Analyzer. Dieser Block ist eine reale Entscheidung ohne dokumentierte Konvention.

  • N05specbelegt

    Setze X-Robots-Tag: noindex für interne Admin-Apps.

    Beleg: docs/10-security.md:317

§8 Environment

Die Variablen sind gesetzt und richtig einsortiert.

Fertig, wenn: /api/health meldet Version und Commit; kein Secret steht unter NEXT_PUBLIC_*.

  • E01guidelinebelegt

    Behandle NEXT_PUBLIC_* als client-bundle-safe und alles andere als Server-only — packe niemals ein Secret unter ein NEXT_PUBLIC_-Präfix.

    Beleg: docs/07-tech-stack.md:180-181

  • E02guidelinebelegt

    Ignoriere die Env-Dateien in git. Die Doku fordert nur .env.local, das Referenz-Repo ignoriert das breitere .env* — halte dich an die schärfere Praxis.

    Beleg: docs/07-tech-stack.md:182·example/.gitignore:34

  • E03vorbildentschieden

    Reiche NEXT_PUBLIC_APP_VERSION aus der package.json durch und übernimm den Beispielwert 0.64.6 des Doku-Schemas nicht — das ist eine FZDB-spezifische Zahl ohne Herkunftsangabe.

    Doku sagt
    NEXT_PUBLIC_APP_VERSION=0.64.6
    Repo tut
    Wert stammt aus der package.json und wird beim Build durchgereicht

    Beleg: example/lib/atlas/app-meta.ts:57

    Dagegen: docs/07-tech-stack.md:168

  • E04vorbildungedeckt

    Nimm die Env-Tabelle unten statt des Doku-Schemas allein: fünf real gelesene Variablen-Paare kennt das Schema nicht, und das dokumentierte NEXT_PUBLIC_APP_NAME liest der Showcase nirgends.

    Beleg: example/lib/atlas/app-meta.ts:104-112·docs/07-tech-stack.md:167

    Das Schema ist zugleich unvollständig und teilweise tot.

§9 Präsenz

Die App erscheint dort richtig, wo sie noch niemand geöffnet hat — Link-Vorschau, Registerkarte, Home-Bildschirm.

Fertig, wenn: Die vier Dateien existieren, das PNG wurde angesehen, und pruefeSatori() läuft im Test über die eigene Karte.

  • PR01specbelegt

    Liefere alle vier Metadaten-Dateien aus — app/icon.svg, app/apple-icon.tsx, app/opengraph-image.tsx, app/manifest.ts. Fehlt eine, fällt genau eine Fläche aus, und zwar unbemerkt.

    Beleg: docs/13-praesenz.md:288·example/app/apple-icon.tsx·example/app/manifest.ts

    iOS liest kein SVG als Touch-Icon: Ohne apple-icon.tsx erzeugt Safari einen verkleinerten Bildschirmabzug der Startseite als Symbol. Sichtbar nur auf einem Gerät, das beim Entwickeln selten aufliegt.

  • PR02specbelegt

    Baue die OG-Karte mit ogCard() aus @zvvch/atlas/brand, statt Layout abzuschreiben — der Unterschied zwischen zwei ZVV-Apps sind sechs Angaben, nicht 200 Zeilen.

    Beleg: patterns/og-card.md·example/app/opengraph-image.tsx:29

    Bis Atlas v0.2 führten zvv-fzdb und der Showcase zwei fast gleiche Fassungen derselben 200 Zeilen — mit Titelgrösse 72 gegen 96 px und Eckenradius 0.19 gegen 0.22, ohne dass jemand das entschieden hätte.

  • PR03specbelegt

    Übergib der ImageResponse die Hausschrift als TTF unter fonts — Satori liest kein WOFF2, und der ZVV liefert Brown Narrow ausschliesslich als WOFF2.

    Beleg: docs/13-praesenz.md:182·example/app/opengraph-image.tsx:45·example/scripts/generate-og-fonts.mjs

    Ohne diesen Schritt rendert die Karte in der Standardschrift von next/og — auf der meistgesehenen Markenfläche der App, und niemandem fällt es auf, weil sie nur in fremden Oberflächen erscheint. fontFamily allein genügt nicht: der Name ohne die Daten greift stumm auf die Ersatzschrift zurück.

  • PR04specbelegt

    Zeichne das ZVV-Logo nie nach — zvvLogo() liefert die Geometrie der kanonischen assets/zvv-logo.svg, auch dort, wo es scheinbar kein Logo gibt.

    Beleg: docs/13-praesenz.md:144·patterns/logo-anwendung.md·assets/generate-logo-module.mjs

    Die OG-Karte ist die Fläche, auf der der Nachbau am häufigsten passiert, weil next/og ohne CSS und ohne Zugriff auf /zvv-logo.svg arbeitet. Merksatz: Wer das Logo gerade zeichnet statt es zu laden, ist auf dem falschen Weg — auch wenn das Ergebnis gut aussieht.

  • PR05specbelegt

    Rechne den Eckenradius der App-Kachel aus ICON_RADIUS (22 %) statt aus einer festen Pixelzahl — sonst trägt dieselbe App in der Registerkarte eine andere Form als auf dem Startbildschirm.

    Beleg: docs/13-praesenz.md:80·example/lib/atlas/brand/brand.ts:110

    Genau dieser Fall lag in zvv-fzdb vor: 6/32 = 0.19 im Favicon gegen 36/180 = 0.20 im Touch-Icon.

  • PR06specbelegt

    Setze metadataBase und twitter.card: summary_large_image — ohne metadataBase baut Next relative OG-Bild-URLs, die kein fremder Crawler auflösen kann.

    Beleg: docs/13-praesenz.md:213·docs/13-praesenz.md:215·example/app/layout.tsx:20

    openGraph.images gehört NICHT dazu: Next füllt es aus app/opengraph-image.tsx und baut die absolute URL aus metadataBase. Wer es zusätzlich einträgt, führt eine zweite Quelle.

  • PR07specbelegt

    Halte die Metadaten-Routen öffentlich, auch wenn die App hinter Login liegt — Teams und Slack holen die Vorschau ohne Sitzung.

    Beleg: docs/13-praesenz.md:288

    In zvv-fzdb lagen /opengraph-image, /icon und /apple-icon hinter dem Session-Gate und antworteten mit 307 auf /login; in jedem Chat stand eine graue Zeile statt der Karte. Es fiel nie auf, weil dieselbe URL im angemeldeten Browser ein einwandfreies Bild liefert. Datenführende OG-Routen pro Datensatz bleiben davon ausgenommen — sie laufen im Sitzungskontext und rendern ohne Anmeldung eine generische Karte.

  • PR08specbelegt

    Lass pruefeSatori() im Test über die eigene Karte laufen und sieh dir das PNG danach an — vier Fallen, drei davon schweigen.

    Beleg: docs/13-praesenz.md:251·example/tests/brand.test.ts·example/lib/atlas/brand/satori.ts

    Die Prüfung ist statisch und findet nur die vier Fallen. Was sie nicht sieht: eine verzerrte Grafik (Satori beachtet preserveAspectRatio nicht), einen Titel, der gegen das Bildzeichen drängt, oder eine Schrift, die gar nicht geladen wurde. Beides ist in zvv-fzdb aufgetreten und nur am gerenderten PNG aufgefallen.

§10 Abnahme

Die App ist prüfbar fertig — nicht gefühlt fertig.

Fertig, wenn: Die vier Kommandos sind grün und die Abweichungstabelle ist geschrieben.

  • A01guidelinebelegt

    Lass npm run lint, npm run typecheck, npm test und npm run build grün laufen — alle vier Stages sind CI-Bedingung, keine Empfehlung.

    Beleg: docs/08-engineering.md:161-164·.github/workflows/example-ci.yml:51-80

  • A02guidelinebelegt

    Führe eine CHANGELOG.md im Repo-Root nach Keep a Changelog und erwähne den Atlas-Conformance-Bump ausdrücklich.

    Beleg: docs/08-engineering.md:107-109

  • A03guidelinebelegt

    Melde jede Abweichung mit ihrer Regel-ID zurück an Atlas, statt sie in der App auszusitzen — Atlas versteht Drift als eigenen Fehler und will das Pattern geschärft, nicht die App gebogen.

    Beleg: CLAUDE.md:63

Paket-Subpaths und Server-Safety

Die zehn Einträge der exports-Map — etwas anderes ist nicht importierbar. Diese Tabelle steht in keiner Atlas-Datei; sie ist heute nur per Grep im installierten Package zu gewinnen.

SubpathInhaltServer-KomponentePeersBeleg
@zvvch/atlasUtilities, App-Meta, VBZ-Farben, Lottie-Meta, Mermaid-Theme, Zürich-Zeit, Easingsjaclsx, tailwind-mergepackages/atlas/src/index.ts:8-14packages/atlas/package.json:32
@zvvch/atlas/uiBarrel über alle 19 shadcn-Primitivesneinzieht sonner und cmdk mit — beide optionale Peerspackages/atlas/src/ui/index.tspackages/atlas/src/ui/command.tsx:4
@zvvch/atlas/ui/*Einzelne Komponente. Nur .tsx — es gibt kein .ts-Fallback.teilsje Komponentepackages/atlas/package.json:34
@zvvch/atlas/motionMotion-System, Lottie-Tools, Heroes-Registryneinframer-motion, lottie-react, html-to-imagepackages/atlas/src/motion/index.ts
@zvvch/atlas/motion/*Einzelmodul. Fallback-Array mit .tsx und .ts — anders als bei /ui/*.teilsje Modulpackages/atlas/package.json:36
@zvvch/atlas/themeThemeProvider + ThemeToggle (Datei, kein Verzeichnis)neinnext-themes UND framer-motion (hart importiert, siehe P06)packages/atlas/package.json:38
@zvvch/atlas/styles.cssBrand-Tokens + VBZ-Linienfarben (Default-Import)japackages/atlas/package.json:39
@zvvch/atlas/styles/tokens.cssnur Brand-Tokensjapackages/atlas/package.json:40
@zvvch/atlas/styles/vbz-lines.cssnur VBZ-Linienfarbenjapackages/atlas/package.json:41
@zvvch/atlas/assets/*zvv-logo.svg und zvv-logo-white.svg — sonst nichtsjapackages/atlas/package.json:42

Server-safe UI-Dateien (6 von 19)

badge · button · card · empty-state · input · skeleton

Client-UI-Dateien (13)

checkbox · command · dialog · dropdown-menu · label · lightbox · popover · select · separator · sheet · sonner · tabs · tooltip

Auflösungs-Fallen — in keiner Doku beschrieben

  • F1 @zvvch/atlas/motion/heroes löst nicht auf

    Das Wildcard-Ziel erwartet src/motion/heroes.tsx oder .ts, real existiert src/motion/heroes/index.tsx. Der einzige Weg an HEROES führt über den motion-Barrel.

    Du tust: Importiere HEROES aus @zvvch/atlas/motion, nicht aus /motion/heroes.

    Beleg: packages/atlas/package.json:36·packages/atlas/src/motion/heroes/index.tsx

  • F2 useReducedMotion ist kein Atlas-Export

    Die Package-README führt useReducedMotion als Hook des Motion-Subpaths. Die Datei importiert den Hook nur aus framer-motion und exportiert useMotionSafe und motionOr.

    Du tust: Importiere useMotionSafe aus @zvvch/atlas/motion oder useReducedMotion direkt aus framer-motion.

    Beleg: packages/atlas/README.md:187·packages/atlas/src/motion/use-reduced-motion.ts:14

  • F3 Der /ui-Barrel zieht optionale Peers mit

    ui/sonner.tsx importiert sonner, ui/command.tsx importiert cmdk. Beide sind als optionale Peers deklariert — der Barrel-Import bricht ohne sie.

    Du tust: Nutze Deep-Subpath-Importe oder installiere sonner und cmdk mit.

    Beleg: packages/atlas/src/ui/command.tsx:4·packages/atlas/package.json:60-65

  • F4 @zvvch/atlas/ui/* hat kein .ts-Fallback

    Das Wildcard zeigt nur auf ./src/ui/*.tsx. Alles, was in src/ui keine .tsx-Datei ist, ist per Deep-Subpath unerreichbar — bei /motion/* ist es ein Array mit beiden Endungen.

    Du tust: Bei einem nicht auflösbaren /ui/-Import den Barrel nehmen und die Asymmetrie melden.

    Beleg: packages/atlas/package.json:34

Environment-Variablen

VariableHerkunftClient-sichtbarWo real gelesenBeleg
NEXT_PUBLIC_APP_URLdoku + repojaapp/layout.tsx (metadataBase); die CI setzt sie beim Builddocs/07-tech-stack.md:166example/app/layout.tsx:20
NEXT_PUBLIC_APP_NAMEnur Dokujanirgends — im ganzen Showcase kein Lesezugriffdocs/07-tech-stack.md:167
NEXT_PUBLIC_APP_VERSIONdoku + repojaapi/health/route.ts, components/layout/footer.tsxdocs/07-tech-stack.md:168example/components/layout/footer.tsx:5
NEXT_PUBLIC_GIT_COMMITdoku + repojaapi/health/route.ts, components/layout/footer.tsxdocs/07-tech-stack.md:169example/components/layout/footer.tsx:6
NEXT_PUBLIC_SUPABASE_URL / _ANON_KEYnur Dokujaim Showcase nicht anwendbar (kein Backend)docs/07-tech-stack.md:171-172
SUPABASE_SERVICE_ROLE_KEYnur Dokuneinim Showcase nicht anwendbar — in Vercel als Encrypted markierendocs/07-tech-stack.md:173
ZVV_MAILER_BASE_URL / _API_KEYnur Dokuneinim Showcase nicht anwendbar — Server-only, API-Key encrypteddocs/07-tech-stack.md:175-176
NEXT_PUBLIC_APP_ENV / APP_ENVnur Repojalib/atlas/app-meta.ts — ausdrückliche Umgebungs-Angabe schlägt die Ableitungexample/lib/atlas/app-meta.ts:104
NEXT_PUBLIC_VERCEL_ENV / VERCEL_ENVnur Repojalib/atlas/app-meta.ts — Ableitung production/previewexample/lib/atlas/app-meta.ts:109
NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF / VERCEL_GIT_COMMIT_REFnur Repojalib/atlas/app-meta.ts — erkennt den integration-Branch im Previewexample/lib/atlas/app-meta.ts:112
NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA / VERCEL_GIT_COMMIT_SHAnur Repojalib/atlas/app-meta.ts — Commit-Fallbackexample/lib/atlas/app-meta.ts:148
NEXT_PUBLIC_BUILD_TIMEnur Repojalib/atlas/app-meta.ts — ohne Wert bleibt buildTime bewusst nullexample/lib/atlas/app-meta.ts:154

Was du nicht erfindest

Diese Punkte füllst du nicht auf eigene Faust. Jede Zeile nennt den Beleg dafür, dass Atlas es fordert — und den Beleg dafür, dass es das im Repo nicht gibt.

  • L04 Baseline-CSP — Report-Only bis zum sauberen Release-Zyklus

    Die Baseline steht (entschieden MRA, 6. August 2026): docs/10 §7.1 führt die kopierbare Richtlinie — Nonces aus der Proxy-Middleware, strict-dynamic, die style-src-Ausnahme benannt — und example/proxy.ts setzt sie als Referenz um. Halb offen bleibt die Lücke bewusst: Der Header läuft in Stufe 1 als Content-Security-Policy-Report-Only. Durchsetzend wird er erst nach einem vollen Release-Zyklus ohne Verstösse; bis dahin ist Report-Only der korrekte Zustand, kein Versäumnis.

    Du tust: Kopiere die Baseline aus 10 §7.1 samt Zweistufigkeit. Stell den Header nicht eigenmächtig auf durchsetzend — das verlangt den belegten Release-Zyklus ohne Verstösse.

    Beleg: docs/10-security.md:265·example/proxy.ts:46·example/next.config.ts:29-31

  • L05 Kanonische Rollen-Namen

    Eine ZVV-weite Rollenliste gibt es nicht, und das ist entschieden so (MRA, 6. August 2026): Rollen bleiben pro App definiert, das normative RLS-Pattern in 10 §5 führt Platzhalter statt der FZDB-Namen, die §4.1-Beispiele sind ausdrücklich als FZDB markiert, und die Namenskonvention steht in 10 §5.1 — das Präfix zvv_ ist Rollen mit ZVV-weiter Bedeutung vorbehalten. Dieser Eintrag bleibt bewusst stehen und wird per Sichtprüfung gepflegt: L05 ist die einzige Lücke ohne maschinellen Wächter; nur die Ankerprüfung der Belege warnt, wenn die Fundstellen wandern.

    Du tust: Uebernimm die FZDB-Werte nicht als Standard. Benenne app-lokale Rollen ohne zvv_-Präfix; zvv_ heisst ZVV-weite Bedeutung (10 §5.1).

    Beleg: docs/10-security.md:111·docs/10-security.md:203·docs/10-security.md:221

Selbstprüfung

Definition of Donebash
npm run lint && npm run typecheck && npm test && npm run build

Danach: Conformance an beiden Orten deklariert (G05), Abweichungstabelle geschrieben (G06).

Selbstprüfung am Referenz-Repo · bestanden

Keine offenen Befunde: example/ steht auf Version 0.3.0 auf der Linie von Atlas 0.3 und führt das Conformance-Feld "zvvAtlas": "0.3". Geprüft wird bei jedem Aufruf gegen die tatsächliche package.json, nicht gegen einen festen Satz. Die zweite von G05 geforderte Stelle, die Conformance-Zeile in example/README.md, prüft dieses Modul nicht selbst — dafür bräuchte es einen Dateizugriff, den es bewusst nicht hat; sie ist in example/tests/techstack.test.ts abgesichert.

Beleg: example/package.json:3-5·docs/08-engineering.md:199-219

Maschinenlesbar

/llms.txt liefert denselben Vertrag als Klartext. Seite und Endpunkt rendern dasselbe Modul lib/atlas/techstack.ts; ein Test hält beide deckungsgleich und prüft jeden Beleg gegen das Dateisystem — Pfad, Zeilenzahl und den erwarteten Text an der zitierten Stelle. Zwei handgepflegte Fassungen würden auseinanderlaufen, und eine Seite über Drift darf diesen Fehler nicht in sich selbst einbauen.

Abgrenzung zu den bestehenden Endpunkten: /api/meta und /api/atlas liefern die Selbstauskunft der App als JSON (Version, Commit, Umgebung). /llms.txt liefert Regeln als Prosa. Keine Überschneidung.