Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 10 additions & 4 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@
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

```
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
Expand Down Expand Up @@ -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.
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<key>` (German mirror `/de/audits/<key>`) 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/<slug>` 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/<key>.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
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
9 changes: 6 additions & 3 deletions mcp/src/catalogue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<key>` derived from the `audit-prompts/<key>-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 = {
Expand Down
31 changes: 30 additions & 1 deletion mcp/test/lib.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 `<key>-audit-master-prompt.md` contributes
* `<key>`; 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, "..", ".."));
});
Expand All @@ -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 <key> from audit-prompts/<key>-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);
Expand Down
56 changes: 39 additions & 17 deletions web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<key>`) 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
Expand Down Expand Up @@ -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
Expand All @@ -48,39 +52,57 @@ web/
│ ├── shell.tsx shared <html> shell (fonts, base metadata, ThemeProvider, Analytics)
│ ├── (en)/
│ │ ├── layout.tsx English root layout (<html lang="en">, 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/<key> (+ per-audit OG image)
│ │ └── reports/ /reports gallery + /reports/[slug] detail
│ ├── (de)/
│ │ ├── layout.tsx German root layout (<html lang="de">, DE metadata)
│ │ └── de/
│ │ ├── page.tsx the landing page at /de
│ │ └── opengraph-image.tsx German 1200×630 social card
│ │ ├── layout.tsx German root layout (<html lang="de">, DE metadata)
│ │ ├── page.tsx the landing page at /de
│ │ ├── opengraph-image.tsx German 1200×630 social card
│ │ ├── audits/[slug]/ German /de/audits/<key> (+ 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/
│ └── <key>.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/<key> detail page
├── reports.ts the sample-report data (/reports) — verifiable GitHub artifacts
├── heroes.ts per-audit hero-image resolver (public/<key>.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 `<html lang>`. To change the content (audits, phases, copy), edit
`lib/content.ts` and the `lib/i18n.ts` dictionary.
document carries the correct `<html lang>`. Each audit has a detail page at `/audits/<key>` (German
mirror `/de/audits/<key>`); short vanity slugs like `/security-audit` 308-redirect to the canonical
path. Per-audit hero images follow a convention — drop `public/<key>.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

Expand Down
1 change: 0 additions & 1 deletion web/app/(de)/de/reports/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ export async function generateMetadata({
title: prose.title,
description: prose.summary,
publishedTime: report.date,
images: [`${SITE_URL}/de/opengraph-image`],
},
};
}
Expand Down
1 change: 0 additions & 1 deletion web/app/(de)/de/reports/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export function generateMetadata(): Metadata {
url: PATH,
title: tt.repIndexTitle,
description: tt.repIndexLead,
images: [`${SITE_URL}/de/opengraph-image`],
},
};
}
Expand Down
1 change: 0 additions & 1 deletion web/app/(en)/reports/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export async function generateMetadata({
title: prose.title,
description: prose.summary,
publishedTime: report.date,
images: [`${SITE_URL}/opengraph-image`],
},
};
}
Expand Down
1 change: 0 additions & 1 deletion web/app/(en)/reports/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export function generateMetadata(): Metadata {
url: PATH,
title: tt.repIndexTitle,
description: tt.repIndexLead,
images: [`${SITE_URL}/opengraph-image`],
},
};
}
Expand Down
11 changes: 8 additions & 3 deletions web/components/audit-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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. */}
<div
aria-hidden
className="pointer-events-none absolute inset-0 bg-gradient-to-r from-background via-background/85 to-background/30 md:to-transparent"
className="pointer-events-none absolute inset-0 bg-gradient-to-r from-background via-background/90 to-background/80 dark:via-background/80 dark:to-transparent"
/>
</>
) : (
Expand Down
1 change: 0 additions & 1 deletion web/components/landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading
Loading