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
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.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.curl -s https://atlas.zvv.dev/llms.txtKurzantwort
Die Ist-Spalte kommt für vier der fünf Zeilen aus der package.json dieses Projekts — sie kann darum nicht veralten.
| Baustein | Atlas-Soll | Referenz-Ist | Beleg |
|---|---|---|---|
| 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 |
Wie du das liest
Verbindlichkeit — was Atlas sagt
- Aus 01 Identity, 02 Auth, 06 Mailing, 10 Security oder patterns/login-page.md. MUSS.
- Aus einem Guideline-Kapitel. SOLL — Abweichung begründungspflichtig.
- 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
- Doku und Repo stimmen überein. Befolge sie.
- Sie widersprechen sich. Der Vertrag entscheidet und nennt beide Fundstellen.
- 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.
- 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.
- 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.
- V01guidelinebelegt
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.
- V02guidelinebelegt
Benutze React 19.2 — React 18.x ist ausdrücklich Atlas-NICHT-konform.
- V03guidelinebelegt
Benutze TypeScript aus dem Korridor 5.x/6.x mit strict.
- V04guidelinebelegt
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
- V05guidelinebelegt
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
- V06vorbildungedeckt
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
Kapitel 07 sagt zur Pinning-Strategie kein Wort. Die Regel ist aus der realen package.json abgelesen — belegt, aber nicht normiert.
§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".
- 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.
- AR01guidelinebelegt
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
- AR02vorbildbelegt
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
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.
- AR03guidelinebelegt
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
- AR04guidelinebelegt
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
- AR05guidelinebelegt
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
- AR06vorbildungedeckt
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
Kapitel 07 sagt dazu nichts; die Begründung steht nur als Kommentar im Showcase-Layout.
§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.
- P01guidelinebelegt
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
- P02guidelinebelegt
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
- P03guidelinebelegt
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
- P04guidelinebelegt
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
- P05vorbildentschieden
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
Wer der pauschalen Zeile glaubt, packt sechs Komponenten ohne Not in "use client"-Wrapper.
- P06vorbildungedeckt
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
Ohne framer-motion bricht der in der README vorgeschriebene ThemeProvider mit Module-not-found. Die Doku beschreibt diesen Fehlermodus nirgends.
- P07vorbildungedeckt
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
Dasselbe gilt für die Corporate-Fonts. Wer sie in node_modules/@zvvch/atlas sucht, findet nichts.
§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.
- 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.
- 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.
- 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.
| Subpath | Inhalt | 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-14packages/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.tspackages/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 (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.
Environment-Variablen
| 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:166example/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:168example/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:169example/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. 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
npm run lint && npm run typecheck && npm test && npm run buildDanach: 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.