From a1ade04ca9a49fcba438b5ba799548f485affb1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=7BAI=7Df=20D=2E=20M=C3=BCller?= Date: Tue, 2 Jun 2026 17:33:42 +0200 Subject: [PATCH 1/6] fix: structure teaching-contract template + cap contract box height - contracts.json: the 'Explaining and Teaching' template was one unbroken paragraph and rendered as a wall of text. Add paragraph breaks and a bulleted loop list (EN + DE), matching the sibling contracts. Wording unchanged. - contracts-page.js: cap each contract's definition box at max-h-64 with overflow-y-auto, so long contracts (e.g. Architecture Documentation) scroll instead of dominating the page. Verified in-browser: box maxHeight 256px, overflow-y auto, scrolls; template renders 6 paragraphs + 3 bullets. 98/98 unit tests pass. --- website/public/data/contracts.json | 4 ++-- website/src/components/contracts-page.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/website/public/data/contracts.json b/website/public/data/contracts.json index af800f5..4fb680d 100644 --- a/website/public/data/contracts.json +++ b/website/public/data/contracts.json @@ -188,8 +188,8 @@ "description": "How to teach a topic until understanding is verified, not just explained", "descriptionDe": "Wie man ein Thema lehrt, bis das Verständnis geprüft ist — nicht nur erklärt", "anchors": ["4mat", "mental-model-according-to-naur", "socratic-method", "feynman-technique", "blooms-taxonomy", "definition-of-done"], - "template": "Teach until the learner genuinely understands — don't just explain. Sequence each unit Why → What → How → What-If (4MAT): motivation before detail. Treat the Why as Naur's program theory — the reasoning, trade-offs, and branches not taken behind the design, not merely what the code does; drill recursively into why. Diagnose first (Socratic Method): have the learner restate their current understanding, then fill the gaps with questions, not answers, adjusting depth on request (ELI5 / ELI-intern). The sharpest check is having them explain it back in plain words (Feynman Technique) — where they stall is the gap. Keep a running, written checklist of what must be grasped — high level (why it matters, what it impacts) and low level (logic, edge cases, design decisions) — a Definition of Done for understanding. After each point, verify by active recall — an open or multiple-choice question, a code walkthrough, the debugger — never \"makes sense?\". \"Understood\" means Bloom's Apply/Analyze level (use it on a new case, trace the edge cases), not recall. Don't advance until the current point is demonstrated, and don't end until the whole checklist is. Scale the ceremony to the size of the question.", - "templateDe": "Lehren, bis die lernende Person es wirklich versteht — nicht nur erklären. Jede Einheit in der Reihenfolge Why → What → How → What-If aufbauen (4MAT): Motivation vor Detail. Das Why als Naurs Programm-Theory behandeln — die Begründung, die Trade-offs und die nicht gewählten Alternativen hinter dem Design, nicht bloß was der Code tut; rekursiv ins Warum bohren. Erst diagnostizieren (Socratic Method): die Person ihr aktuelles Verständnis zurückgeben lassen, dann die Lücken mit Fragen statt Antworten füllen und die Tiefe auf Wunsch anpassen (ELI5 / ELI-intern). Die schärfste Probe ist das Zurückerklären in einfachen Worten (Feynman Technique) — wo sie stockt, ist die Lücke. Eine laufende, schriftliche Checkliste führen, was verstanden sein muss — high level (warum es zählt, was es beeinflusst) und low level (Logik, Edge Cases, Design-Entscheidungen) — eine Definition of Done fürs Verständnis. Nach jedem Punkt durch active recall prüfen — eine offene oder Multiple-Choice-Frage, ein Code-Walkthrough, der Debugger — nie \"passt das?\". \"Verstanden\" heißt Blooms Apply/Analyze-Stufe (auf einen neuen Fall anwenden, Edge Cases durchspielen), nicht Abrufen. Nicht weitergehen, bis der aktuelle Punkt demonstriert ist, und nicht enden, bis die ganze Checkliste es ist. Die Zeremonie an die Größe der Frage anpassen.", + "template": "Teach until the learner genuinely understands — don't just explain.\n\nSequence each unit Why → What → How → What-If (4MAT): motivation before detail. Treat the Why as Naur's program theory — the reasoning, trade-offs, and branches not taken behind the design, not merely what the code does; drill recursively into why.\n\nDiagnose first (Socratic Method): have the learner restate their current understanding, then fill the gaps with questions, not answers, adjusting depth on request (ELI5 / ELI-intern). The sharpest check is having them explain it back in plain words (Feynman Technique) — where they stall is the gap.\n\nKeep a running, written checklist of what must be grasped — high level (why it matters, what it impacts) and low level (logic, edge cases, design decisions) — a Definition of Done for understanding.\n\nThe loop:\n- After each point, verify by active recall — an open or multiple-choice question, a code walkthrough, the debugger — never \"makes sense?\".\n- \"Understood\" means Bloom's Apply/Analyze level (use it on a new case, trace the edge cases), not recall.\n- Don't advance until the current point is demonstrated, and don't end until the whole checklist is.\n\nScale the ceremony to the size of the question.", + "templateDe": "Lehren, bis die lernende Person es wirklich versteht — nicht nur erklären.\n\nJede Einheit in der Reihenfolge Why → What → How → What-If aufbauen (4MAT): Motivation vor Detail. Das Why als Naurs Programm-Theory behandeln — die Begründung, die Trade-offs und die nicht gewählten Alternativen hinter dem Design, nicht bloß was der Code tut; rekursiv ins Warum bohren.\n\nErst diagnostizieren (Socratic Method): die Person ihr aktuelles Verständnis zurückgeben lassen, dann die Lücken mit Fragen statt Antworten füllen und die Tiefe auf Wunsch anpassen (ELI5 / ELI-intern). Die schärfste Probe ist das Zurückerklären in einfachen Worten (Feynman Technique) — wo sie stockt, ist die Lücke.\n\nEine laufende, schriftliche Checkliste führen, was verstanden sein muss — high level (warum es zählt, was es beeinflusst) und low level (Logik, Edge Cases, Design-Entscheidungen) — eine Definition of Done fürs Verständnis.\n\nDie Schleife:\n- Nach jedem Punkt durch active recall prüfen — eine offene oder Multiple-Choice-Frage, ein Code-Walkthrough, der Debugger — nie \"passt das?\".\n- \"Verstanden\" heißt Blooms Apply/Analyze-Stufe (auf einen neuen Fall anwenden, Edge Cases durchspielen), nicht Abrufen.\n- Nicht weitergehen, bis der aktuelle Punkt demonstriert ist, und nicht enden, bis die ganze Checkliste es ist.\n\nDie Zeremonie an die Größe der Frage anpassen.", "category": "communication" }, { diff --git a/website/src/components/contracts-page.js b/website/src/components/contracts-page.js index a265c13..b176396 100644 --- a/website/src/components/contracts-page.js +++ b/website/src/components/contracts-page.js @@ -129,7 +129,7 @@ function renderContractCard(contract, isSelected) {

${esc(title)}

${esc(description)}

-
+
${templateHtml}
From 0d16e8781501d2b017d5e8b3349a7c734bba9d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=7BAI=7Df=20D=2E=20M=C3=BCller?= Date: Tue, 2 Jun 2026 18:00:12 +0200 Subject: [PATCH 2/6] feat: highlight declared anchors inside contract text (display only) Verbatim mentions of a contract's declared anchor titles are now highlighted and linked to the anchor within the rendered template box. Scoped to the contract's own anchors array (no global false positives), longest-title-first, word-boundary matched. Highlighting is applied only in the rendered DOM (renderContractCard). The copy/download export (generateMarkdown) uses the raw template string, so the export stays plain CLAUDE.md-ready text. A null-char token in the matcher prevents collisions with literal digits in other templates (e.g. 'at most 3 questions'). main.js now loads anchor titles (fetchAnchorsData) alongside contracts and passes an id->title map into initContractsPage. --- website/src/components/contracts-page.js | 54 ++++++++++++++++++++++-- website/src/main.js | 8 ++-- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/website/src/components/contracts-page.js b/website/src/components/contracts-page.js index b176396..0ec3b80 100644 --- a/website/src/components/contracts-page.js +++ b/website/src/components/contracts-page.js @@ -2,12 +2,58 @@ import { i18n } from '../i18n.js' const STORAGE_KEY = 'selected-contracts' +// id -> title map for the anchors a contract declares; set by initContractsPage. +// Used to highlight verbatim anchor mentions inside the rendered template text. +// Copy/download use the raw template, so highlighting never leaks into the export. +let anchorTitleMap = {} + function esc(str) { const d = document.createElement('div') d.textContent = str return d.innerHTML } +function escapeRegex(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +// Highlight verbatim mentions of a contract's declared anchors, linked to the anchor. +// Operates on raw text and returns escaped HTML with links injected. +function highlightAnchors(text, anchorIds) { + const entries = (anchorIds || []) + .map((id) => ({ id, title: anchorTitleMap[id] })) + .filter((e) => e.title) + .sort((a, b) => b.title.length - a.title.length) // longest title first + + if (!entries.length) return esc(text) + + // Collect non-overlapping verbatim matches of each declared anchor's title. + const matches = [] + for (const { id, title } of entries) { + const re = new RegExp(`(? start < x.end && end > x.start)) { + matches.push({ start, end, id, text: m[0] }) + } + } + } + matches.sort((a, b) => a.start - b.start) + + // Rebuild the line, escaping plain text and linking matched anchor names. + let html = '' + let pos = 0 + for (const mt of matches) { + html += esc(text.slice(pos, mt.start)) + html += `${esc(mt.text)}` + pos = mt.end + } + html += esc(text.slice(pos)) + return html +} + function getSelectedContracts() { try { const val = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]') @@ -103,13 +149,14 @@ function renderContractCard(contract, isSelected) { ) .join(' ') + const anchorIds = contract.anchors || [] const templateHtml = template .split('\n') .map((line) => { if (line.startsWith('- ')) { - return `• ${esc(line.slice(2))}` + return `• ${highlightAnchors(line.slice(2), anchorIds)}` } - return `${esc(line)}` + return `${highlightAnchors(line, anchorIds)}` }) .join('
') @@ -142,7 +189,8 @@ function renderContractCard(contract, isSelected) { ` } -export function initContractsPage(contracts) { +export function initContractsPage(contracts, anchorTitles) { + if (anchorTitles) anchorTitleMap = anchorTitles const oldGrid = document.getElementById('contracts-grid') if (!oldGrid || !contracts) return diff --git a/website/src/main.js b/website/src/main.js index a864a17..5fb212b 100644 --- a/website/src/main.js +++ b/website/src/main.js @@ -11,7 +11,7 @@ import { updateAnchorCount, setFeedbackData, } from './components/card-grid.js' -import { fetchData, fetchContractsData } from './utils/data-loader.js' +import { fetchData, fetchContractsData, fetchAnchorsData } from './utils/data-loader.js' import { buildSearchIndex, isIndexReady, isIndexBuilding } from './utils/search-index.js' import { initRouter, @@ -333,8 +333,10 @@ function renderContractsPageHandler() { pageContent.innerHTML = renderContractsPage() updateActiveNavLink() - fetchContractsData().then((contracts) => { - initContractsPage(contracts) + Promise.all([fetchContractsData(), fetchAnchorsData()]).then(([contracts, anchors]) => { + const anchorTitles = {} + for (const a of anchors || []) anchorTitles[a.id] = a.title + initContractsPage(contracts, anchorTitles) }) } From e1a01e89fe7d0e41a78ef0fa84ac2ddee4106571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=7BAI=7Df=20D=2E=20M=C3=BCller?= Date: Tue, 2 Jun 2026 18:15:48 +0200 Subject: [PATCH 3/6] fix: correct dangling anchor ref in code-quality contract (dry-principle -> dry) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The code-quality contract referenced anchor id 'dry-principle', which does not exist — the DRY anchor's id is 'dry' (added in #569). The chip linked to a 404. Point it at the real anchor. --- website/public/data/contracts.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/public/data/contracts.json b/website/public/data/contracts.json index 4fb680d..11579de 100644 --- a/website/public/data/contracts.json +++ b/website/public/data/contracts.json @@ -115,7 +115,7 @@ "titleDe": "Code-Qualität", "description": "Coding conventions and design principles", "descriptionDe": "Coding-Konventionen und Design-Prinzipien", - "anchors": ["solid-principles", "dry-principle", "kiss-principle", "domain-driven-design"], + "anchors": ["solid-principles", "dry", "kiss-principle", "domain-driven-design"], "template": "Our code follows:\n- SOLID principles\n- DRY, KISS\n- Ubiquitous Language from Domain-Driven Design (same terms in code as in the specification)", "templateDe": "Unser Code folgt:\n- SOLID-Prinzipien\n- DRY, KISS\n- Ubiquitous Language aus Domain-Driven Design (gleiche Begriffe im Code wie in der Spezifikation)", "category": "development" From d7bbbab152825290ab8f0b44f81f5a64e84682e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=7BAI=7Df=20D=2E=20M=C3=BCller?= Date: Tue, 2 Jun 2026 19:50:39 +0200 Subject: [PATCH 4/6] feat: curated anchor aliases for broader in-text highlighting The verbatim-title matcher missed anchors whose prose mention differs from the canonical title (e.g. 'MECE Principle' written as 'MECE', 'arc42 Architecture Documentation' as 'arc42'). Add a curated ANCHOR_ALIASES map of surface forms, each verified to appear verbatim in at least one contract template (EN or DE) and to be unambiguous among a contract's declared anchors. Titles and aliases are matched uniformly, longest-first, with the existing overlap guard. Coverage now spans every contract (e.g. requirements-discovery gains MECE; architecture-documentation gains arc42/C4/ADRs/Nygard/Pugh Matrix; code-quality gains SOLID/DRY/KISS/DDD). Still display-only; copy/download remain raw. --- website/src/components/contracts-page.js | 57 +++++++++++++++++++----- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/website/src/components/contracts-page.js b/website/src/components/contracts-page.js index 0ec3b80..8261d41 100644 --- a/website/src/components/contracts-page.js +++ b/website/src/components/contracts-page.js @@ -17,20 +17,55 @@ function escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } -// Highlight verbatim mentions of a contract's declared anchors, linked to the anchor. -// Operates on raw text and returns escaped HTML with links injected. -function highlightAnchors(text, anchorIds) { - const entries = (anchorIds || []) - .map((id) => ({ id, title: anchorTitleMap[id] })) - .filter((e) => e.title) - .sort((a, b) => b.title.length - a.title.length) // longest title first +// Curated surface-form aliases for anchors whose prose mention differs from the +// canonical title (e.g. "MECE Principle" written as just "MECE"). Each alias was +// verified to appear verbatim in at least one contract template (EN or DE) and to +// be unambiguous among the anchors a contract declares. Keyed by anchor id. +const ANCHOR_ALIASES = { + 'cockburn-use-cases': ['Cockburn'], + 'ears-requirements': ['EARS'], + mece: ['MECE'], + arc42: ['arc42'], + 'c4-diagrams': ['C4'], + 'adr-according-to-nygard': ['ADRs', 'Nygard'], + 'pugh-matrix': ['Pugh Matrix'], + 'quality-attribute-scenario': ['quality attribute scenario'], + stride: ['STRIDE'], + 'testing-pyramid': ['testing pyramid'], + 'domain-driven-design': ['Domain-Driven Design', 'Ubiquitous Language'], + 'solid-dip': ['DIP'], + 'solid-principles': ['SOLID'], + 'walking-skeleton': ['walking skeleton'], + 'tracer-bullet': ['Tracer'], + 'spike-solution': ['spike'], + 'definition-of-done': ['Definition of Done'], + 'code-smells': ['code smells', 'Code Smells'], + dry: ['DRY'], + 'kiss-principle': ['KISS'], + 'socratic-method': ['Socratic Method'], + 'mental-model-according-to-naur': ['Naur'], + bluf: ['BLUF'], + 'plain-english-strunk-white': ['Strunk & White', 'Plain English'], + 'blooms-taxonomy': ["Bloom's", 'Blooms'], +} - if (!entries.length) return esc(text) +// Highlight mentions of a contract's declared anchors, linked to the anchor. +// Matches each anchor's title plus the curated aliases above, scoped to the +// declared anchors only. Operates on raw text and returns escaped HTML. +function highlightAnchors(text, anchorIds) { + const terms = [] + for (const id of anchorIds || []) { + const title = anchorTitleMap[id] + if (title) terms.push({ id, term: title }) + for (const alias of ANCHOR_ALIASES[id] || []) terms.push({ id, term: alias }) + } + if (!terms.length) return esc(text) + terms.sort((a, b) => b.term.length - a.term.length) // longest term first - // Collect non-overlapping verbatim matches of each declared anchor's title. + // Collect non-overlapping matches; the longest term wins a contested span. const matches = [] - for (const { id, title } of entries) { - const re = new RegExp(`(? Date: Tue, 2 Jun 2026 20:07:43 +0200 Subject: [PATCH 5/6] fix: use i18n.currentLang() and make anchor fetch non-fatal (CodeRabbit) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues from CodeRabbit review of #574: 1. getLocalizedField and the markdown export read i18n.currentLanguage, which does not exist — i18n exposes currentLang() as a method. The property was always undefined, so the contracts page (and its copy/ download) always fell back to English even in the German UI. Switch both reads to i18n.currentLang(). Verified in-browser: the German templates now render in DE. 2. renderContractsPageHandler used Promise.all, so a fetchAnchorsData rejection would abort the whole contracts page. Anchor titles only power the highlight aliases, so make it non-fatal via Promise.allSettled: render contracts whenever they load, attach anchor titles only if that fetch succeeded. Lint clean, 98/98 unit tests pass. --- website/src/components/contracts-page.js | 4 ++-- website/src/main.js | 14 +++++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/website/src/components/contracts-page.js b/website/src/components/contracts-page.js index 8261d41..a335526 100644 --- a/website/src/components/contracts-page.js +++ b/website/src/components/contracts-page.js @@ -107,7 +107,7 @@ function setSelectedContracts(ids) { } function getLocalizedField(contract, field) { - const lang = i18n.currentLanguage || 'en' + const lang = i18n.currentLang() || 'en' if (lang === 'de' && contract[field + 'De']) { return contract[field + 'De'] } @@ -316,7 +316,7 @@ function updateUI() { function buildContractsMarkdown(contracts) { const selected = getSelectedContracts() - const lang = i18n.currentLanguage || 'en' + const lang = i18n.currentLang() || 'en' const filtered = contracts.filter((c) => selected.includes(c.id)) if (filtered.length === 0) return null diff --git a/website/src/main.js b/website/src/main.js index 5fb212b..621bcbb 100644 --- a/website/src/main.js +++ b/website/src/main.js @@ -333,10 +333,18 @@ function renderContractsPageHandler() { pageContent.innerHTML = renderContractsPage() updateActiveNavLink() - Promise.all([fetchContractsData(), fetchAnchorsData()]).then(([contracts, anchors]) => { + // Contracts must load to render the page; anchor titles only power the + // in-text highlight aliases, so an anchors failure is non-fatal. + Promise.allSettled([fetchContractsData(), fetchAnchorsData()]).then(([contractsRes, anchorsRes]) => { + if (contractsRes.status !== 'fulfilled') { + console.error('Failed to load contracts:', contractsRes.reason) + return + } const anchorTitles = {} - for (const a of anchors || []) anchorTitles[a.id] = a.title - initContractsPage(contracts, anchorTitles) + if (anchorsRes.status === 'fulfilled') { + for (const a of anchorsRes.value || []) anchorTitles[a.id] = a.title + } + initContractsPage(contractsRes.value, anchorTitles) }) } From 82ee5af7f02292b994d76273d081dbc2cea778a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=7BAI=7Df=20D=2E=20M=C3=BCller?= Date: Tue, 2 Jun 2026 20:12:33 +0200 Subject: [PATCH 6/6] style: wrap Promise.allSettled callback to satisfy Prettier (100-col) --- website/src/main.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/website/src/main.js b/website/src/main.js index 621bcbb..6294b83 100644 --- a/website/src/main.js +++ b/website/src/main.js @@ -335,17 +335,19 @@ function renderContractsPageHandler() { // Contracts must load to render the page; anchor titles only power the // in-text highlight aliases, so an anchors failure is non-fatal. - Promise.allSettled([fetchContractsData(), fetchAnchorsData()]).then(([contractsRes, anchorsRes]) => { - if (contractsRes.status !== 'fulfilled') { - console.error('Failed to load contracts:', contractsRes.reason) - return - } - const anchorTitles = {} - if (anchorsRes.status === 'fulfilled') { - for (const a of anchorsRes.value || []) anchorTitles[a.id] = a.title + Promise.allSettled([fetchContractsData(), fetchAnchorsData()]).then( + ([contractsRes, anchorsRes]) => { + if (contractsRes.status !== 'fulfilled') { + console.error('Failed to load contracts:', contractsRes.reason) + return + } + const anchorTitles = {} + if (anchorsRes.status === 'fulfilled') { + for (const a of anchorsRes.value || []) anchorTitles[a.id] = a.title + } + initContractsPage(contractsRes.value, anchorTitles) } - initContractsPage(contractsRes.value, anchorTitles) - }) + ) } function renderEvaluationsPage() {