diff --git a/website/public/data/contracts.json b/website/public/data/contracts.json
index af800f5..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"
@@ -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..a335526 100644
--- a/website/src/components/contracts-page.js
+++ b/website/src/components/contracts-page.js
@@ -2,12 +2,93 @@ 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, '\\$&')
+}
+
+// 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'],
+}
+
+// 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 matches; the longest term wins a contested span.
+ const matches = []
+ for (const { id, term } of terms) {
+ 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) || '[]')
@@ -26,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']
}
@@ -103,13 +184,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('
')
@@ -129,7 +211,7 @@ function renderContractCard(contract, isSelected) {
${esc(description)}
-