The 32 are scattered through the life now, not stacked into a bar
rega's contact sheet showed what no gate could: page 3 read as ONE SOLID GREEN BAR. 32 contiguous cells fill most of row 1 of 52, so the page said "a big green block" where the Driver's intent is "32 boxes lost in 5,200". Rarity is the whole message and a bar cannot carry it. ⚠ AND THE COMMENT I DELETED SAID THE OPPOSITE, WHICH IS THE PART WORTH KEEPING. It read: "Row-major, so the shaded run reads as a contiguous block from the top — a scattered shading of the same COUNT would be honest arithmetic and an unreadable picture." I wrote that reasoning about a picture I had never looked at, and it was exactly backwards. Scattering is what makes the eye hunt for them, which IS the point. DETERMINISTIC, NEVER Math.random(): a 32-bit FNV-1a hash of the ZONE ID feeds a mulberry32 PRNG, so the same packet prints identically every time — two copies of one zone must not disagree — while different zones get different constellations, so a print run is not 28 sheets wearing the same pattern. Verified: two consecutive renders produce byte-identical files (md5 febafc1813c6 both times). Partial Fisher-Yates over the index space rather than shuffling all 5,200, so a large share degrades into an even spread rather than clumping. ★ AND THE NEW ASSERTION IS THE ONE THAT WOULD HAVE CAUGHT THIS. check-packet-render now measures the shaded cells' VERTICAL SPREAD as a fraction of the grid height, at the RENDERED layer via getBoundingClientRect — because a COUNT cannot tell a scatter from a bar. 32 shaded is 32 shaded either way, which is precisely why every existing assertion stayed green on a broken page. POISON-TESTED rather than trusted: rebuilding the same 32 cells as a contiguous run makes it FAIL with "span only 0% of the grid height". The real packet measures 95% (32 cells across 26 distinct rows). The 50% floor is deliberately loose — a genuine scatter spans nearly the whole page, a bar spans ~1%, and anything between deserves a human look. ⚠ The poison test first ran on a copy in /tmp and the gate REFUSED it — "outside any checkout, so which build produced it cannot be checked". A sensible guard, and it meant my first poison run proved nothing; re-ran inside the tree. en and ar both PASS at 9 sheets. Page 2 (the empty life grid reading as blank graph paper) is UNTOUCHED and held for the Driver with the page-order question — shading a reader's life we do not know would be a guess presented as a fact, which is a product decision, not mine.
This commit is contained in:
parent
3bb5fd7b86
commit
35f682f7cb
2 changed files with 65 additions and 5 deletions
|
|
@ -247,6 +247,19 @@ const r = await page.evaluate(() => {
|
|||
prayBoxes: document.querySelectorAll('.pray-box').length,
|
||||
funnelTip: document.querySelectorAll('.funnel-tip').length,
|
||||
lifeShaded: document.querySelectorAll('.lc-grid .lc-reached').length,
|
||||
// ★ THE SHADED CELLS' VERTICAL SPREAD, as a fraction of the grid's own height. A COUNT cannot tell a
|
||||
// scatter from a bar — 32 shaded is 32 shaded whether they are spread through a life or stacked in
|
||||
// row 1 — so this measures the SHAPE, which is the thing the page is actually claiming.
|
||||
lifeSpread: (() => {
|
||||
const grid = document.querySelector('.sheet-christian .lc-grid')
|
||||
if (!grid) return null
|
||||
const on = [...grid.querySelectorAll('.lc-reached')]
|
||||
if (on.length < 2) return null
|
||||
const gb = grid.getBoundingClientRect()
|
||||
if (!(gb.height > 0)) return null
|
||||
const tops = on.map((el) => el.getBoundingClientRect().top)
|
||||
return (Math.max(...tops) - Math.min(...tops)) / gb.height
|
||||
})(),
|
||||
funnelEq: document.querySelectorAll('.funnel-eq').length,
|
||||
funnelSteps: document.querySelectorAll('.funnel-mine').length,
|
||||
// ★ MEASURED SO THE GATE CAN REPORT WHAT IT ASSERTED. rega: "the gate prints no funnel line, even
|
||||
|
|
@ -672,6 +685,18 @@ if (r.lifeSheets) {
|
|||
+ `but rendered ${perGrid} — the grid changed size, so the page no longer shows a human life`)
|
||||
}
|
||||
}
|
||||
// ⚠ AND THE SHADE MUST BE SCATTERED, NOT STACKED — the assertion that would have caught what three gates
|
||||
// and a passing suite did not. rega's contact sheet (2026-09-28) showed page 3 reading as ONE SOLID BAR:
|
||||
// 32 contiguous cells fill most of row 1 of 52, so the page said "a big green block" where the Driver's
|
||||
// intent is "32 boxes lost in 5,200". Every assertion passed, because they all measured the COUNT.
|
||||
// Half the grid height is a deliberately loose floor: a genuine scatter of even a few cells spans most of
|
||||
// the page (measured: 32 cells over 26 rows, 95% of the height), and a bar spans ~1%. Anything in between
|
||||
// is worth a human look.
|
||||
if (r.christianSheets && r.lifeSpread !== null && r.lifeSpread !== undefined && r.lifeSpread < 0.5) {
|
||||
fails.push(`the christian-share page's shaded cells span only ${(r.lifeSpread * 100).toFixed(0)}% of the `
|
||||
+ 'grid height — they are stacked, not scattered, so the page reads as a bar rather than as rare marks '
|
||||
+ 'in a whole life. Shade by a deterministic scatter, not a contiguous run.')
|
||||
}
|
||||
// ★ THE SHADED SHARE MUST BE NON-ZERO AND NOT EVERYTHING. 0 shaded means the percentage never
|
||||
// reached the page (a null christian-share file renders the page with an empty grid, which reads as
|
||||
// "nobody is reached" — a false claim rather than missing data); 5,200 means the maths inverted.
|
||||
|
|
|
|||
|
|
@ -518,14 +518,49 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null })
|
|||
// ★ 5,200 IS NOT ROUNDED OR CHOSEN FOR LAYOUT: 100 years x 52 weeks, the Driver's own numbers
|
||||
// ("a 100 year life in weeks"). Everything else here is derived from it, so the two pages cannot
|
||||
// drift apart into two grids that merely look alike.
|
||||
function lifeGrid({ shaded = 0, shadeClass = 'lc-on' } = {}) {
|
||||
function lifeGrid({ shaded = 0, shadeClass = 'lc-on', seed = '' } = {}) {
|
||||
const YEARS = 100, WEEKS = 52, TOTAL = YEARS * WEEKS;
|
||||
const n = Math.max(0, Math.min(TOTAL, Math.round(Number(shaded) || 0)));
|
||||
// Row-major, so the shaded run reads as a contiguous block from the top — a scattered
|
||||
// shading of the same COUNT would be honest arithmetic and an unreadable picture.
|
||||
|
||||
// ⚠ SCATTERED, NOT STACKED — and the comment that used to sit here said the opposite. It read:
|
||||
// "Row-major, so the shaded run reads as a contiguous block from the top — a scattered shading of the
|
||||
// same COUNT would be honest arithmetic and an unreadable picture." That was reasoning about a picture
|
||||
// I had never looked at, and it was exactly backwards.
|
||||
// ★ WHAT THE RENDER ACTUALLY SHOWED (rega's contact sheet, 2026-09-28): 32 contiguous cells fill most
|
||||
// of row 1 of 52, so the page reads as ONE SOLID GREEN BAR across the header — the ordinary chart the
|
||||
// whole life-grid idea exists to avoid. The Driver's intent is "32 boxes lost in 5,200"; rarity is the
|
||||
// message, and a bar cannot say it. Scattering makes the eye hunt for them, which IS the point.
|
||||
// ⚠ AND NO GATE COULD SEE IT. check-packet-render asserts the shaded COUNT is strictly between 0 and
|
||||
// 5,200 — true of a scatter and equally true of a bar. A count cannot distinguish a distribution.
|
||||
//
|
||||
// DETERMINISTIC, NEVER Math.random(): the same packet must print identically every time, or two copies
|
||||
// of one zone disagree and neither is reproducible. A 32-bit FNV-1a hash of the seed (the zone id) feeds
|
||||
// a mulberry32 PRNG, so the pattern is stable per zone and DIFFERENT between zones — which also avoids
|
||||
// every packet in a print run carrying the same constellation.
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < String(seed).length; i++) {
|
||||
h ^= String(seed).charCodeAt(i); h = Math.imul(h, 16777619);
|
||||
}
|
||||
const rand = () => {
|
||||
h |= 0; h = (h + 0x6D2B79F5) | 0;
|
||||
let t = Math.imul(h ^ (h >>> 15), 1 | h);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
// Partial Fisher-Yates over the index space: pick n distinct cells without materialising a shuffle of
|
||||
// all 5,200. Sparse by construction, so a large n degrades gracefully rather than clumping.
|
||||
const picked = new Set();
|
||||
const idx = [];
|
||||
for (let i = 0; i < TOTAL; i++) idx.push(i);
|
||||
for (let k = 0; k < n; k++) {
|
||||
const j = k + Math.floor(rand() * (TOTAL - k));
|
||||
const t = idx[k]; idx[k] = idx[j]; idx[j] = t;
|
||||
picked.add(idx[k]);
|
||||
}
|
||||
|
||||
const cells = [];
|
||||
for (let i = 0; i < TOTAL; i++) {
|
||||
cells.push(`<i class="lc-c${i < n ? ' ' + shadeClass : ''}"></i>`);
|
||||
cells.push(`<i class="lc-c${picked.has(i) ? ' ' + shadeClass : ''}"></i>`);
|
||||
}
|
||||
return { TOTAL, YEARS, WEEKS, shadedCount: n, html: `<div class="lc-grid">${cells.join('')}</div>` };
|
||||
}
|
||||
|
|
@ -560,7 +595,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null })
|
|||
function christianPage(zone, christian) {
|
||||
if (!christian || !Number.isFinite(Number(christian.pctEvangelical))) return '';
|
||||
const pct = Number(christian.pctEvangelical);
|
||||
const g = lifeGrid({ shaded: (pct / 100) * 5200, shadeClass: 'lc-reached' });
|
||||
const g = lifeGrid({ shaded: (pct / 100) * 5200, shadeClass: 'lc-reached', seed: zone.id || zone.zoneKey || 'upg-100' });
|
||||
const lostPct = 100 - pct;
|
||||
// ⚠ TWO DECIMALS ON BOTH SIDES, not rounded above 10. The reached share is 0.61% and rounding the
|
||||
// other side to "99%" loses the asymmetry that is the whole point: 99.39 and 0.61 sum to 100, while
|
||||
|
|
|
|||
Loading…
Reference in a new issue