setsPage landed as a renderer capability with no data supplier: it returns '' without data -- correctly -- which meant the FULL-DETAIL packet had never once printed its distinguishing back page in real use. The full-vs-lightweight split existed in code while the full packet was missing the page that makes it full. packetSets.js is the supplier: a pure decode+join with zero imports, so the identical function runs under Node (the generator reads the table from disk) and under Vite (the browser fetches it) and the join cannot fork between the two. The table stays with the caller -- the same seam as strings, christian and describe. The join is measured, not assumed: 2,091 of 2,107 groups match by slug, rings distribute 1-10 with real shape, and the 16 unmatched are inked as UNKNOWN on the bounded figure rather than guessed onto a side -- an un-inked category reads as zero on paper. Wired at every path that prints a packet: the offline generator (all 28 zones verified carrying the page, e.g. laos: 0 inside, 94 outside, 1 unknown; 94 placed with "25 same dialect, same people"), the legend Export menu, the poster wall, and the selection packet -- the last three through one opts.sets loader fetching the SAME served asset-pool copy the generator reads, so an embedded bundle prints the same page as the dev tree. The decode is a third reading of near-neighbors.json's cols/dict format (nearRows.js documents being a copy of upg-neighbor's, pending promotion to library/). Flagged in the header rather than hidden: when the loader is promoted, the decode here collapses into it and only the join remains.
81 lines
4 KiB
JavaScript
81 lines
4 KiB
JavaScript
/**
|
|
* packetSets — join a packet's people groups to the near-neighbours table and shape the result
|
|
* for setsPage's two figures (bounded + centered).
|
|
*
|
|
* WHY THIS EXISTS: setsPage landed as a renderer capability with NO DATA SUPPLIER — it returns ''
|
|
* without data, correctly, which meant the FULL-DETAIL packet had never once printed its
|
|
* distinguishing back page in real use. This module is the supplier. It is PURE and has ZERO
|
|
* imports, deliberately: the same function runs under Node (the offline generator reads the table
|
|
* from disk) and under Vite (the browser path fetches it), so the JOIN cannot fork between the two.
|
|
*
|
|
* THE TABLE ARRIVES RAW ({ cols, dict, rows }) AND FETCHING STAYS WITH THE CALLER — same seam
|
|
* pattern as `strings` and `describe`: each host supplies what it can reach, and this module owns
|
|
* only the decode + join. The decode duplicates the shape gospel's nearRows.js reads (which itself
|
|
* documents being a copy of upg-neighbor's, pending promotion to library/) — a THIRD reading of one
|
|
* format is real debt, flagged rather than hidden: when the loader is promoted, this module's
|
|
* decode collapses into it and only the join below remains.
|
|
*
|
|
* THE RING STATES, as the data owner defines them (nearRows.js:12-17, kept in the same words so
|
|
* the two files cannot drift silently):
|
|
* 1-10 placed 0 REACHED (the centre) -1 unreached, no relation found
|
|
* absent row = the source does not carry the group; counted here as `unmatched` and REPORTED,
|
|
* because on a printed page a category the reader cannot see reads as zero.
|
|
*/
|
|
|
|
/** Decode the raw { cols, dict, rows } table into a Map keyed by slug. */
|
|
export function decodeNearTable(raw) {
|
|
if (!raw || !Array.isArray(raw.cols) || !Array.isArray(raw.rows)) return new Map()
|
|
const ix = Object.fromEntries(raw.cols.map((c, i) => [c, i]))
|
|
const dict = raw.dict || {}
|
|
const dec = (r, c) => (c in ix ? (dict[c] ? dict[c][r[ix[c]]] : r[ix[c]]) : undefined)
|
|
const bySlug = new Map()
|
|
for (const r of raw.rows) {
|
|
const slug = dec(r, 'slug')
|
|
if (!slug) continue
|
|
bySlug.set(String(slug), {
|
|
reached: dec(r, 'reached') === 1,
|
|
ring: dec(r, 'ring'),
|
|
ringNb: dec(r, 'ringNb'),
|
|
})
|
|
}
|
|
return bySlug
|
|
}
|
|
|
|
/**
|
|
* buildPacketSets(groups, rawTable) -> { bounded, centered, unmatched } | null
|
|
*
|
|
* bounded: [{ id, label, reached }] reached undefined = the table lacks the group —
|
|
* UNKNOWN, which layoutBoundedSet inks as its own
|
|
* category rather than guessing a side.
|
|
* centered: [{ id, label, ring, neighborId }] ring passed through untouched: 0 goes to the
|
|
* centre, -1 to unplaced, both by the layout's own
|
|
* rules — mapping them here would duplicate its
|
|
* policy and drift.
|
|
* Returns null when NOTHING joined: a sets page about none of the packet's groups would be a
|
|
* chart of the join failure, not of the people, and setsPage's ''-without-data rule is the
|
|
* honest rendering of that.
|
|
*/
|
|
export function buildPacketSets(groups, rawTable) {
|
|
const bySlug = decodeNearTable(rawTable)
|
|
if (!bySlug.size || !Array.isArray(groups) || !groups.length) return null
|
|
const bounded = []
|
|
const centered = []
|
|
let matched = 0
|
|
for (const g of groups) {
|
|
const id = String(g.slug || g.id || '')
|
|
if (!id) continue
|
|
const label = g.name || id
|
|
const row = bySlug.get(id)
|
|
if (!row) {
|
|
// Unknown side of the boundary — inked, never guessed. No centered entry: a ring is a
|
|
// measurement, and an absent row has none to report.
|
|
bounded.push({ id, label, reached: undefined })
|
|
continue
|
|
}
|
|
matched++
|
|
bounded.push({ id, label, reached: row.reached })
|
|
centered.push({ id, label, ring: row.ring, neighborId: row.ringNb == null ? null : String(row.ringNb) })
|
|
}
|
|
if (!matched) return null
|
|
return { bounded, centered, unmatched: bounded.length - matched }
|
|
}
|