From 5c23cced9a027ae13acb171d3384da2c02180546 Mon Sep 17 00:00:00 2001 From: muraschal Date: Thu, 25 Jun 2026 18:13:23 +0200 Subject: [PATCH 1/5] docs(llms): add the 13th audit (lean) to the summary blurb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first paragraph an agent reads enumerated 12 audits and omitted lean — the newest differentiating template — even though the machine-readable list below had all 13. Closes #135. Part of #131. Co-Authored-By: Claude Opus 4.8 (1M context) --- web/public/llms.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/public/llms.txt b/web/public/llms.txt index cd1e539..b2b2cd3 100644 --- a/web/public/llms.txt +++ b/web/public/llms.txt @@ -2,8 +2,9 @@ > A library of master prompts that turn any AI coding agent into a swarm of specialist auditors — > security, engineering, frontend, API, performance, data, infrastructure, AI/LLM, compliance, -> accessibility, documentation, and content. Findings are evidence-bound, adversarially verified, and filed -> as GitHub issues (German or English) led by a priority-sorted tracker. +> accessibility, documentation, content, and lean (anti-bloat & dependency transparency). Findings +> are evidence-bound, adversarially verified, and filed as GitHub issues (German or English) led by +> a priority-sorted tracker. You (the AI agent) reached this file because someone pointed you at https://auditor.rapold.io. Act as the audit orchestrator: scope the work with the user, then fetch and run the right From 40acc06797aa009035f747c9068c7d2c6c0ad5c8 Mon Sep 17 00:00:00 2001 From: muraschal Date: Thu, 25 Jun 2026 18:13:23 +0200 Subject: [PATCH 2/5] fix(web): re-key index-coupled i18n + add drift invariant tests German translations were coupled to source data by array index, so a reorder in content.ts silently mistranslated with no failing test. - principleDe: keyed by the principle English title (stable id), not [i] - proofRows: keyed by BACKLOG_SAMPLE issue number; landing.tsx public shape (proofRows[i]) unchanged but now derived from the keyed source - i18n.test.ts: 9 invariants (locale key-parity, audit-details en+de coverage, principle coverage, proofRows == BACKLOG_SAMPLE) Closes #134. Part of #131. Co-Authored-By: Claude Opus 4.8 (1M context) --- web/lib/content.ts | 3 +- web/lib/i18n.test.ts | 94 ++++++++++++++++++++++++++++++++++++++++++++ web/lib/i18n.ts | 65 ++++++++++++++++++------------ 3 files changed, 136 insertions(+), 26 deletions(-) create mode 100644 web/lib/i18n.test.ts diff --git a/web/lib/content.ts b/web/lib/content.ts index f6095f0..9ad1160 100644 --- a/web/lib/content.ts +++ b/web/lib/content.ts @@ -52,7 +52,8 @@ export const SAMPLE_FINDING = { } as const; /** Six real findings from this page's own content audit (#123) — backlog exhibit. - * Titles are localized in i18n (proofRows), matched by index. */ + * Titles are localized in i18n (proofRowsByIssue), keyed by the issue number `n` — + * reordering this list re-orders the localized titles to match (no index drift). */ export const BACKLOG_SAMPLE = [ { n: 100, sev: "P1" }, { n: 103, sev: "P1" }, diff --git a/web/lib/i18n.test.ts b/web/lib/i18n.test.ts new file mode 100644 index 0000000..2f66938 --- /dev/null +++ b/web/lib/i18n.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect } from "vitest"; +import { ui, principles, t, type Lang } from "./i18n"; +import { AUDITS, BACKLOG_SAMPLE, PRINCIPLES } from "./content"; +import { AUDIT_DETAILS } from "./audit-details"; + +const LANGS: Lang[] = ["en", "de"]; + +/** Recursive, order-independent key map of a translation tree. Objects contribute their + * key names (recursing into values); arrays recurse into a single representative element + * so [{q,a},{q,a}] and [{q,a}] share a shape — we lock the *shape*, not the length. */ +function keyShape(value: unknown): unknown { + if (Array.isArray(value)) { + return { "[]": value.length ? keyShape(value[0]) : null }; + } + if (value && typeof value === "object") { + const out: Record = {}; + for (const k of Object.keys(value as object).sort()) { + out[k] = keyShape((value as Record)[k]); + } + return out; + } + return null; // leaf (string/number/etc.) — only the key path matters, not the prose +} + +describe("i18n locale parity", () => { + it("en and de expose the exact same key set (deep, order-independent)", () => { + // Catches a key added/renamed/removed in one locale but not the other. + expect(keyShape(ui.en)).toEqual(keyShape(ui.de)); + }); + + it("t() returns the same shape for every locale", () => { + expect(keyShape(t("en"))).toEqual(keyShape(t("de"))); + }); +}); + +describe("audit-details coverage", () => { + it("every audit has an en + de detail entry", () => { + for (const a of AUDITS) { + const detail = AUDIT_DETAILS[a.name]; + expect(detail, `missing audit-details entry for "${a.name}"`).toBeDefined(); + expect(detail.en, `missing en detail for "${a.name}"`).toBeDefined(); + expect(detail.de, `missing de detail for "${a.name}"`).toBeDefined(); + } + }); + + it("en and de details share the same shape per audit", () => { + for (const a of AUDITS) { + const d = AUDIT_DETAILS[a.name]; + expect(keyShape(d.en), `detail shape drift for "${a.name}"`).toEqual(keyShape(d.de)); + } + }); + + it("has no orphan audit-details entry without a matching audit", () => { + const names = new Set(AUDITS.map((a) => a.name)); + for (const key of Object.keys(AUDIT_DETAILS)) { + expect(names.has(key), `audit-details entry "${key}" has no matching audit`).toBe(true); + } + }); +}); + +describe("principles translation coverage", () => { + it("every English principle has a German translation (title + body)", () => { + const de = principles("de"); + const en = principles("en"); + expect(de.length).toBe(en.length); + for (let i = 0; i < en.length; i++) { + // German must differ from English (i.e. a translation was actually applied)… + expect(de[i].title, `principle "${en[i].title}" not translated`).not.toBe(en[i].title); + expect(de[i].body).toBeTruthy(); + // …and the shared, non-localized field (icon) must be preserved. + expect(de[i].icon).toBe(en[i].icon); + } + }); + + it("counts match between PRINCIPLES and both locale outputs", () => { + expect(principles("en").length).toBe(PRINCIPLES.length); + expect(principles("de").length).toBe(PRINCIPLES.length); + }); +}); + +describe("proofRows / BACKLOG_SAMPLE coverage", () => { + const issues = BACKLOG_SAMPLE.map((b) => b.n); + + for (const lang of LANGS) { + it(`[${lang}] covers exactly the BACKLOG_SAMPLE issue numbers, in order`, () => { + const rows = t(lang).proofRows; + expect(rows.length).toBe(issues.length); + // Every backlog row has a non-empty localized title (no silent "" fallback). + for (let i = 0; i < issues.length; i++) { + expect(rows[i], `proofRows[${i}] (issue #${issues[i]}) missing in "${lang}"`).toBeTruthy(); + } + }); + } +}); diff --git a/web/lib/i18n.ts b/web/lib/i18n.ts index 74815e7..04f0f2f 100644 --- a/web/lib/i18n.ts +++ b/web/lib/i18n.ts @@ -1,7 +1,34 @@ -import { AUDITS, PHASES, PRINCIPLES, STANDARDS, type Audit } from "./content"; +import { AUDITS, BACKLOG_SAMPLE, PHASES, PRINCIPLES, STANDARDS, type Audit } from "./content"; export type Lang = "en" | "de"; +// Proof-table finding titles, keyed by the BACKLOG_SAMPLE issue number `n` (stable), +// not by array position — reordering content.ts can no longer mistranslate a row. +const proofRowsByIssue: Record> = { + en: { + 100: 'No visible output proof — it preaches "evidence", ships prose', + 103: "Activation command is not copyable", + 104: '"any AI agent" — an unsupported universal claim', + 107: "End-CTA repeats the hero instead of closing", + 113: '"Google-grade" — an unsupported superlative', + 122: 'Hero badge "copy & paste" duplicates the subhead', + }, + de: { + 100: 'Kein sichtbarer Output-Beweis — predigt „Evidence“, liefert Prosa', + 103: "Aktivierungs-Befehl ist nicht kopierbar", + 104: '„any AI agent“ — unbelegte Universalbehauptung', + 107: "End-CTA wiederholt den Hero statt zu schließen", + 113: '„Google-grade“ — unbelegter Superlativ', + 122: 'Hero-Badge „copy & paste“ doppelt den Subhead', + }, +}; + +// Public array shape consumed by landing.tsx (by index, paired with BACKLOG_SAMPLE). +// Derived from the keyed source above in BACKLOG_SAMPLE order, so a content.ts reorder +// re-orders these titles to match instead of silently mistranslating. +const proofRowsFor = (lang: Lang): string[] => + BACKLOG_SAMPLE.map((b) => proofRowsByIssue[lang][b.n] ?? ""); + // UI chrome strings. export const ui = { en: { @@ -67,14 +94,6 @@ export const ui = { "The real backlog from auditing this very page — 23 findings, every one now fixed.", proofVerifyHint: "Open any issue to check the evidence on GitHub.", proofFindings: "findings", - proofRows: [ - 'No visible output proof — it preaches "evidence", ships prose', - "Activation command is not copyable", - '"any AI agent" — an unsupported universal claim', - "End-CTA repeats the hero instead of closing", - '"Google-grade" — an unsupported superlative', - 'Hero badge "copy & paste" duplicates the subhead', - ], proofEvidence: "Evidence", proofBefore: "Before", proofAfter: "After", @@ -177,14 +196,6 @@ export const ui = { "Der echte Backlog aus dem Audit genau dieser Seite — 23 Befunde, jeder davon jetzt behoben.", proofVerifyHint: "Öffne ein Issue und prüfe den Beleg auf GitHub.", proofFindings: "Befunde", - proofRows: [ - 'Kein sichtbarer Output-Beweis — predigt „Evidence“, liefert Prosa', - "Aktivierungs-Befehl ist nicht kopierbar", - '„any AI agent“ — unbelegte Universalbehauptung', - "End-CTA wiederholt den Hero statt zu schließen", - '„Google-grade“ — unbelegter Superlativ', - 'Hero-Badge „copy & paste“ doppelt den Subhead', - ], proofEvidence: "Beleg", proofBefore: "Vorher", proofAfter: "Nachher", @@ -243,24 +254,26 @@ const auditBlurbDe: Record = { lean: "Schlankheit: toter Code, ungenutzte/Phantom-Deps, Duplikation, AI-Slop, Dependency-Transparenz — sicherer Strip-down ohne Über-Löschen.", }; -const principleDe: { title: string; body: string }[] = [ - { +// German prose for each principle, keyed by the principle's English `title` (stable), +// not by array position — reordering PRINCIPLES can no longer mistranslate a card. +const principleDe: Record = { + "Evidence or it didn't happen": { title: "Beleg oder nichts", body: "Jeder Befund nennt ein konkretes Artefakt — file:line, einen Query-Plan, einen Request, einen Config-Wert, eine gemessene Metrik. Kein Beleg, kein Befund.", }, - { + "Adversarial self-challenge": { title: "Adversarielle Selbst-Challenge", body: "Kein Befund überlebt, bevor unabhängige Skeptiker-Agenten ihn zu widerlegen versucht haben — er muss mindestens zwei von drei überstehen, sonst wird er verworfen. Wer eine feindselige Lesart nicht übersteht, ist kein Befund.", }, - { + "Blind-spot hunting": { title: "Blind-Spot-Jagd", body: "Ein Completeness-Critic fragt jede Runde, welche Oberfläche, welcher Use-Case oder welche Annahme ungeprüft blieb. Lücken werden deklariert, nie verschwiegen.", }, - { + "Actionable issue tracker": { title: "Umsetzbarer Issue-Tracker", body: "Ausgabe sind GitHub-Issues, angeführt von einem nach Priorität sortierten Tracking-Issue, jedes mit Management-Summary und Vorher/Nachher-Fix. Ein Befund, den du nicht umsetzen kannst, ist nur eine Meinung.", }, -]; +}; const phaseDe: Record = { "0": { title: "Reconnaissance", body: "Faktisches Inventar + Surface-Map. Noch keine Meinungen." }, @@ -279,7 +292,9 @@ const standardBlurbDe: Record = { }; export function t(lang: Lang) { - return ui[lang]; + // proofRows is derived from BACKLOG_SAMPLE (keyed by issue number), not stored as a + // hand-ordered literal — so it stays correct if content.ts reorders the backlog. + return { ...ui[lang], proofRows: proofRowsFor(lang) }; } export function audits(lang: Lang): Audit[] { @@ -289,7 +304,7 @@ export function audits(lang: Lang): Audit[] { export function principles(lang: Lang) { if (lang === "en") return PRINCIPLES; - return PRINCIPLES.map((p, i) => ({ ...p, ...principleDe[i] })); + return PRINCIPLES.map((p) => ({ ...p, ...(principleDe[p.title] ?? {}) })); } export function phases(lang: Lang) { From 3a442b0f80ff6b649391a2d67d571d49e51e608a Mon Sep 17 00:00:00 2001 From: muraschal Date: Thu, 25 Jun 2026 18:13:23 +0200 Subject: [PATCH 3/5] feat(web): TechArticle + BreadcrumbList JSON-LD on audit pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 26 audit detail pages emitted no structured data. Add localized TechArticle + BreadcrumbList JSON-LD (same server-rendered pattern as the homepage SoftwareApplication block) — a durable organic-search win that scales with the catalogue. Closes #136. Part of #131. Co-Authored-By: Claude Opus 4.8 (1M context) --- web/components/audit-page.tsx | 41 ++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/web/components/audit-page.tsx b/web/components/audit-page.tsx index f61e050..c75bbe2 100644 --- a/web/components/audit-page.tsx +++ b/web/components/audit-page.tsx @@ -5,9 +5,10 @@ import { Reveal } from "@/components/reveal"; import { SiteFooter, SiteHeader } from "@/components/site-chrome"; import { buttonVariants } from "@/components/ui/button"; import { cn } from "@/lib/utils"; -import { AUDITS, PROMPTS, auditCommand } from "@/lib/content"; +import { AUDITS, PROMPTS, auditCommand, auditTitle } from "@/lib/content"; import { auditDetail } from "@/lib/audit-details"; import { glossify } from "@/lib/glossary"; +import { SITE_URL } from "@/lib/site"; import { type Lang, t } from "@/lib/i18n"; /** A per-audit detail page: approach + concrete use cases, for deep-linking. */ @@ -21,6 +22,36 @@ export function AuditDetailPage({ name, lang }: { name: string; lang: Lang }) { const home = lang === "de" ? "/de" : "/"; const langHref = lang === "de" ? `/audits/${name}` : `/de/audits/${name}`; + // Canonical URLs mirror the visual breadcrumb and the page's `alternates`. + const title = auditTitle(audit); + const base = lang === "de" ? "/de" : ""; + const pageUrl = `${SITE_URL}${base}/audits/${name}`; + const homeUrl = `${SITE_URL}${base || "/"}`; + const auditsUrl = `${SITE_URL}${base}/#audits`; + + const articleLd = { + "@context": "https://schema.org", + "@type": "TechArticle", + headline: title, + name: title, + description: detail.tagline, + url: pageUrl, + inLanguage: lang === "de" ? "de" : "en", + isPartOf: { "@type": "WebSite", name: "auditor", url: SITE_URL }, + author: { "@type": "Person", name: "Marcel Rapold" }, + image: `${pageUrl}/opengraph-image`, + }; + + const breadcrumbLd = { + "@context": "https://schema.org", + "@type": "BreadcrumbList", + itemListElement: [ + { "@type": "ListItem", position: 1, name: "auditor", item: homeUrl }, + { "@type": "ListItem", position: 2, name: tt.nav.audits, item: auditsUrl }, + { "@type": "ListItem", position: 3, name: audit.name, item: pageUrl }, + ], + }; + const copy = ( +