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
6 changes: 3 additions & 3 deletions website/public/data/contracts.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
},
{
Expand Down
95 changes: 89 additions & 6 deletions website/src/components/contracts-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(`(?<![\\w])${escapeRegex(term)}(?![\\w])`, 'g')
let m
while ((m = re.exec(text)) !== null) {
const start = m.index
const end = start + m[0].length
if (!matches.some((x) => 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 += `<a href="#/anchor/${esc(mt.id)}" class="font-medium text-blue-700 dark:text-blue-300 hover:underline">${esc(mt.text)}</a>`
pos = mt.end
}
html += esc(text.slice(pos))
return html
}

function getSelectedContracts() {
try {
const val = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
Expand All @@ -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']
}
Expand Down Expand Up @@ -103,13 +184,14 @@ function renderContractCard(contract, isSelected) {
)
.join(' ')

const anchorIds = contract.anchors || []
const templateHtml = template
.split('\n')
.map((line) => {
if (line.startsWith('- ')) {
return `<span class="text-[var(--color-text-secondary)]">• ${esc(line.slice(2))}</span>`
return `<span class="text-[var(--color-text-secondary)]">• ${highlightAnchors(line.slice(2), anchorIds)}</span>`
}
return `<span>${esc(line)}</span>`
return `<span>${highlightAnchors(line, anchorIds)}</span>`
Comment on lines +187 to +194

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Die neue Render-Strecke bleibt dadurch effektiv EN-only.

highlightAnchors() ist hier nicht das Problem — renderContractCard() zieht den Text weiter über getLocalizedField(), und der Helper liest in derselben Datei i18n.currentLanguage. In diesem Repo ist diese Property undefined, daher fallen die Karten immer auf EN zurück; die neu formatierten templateDe-Texte und auch der Copy/Download-Pfad in buildContractsMarkdown() werden auf der Contracts-Seite nicht korrekt lokalisiert. Bitte den Sprachzugriff konsistent auf i18n.currentLang() umstellen.

Based on learnings: In this repo’s i18n integration (website/src/i18n.js), the active language must be read via the method i18n.currentLang() rather than a property like i18n.currentLanguage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@website/src/components/contracts-page.js` around lines 152 - 159, The cards
and markdown are falling back to English because code in
renderContractCard/getLocalizedField and the highlightAnchors helper reads
i18n.currentLanguage (which is undefined); replace all uses of the property with
the function call i18n.currentLang() so localization uses the active
language—update highlightAnchors, renderContractCard, getLocalizedField and
buildContractsMarkdown to call i18n.currentLang() instead of accessing
i18n.currentLanguage.

})
.join('<br>')

Expand All @@ -129,7 +211,7 @@ function renderContractCard(contract, isSelected) {
<div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold text-[var(--color-text)] mb-1">${esc(title)}</h3>
<p class="text-sm text-[var(--color-text-secondary)] mb-3">${esc(description)}</p>
<div class="rounded-md bg-[var(--color-bg-secondary)] p-3 mb-3 text-sm leading-relaxed">
<div class="rounded-md bg-[var(--color-bg-secondary)] p-3 mb-3 text-sm leading-relaxed max-h-64 overflow-y-auto">
${templateHtml}
</div>
<div class="flex flex-wrap gap-1.5">
Expand All @@ -142,7 +224,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

Expand Down Expand Up @@ -233,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
Expand Down
20 changes: 16 additions & 4 deletions website/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -333,9 +333,21 @@ function renderContractsPageHandler() {
pageContent.innerHTML = renderContractsPage()
updateActiveNavLink()

fetchContractsData().then((contracts) => {
initContractsPage(contracts)
})
// 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
}
initContractsPage(contractsRes.value, anchorTitles)
}
)
}

function renderEvaluationsPage() {
Expand Down
Loading