diff --git a/internal/check-packet-variants.mjs b/internal/check-packet-variants.mjs
index a18be11b6..25040a0f7 100644
--- a/internal/check-packet-variants.mjs
+++ b/internal/check-packet-variants.mjs
@@ -217,6 +217,22 @@ ok(!/[^<]*<\/em><\/span>/.test(full.slice(full.indexOf('class="card"'))), 't
ok(/10/.test(caps.join(' ')), 'the bounded tally does not state its unknown count — an un-inked category reads as zero in print')
}
+// ★ EVERY GROUP OF THE LIST IS COUNTED (2026-10-03): a group with no record in a figure's data is named in
+// that figure's caption ("15 without data"), so each caption's parts add up to the list. Both ways: the
+// fixture's figures lack records (45 groups; 30 bounded, 24 centered), and complete data prints no such part.
+{
+ const sum = (cap) => [...cap.matchAll(/\d[\d,]*/g)].reduce((a, m) => a + Number(m[0].replace(/,/g, '')), 0)
+ const capsOf = (html) => [...html.matchAll(/class="set-cap">([^<]*) m[1])
+ const nd = String(strings.en.sets_no_data).replace('{n}', '').trim()
+ const short = capsOf(full)
+ ok(short.length === 2 && short.every((c) => sum(c) === 45 && c.includes(nd)),
+ `each set caption should add up to the list's 45 groups, naming the ones ${nd}: ${short.join(' | ')}`)
+ const whole = capsOf(R.buildPacket(zone, groups, new Map(), '', funnel, christian, 'full', {
+ bounded: groups.map((g, i) => ({ id: g.slug, reached: i % 2 === 0 })), centered: groups.map((g, i) => ({ id: g.slug, ring: (i % 10) + 1 })) }))
+ ok(whole.length === 2 && whole.every((c) => sum(c) === 45 && !c.includes(nd)),
+ `with a record for every group, each caption should add up to 45 and name none ${nd}: ${whole.join(' | ')}`)
+}
+
// ── THE RING LEGEND: THE AGREED VOCABULARY, AND ONLY THE RINGS THAT HOLD SOMEONE ────────────────
// ★ The names are COMPOSED from band x culture rather than a flat 1..10 table, because "same
// people" (provisional, pending a product decision) appears in rings 1, 4 AND 7 — three cells that a
diff --git a/library/packet/packet-render.js b/library/packet/packet-render.js
index 89f12d3e0..ac4f70533 100644
--- a/library/packet/packet-render.js
+++ b/library/packet/packet-render.js
@@ -1632,13 +1632,18 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
+ ` ${esc(fill(t('sets_bounded_tally'),
- { inside: num(T.inside), outside: num(T.outside), unknown: num(T.unknown) }))}
${esc(fill(t('sets_centered_tally'), - { placed: num(bf.placed.length), centre: num(T.centre), unplaced: num(T.unplaced + bf.unplaced.length) }))}
+ { placed: num(bf.placed.length), centre: num(T.centre), unplaced: num(T.unplaced + bf.unplaced.length) }) + noData(centered.length))} ` }); const rows = ringLegend(T); // The legend first says what a ring IS, in plain words, then lists the rings that hold someone. @@ -1880,7 +1885,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de // packet lists the SAME groups as a worksheet (LIST_STYLE); the full packet as profiles. ...(LIST_STYLE[variant] ? LIST_STYLE[variant](zone, groups, photos) : listPages(zone, groups, photos)), // BACK MATTER: after the profiles (brief A1). Full-detail only. - keep('sets') && setsPage(zone, sets), + keep('sets') && setsPage(zone, sets, Array.isArray(groups) ? groups.length : NaN), keep('wheel') && wheelPage(), keep('points') && pointsPage(), ].filter(Boolean); diff --git a/library/scripts/posters/packet-strings.json b/library/scripts/posters/packet-strings.json index dfafecbf2..915547300 100644 --- a/library/scripts/posters/packet-strings.json +++ b/library/scripts/posters/packet-strings.json @@ -263,6 +263,7 @@ "sets_bounded_title": "Reached, and not", "sets_centered_title": "Distance from a neighbour", "sets_bounded_tally": "{inside} inside · {outside} outside · {unknown} unknown", + "sets_no_data": "{n} without data", "sets_centered_tally": "{placed} placed · {centre} at the centre · {unplaced} unplaced", "ring_band_dialect": "Same dialect", "ring_band_language": "Same language", @@ -608,6 +609,7 @@ "sets_bounded_title": "Alcanzados y no alcanzados", "sets_centered_title": "Distancia a un vecino", "sets_bounded_tally": "{inside} dentro · {outside} fuera · {unknown} desconocido", + "sets_no_data": "{n} sin datos", "sets_centered_tally": "{placed} ubicados · {centre} en el centro · {unplaced} sin ubicar", "ring_band_dialect": "Mismo dialecto", "ring_band_language": "Misma lengua", @@ -939,6 +941,7 @@ "sets_bounded_title": "Atteints, ou non", "sets_centered_title": "Distance d’un voisin", "sets_bounded_tally": "{inside} à l’intérieur · {outside} à l’extérieur · {unknown} inconnu", + "sets_no_data": "{n} sans données", "sets_centered_tally": "{placed} placés · {centre} au centre · {unplaced} non placés", "ring_band_dialect": "Même dialecte", "ring_band_language": "Même langue", @@ -1267,6 +1270,7 @@ "sets_bounded_title": "Alcançados e não alcançados", "sets_centered_title": "Distância de um vizinho", "sets_bounded_tally": "{inside} dentro · {outside} fora · {unknown} desconhecido", + "sets_no_data": "{n} sem dados", "sets_centered_tally": "{placed} posicionados · {centre} no centro · {unplaced} sem posição", "ring_band_dialect": "Mesmo dialeto", "ring_band_language": "Mesma língua", @@ -1598,6 +1602,7 @@ "sets_bounded_title": "Erreicht und nicht erreicht", "sets_centered_title": "Abstand zu einem Nachbarn", "sets_bounded_tally": "{inside} innen · {outside} außen · {unknown} unbekannt", + "sets_no_data": "{n} ohne Daten", "sets_centered_tally": "{placed} platziert · {centre} in der Mitte · {unplaced} ohne Platz", "ring_band_dialect": "Gleicher Dialekt", "ring_band_language": "Gleiche Sprache", @@ -1926,6 +1931,7 @@ "sets_bounded_title": "Raggiunti e non raggiunti", "sets_centered_title": "Distanza da un vicino", "sets_bounded_tally": "{inside} dentro · {outside} fuori · {unknown} sconosciuto", + "sets_no_data": "{n} senza dati", "sets_centered_tally": "{placed} collocati · {centre} al centro · {unplaced} non collocati", "ring_band_dialect": "Stesso dialetto", "ring_band_language": "Stessa lingua", @@ -2254,6 +2260,7 @@ "sets_bounded_title": "Atinse și neatinse", "sets_centered_title": "Distanța față de un vecin", "sets_bounded_tally": "{inside} înăuntru · {outside} în afară · {unknown} necunoscut", + "sets_no_data": "{n} fără date", "sets_centered_tally": "{placed} plasate · {centre} în centru · {unplaced} neplasate", "ring_band_dialect": "Același dialect", "ring_band_language": "Aceeași limbă", @@ -2583,6 +2590,7 @@ "sets_bounded_title": "Достигнутые и нет", "sets_centered_title": "Расстояние до соседа", "sets_bounded_tally": "{inside} внутри · {outside} снаружи · {unknown} неизвестно", + "sets_no_data": "{n} без данных", "sets_centered_tally": "{placed} размещено · {centre} в центре · {unplaced} без места", "ring_band_dialect": "Тот же диалект", "ring_band_language": "Тот же язык", @@ -2921,6 +2929,7 @@ "sets_bounded_title": "وصلتهم البشارة ومن لم تصلهم", "sets_centered_title": "المسافة إلى جار", "sets_bounded_tally": "{inside} في الداخل · {outside} في الخارج · {unknown} غير معروف", + "sets_no_data": "{n} بلا بيانات", "sets_centered_tally": "{placed} موضوعون · {centre} في المركز · {unplaced} بلا موضع", "ring_band_dialect": "اللهجة نفسها", "ring_band_language": "اللغة نفسها", @@ -3249,6 +3258,7 @@ "sets_bounded_title": "पहुँचे और नहीं पहुँचे", "sets_centered_title": "पड़ोसी से दूरी", "sets_bounded_tally": "{inside} भीतर · {outside} बाहर · {unknown} अज्ञात", + "sets_no_data": "{n} बिना आँकड़ों के", "sets_centered_tally": "{placed} स्थित · {centre} केंद्र में · {unplaced} अस्थित", "ring_band_dialect": "वही बोली", "ring_band_language": "वही भाषा", @@ -3575,6 +3585,7 @@ "sets_bounded_title": "已触及与未触及", "sets_centered_title": "与邻近族群的距离", "sets_bounded_tally": "{inside} 圈内 · {outside} 圈外 · {unknown} 未知", + "sets_no_data": "{n} 无数据", "sets_centered_tally": "{placed} 已定位 · {centre} 在中心 · {unplaced} 未定位", "ring_band_dialect": "同一方言", "ring_band_language": "同一语言",