The full packet's sets page gets its data, from every print path
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.
This commit is contained in:
parent
45e31f1028
commit
9799ebdba4
5 changed files with 124 additions and 3 deletions
|
|
@ -471,6 +471,12 @@ const prints = usePosterPrints({
|
|||
},
|
||||
stock: () => pickedStock.value,
|
||||
label: (item) => item?.label || item?.id || '',
|
||||
// The near-neighbours table for the FULL packet's bounded/centered back page — fetched from the
|
||||
// SAME served asset pool the posters come from, so an embedded copy of this bundle prints the
|
||||
// same page as the dev tree. null on any failure: the packet prints without the page rather
|
||||
// than not printing.
|
||||
sets: () => fetch(`${props.assetsBase}/tables/near-neighbors.json`)
|
||||
.then((r) => (r.ok ? r.json() : null)).catch(() => null),
|
||||
})
|
||||
const canPrintRows = computed(() => typeof props.records === 'function')
|
||||
const previewIsControl = computed(() => props.previewAction === 'print')
|
||||
|
|
|
|||
|
|
@ -239,10 +239,16 @@ export function usePosterPrints(opts = {}) {
|
|||
// Node generator passes it (packet.mjs reads data/people-groups/christian-share.json); this
|
||||
// path did not, so buildPacket's christianPage returned '' and a browser print had one sheet
|
||||
// fewer with nothing to say so. Same seam as strings: a host may supply its own figure.
|
||||
const [{ createPacketRenderer }, strings, christian] = await Promise.all([
|
||||
// `nearTable` feeds the FULL packet's bounded/centered sets page. Same seam as strings and
|
||||
// christian: a host that can reach the served asset pool supplies a loader (opts.sets), and a
|
||||
// host that cannot simply prints a packet without that page — setsPage's ''-without-data rule.
|
||||
// .catch(() => null): the sets page is the one page whose absence must never block a print.
|
||||
const [{ createPacketRenderer }, { buildPacketSets }, strings, christian, nearTable] = await Promise.all([
|
||||
import('../packet/packet-render.js'),
|
||||
import('../packet/packetSets.js'),
|
||||
opts.strings ? opts.strings() : import('../scripts/posters/packet-strings.json').then((m) => m.default || m),
|
||||
opts.christian ? opts.christian() : import('../../data/people-groups/christian-share.json').then((m) => m.default || m),
|
||||
opts.sets ? Promise.resolve().then(() => opts.sets()).catch(() => null) : Promise.resolve(null),
|
||||
]);
|
||||
// ★ THE DESCRIBER IS COMPOSED HERE AND INJECTED, because this is the BROWSER path and only it
|
||||
// can reach the Vite-only pieces. packet-render.js runs under Node too (the offline generator),
|
||||
|
|
@ -266,7 +272,10 @@ export function usePosterPrints(opts = {}) {
|
|||
data.filter((g) => g.image_url).map((g) => [g.slug, g.image_url]),
|
||||
);
|
||||
set(item, kind, 'render', `printing ${data.length} people groups`, 70);
|
||||
openPrintFrame(R.buildPacket(item, data, photos, mapSvg, funnel, christian, variant), { autoprint: true });
|
||||
// The join runs over the SAME rows the packet prints, so the sets page cannot describe a
|
||||
// different population than the profile table beside it.
|
||||
const sets = nearTable ? buildPacketSets(data, nearTable) : null;
|
||||
openPrintFrame(R.buildPacket(item, data, photos, mapSvg, funnel, christian, variant, sets), { autoprint: true });
|
||||
done(item, kind);
|
||||
} catch (e) { fail(item, kind, e); }
|
||||
}
|
||||
|
|
|
|||
81
library/packet/packetSets.js
Normal file
81
library/packet/packetSets.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* 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 }
|
||||
}
|
||||
|
|
@ -136,6 +136,11 @@ const { createPacketRenderer } = await import(
|
|||
// `describe` seam below.
|
||||
const { describeGroup } = await import(
|
||||
join(__dirname, '..', '..', 'packets', 'describeGroup.js'));
|
||||
// The sets supplier for the FULL packet's bounded/centered back page. Pure decode+join; the
|
||||
// TABLE is read from the served asset pool below, same as the browser fetches it, so the two
|
||||
// paths chart the same rows.
|
||||
const { buildPacketSets } = await import(
|
||||
join(__dirname, '..', '..', 'packet', 'packetSets.js'));
|
||||
// SCRIPT FONT, inlined. Some languages need a face the system stack does not
|
||||
// carry — Hindi renders as tofu boxes in Georgia/Palatino/serif. The packet is a
|
||||
// standalone file meant to print offline, so the font is base64'd INTO it rather
|
||||
|
|
@ -404,9 +409,23 @@ async function fetchPhotos(groups, limit = 16) {
|
|||
// inlines base64 so the file is self-contained and printable offline; the browser
|
||||
// hands the renderer plain URLs the page already has). Everything downstream is
|
||||
// the shared renderer, so the two packets cannot drift.
|
||||
// ★ THE NEAR-NEIGHBOURS TABLE, read once for every zone's sets page. From the SERVED asset pool
|
||||
// (public/dx-maps/assets/tables/), not a plugin folder — the same copy the browser fetches, so the
|
||||
// offline packet and a printed-from-the-app packet chart identical rows. A missing table yields
|
||||
// null and buildPacketSets -> setsPage omit the page rather than failing the run: one way for the
|
||||
// page to be absent, not two (the FUNNEL/CHRISTIAN pattern above, applied unchanged).
|
||||
let NEAR_TABLE = null;
|
||||
try {
|
||||
NEAR_TABLE = JSON.parse(readFileSync(
|
||||
join(__dirname, '..', '..', '..', 'public', 'dx-maps', 'assets', 'tables', 'near-neighbors.json'), 'utf8'));
|
||||
} catch (e) {
|
||||
console.warn(`⚠ near-neighbors.json unreadable (${e?.message || e}) — every packet will omit the bounded/centered sets page.`);
|
||||
}
|
||||
|
||||
async function buildPacket(zone, groups, mapSvg) {
|
||||
const photos = await fetchPhotos(groups);
|
||||
const html = R.buildPacket(zone, groups, photos, mapSvg, FUNNEL, CHRISTIAN);
|
||||
const sets = NEAR_TABLE ? buildPacketSets(groups, NEAR_TABLE) : null;
|
||||
const html = R.buildPacket(zone, groups, photos, mapSvg, FUNNEL, CHRISTIAN, 'full', sets);
|
||||
const withPhoto = groups.filter((g) => photos.has(g.slug)).length;
|
||||
// ★ COUNT THE SHEETS IN THE EMITTED HTML, do not re-derive them from a formula. The old line
|
||||
// computed `1 + ceil(n/PER_PAGE) + 2` while its own comment claimed the number was "asked of the
|
||||
|
|
|
|||
|
|
@ -1367,6 +1367,12 @@ const packetPrints = usePosterPrints({
|
|||
// Resolved here rather than inside the shared action, which knows nothing about a
|
||||
// bundle's asset layout — the same seam printPacket uses for its map svg.
|
||||
assetsBase: () => resolveAssetsBase(profileConfig.value),
|
||||
// The near-neighbours table for the FULL packet's bounded/centered back page — fetched from the
|
||||
// SAME served asset pool the posters come from, so an embedded copy of this bundle prints the
|
||||
// same page as the dev tree. null on any failure: the packet prints without the page rather
|
||||
// than not printing.
|
||||
sets: () => fetch(`${resolveAssetsBase(profileConfig.value)}/tables/near-neighbors.json`)
|
||||
.then((r) => (r.ok ? r.json() : null)).catch(() => null),
|
||||
downloadPrefix: 'doxa-upg-100',
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue