# ZVV Atlas — Tech-Stack-Vertrag > Regelwerk für Agenten, die eine ZVV-App bauen oder eine bestehende angleichen. Atlas: v0.3 · Kapitel: docs/07-tech-stack.md · Stand des Abgleichs: 2026-08-05 Menschenfassung: https://atlas.zvv.dev/techstack · Quelle: example/lib/atlas/techstack.ts Nicht-Behauptung: Manueller Abgleich der Atlas-Doku gegen dieses Repo, kein Scanner. Misst keine fremden App-Repos. ## WIE DU DAS LIEST Jede Regel trägt zwei Angaben. 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 erlaubt, aber 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 unten. Klasse — Verhältnis von Doku und Referenz-Repo: BELEGT Doku und Repo stimmen überein. Befolge sie. ENTSCHIEDEN Sie widersprechen sich. Der Vertrag entscheidet; beide Fundstellen stehen dabei. 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 nicht als Fundstelle: CLAUDE.md:63 ## KURZANTWORT | baustein | atlas_soll | referenz_ist | beleg_soll | beleg_ist | | Next.js | 15.5+ App Router, 16+ erlaubt | 16.2.6 | docs/07-tech-stack.md:28 | example/package.json:36 | | React | 19.2 (neueste LTS) | 19.2.7 | docs/07-tech-stack.md:46 | example/package.json:39 | | TypeScript | 5.x/6.x, strict | ^6 | docs/07-tech-stack.md:15 | example/package.json:58 | | Node | 22+ (Vercel auto-managed) | 22 (.nvmrc) | docs/07-tech-stack.md:16 | .nvmrc:1 | | Tailwind | 4.x — steht in Kapitel 04, nicht in 07 | ^4 | docs/04-ui-stack.md:16 | example/package.json:57 | ## AUFTRAG — MODUS NEU 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. ## AUFTRAG — MODUS ANGLEICHEN 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. ## ABRUF $ curl -s https://atlas.zvv.dev/llms.txt ## 0. GELTUNG ZIEL: Du weisst, was MUSS und was SOLL ist — und wo eine Abweichung hingehört. FERTIG: README-Zeile und package.json-Feld stehen, die Abweichungstabelle existiert. G01 [SPEC · BELEGT] 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 G02 [GUIDELINE · BELEGT] 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 G03 [SPEC · BELEGT] 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 G04 [GUIDELINE · BELEGT] 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 G05 [GUIDELINE · BELEGT] 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 Hinweis: Welcher der beiden Mechanismen im Zweifel gilt, sagt keine der Stellen. Setze beide. G06 [VORBILD · UNGEDECKT] 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 Hinweis: 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 ZIEL: Das Projekt startet auf der richtigen Runtime, mit genau einem Lockfile. FERTIG: `node --version` passt zur .nvmrc, ein Lockfile ist committet, `npm run dev` läuft. T01 [GUIDELINE · BELEGT] 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 T02 [VORBILD · ENTSCHIEDEN] 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 Hinweis: 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. T03 [GUIDELINE · BELEGT] Committe genau ein Lockfile pro Install-Root und niemals pnpm-lock.yaml und package-lock.json nebeneinander. Beleg: docs/07-tech-stack.md:95 Hinweis: Zum realen Fall dieses Repos — zwei package-lock.json in getrennten Install-Roots, weil example/ nicht in den Workspaces steht — sagt die Regel nichts. T04 [VORBILD · UNGEDECKT] 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 Hinweis: Die Manager-Wahl ist damit nirgends maschinenlesbar. Wer das Feld setzt, verbessert bewusst — es ist keine Atlas-Regel. T05 [VORBILD · BELEGT] 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 Hinweis: Kapitel 07 nennt keine Script-Namen. Ein lint:fix-Script existiert im Vorbild nicht; format schreibt, format:check prüft nur. T07 [GUIDELINE · BELEGT] Ü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 Hinweis: 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. T06 [VORBILD · UNGEDECKT] 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 Hinweis: 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 ZIEL: Die package.json trägt die Atlas-Versionen. FERTIG: Die fünf Zeilen der Kurzantwort stimmen mit deiner package.json überein. V01 [GUIDELINE · BELEGT] Starte auf Next.js 16.x mit App Router: Kapitel 07 fordert mindestens 15.5 und erlaubt 16+ ausdrücklich, die Spec-Quellen FZDB und Mailer stehen bereits auf 16.2. Beleg: docs/07-tech-stack.md:28 · example/package.json:36 V02 [GUIDELINE · BELEGT] Benutze React 19.2 — React 18.x ist ausdrücklich Atlas-NICHT-konform. Beleg: docs/07-tech-stack.md:49 · example/package.json:39-40 V03 [GUIDELINE · BELEGT] Benutze TypeScript aus dem Korridor 5.x/6.x mit strict. Beleg: docs/07-tech-stack.md:15 · example/package.json:58 V04 [GUIDELINE · BELEGT] Benutze Tailwind 4.x — und suche die verbindliche Angabe nicht in Kapitel 07: die Stack-Uebersicht dort führt Tailwind gar nicht, obwohl die Drift-Tabelle desselben Kapitels es als vierte Achse bewertet. Die Regel steht in Kapitel 04. Beleg: docs/04-ui-stack.md:16 · docs/04-ui-stack.md:33 · docs/07-tech-stack.md:188 V05 [GUIDELINE · BELEGT] Starte eine neue App niemals auf Next 14.x und niemals auf dem Pages Router — das eine ist Atlas-NICHT-konform, das andere deprecated. Beleg: docs/07-tech-stack.md:31-32 V06 [VORBILD · UNGEDECKT] Pinne next, eslint-config-next, react und react-dom exakt ohne Caret und halte eslint-config-next versionsgleich zu next; alles übrige läuft als Caret-Range. Beleg: example/package.json:36 · example/package.json:54 Hinweis: Kapitel 07 sagt zur Pinning-Strategie kein Wort. Die Regel ist aus der realen package.json abgelesen — belegt, aber nicht normiert. ## 3. TYPESCRIPT ZIEL: Die tsconfig steht, bevor die erste Route entsteht. FERTIG: `npm run typecheck` läuft sauber durch. TS01 [GUIDELINE · BELEGT] 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 TS02 [GUIDELINE · BELEGT] Setze moduleResolution auf "bundler". Beleg: docs/07-tech-stack.md:70 · example/tsconfig.json:11 TS03 [GUIDELINE · BELEGT] 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 TS04 [VORBILD · ENTSCHIEDEN] 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 Hinweis: 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. TS05 [VORBILD · ENTSCHIEDEN] 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 Hinweis: 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). TS06 [GUIDELINE · BELEGT] 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 Hinweis: 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 ZIEL: Routen sind server-first und bleiben ohne JavaScript lesbar. Hier entstehen die meisten Laufzeitfehler einer neuen App. FERTIG: Keine Route trägt ohne Not "use client"; `npm run build` läuft durch. AR01 [GUIDELINE · BELEGT] Schreibe jede Route als Server-Komponente und setze "use client" nur dort, wo Interaktivität wirklich nötig ist — Server Components sind der Default des App Routers. Beleg: docs/07-tech-stack.md:36 · example/app/(showcase)/mermaid/page.tsx:5 AR02 [VORBILD · BELEGT] Lege das Interaktive in eine eigene "use client"-Datei neben die Route, statt die Route selbst zur Client-Komponente zu machen. Beleg: example/app/(showcase)/mermaid/mermaid-demos.tsx:1 Hinweis: Dass Props über die Server/Client-Grenze serialisierbar sein müssen — also keine Funktionen —, ist eine Next-Vorgabe. In keinem Atlas-Kapitel steht sie, und sie ist die häufigste 500er-Ursache einer neuen Route. AR03 [GUIDELINE · BELEGT] Benutze Server Actions für Mutations statt Route-Handler plus fetch. Beleg: docs/07-tech-stack.md:37 · example/lib/actions/submit-form.ts:1 AR04 [GUIDELINE · BELEGT] Benutze useActionState für den State einer Server-Action — es ist der Atlas-Default unter React 19. Beleg: docs/07-tech-stack.md:51 · example/app/(showcase)/forms/simple/page.tsx:19 AR05 [GUIDELINE · BELEGT] Exportiere metadata je Route und halte viewport als getrennten Export daneben — die Trennung ist die Next-16-Konvention. Beleg: docs/07-tech-stack.md:39-40 · example/app/layout.tsx:10 · example/app/layout.tsx:19 AR06 [VORBILD · UNGEDECKT] Nutze framer-motion ausschliesslich als m.* unterhalb eines LazyMotion-Providers, wenn du das Vorbild kopierst — sonst lädt das volle Bundle eager. Beleg: example/app/(showcase)/layout.tsx:11-13 Hinweis: Kapitel 07 sagt dazu nichts; die Begründung steht nur als Kommentar im Showcase-Layout. ## 5. ATLAS-PACKAGE KONSUMIEREN ZIEL: @zvvch/atlas ist installiert, gestylt und importierbar — ohne Auflösungs-Fehler. FERTIG: Ein Button aus @zvvch/atlas/ui/button rendert in einer Server-Komponente. P01 [GUIDELINE · BELEGT] Lege eine .npmrc mit der Scope-Registry und dem Auth-Token-Platzhalter an; der Token ist ein Classic-PAT mit Scope read:packages und wird nie committet. Beleg: packages/atlas/README.md:25-32 · docs/12-atlas-package.md:24 · .npmrc:1 P02 [GUIDELINE · BELEGT] Setze transpilePackages: ['@zvvch/atlas'] — Pflicht, nicht Kür, weil das Package rohe TypeScript-Source ohne dist ausliefert (files sind nur src und README.md). Beleg: docs/12-atlas-package.md:11 · packages/atlas/package.json:27-30 · packages/atlas/README.md:50 P03 [GUIDELINE · BELEGT] Ergänze experimental.optimizePackageImports: ['@zvvch/atlas'] und bevorzuge Deep-Subpath-Importe, sonst zieht ein Barrel-Import den ganzen UI-Barrel in den Chunk. Beleg: packages/atlas/README.md:55 · packages/atlas/README.md:61 P04 [GUIDELINE · BELEGT] Importiere in globals.css in genau dieser Reihenfolge: @import 'tailwindcss', dann @source auf das Package-src, dann @import '@zvvch/atlas/styles.css'. Beleg: packages/atlas/README.md:68-72 · docs/12-atlas-package.md:52-54 P05 [VORBILD · ENTSCHIEDEN] Prüfe vor jedem Import in einer Server-Komponente die Subpath-Tabelle unten: sechs der 19 UI-Dateien tragen kein "use client" und sind direkt server-nutzbar. Doku sagt: alle UI/Motion/Theme-Subpaths sind Client Repo tut: 6 von 19 UI-Dateien ohne "use client" — die README-Tabelle sagt eine Zeile später selbst «je Komponente» Beleg: packages/atlas/src/ui/button.tsx · packages/atlas/README.md:124 Dagegen: packages/atlas/README.md:119 Hinweis: Wer der pauschalen Zeile glaubt, packt sechs Komponenten ohne Not in "use client"-Wrapper. P06 [VORBILD · UNGEDECKT] Installiere framer-motion, sobald du @zvvch/atlas/theme nutzt — der ThemeProvider importiert MotionConfig hart, obwohl framer-motion als optionaler Peer geführt wird. Beleg: packages/atlas/src/components/theme-provider.tsx:4 · packages/atlas/package.json:59-64 Hinweis: Ohne framer-motion bricht der in der README vorgeschriebene ThemeProvider mit Module-not-found. Die Doku beschreibt diesen Fehlermodus nirgends. P07 [VORBILD · UNGEDECKT] Kopiere die Lottie-JSONs aus assets/lotties/ nach public/lotties/ deiner App — sie liegen nicht im installierten Package, weil files nur src und README.md ausliefert. Beleg: packages/atlas/README.md:191 · packages/atlas/package.json:27-30 Hinweis: Dasselbe gilt für die Corporate-Fonts. Wer sie in node_modules/@zvvch/atlas sucht, findet nichts. ## 6. QUALITÄTSTORE ZIEL: Lint, Typecheck und Tests greifen wirklich — statt nur zu existieren. FERTIG: `npm run lint`, `npm run typecheck` und `npm test` sind grün. Q01 [GUIDELINE · ENTSCHIEDEN] 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 Hinweis: Dasselbe Kapitel verlangt ESLint 9+ und zeigt eine Legacy-Config. Ein Agent, der das Snippet kopiert, baut eine Datei, die nie gelesen wird. Q02 [GUIDELINE · UNGEDECKT] 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 Hinweis: 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. Q03 [VORBILD · ENTSCHIEDEN] Lege Tests flach unter tests/.test.ts ab. Co-located Tests laufen stillschweigend nicht mit — beide Vitest-Configs sammeln ausschliesslich tests/**/*.test.ts ein. Doku sagt: co-located .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 Hinweis: Drei Varianten dokumentiert oder impliziert, eine funktioniert. Ein tests/unit/-Verzeichnis existiert im Repo nicht. Q04 [GUIDELINE · BELEGT] 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 ZIEL: Die Pflicht-Header stehen. Als einziger Block dieses Vertrags kommt er aus einem Spec-Kapitel. FERTIG: `curl -I` auf eine beliebige Route zeigt alle fünf Header. N01 [SPEC · BELEGT] 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 N02 [SPEC · ENTSCHIEDEN] 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 Hinweis: Ein Spec-Kapitel, das seine eigene Referenz-Implementierung nicht deckt: hier ist die Meldung an Atlas wichtiger als die Wahl. N03 [SPEC · BELEGT] 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 N04 [VORBILD · UNGEDECKT] 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 Hinweis: Kapitel 07 nennt unter Build-Tooling nur Turbopack und den Bundle-Analyzer. Dieser Block ist eine reale Entscheidung ohne dokumentierte Konvention. N05 [SPEC · BELEGT] Setze X-Robots-Tag: noindex für interne Admin-Apps. Beleg: docs/10-security.md:317 ## 8. ENVIRONMENT ZIEL: Die Variablen sind gesetzt und richtig einsortiert. FERTIG: /api/health meldet Version und Commit; kein Secret steht unter NEXT_PUBLIC_*. E01 [GUIDELINE · BELEGT] 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 E02 [GUIDELINE · BELEGT] 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 E03 [VORBILD · ENTSCHIEDEN] 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 E04 [VORBILD · UNGEDECKT] 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 Hinweis: Das Schema ist zugleich unvollständig und teilweise tot. ## 9. PRÄSENZ ZIEL: Die App erscheint dort richtig, wo sie noch niemand geöffnet hat — Link-Vorschau, Registerkarte, Home-Bildschirm. FERTIG: Die vier Dateien existieren, das PNG wurde angesehen, und pruefeSatori() läuft im Test über die eigene Karte. PR01 [SPEC · BELEGT] 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 Hinweis: 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. PR02 [SPEC · BELEGT] 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 Hinweis: 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. PR03 [SPEC · BELEGT] Ü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 Hinweis: 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. PR04 [SPEC · BELEGT] 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 Hinweis: 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. PR05 [SPEC · BELEGT] 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 Hinweis: Genau dieser Fall lag in zvv-fzdb vor: 6/32 = 0.19 im Favicon gegen 36/180 = 0.20 im Touch-Icon. PR06 [SPEC · BELEGT] 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 Hinweis: 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. PR07 [SPEC · BELEGT] 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 Hinweis: 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. PR08 [SPEC · BELEGT] 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 Hinweis: 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 ZIEL: Die App ist prüfbar fertig — nicht gefühlt fertig. FERTIG: Die vier Kommandos sind grün und die Abweichungstabelle ist geschrieben. A01 [GUIDELINE · BELEGT] 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 A02 [GUIDELINE · BELEGT] 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 A03 [GUIDELINE · BELEGT] 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 erste Frage jeder neuen Route. Diese Tabelle existiert in keiner Atlas-Datei. | subpath | inhalt | in_server_komponente | peers | beleg | | @zvvch/atlas | Utilities, App-Meta, VBZ-Farben, Lottie-Meta, Mermaid-Theme, Zürich-Zeit, Easings | ja | clsx, tailwind-merge | packages/atlas/src/index.ts:8-14 · packages/atlas/package.json:32 | | @zvvch/atlas/ui | Barrel über alle 19 shadcn-Primitives | nein | zieht sonner und cmdk mit — beide optionale Peers | packages/atlas/src/ui/index.ts · packages/atlas/src/ui/command.tsx:4 | | @zvvch/atlas/ui/* | Einzelne Komponente. Nur .tsx — es gibt kein .ts-Fallback. | teils | je Komponente | packages/atlas/package.json:34 | | @zvvch/atlas/motion | Motion-System, Lottie-Tools, Heroes-Registry | nein | framer-motion, lottie-react, html-to-image | packages/atlas/src/motion/index.ts | | @zvvch/atlas/motion/* | Einzelmodul. Fallback-Array mit .tsx und .ts — anders als bei /ui/*. | teils | je Modul | packages/atlas/package.json:36 | | @zvvch/atlas/theme | ThemeProvider + ThemeToggle (Datei, kein Verzeichnis) | nein | next-themes UND framer-motion (hart importiert, siehe P06) | packages/atlas/package.json:38 | | @zvvch/atlas/styles.css | Brand-Tokens + VBZ-Linienfarben (Default-Import) | ja | — | packages/atlas/package.json:39 | | @zvvch/atlas/styles/tokens.css | nur Brand-Tokens | ja | — | packages/atlas/package.json:40 | | @zvvch/atlas/styles/vbz-lines.css | nur VBZ-Linienfarben | ja | — | packages/atlas/package.json:41 | | @zvvch/atlas/assets/* | zvv-logo.svg und zvv-logo-white.svg — sonst nichts | ja | — | packages/atlas/package.json:42 | Server-safe UI-Dateien (kein "use client"): badge, button, card, empty-state, input, skeleton Client-UI-Dateien: checkbox, command, dialog, dropdown-menu, label, lightbox, popover, select, separator, sheet, sonner, tabs, tooltip ## PAKET — AUFLOESUNGS-FALLEN F1 @zvvch/atlas/motion/heroes löst nicht auf Befund: 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 Befund: 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 Befund: 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 Befund: 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 | variable | herkunft | client_sichtbar | wo_real_gelesen | beleg | | NEXT_PUBLIC_APP_URL | doku + repo | ja | app/layout.tsx (metadataBase); die CI setzt sie beim Build | docs/07-tech-stack.md:166 · example/app/layout.tsx:20 | | NEXT_PUBLIC_APP_NAME | nur Doku | ja | nirgends — im ganzen Showcase kein Lesezugriff | docs/07-tech-stack.md:167 | | NEXT_PUBLIC_APP_VERSION | doku + repo | ja | api/health/route.ts, components/layout/footer.tsx | docs/07-tech-stack.md:168 · example/components/layout/footer.tsx:5 | | NEXT_PUBLIC_GIT_COMMIT | doku + repo | ja | api/health/route.ts, components/layout/footer.tsx | docs/07-tech-stack.md:169 · example/components/layout/footer.tsx:6 | | NEXT_PUBLIC_SUPABASE_URL / _ANON_KEY | nur Doku | ja | im Showcase nicht anwendbar (kein Backend) | docs/07-tech-stack.md:171-172 | | SUPABASE_SERVICE_ROLE_KEY | nur Doku | nein | im Showcase nicht anwendbar — in Vercel als Encrypted markieren | docs/07-tech-stack.md:173 | | ZVV_MAILER_BASE_URL / _API_KEY | nur Doku | nein | im Showcase nicht anwendbar — Server-only, API-Key encrypted | docs/07-tech-stack.md:175-176 | | NEXT_PUBLIC_APP_ENV / APP_ENV | nur Repo | ja | lib/atlas/app-meta.ts — ausdrückliche Umgebungs-Angabe schlägt die Ableitung | example/lib/atlas/app-meta.ts:104 | | NEXT_PUBLIC_VERCEL_ENV / VERCEL_ENV | nur Repo | ja | lib/atlas/app-meta.ts — Ableitung production/preview | example/lib/atlas/app-meta.ts:109 | | NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF / VERCEL_GIT_COMMIT_REF | nur Repo | ja | lib/atlas/app-meta.ts — erkennt den integration-Branch im Preview | example/lib/atlas/app-meta.ts:112 | | NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA / VERCEL_GIT_COMMIT_SHA | nur Repo | ja | lib/atlas/app-meta.ts — Commit-Fallback | example/lib/atlas/app-meta.ts:148 | | NEXT_PUBLIC_BUILD_TIME | nur Repo | ja | lib/atlas/app-meta.ts — ohne Wert bleibt buildTime bewusst null | example/lib/atlas/app-meta.ts:154 | ## WAS DU NICHT ERFINDEST Diese Punkte füllst du nicht auf eigene Faust. L04 Baseline-CSP — Report-Only bis zum sauberen Release-Zyklus Befund: 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 Befund: 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 ## SELBSTPRUEFUNG $ npm run lint && npm run typecheck && npm test && npm run build Danach: Conformance an beiden Orten deklariert (G05), Abweichungstabelle geschrieben (G06). Befund am Referenz-Repo selbst: 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 ## ENDPUNKTE https://atlas.zvv.dev/techstack diese Regeln als Seite https://atlas.zvv.dev/llms.txt dieser Text https://atlas.zvv.dev/api/meta Selbstauskunft der App als JSON (Version, Commit, Umgebung) https://atlas.zvv.dev/api/health Health-Check https://github.com/zvvch/zvv-atlas