diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a0db57c..ef63851 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,6 +11,17 @@ updates: web-dependencies: patterns: - "*" + - package-ecosystem: npm + directory: /mcp + schedule: + interval: weekly + open-pull-requests-limit: 5 + labels: + - dependencies + groups: + mcp-dependencies: + patterns: + - "*" - package-ecosystem: github-actions directory: / schedule: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9f1034c..f47aa21 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,7 +3,9 @@ name: docs on: push: branches: [main] + paths: ["**/*.md", ".github/workflows/docs.yml"] pull_request: + paths: ["**/*.md", ".github/workflows/docs.yml"] permissions: contents: read diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 9f10606..e4296bf 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -3,10 +3,11 @@ This document records how the repository is structured and the design decisions behind it. > [!NOTE] -> **Summary.** `auditor` is a monorepo with two parts: a library of audit *master prompts* -> (`audit-prompts/`) and a Next.js landing page (`web/`). Two normative standards govern how the -> prompts behave and what they output. Everything is provider-agnostic Markdown plus one small -> static site. +> **Summary.** `auditor` is a monorepo with three parts: a library of audit *master prompts* +> (`audit-prompts/`), a Next.js landing page (`web/`), and a stdio MCP server (`mcp/`) that exposes +> the prompts as native agent tools. Two normative standards govern how the prompts behave and what +> they output. Everything is provider-agnostic Markdown plus one small static site and one small +> server. ## Layout @@ -14,6 +15,7 @@ This document records how the repository is structured and the design decisions auditor/ ├── audit-prompts/ the 13 audit master prompts (the product) ├── web/ the landing page (Next.js 16) → auditor.rapold.io +├── mcp/ a stdio MCP server exposing the prompts as native agent tools ├── templates/ a canonical README skeleton implementing the doc standard ├── DOCUMENTATION-STANDARD.md (+ .en.md) the doc-quality yardstick └── ISSUE-OUTPUT-STANDARD.md the mandatory GitHub-issue output contract @@ -63,3 +65,7 @@ Each prompt also shares a severity scale (P0–P3), a finding schema, and a mand ## Web app See [`web/README.md`](web/README.md) for the `web/` stack, local development, and deployment. + +## MCP server + +See [`mcp/README.md`](mcp/README.md) for the `mcp/` stdio server, its tools, and installation. diff --git a/CHANGELOG.md b/CHANGELOG.md index e37ace9..0dff1eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,32 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/), die Versionier ## [Unreleased] +### Added +- **MCP server (`mcp/`)** — a dependency-light stdio Model Context Protocol server that exposes the + verified, version-pinned audit prompts as native agent tools (`list_audits`, `get_audit_prompt`, + `get_orchestrator`, `get_standard`) for Claude Desktop, Claude Code, Cursor, and any MCP-capable + agent. Mirrors the canonical `AUDITS` catalogue and reads prompts live from `audit-prompts/`. +- **Per-audit detail pages** — `/audits/` (German mirror `/de/audits/`) with long-form + copy, an audit-specific activation prompt, and per-audit Open Graph images, for deep-linking and + SEO. Short vanity slugs (e.g. `/security-audit`, `/a11y-audit`) 308-redirect to the canonical path. +- **Sample-report gallery** — `/reports` and `/reports/` rendering the real #97 self-audit run + (scorecard, not-applicable reasons, headline findings, and the cross-audit dedup exhibit), every + field mapped to a verifiable GitHub artifact and localized EN/DE. +- **Per-audit photographic hero images** ("Verified Systems Lab") shown on the audit detail pages and + the homepage audit cards, auto-detected via the `public/.webp` convention. + +### Changed +- The landing page is now a multi-route site (home, per-audit details, reports) sharing a common + header/footer/nav chrome, rather than a single page. +- Vercel deploys are git-connected and automatic (production on push to `main`, previews per branch), + with the monorepo build unblocked (Root Directory = `web`). + +### Fixed +- **CI/release hardening** — automated `CHECKSUMS.txt` regeneration and verification, a version-pin + verification gate so release pins can't silently drift, an ESLint / jsx-a11y gate for `web/`, and a + CI workflow for the `mcp/` package. +- Dropped dead exports in `reports.ts` flagged by the lean audit. + ## [0.8.0] - 2026-06-18 ### Added diff --git a/README.md b/README.md index d0df5b9..42ebf1b 100644 --- a/README.md +++ b/README.md @@ -279,6 +279,7 @@ auditor/ │ ├── PULL_REQUEST_TEMPLATE.md │ └── ISSUE_TEMPLATE/ findings + new-template + chooser config ├── web/ landing page (Next.js 16) → auditor.rapold.io +├── mcp/ stdio MCP server (exposes the prompts as agent tools) └── audit-prompts/ ├── full-audit-master-prompt.md (orchestrator) ├── security-audit-master-prompt.md @@ -291,7 +292,9 @@ auditor/ ├── ai-llm-audit-master-prompt.md ├── compliance-privacy-audit-master-prompt.md ├── accessibility-audit-master-prompt.md - └── documentation-audit-master-prompt.md + ├── documentation-audit-master-prompt.md + ├── content-audit-master-prompt.md + └── lean-audit-master-prompt.md ``` The landing page lives in `web/` — its stack, local development, and deployment are documented in diff --git a/mcp/src/catalogue.ts b/mcp/src/catalogue.ts index bc40243..2fd0a86 100644 --- a/mcp/src/catalogue.ts +++ b/mcp/src/catalogue.ts @@ -6,9 +6,12 @@ * This mirrors the canonical `AUDITS` list in `web/lib/content.ts`. It is kept * as a standalone, dependency-free copy on purpose: the MCP package must build * and run on its own without importing the Next.js `web/` workspace (which pulls - * in React, lucide-react, etc.). The `prompts` CI gate plus `CHECKSUMS.txt` - * keep the two lists honest — and `npm test` asserts every `file` here resolves - * to a real prompt on disk, so drift fails loudly. + * in React, lucide-react, etc.). The parity test in `mcp/test/lib.test.js` + * verifies this catalogue: it asserts the catalogue's audit keys exactly equal + * the set of `` derived from the `audit-prompts/-audit-master-prompt.md` + * files on disk (excluding the orchestrator), so the catalogue cannot silently + * diverge from the prompt files — the single source of truth. `web/lib/content.ts` + * is pinned to the same prompt-file key set by an equivalent test on the web side. */ export type AuditEntry = { diff --git a/mcp/test/lib.test.js b/mcp/test/lib.test.js index 087d456..1402234 100644 --- a/mcp/test/lib.test.js +++ b/mcp/test/lib.test.js @@ -7,8 +7,9 @@ import test from "node:test"; import assert from "node:assert/strict"; +import { readdirSync } from "node:fs"; import { fileURLToPath } from "node:url"; -import { dirname, resolve } from "node:path"; +import { dirname, join, resolve } from "node:path"; import { listAudits, @@ -23,6 +24,20 @@ const here = dirname(fileURLToPath(import.meta.url)); // repo root is one level up from mcp/ const repoRoot = findRepoRoot(resolve(here, "..")); +/** + * The set of audit keys derived from the prompt files on disk — the single + * source of truth. A file named `-audit-master-prompt.md` contributes + * ``; the orchestrator (`full-audit-master-prompt.md`) is excluded. + */ +function promptFileKeys() { + const SUFFIX = "-audit-master-prompt.md"; + return new Set( + readdirSync(join(repoRoot, "audit-prompts")) + .filter((f) => f.endsWith(SUFFIX) && f !== "full-audit-master-prompt.md") + .map((f) => f.slice(0, -SUFFIX.length)), + ); +} + test("findRepoRoot locates a directory with audit-prompts/ and CHECKSUMS.txt", () => { assert.equal(repoRoot, resolve(here, "..", "..")); }); @@ -44,6 +59,20 @@ test("list_audits returns exactly 13 audits with keys, descriptions, and mapping assert.ok(keys.includes("lean"), "includes lean"); }); +test("catalogue keys exactly match the audit-prompts/ files (single source of truth)", () => { + // Pins the catalogue to the prompt files: the keys the MCP server advertises + // must be exactly the set of from audit-prompts/-audit-master-prompt.md + // (excluding the orchestrator). Catches a prompt added/removed/renamed without + // updating the catalogue, in either direction. + const catalogueKeys = new Set(listAudits().audits.map((a) => a.key)); + const fileKeys = promptFileKeys(); + assert.deepEqual( + [...catalogueKeys].sort(), + [...fileKeys].sort(), + "catalogue keys must equal the prompt-file key set", + ); +}); + test("every catalogued audit prompt resolves to non-empty content on disk", async () => { for (const { key } of listAudits().audits) { const text = await getAuditPrompt(repoRoot, key); diff --git a/web/README.md b/web/README.md index d2ade3f..6abdb63 100644 --- a/web/README.md +++ b/web/README.md @@ -4,8 +4,10 @@ The landing page for the [auditor](https://github.com/marcelrapold/auditor) mast library, served at [auditor.rapold.io](https://auditor.rapold.io). > [!NOTE] -> **Management summary.** A single-page Next.js 16 site that presents the audit library, the -> shared six-phase method, and the two governing standards. It is **ZVV-Atlas-conformant** — it +> **Management summary.** A multi-route Next.js 16 site that presents the audit library, the +> shared six-phase method, and the two governing standards — with per-audit detail pages +> (`/audits/`) and a sample-report gallery (`/reports`), each mirrored in German under `/de`. +> It is **ZVV-Atlas-conformant** — it > follows the Atlas tech stack and conventions (Next.js 16 App Router, Tailwind v4, shadcn/ui > new-york + neutral with HSL CSS-variable tokens, next-themes) — but ships auditor's own brand > (an emerald "verified" accent) and uses **no ZVV brand assets**, so it stays clean for a public @@ -37,6 +39,8 @@ Other scripts: npm run build # production build npm run start # serve the production build npm run typecheck # tsc --noEmit +npm run lint # eslint . (jsx-a11y gate) +npm run test # vitest run (content/i18n/reports parity tests) ``` ## Structure @@ -48,39 +52,57 @@ web/ │ ├── shell.tsx shared shell (fonts, base metadata, ThemeProvider, Analytics) │ ├── (en)/ │ │ ├── layout.tsx English root layout (, EN metadata) -│ │ └── page.tsx the landing page at / +│ │ ├── page.tsx the landing page at / +│ │ ├── opengraph-image.tsx English 1200×630 social card (next/og) +│ │ ├── audits/[slug]/ per-audit detail page /audits/ (+ per-audit OG image) +│ │ └── reports/ /reports gallery + /reports/[slug] detail │ ├── (de)/ -│ │ ├── layout.tsx German root layout (, DE metadata) │ │ └── de/ -│ │ ├── page.tsx the landing page at /de -│ │ └── opengraph-image.tsx German 1200×630 social card +│ │ ├── layout.tsx German root layout (, DE metadata) +│ │ ├── page.tsx the landing page at /de +│ │ ├── opengraph-image.tsx German 1200×630 social card +│ │ ├── audits/[slug]/ German /de/audits/ (+ per-audit OG image) +│ │ └── reports/ /de/reports gallery + /de/reports/[slug] detail │ ├── not-found.tsx branded 404 (renders its own shell) │ ├── icon.svg favicon (ShieldCheck mark) │ ├── apple-icon.tsx 180×180 apple-touch-icon (next/og) -│ ├── opengraph-image.tsx English 1200×630 social card (next/og) │ ├── manifest.ts PWA web app manifest │ ├── robots.ts robots.txt -│ └── sitemap.ts sitemap.xml (/ and /de) +│ └── sitemap.ts sitemap.xml (/, /de, /audits/*, /reports/*) ├── components/ -│ ├── landing.tsx page sections (server component + client islands) + JSON-LD +│ ├── landing.tsx home-page sections (server component + client islands) + JSON-LD +│ ├── audit-page.tsx per-audit detail page (hero, activation, what it covers) +│ ├── reports-page.tsx /reports gallery + report detail rendering +│ ├── site-chrome.tsx shared header / footer / nav chrome across routes +│ ├── copy-command.tsx copy-to-clipboard activation CTA +│ ├── term.tsx inline glossary tooltip (pairs with lib/glossary.tsx) │ ├── reveal.tsx scroll-in reveal (CSS + IntersectionObserver, reduced-motion aware) │ ├── theme-provider.tsx next-themes wrapper │ ├── theme-toggle.tsx light/dark toggle │ ├── mobile-nav.tsx mobile disclosure navigation (Escape-to-close) │ ├── icons.tsx inline GitHub mark │ └── ui/ button (buttonVariants), badge (cva) +├── public/ +│ └── .webp per-audit hero images (auto-detected by lib/heroes.ts) └── lib/ - ├── content.ts audits, principles, phases, standards, nav (single source) - ├── i18n.ts EN/DE dictionary + localizers - ├── site.ts SITE_URL, TITLE, DESCRIPTION (shared by metadata/robots/sitemap) - ├── brand.ts brand SVG paths (icon/OG) - ├── og.tsx shared Open Graph image renderer - └── utils.ts cn() + ├── content.ts audits, principles, phases, standards, nav (single source) + ├── audit-details.ts long-form EN/DE copy for each /audits/ detail page + ├── reports.ts the sample-report data (/reports) — verifiable GitHub artifacts + ├── heroes.ts per-audit hero-image resolver (public/.webp convention) + ├── glossary.tsx jargon terms + definitions for inline tooltips + ├── i18n.ts EN/DE dictionary + localizers (incl. report prose helpers) + ├── site.ts SITE_URL, TITLE, DESCRIPTION (shared by metadata/robots/sitemap) + ├── brand.ts brand SVG paths (icon/OG) + ├── og.tsx shared Open Graph image renderer + └── utils.ts cn() ``` The site ships two locales as separate root layouts (route groups `(en)` and `(de)`) so each -document carries the correct ``. To change the content (audits, phases, copy), edit -`lib/content.ts` and the `lib/i18n.ts` dictionary. +document carries the correct ``. Each audit has a detail page at `/audits/` (German +mirror `/de/audits/`); short vanity slugs like `/security-audit` 308-redirect to the canonical +path. Per-audit hero images follow a convention — drop `public/.webp` and `lib/heroes.ts` +auto-detects it; no wiring needed. To change the content (audits, phases, copy), edit +`lib/content.ts`, `lib/audit-details.ts`, and the `lib/i18n.ts` dictionary. ## Deploy to Vercel diff --git a/web/app/(de)/de/reports/[slug]/page.tsx b/web/app/(de)/de/reports/[slug]/page.tsx index 79da78e..f988641 100644 --- a/web/app/(de)/de/reports/[slug]/page.tsx +++ b/web/app/(de)/de/reports/[slug]/page.tsx @@ -37,7 +37,6 @@ export async function generateMetadata({ title: prose.title, description: prose.summary, publishedTime: report.date, - images: [`${SITE_URL}/de/opengraph-image`], }, }; } diff --git a/web/app/(de)/de/reports/page.tsx b/web/app/(de)/de/reports/page.tsx index a04fc35..7057031 100644 --- a/web/app/(de)/de/reports/page.tsx +++ b/web/app/(de)/de/reports/page.tsx @@ -22,7 +22,6 @@ export function generateMetadata(): Metadata { url: PATH, title: tt.repIndexTitle, description: tt.repIndexLead, - images: [`${SITE_URL}/de/opengraph-image`], }, }; } diff --git a/web/app/(en)/reports/[slug]/page.tsx b/web/app/(en)/reports/[slug]/page.tsx index 74c631d..41d3e36 100644 --- a/web/app/(en)/reports/[slug]/page.tsx +++ b/web/app/(en)/reports/[slug]/page.tsx @@ -36,7 +36,6 @@ export async function generateMetadata({ title: prose.title, description: prose.summary, publishedTime: report.date, - images: [`${SITE_URL}/opengraph-image`], }, }; } diff --git a/web/app/(en)/reports/page.tsx b/web/app/(en)/reports/page.tsx index d1c4de7..78e31bb 100644 --- a/web/app/(en)/reports/page.tsx +++ b/web/app/(en)/reports/page.tsx @@ -21,7 +21,6 @@ export function generateMetadata(): Metadata { url: PATH, title: tt.repIndexTitle, description: tt.repIndexLead, - images: [`${SITE_URL}/opengraph-image`], }, }; } diff --git a/web/components/audit-page.tsx b/web/components/audit-page.tsx index f33f42a..e4cb360 100644 --- a/web/components/audit-page.tsx +++ b/web/components/audit-page.tsx @@ -42,7 +42,9 @@ export function AuditDetailPage({ name, lang }: { name: string; lang: Lang }) { inLanguage: lang === "de" ? "de" : "en", isPartOf: { "@type": "WebSite", name: "auditor", url: SITE_URL }, author: { "@type": "Person", name: "Marcel Rapold" }, - image: `${pageUrl}/opengraph-image`, + // The per-audit hero is a real, stable asset; the bare `/opengraph-image` + // path 404s (Next serves it only at a hashed URL), so don't reference it here. + ...(hero ? { image: `${SITE_URL}${hero}` } : {}), }; const breadcrumbLd = { @@ -92,10 +94,13 @@ export function AuditDetailPage({ name, lang }: { name: string; lang: Lang }) { sizes="100vw" className="object-cover object-right" /> - {/* Left-weighted scrim so the headline/CTA stay legible over the image. */} + {/* Scrim for headline/CTA legibility (SC 1.4.3). In dark mode (default) it + fades to reveal the photo; in light mode it stays strong everywhere, + because muted text over a near-black photo would otherwise fail contrast — + so the light-theme hero reads as a subtle texture, the dark one as the full image. */}
) : ( diff --git a/web/components/landing.tsx b/web/components/landing.tsx index 5312a37..f45256e 100644 --- a/web/components/landing.tsx +++ b/web/components/landing.tsx @@ -38,7 +38,6 @@ const JSON_LD = { operatingSystem: "Any", description: DESCRIPTION, url: SITE_URL, - image: `${SITE_URL}/opengraph-image`, author: { "@type": "Person", name: "Marcel Rapold" }, offers: { "@type": "Offer", price: "0", priceCurrency: "USD" }, license: "https://opensource.org/licenses/MIT", diff --git a/web/lib/audit-details.ts b/web/lib/audit-details.ts index b35aca3..be63b20 100644 --- a/web/lib/audit-details.ts +++ b/web/lib/audit-details.ts @@ -108,7 +108,7 @@ export const AUDIT_DETAILS: Record "body": "The EAA deadline is approaching and you need to know where the product fails conformance before an auditor or a complaint finds out. The accessibility agent runs a full WCAG 2.2 AA sweep — semantics, keyboard operability, focus management, contrast, target size — and maps each failure to its success-criterion number and legal context." } ], - "output": "A 0-100 scorecard per dimension plus priority-sorted German GitHub issues, each with a management summary, the evidence, a before/after fix, and a 30/60/90 roadmap." + "output": "A 0-100 scorecard per dimension plus priority-sorted GitHub issues, each with a management summary, the evidence, a before/after fix, and a 30/60/90 roadmap." }, "de": { "tagline": "Sechzehn Frontend-Spezialisten prüfen ein UI — jeder Befund verifiziert, bevor er eingereicht wird.", @@ -127,7 +127,7 @@ export const AUDIT_DETAILS: Record "body": "Die EAA-Frist rückt näher und du musst wissen, wo das Produkt die Konformität verfehlt, bevor ein Prüfer oder eine Beschwerde es findet. Der Accessibility-Agent fährt einen vollständigen WCAG-2.2-AA-Sweep — Semantik, Keyboard-Bedienbarkeit, Focus-Management, Kontrast, Target Size — und ordnet jedem Fehler seine Success-Criterion-Nummer und den rechtlichen Kontext zu." } ], - "output": "Eine 0-100-Scorecard pro Dimension plus nach Priorität sortierte deutsche GitHub-Issues, jedes mit Management-Summary, Beleg, Before/After-Fix und einer 30/60/90-Roadmap." + "output": "Eine 0-100-Scorecard pro Dimension plus nach Priorität sortierte GitHub-Issues, jedes mit Management-Summary, Beleg, Before/After-Fix und einer 30/60/90-Roadmap." } }, "api": { @@ -148,7 +148,7 @@ export const AUDIT_DETAILS: Record "body": "You generate client SDKs from an OpenAPI spec that no longer matches the handlers. The audit lints the spec and produces a contract-drift matrix per operation — removed fields, narrowed types, changed defaults, wrong status codes — so the generated clients stop breaking against real responses." } ], - "output": "A scorecard graded per dimension plus a priority-sorted tracking issue and one German GitHub issue per finding, each with a management summary and a before/after fix." + "output": "A scorecard graded per dimension plus a priority-sorted tracking issue and one GitHub issue per finding, each with a management summary and a before/after fix." }, "de": { "tagline": "Prüfe deine API gegen ihren veröffentlichten Contract — und gegen das, was der Code tatsächlich tut.", @@ -167,7 +167,7 @@ export const AUDIT_DETAILS: Record "body": "Du generierst Client-SDKs aus einer OpenAPI-Spec, die nicht mehr zu den Handlern passt. Das Audit lintet die Spec und erstellt eine Contract-Drift-Matrix pro Operation — entfernte Felder, verengte Typen, geänderte Defaults, falsche Status-Codes — damit die generierten Clients nicht länger an echten Responses brechen." } ], - "output": "Eine pro Dimension benotete Scorecard plus ein nach Priorität sortiertes Tracking-Issue und pro Befund ein deutsches GitHub-Issue, jedes mit Management-Summary und Vorher/Nachher-Fix." + "output": "Eine pro Dimension benotete Scorecard plus ein nach Priorität sortiertes Tracking-Issue und pro Befund ein GitHub-Issue, jedes mit Management-Summary und Vorher/Nachher-Fix." } }, "performance": { @@ -236,11 +236,11 @@ export const AUDIT_DETAILS: Record "useCases": [ { "title": "Vor einer riskanten Produktiv-Migration", - "body": "Du willst eine NOT-NULL-Spalte hinzufügen, eine grosse Tabelle umschreiben oder einen destruktiven Backfill fahren. Der Audit prüft jede Migration auf Locking- und Blocking-Risiko, Reversibilität und einen Rollback-Pfad und markiert jede destruktive Operation ohne Sicherheitsnetz — und sequenziert den Fix als Expand/Contract-Plan mit Backfill und Rollback." + "body": "Du willst eine NOT-NULL-Spalte hinzufügen, eine große Tabelle umschreiben oder einen destruktiven Backfill fahren. Der Audit prüft jede Migration auf Locking- und Blocking-Risiko, Reversibilität und einen Rollback-Pfad und markiert jede destruktive Operation ohne Sicherheitsnetz — und sequenziert den Fix als Expand/Contract-Plan mit Backfill und Rollback." }, { "title": "Wenn falsche Rows im Reporting auftauchen", - "body": "Die Billing-Reconciliation bricht oder ein Join liefert Rows, die es nicht geben dürfte. Der Audit reasoned über — und prüft mit Read-only-Zugriff stichprobenartig auf — Orphans, Duplikate und Nulls in Pflichtfeldern, führt sie auf einen fehlenden FK-, UNIQUE- oder NOT-NULL-Constraint zurück und liefert die Cleanup-Query plus den Constraint, der es künftig verhindert." + "body": "Die Billing-Reconciliation bricht oder ein Join liefert Rows, die es nicht geben dürfte. Der Audit prüft per Reasoning — und mit Read-only-Zugriff stichprobenartig — auf Orphans, Duplikate und Nulls in Pflichtfeldern, führt sie auf einen fehlenden FK-, UNIQUE- oder NOT-NULL-Constraint zurück und liefert die Cleanup-Query plus den Constraint, der es künftig verhindert." }, { "title": "Multi-Tenant-SaaS mit PII", @@ -348,26 +348,26 @@ export const AUDIT_DETAILS: Record "body": "A new profiling or LLM feature goes live without a DPIA or an EU AI Act risk tier. The audit classifies the feature, checks Art. 22 safeguards and AI-transparency duties, and traces whether the data leaves the EEA without a valid transfer mechanism." } ], - "output": "A per-dimension scorecard, a verified data-flow / RoPA map, and a priority-sorted findings register where each finding cites its article and ships a concrete before/after fix, turned into German GitHub issues under one tracking issue." + "output": "A per-dimension scorecard, a verified data-flow / RoPA map, and a priority-sorted findings register where each finding cites its article and ships a concrete before/after fix, turned into GitHub issues under one tracking issue." }, "de": { - "tagline": "Findet, wo du personenbezogene Daten ohne Rechtsgrundlage, Consent oder Loeschmoeglichkeit verarbeitest.", - "approach": "Primaere Linse ist die DSGVO, erweitert um ePrivacy, den EU AI Act und CCPA, wo sie greifen. Ein Schwarm spezialisierter Agents baut eine verifizierte Data-Flow- / RoPA-Karte und prueft dann Rechtsgrundlage, Consent und Cookies, Transparenz, Betroffenenrechte, Aufbewahrung, Drittlandtransfers, Auftragsverarbeiter-Vertraege und Breach-Readiness. Consent wird per Network-Trace geprueft, nicht ueber das Banner-UI, und Loesch- und Auskunftsrechte werden End-to-End im Code verifiziert; jeder Befund zitiert einen konkreten Artikel und ein File, eine Tabelle, ein Cookie oder eine Policy-Klausel.", + "tagline": "Findet, wo du personenbezogene Daten ohne Rechtsgrundlage, Consent oder Löschmöglichkeit verarbeitest.", + "approach": "Primäre Linse ist die DSGVO, erweitert um ePrivacy, den EU AI Act und CCPA, wo sie greifen. Ein Schwarm spezialisierter Agents baut eine verifizierte Data-Flow- / RoPA-Karte und prüft dann Rechtsgrundlage, Consent und Cookies, Transparenz, Betroffenenrechte, Aufbewahrung, Drittlandtransfers, Auftragsverarbeiter-Verträge und Breach-Readiness. Consent wird per Network-Trace geprüft, nicht über das Banner-UI, und Lösch- und Auskunftsrechte werden End-to-End im Code verifiziert; jeder Befund zitiert einen konkreten Artikel und ein File, eine Tabelle, ein Cookie oder eine Policy-Klausel.", "useCases": [ { - "title": "Cookie-Banner, das luegt", - "body": "Marketing liefert ein Consent-Banner aus, aber Analytics- und Ad-Pixel feuern trotzdem beim ersten Laden. Ein Network-Trace faengt jeden nicht-essenziellen Tag, der vor dem Opt-in laeuft, benennt die Zeile, die ihn injiziert, und mappt ihn auf ePrivacy und Art. 6 mit einem Gate-before-Consent-Fix." + "title": "Cookie-Banner, das lügt", + "body": "Marketing liefert ein Consent-Banner aus, aber Analytics- und Ad-Pixel feuern trotzdem beim ersten Laden. Ein Network-Trace fängt jeden nicht-essenziellen Tag, der vor dem Opt-in läuft, benennt die Zeile, die ihn injiziert, und mappt ihn auf ePrivacy und Art. 6 mit einem Gate-before-Consent-Fix." }, { - "title": "Ein Nutzer stellt einen Loeschantrag", - "body": "Der Support verspricht Loeschung in der Datenschutzerklaerung, aber niemand hat verfolgt, wo die Daten wirklich liegen. Das Audit folgt jeder Kategorie personenbezogener Daten und zeigt, wo die Loeschung nicht bis in Backups, Logs oder Drittanbieter durchschlaegt, gemessen an Art. 17." + "title": "Ein Nutzer stellt einen Löschantrag", + "body": "Der Support verspricht Löschung in der Datenschutzerklärung, aber niemand hat verfolgt, wo die Daten wirklich liegen. Das Audit folgt jeder Kategorie personenbezogener Daten und zeigt, wo die Löschung nicht bis in Backups, Logs oder Drittanbieter durchschlägt, gemessen an Art. 17." }, { "title": "AI-Feature-Launch in der EU", - "body": "Ein neues Profiling- oder LLM-Feature geht ohne DPIA und ohne EU-AI-Act-Risikoklasse live. Das Audit klassifiziert das Feature, prueft Art.-22-Schutzmassnahmen und AI-Transparenzpflichten und verfolgt, ob die Daten ohne gueltigen Transfermechanismus den EWR verlassen." + "body": "Ein neues Profiling- oder LLM-Feature geht ohne DPIA und ohne EU-AI-Act-Risikoklasse live. Das Audit klassifiziert das Feature, prüft Art.-22-Schutzmaßnahmen und AI-Transparenzpflichten und verfolgt, ob die Daten ohne gültigen Transfermechanismus den EWR verlassen." } ], - "output": "Eine Scorecard je Dimension, eine verifizierte Data-Flow- / RoPA-Karte und ein nach Prioritaet sortiertes Befundregister, in dem jeder Befund seinen Artikel zitiert und einen konkreten Before/After-Fix liefert, ueberfuehrt in deutsche GitHub-Issues unter einem Tracking-Issue." + "output": "Eine Scorecard je Dimension, eine verifizierte Data-Flow- / RoPA-Karte und ein nach Priorität sortiertes Befundregister, in dem jeder Befund seinen Artikel zitiert und einen konkreten Before/After-Fix liefert, überführt in GitHub-Issues unter einem Tracking-Issue." } }, "accessibility": { @@ -414,7 +414,7 @@ export const AUDIT_DETAILS: Record "en": { "tagline": "Checks your docs against the standard and against the code that proves them right or wrong.", "approach": "Specialist agents walk each critical reader journey with docs only, then verify every claim against reality: documented commands run against the scripts, env vars are checked against what the code reads, and code samples are traced or executed. It scores head-matter, onboarding, doc-code drift, writing quality, and Diátaxis fit on a 0-100 rubric, judging each page against the job it is meant to do.", - "output": "A rubric scorecard with grade band plus a drift register and prioritized German GitHub issues, each with a before/after fix.", + "output": "A rubric scorecard with grade band plus a drift register and prioritized GitHub issues, each with a before/after fix.", "useCases": [ { "title": "Before open-sourcing a repo", @@ -433,7 +433,7 @@ export const AUDIT_DETAILS: Record "de": { "tagline": "Prüft deine Doku gegen den Standard und gegen den Code, der sie bestätigt oder widerlegt.", "approach": "Spezialisten-Agents gehen jede kritische Leser-Journey nur mit der Doku durch und verifizieren dann jede Aussage gegen die Realität: dokumentierte Commands laufen gegen die Scripts, Env-Variablen werden gegen den lesenden Code geprüft, Code-Beispiele werden nachvollzogen oder ausgeführt. Bewertet werden Repo-Kopf, Onboarding, Doc-Code-Drift, Schreibstil und Diátaxis-Passung auf einer 0-100-Rubrik — jede Seite gemessen an dem Job, den sie erfüllen soll.", - "output": "Eine Rubrik-Scorecard mit Notenband plus ein Drift-Register und priorisierte deutsche GitHub-Issues, jedes mit einem Vorher/Nachher-Fix.", + "output": "Eine Rubrik-Scorecard mit Notenband plus ein Drift-Register und priorisierte GitHub-Issues, jedes mit einem Vorher/Nachher-Fix.", "useCases": [ { "title": "Vor dem Open-Sourcing", diff --git a/web/lib/content.test.ts b/web/lib/content.test.ts index e6ec727..da03ceb 100644 --- a/web/lib/content.test.ts +++ b/web/lib/content.test.ts @@ -1,7 +1,26 @@ +import { existsSync, readdirSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; import { describe, it, expect } from "vitest"; import { AUDITS, AUDIT_COUNT } from "./content"; import { SITE_URL, TITLE, DESCRIPTION } from "./site"; +// This file lives at web/lib/content.test.ts, so the repo root (which ships +// audit-prompts/) is two levels up from the directory. +const here = dirname(fileURLToPath(import.meta.url)); +const promptsDir = resolve(here, "..", "..", "audit-prompts"); + +/** Audit keys derived from the prompt files — the single source of truth. + * `-audit-master-prompt.md` contributes ``; the orchestrator is excluded. */ +function promptFileKeys(): Set { + const SUFFIX = "-audit-master-prompt.md"; + return new Set( + readdirSync(promptsDir) + .filter((f) => f.endsWith(SUFFIX) && f !== "full-audit-master-prompt.md") + .map((f) => f.slice(0, -SUFFIX.length)), + ); +} + describe("content", () => { it("exposes a non-empty audit list with a matching count", () => { expect(AUDITS.length).toBeGreaterThan(0); @@ -16,6 +35,19 @@ describe("content", () => { } }); + it("every AUDITS[].file exists in audit-prompts/", () => { + for (const a of AUDITS) { + expect(existsSync(join(promptsDir, a.file)), `missing prompt file for "${a.name}": ${a.file}`).toBe(true); + } + }); + + it("the AUDITS key set exactly equals the audit-prompts/ file key set (single source of truth)", () => { + // Pins web's catalogue to the prompt files just like mcp/test/lib.test.js pins the MCP + // server's — so web and mcp can't silently diverge from the prompts or each other. + const auditKeys = [...new Set(AUDITS.map((a) => a.name))].sort(); + const fileKeys = [...promptFileKeys()].sort(); + expect(auditKeys).toEqual(fileKeys); + }); }); describe("site", () => { diff --git a/web/lib/content.ts b/web/lib/content.ts index 9ad1160..ae3c79c 100644 --- a/web/lib/content.ts +++ b/web/lib/content.ts @@ -300,7 +300,7 @@ export const STANDARDS: Standard[] = [ name: "Issue-output standard", file: "ISSUE-OUTPUT-STANDARD.md", blurb: - "The mandatory contract every audit follows: a priority-sorted tracking issue first, then one German issue per finding, each with its own management summary.", + "The mandatory contract every audit follows: a priority-sorted tracking issue first, then one issue per finding, each with its own management summary.", icon: GitBranch, }, ]; diff --git a/web/lib/i18n.ts b/web/lib/i18n.ts index 3361b4d..e3bb7bd 100644 --- a/web/lib/i18n.ts +++ b/web/lib/i18n.ts @@ -315,7 +315,7 @@ export const ui = { repDedupFoundBy: "Unabhängig gefunden von", repMethodTitle: "Wie dieser Lauf arbeitete", repMethodLead: - "Recon wählte die anwendbaren Audits, ein Spezialisten-Schwarm führte sie parallel aus, Befunde wurden cross-pollinatet und dedupliziert, und jeder P0/P1 musste unabhängige Skeptiker überstehen, bevor er in den Bericht kam.", + "Recon wählte die anwendbaren Audits, ein Spezialisten-Schwarm führte sie parallel aus, Befunde wurden zusammengeführt und dedupliziert, und jeder P0/P1 musste unabhängige Skeptiker überstehen, bevor er in den Bericht kam.", repViewIssue: "Issue ansehen", repFullRun: "Ganzen Lauf ansehen", repEvidence: "Beleg", @@ -344,7 +344,7 @@ const auditBlurbDe: Record = { "compliance-privacy": "Datenschutz: Rechtsgrundlage, Consent/Cookies, Betroffenenrechte, Aufbewahrung, Transfers, Breach-Readiness.", accessibility: "Tiefes A11y: Semantik, Tastatur, Fokus, Screenreader, Kontrast, Formulare, Zoom, Motorik, Motion, Kognition.", documentation: "Doku-Qualität vs. Standard: Repo-Kopf, Onboarding, Doc-Code-Drift, Schreibstil, Diátaxis, Repo-Health.", - content: "Inhalt & Botschaft: These challengen, Zielgruppen-Fit, Belege & Originalität, Struktur, Stimme, konkrete Umformulierungen.", + content: "Inhalt & Botschaft: These hinterfragen, Zielgruppen-Fit, Belege & Originalität, Struktur, Stimme, konkrete Umformulierungen.", lean: "Schlankheit: toter Code, ungenutzte/Phantom-Deps, Duplikation, AI-Slop, Dependency-Transparenz — sicherer Strip-down ohne Über-Löschen.", }; diff --git a/web/lib/reports.test.ts b/web/lib/reports.test.ts new file mode 100644 index 0000000..718cf1b --- /dev/null +++ b/web/lib/reports.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect } from "vitest"; +import { REPORTS } from "./reports"; +import { + reportVerdict, + reportReason, + reportFinding, + reportDedupProse, + type Lang, +} from "./i18n"; + +const LANGS: Lang[] = ["en", "de"]; + +describe("reports i18n resolution", () => { + for (const report of REPORTS) { + describe(`report "${report.slug}"`, () => { + for (const lang of LANGS) { + it(`[${lang}] verdictKey resolves`, () => { + // reportVerdict falls back to "" on a missing key, so a non-empty string proves it resolved. + const verdict = reportVerdict(lang, report.verdictKey); + expect(verdict, `verdictKey "${report.verdictKey}" missing in "${lang}"`).toBeTruthy(); + }); + + it(`[${lang}] every notApplicable reasonKey resolves`, () => { + for (const na of report.notApplicable) { + const reason = reportReason(lang, na.reasonKey); + expect(reason, `reasonKey "${na.reasonKey}" missing in "${lang}"`).toBeTruthy(); + } + }); + + it(`[${lang}] every finding key resolves to real prose`, () => { + for (const f of report.findings) { + // reportFinding falls back to { title: key } (no evidence) on a miss; a real entry + // carries a title that isn't the bare key and concrete evidence. + const prose = reportFinding(lang, f.key); + expect(prose.title, `finding key "${f.key}" missing in "${lang}"`).toBeTruthy(); + expect(prose.title, `finding key "${f.key}" fell back to the bare key in "${lang}"`).not.toBe(f.key); + expect(prose.evidence, `finding key "${f.key}" has no evidence in "${lang}"`).toBeTruthy(); + } + }); + + it(`[${lang}] dedup key resolves`, () => { + // reportDedupProse falls back to { title: key, body: "" } on a miss; a real entry has a body. + const dedup = reportDedupProse(lang, report.dedup.key); + expect(dedup.title, `dedup key "${report.dedup.key}" missing in "${lang}"`).toBeTruthy(); + expect(dedup.body, `dedup key "${report.dedup.key}" has no body in "${lang}"`).toBeTruthy(); + }); + } + + it("dedup.issue is one of the findings' issues", () => { + const findingIssues = report.findings.map((f) => f.issue); + expect(findingIssues).toContain(report.dedup.issue); + }); + }); + } +}); diff --git a/web/next.config.ts b/web/next.config.ts index 52fccc6..e94f2bf 100644 --- a/web/next.config.ts +++ b/web/next.config.ts @@ -9,6 +9,14 @@ const nextConfig: NextConfig = { // `/.next` and fail (ENOENT .next/package.json) once a second package (mcp/) existed. outputFileTracingRoot: join(__dirname, ".."), + images: { + // Negotiate AVIF first (~20-30% smaller than WebP) before falling back to WebP. + formats: ["image/avif", "image/webp"], + // Optimized variants are immutable (content-hashed by the `dpl` query param), so let + // browsers and the edge cache hold them for a year instead of revalidating every visit. + minimumCacheTTL: 31536000, + }, + // Short, shareable vanity URLs (e.g. /frontend-audit, /privacy-audit) that permanently // redirect to the canonical per-audit page in both languages. Derived from AUDITS so the // list never drifts from the audit catalogue.