From 200218ba86a66cc45d19ecd83b3f5b5816b8d098 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Fri, 30 Oct 2026 21:37:12 +0100 Subject: [PATCH 1/3] Chooser: update matching projects in place as answers change Show the best matches under every question instead of only at the end, and re-rank them live as options are toggled, before pressing Next. Project cards are built once and reused, so moving between questions only swaps the question block rather than re-rendering the whole stage. "Show matches now" also keeps any options ticked on the current question. Generated-by: Claude Opus 5 --- site/chooser.html | 98 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 73 insertions(+), 25 deletions(-) diff --git a/site/chooser.html b/site/chooser.html index cff357b7..e3ac3d4d 100644 --- a/site/chooser.html +++ b/site/chooser.html @@ -31,6 +31,9 @@ .result h3 { margin: 0 0 4px; } .result .why { font-size: 85%; color: #666; margin-top: 6px; } .result .links a { margin-right: 12px; font-size: 90%; } + .result.new { animation: fadein .4s; } + @keyframes fadein { from { opacity: 0; transform: translateY(-4px); } } + #matches-h { margin-top: 28px; } .score { float: right; font-size: 85%; color: #c6262e; } .mock { background: #fff6d5; border: 1px solid #e6c84f; padding: 8px 12px; border-radius: 4px; font-size: 90%; } @@ -50,6 +53,12 @@

Help me choose a project

support, design, translation and outreach just as much. Skip anything you don't care about.

Loading project data…
+ +

@@ -86,13 +95,12 @@

Help me choose a project

(answers[qid] || []).some(v => vals.includes(v))); } -function score(p) { - let s = 0, why = []; - for (const [qid, vals] of Object.entries(answers)) { - if (!vals.length || !isFilter(qid)) continue; - if (matches(p, qid, vals)) { s++; why.push(vals.filter(v => p[qid].includes(v)).map(labelFor.bind(null, qid)).join(', ')); } +function score(p, ans) { + let s = 0; + for (const [qid, vals] of Object.entries(ans)) { + if (vals.length && isFilter(qid) && matches(p, qid, vals)) s++; } - return {s, why}; + return s; } function labelFor(qid, v) { @@ -102,9 +110,9 @@

Help me choose a project

} // projects matching every non-skipped answer -function candidates() { +function candidates(ans = answers) { return DATA.projects.filter(p => - Object.entries(answers).every(([qid, vals]) => !vals.length || matches(p, qid, vals))); + Object.entries(ans).every(([qid, vals]) => !vals.length || matches(p, qid, vals))); } // entropy of a question's split over the current candidates @@ -133,6 +141,7 @@

Help me choose a project

document.getElementById('prog').style.width = Math.round(100 * history.length / DATA.questions.filter(eligible).length) + '%'; if (!q) return showResults(); + document.getElementById('advice').innerHTML = ''; const sel = new Set(); const opts = q.options.map(o => { @@ -149,19 +158,31 @@

${esc(q.text)}

${history.length ? '' : ''} - ${cands.length} communities match so far + ` + answerSummary(); + // Toggling an option previews its effect on the match list straight away; + // Next commits it and moves on to the following question. + const pending = () => sel.size ? {...answers, [q.id]: [...sel]} : answers; + const preview = () => { + document.getElementById('remaining').textContent = + `${candidates(pending()).length} communities match so far`; + showMatches(pending(), 'Best matches so far'); + }; $s().querySelectorAll('.opt').forEach(el => el.onclick = () => { const v = el.dataset.v; if (!q.multi) { sel.clear(); $s().querySelectorAll('.opt').forEach(e => e.classList.remove('on')); } if (sel.has(v)) { sel.delete(v); el.classList.remove('on'); } else { sel.add(v); el.classList.add('on'); } - if (!q.multi) answer(q, [...sel]); + if (!q.multi) answer(q, [...sel]); else preview(); }); document.getElementById('next').onclick = () => answer(q, [...sel]); document.getElementById('skip').onclick = () => answer(q, []); - document.getElementById('now').onclick = showResults; + document.getElementById('now').onclick = () => { + if (sel.size) { answers[q.id] = [...sel]; history.push(q.id); } + showResults(); + }; const b = document.getElementById('back'); if (b) b.onclick = back; + preview(); } function answer(q, vals) { answers[q.id] = vals; history.push(q.id); render(); } @@ -173,15 +194,15 @@

${esc(q.text)}

return parts.length ? `

Your choices: ${parts.join(' ')}

` : ''; } -function showResults() { - document.getElementById('prog').style.width = '100%'; - const asked = Object.entries(answers).filter(([k, v]) => v.length && isFilter(k)).length; - const ranked = DATA.projects.map(p => ({p, ...score(p)})) - .filter(r => r.s > 0 || !asked) - .sort((a, b) => b.s - a.s || (b.p.committers || 0) - (a.p.committers || 0)) - .slice(0, SHOW); - const html = ranked.map(({p, s, why}) => `
- ${asked ? `matches ${s} of ${asked}` : ''} +// One card per project, built on first use and reused afterwards, so the +// list can be re-ranked by moving nodes instead of rebuilding the page. +const cards = new Map(); +function card(p) { + if (cards.has(p.id)) return cards.get(p.id); + const el = document.createElement('div'); + el.className = 'result'; + el.dataset.id = p.id; + el.innerHTML = `

${esc(p.name)}

${esc(p.desc)}
${p.language.length ? 'Languages: ' + esc(p.language.join(', ')) + ' ยท ' : ''}${ @@ -191,12 +212,39 @@

${esc(p.name)}

${p.homepage ? `Website` : ''} ${p.podling ? '' : `Project details`} dev@ list -
`).join(''); - $s().innerHTML = `

Projects you might like

- ${html || '

No matches - try going back and loosening an answer.

'} - ${gettingStarted()} -
${history.length ? '' : ''} +
`; + cards.set(p.id, el); + return el; +} + +// Update the match list in place: drop cards that fell out of the top +// SHOW, move the rest into rank order, and fade in newcomers. +function showMatches(ans, heading) { + const asked = Object.entries(ans).filter(([k, v]) => v.length && isFilter(k)).length; + const ranked = DATA.projects.map(p => ({p, s: score(p, ans)})) + .filter(r => r.s > 0 || !asked) + .sort((a, b) => b.s - a.s || (b.p.committers || 0) - (a.p.committers || 0)) + .slice(0, SHOW); + const list = document.getElementById('list'); + const keep = new Set(ranked.map(r => r.p.id)); + [...list.children].forEach(el => { if (!keep.has(el.dataset.id)) el.remove(); }); + ranked.forEach(({p, s}, i) => { + const el = card(p); + el.querySelector('.score').textContent = asked ? `matches ${s} of ${asked}` : ''; + if (!el.isConnected) { el.classList.remove('new'); void el.offsetWidth; el.classList.add('new'); } + if (list.children[i] !== el) list.insertBefore(el, list.children[i] || null); + }); + document.getElementById('matches-h').textContent = heading; + document.getElementById('empty').hidden = ranked.length > 0; + document.getElementById('matches').hidden = false; +} + +function showResults() { + document.getElementById('prog').style.width = '100%'; + $s().innerHTML = `
${history.length ? '' : ''}
` + answerSummary(); + showMatches(answers, 'Projects you might like'); + document.getElementById('advice').innerHTML = gettingStarted(); document.getElementById('restart').onclick = () => { answers = {}; history = []; render(); }; const b = document.getElementById('back'); if (b) b.onclick = back; } From e69745c983b2d46e0e96be25920576f993d9bc90 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Fri, 30 Oct 2026 21:37:13 +0100 Subject: [PATCH 2/3] Chooser: count podling committers from the podling's own group Podling records have pmc 'incubator', so every podling was shown with the committer count of the whole Incubator group (4284) and bucketed as a large community. Use the podling's own group instead. Generated-by: Claude Opus 5 --- scripts/cronjobs/buildchooser.py | 9 ++- site/json/foundation/chooser.json | 104 +++++++++++++++--------------- 2 files changed, 58 insertions(+), 55 deletions(-) diff --git a/scripts/cronjobs/buildchooser.py b/scripts/cronjobs/buildchooser.py index 837ac49b..bfd3c1d5 100644 --- a/scripts/cronjobs/buildchooser.py +++ b/scripts/cronjobs/buildchooser.py @@ -187,7 +187,9 @@ def owner(pid, p): text = ' '.join(str(p.get(k) or '') for k in ('name', 'shortdesc', 'description')) langs = sorted({LANGUAGES.get(l.lower(), 'Other') for l in as_list(p.get('programming-language'))}) - committers = len(groups.get(pmc, [])) or None + # podlings have their own group; 'incubator' is the whole Incubator + group = pid[len('incubator-'):] if pmc == 'incubator' else pmc + committers = len(groups.get(group, [])) or None est = (committees.get(pmc) or {}).get('established', '') rel = last_release(pmc) or last_release(pid) # newest release of anything the committee ships records.append({ @@ -212,13 +214,14 @@ def owner(pid, p): if any(r['id'] == pid or r['id'] == 'incubator-' + pid for r in records): continue text = (p.get('name') or '') + ' ' + (p.get('description') or '') + committers = len(groups.get(pid, [])) or None records.append({ 'id': pid, 'name': p.get('name', pid), 'desc': ' '.join((p.get('description') or '').split())[:300], 'homepage': p.get('homepage'), 'pmc': 'incubator', 'podling': True, 'language': [], 'area': classify([], text, AREAS), 'kind': classify([], text, KINDS), - 'maturity': ['incubating'], 'size': [], 'activity': [], - 'lastRelease': None, 'committers': None, + 'maturity': ['incubating'], 'size': [size_bucket(committers)] if committers else [], + 'activity': [], 'lastRelease': None, 'committers': committers, }) def opts(field, labels): diff --git a/site/json/foundation/chooser.json b/site/json/foundation/chooser.json index 7e339c8c..4dc0af33 100644 --- a/site/json/foundation/chooser.json +++ b/site/json/foundation/chooser.json @@ -1,5 +1,5 @@ { - "generated": "2026-10-30 18:01 UTC", + "generated": "2026-10-30 20:37 UTC", "questions": [ { "id": "area", @@ -215,17 +215,17 @@ { "value": "small", "label": "Small - where every contribution is noticed", - "count": 66 + "count": 85 }, { "value": "medium", "label": "Medium", - "count": 109 + "count": 114 }, { "value": "large", "label": "Large - lots of people to learn from", - "count": 63 + "count": 39 } ] }, @@ -492,13 +492,13 @@ "incubating" ], "size": [ - "large" + "medium" ], "activity": [ "active" ], "lastRelease": "2025-08-28", - "committers": 4284 + "committers": 27 }, { "id": "answer", @@ -690,13 +690,13 @@ "incubating" ], "size": [ - "large" + "small" ], "activity": [ "very" ], "lastRelease": "2026-10-25", - "committers": 4284 + "committers": 9 }, { "id": "atlas", @@ -853,13 +853,13 @@ "incubating" ], "size": [ - "large" + "small" ], "activity": [ "active" ], "lastRelease": "2025-12-30", - "committers": 4284 + "committers": 11 }, { "id": "bigtop", @@ -1010,13 +1010,13 @@ "incubating" ], "size": [ - "large" + "small" ], "activity": [ "very" ], "lastRelease": "2026-08-25", - "committers": 4284 + "committers": 10 }, { "id": "bval", @@ -1089,13 +1089,13 @@ "incubating" ], "size": [ - "large" + "small" ], "activity": [ "quiet" ], "lastRelease": null, - "committers": 4284 + "committers": 9 }, { "id": "camel", @@ -1169,13 +1169,13 @@ "incubating" ], "size": [ - "large" + "small" ], "activity": [ "very" ], "lastRelease": "2026-10-13", - "committers": 4284 + "committers": 13 }, { "id": "cassandra", @@ -1327,13 +1327,13 @@ "incubating" ], "size": [ - "large" + "medium" ], "activity": [ "active" ], "lastRelease": "2026-04-13", - "committers": 4284 + "committers": 34 }, { "id": "cloudstack", @@ -2055,13 +2055,13 @@ "incubating" ], "size": [ - "large" + "small" ], "activity": [ "very" ], "lastRelease": "2026-10-07", - "committers": 4284 + "committers": 13 }, { "id": "fineract", @@ -2281,13 +2281,13 @@ "incubating" ], "size": [ - "large" + "small" ], "activity": [ "very" ], "lastRelease": "2026-06-10", - "committers": 4284 + "committers": 13 }, { "id": "geode", @@ -2438,13 +2438,13 @@ "incubating" ], "size": [ - "large" + "small" ], "activity": [ "active" ], "lastRelease": "2025-08-19", - "committers": 4284 + "committers": 14 }, { "id": "gravitino", @@ -2591,13 +2591,13 @@ "incubating" ], "size": [ - "large" + "small" ], "activity": [ "very" ], "lastRelease": "2026-07-14", - "committers": 4284 + "committers": 10 }, { "id": "hbase", @@ -3289,13 +3289,13 @@ "incubating" ], "size": [ - "large" + "medium" ], "activity": [ "active" ], "lastRelease": "2026-03-29", - "committers": 4284 + "committers": 66 }, { "id": "knox", @@ -3653,13 +3653,13 @@ "incubating" ], "size": [ - "large" + "medium" ], "activity": [ "very" ], "lastRelease": "2026-10-30", - "committers": 4284 + "committers": 29 }, { "id": "manifoldcf", @@ -3901,13 +3901,13 @@ "incubating" ], "size": [ - "large" + "small" ], "activity": [ "quiet" ], "lastRelease": null, - "committers": 4284 + "committers": 6 }, { "id": "nutch", @@ -4146,13 +4146,13 @@ "incubating" ], "size": [ - "large" + "small" ], "activity": [ "quiet" ], "lastRelease": null, - "committers": 4284 + "committers": 14 }, { "id": "openwebbeans", @@ -4246,13 +4246,13 @@ "incubating" ], "size": [ - "large" + "small" ], "activity": [ "quiet" ], "lastRelease": null, - "committers": 4284 + "committers": 17 }, { "id": "incubator-otava", @@ -4267,13 +4267,13 @@ "incubating" ], "size": [ - "large" + "small" ], "activity": [ "very" ], "lastRelease": "2026-05-08", - "committers": 4284 + "committers": 8 }, { "id": "incubator-ozhera", @@ -4288,13 +4288,13 @@ "incubating" ], "size": [ - "large" + "small" ], "activity": [ "very" ], "lastRelease": "2026-06-05", - "committers": 4284 + "committers": 14 }, { "id": "ozone", @@ -4416,13 +4416,13 @@ "incubating" ], "size": [ - "large" + "small" ], "activity": [ "quiet" ], "lastRelease": "2023-12-11", - "committers": 4284 + "committers": 19 }, { "id": "pekko", @@ -4692,13 +4692,13 @@ "incubating" ], "size": [ - "large" + "small" ], "activity": [ "quiet" ], "lastRelease": null, - "committers": 4284 + "committers": 10 }, { "id": "pulsar", @@ -5102,13 +5102,13 @@ "incubating" ], "size": [ - "large" + "medium" ], "activity": [ "active" ], "lastRelease": "2026-01-28", - "committers": 4284 + "committers": 49 }, { "id": "seatunnel", @@ -5843,13 +5843,13 @@ "incubating" ], "size": [ - "large" + "small" ], "activity": [ "very" ], "lastRelease": "2026-08-11", - "committers": 4284 + "committers": 16 }, { "id": "tez", @@ -6038,13 +6038,13 @@ "incubating" ], "size": [ - "large" + "small" ], "activity": [ "quiet" ], "lastRelease": "2022-02-16", - "committers": 4284 + "committers": 12 }, { "id": "trafficserver-traffic-server", @@ -6502,13 +6502,13 @@ "incubating" ], "size": [ - "large" + "small" ], "activity": [ "very" ], "lastRelease": "2026-08-24", - "committers": 4284 + "committers": 12 }, { "id": "yetus", From 1ab046254e57fa0301a0765203844b087249b9c2 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Fri, 30 Oct 2026 21:48:28 +0100 Subject: [PATCH 3/3] Chooser: show at most 10 matches and say that it is only the top 10 Every time the match list is shown, cap it at 10 and add a line saying how many communities match in total and how the top 10 were picked, so the list is not mistaken for every match. When 10 or fewer match, say that all of them are shown instead. Generated-by: Claude Opus 5 --- site/chooser.html | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/site/chooser.html b/site/chooser.html index e3ac3d4d..44521465 100644 --- a/site/chooser.html +++ b/site/chooser.html @@ -33,7 +33,8 @@ .result .links a { margin-right: 12px; font-size: 90%; } .result.new { animation: fadein .4s; } @keyframes fadein { from { opacity: 0; transform: translateY(-4px); } } - #matches-h { margin-top: 28px; } + #matches-h { margin-top: 28px; margin-bottom: 4px; } + #shown { color: #666; font-size: 90%; margin-top: 0; } .score { float: right; font-size: 85%; color: #c6262e; } .mock { background: #fff6d5; border: 1px solid #e6c84f; padding: 8px 12px; border-radius: 4px; font-size: 90%; } @@ -55,6 +56,7 @@

Help me choose a project

Loading project data…
@@ -80,7 +82,7 @@

* projects with incomplete metadata aren't thrown out entirely. * - We stop when few enough projects remain, or questions run out. */ -const STOP_AT = 8, SHOW = 12; +const STOP_AT = 8, SHOW = 10; let DATA, answers = {}, history = []; const $s = () => document.getElementById('stage'); @@ -167,7 +169,7 @@

${esc(q.text)}

const preview = () => { document.getElementById('remaining').textContent = `${candidates(pending()).length} communities match so far`; - showMatches(pending(), 'Best matches so far'); + showMatches(pending(), 'matches so far'); }; $s().querySelectorAll('.opt').forEach(el => el.onclick = () => { const v = el.dataset.v; @@ -219,12 +221,12 @@

${esc(p.name)}

// Update the match list in place: drop cards that fell out of the top // SHOW, move the rest into rank order, and fade in newcomers. -function showMatches(ans, heading) { +function showMatches(ans, what) { const asked = Object.entries(ans).filter(([k, v]) => v.length && isFilter(k)).length; - const ranked = DATA.projects.map(p => ({p, s: score(p, ans)})) + const all = DATA.projects.map(p => ({p, s: score(p, ans)})) .filter(r => r.s > 0 || !asked) - .sort((a, b) => b.s - a.s || (b.p.committers || 0) - (a.p.committers || 0)) - .slice(0, SHOW); + .sort((a, b) => b.s - a.s || (b.p.committers || 0) - (a.p.committers || 0)); + const ranked = all.slice(0, SHOW); const list = document.getElementById('list'); const keep = new Set(ranked.map(r => r.p.id)); [...list.children].forEach(el => { if (!keep.has(el.dataset.id)) el.remove(); }); @@ -234,7 +236,16 @@

${esc(p.name)}

if (!el.isConnected) { el.classList.remove('new'); void el.offsetWidth; el.classList.add('new'); } if (list.children[i] !== el) list.insertBefore(el, list.children[i] || null); }); - document.getElementById('matches-h').textContent = heading; + document.getElementById('matches-h').textContent = all.length > SHOW + ? `Top ${SHOW} ${what}` : what[0].toUpperCase() + what.slice(1); + // Always say that this is only the top of the list, not every match. + document.getElementById('shown').textContent = all.length <= SHOW + ? `Showing all ${all.length} matching communities.` + : `Showing only the top ${SHOW} of ${all.length} ${asked + ? 'communities that match at least one of your answers, ranked by how many they match, then by community size' + : 'communities, largest first'}.${ + document.getElementById('next') ? ' Answer more questions to narrow it down.' : ''}`; + document.getElementById('shown').hidden = !all.length; document.getElementById('empty').hidden = ranked.length > 0; document.getElementById('matches').hidden = false; } @@ -243,7 +254,7 @@

${esc(p.name)}

document.getElementById('prog').style.width = '100%'; $s().innerHTML = `
${history.length ? '' : ''}
` + answerSummary(); - showMatches(answers, 'Projects you might like'); + showMatches(answers, 'projects you might like'); document.getElementById('advice').innerHTML = gettingStarted(); document.getElementById('restart').onclick = () => { answers = {}; history = []; render(); }; const b = document.getElementById('back'); if (b) b.onclick = back;