doxa-maps/internal/check-gospel-set-charts.mjs
Doxa Maps 4b7f65204f Collect the gospel set-chart layout check in gates:pre
The check itself shipped with the plugin modules it covers, but the line that
runs it existed only in a message, which is not a place code lives. So it sat in
internal/ and would never have run — the same failure it was written to prevent,
one layer up.

This is the whole registration in one commit: the file, the script entry and the
word in the gates:pre list. It imports boundedSet.js and centeredSet.js, so it
must land after the plugin commits that add them; landing it first fails
collection and takes gates:pre red for everyone.
2026-09-28 15:01:19 +00:00

88 lines
6 KiB
JavaScript

#!/usr/bin/env node
/**
* check-gospel-set-charts.mjs — the bounded-set and centered-set LAYOUT maths.
*
* Both modules are pure, which is why they can be checked here at all: a pure function's
* inputs ARE its domain, so these assertions are evidence about the real module and not
* about a fixture that resembles one. What they are NOT evidence about is the shape of
* near-neighbors.json — that binding is checked where the data is read, not here.
*
* The properties worth pinning are the ones where a wrong answer still LOOKS like a chart:
*
* · bounded set — nothing may sit ON the boundary. A dot on the line is the one thing a
* bounded set cannot express, and it would read as a group that is somehow both.
* · bounded set — a group of UNKNOWN reached-ness is its own outcome. If it were folded
* into `outside`, a join failure would render as "unreached" and inflate the count with
* something that is not a fact about the group.
* · centered set — a group with no ring is UNPLACED, never defaulted to the outer ring.
* Ring 10 is a real answer ("beyond"), so it must not double as a bin for missing data.
* · centered set — a REACHED group (ring 0) is the CENTRE, not unplaced. An earlier build
* spelled "reached" and "no relation found" with the same 0; treating that value as
* missing data would report the best outcome in the set as an absence. The three states
* are asserted apart, and a reached group is asserted to reach NO ring at all.
* · centered set — the arrow stops exactly at innerR. An arrow that overshoots covers the
* cross it is meant to point at.
* · both — layout is deterministic, so a group does not move between renders.
*/
import { layoutBoundedSet, boundedSetTally } from '../plugin-designs/gospel-internet/src/boundedSet.js'
import { layoutCenteredSet, ringTally, inwardArrow, ringRadii, RING_MAX, RING_REACHED, RING_NO_RELATION } from '../plugin-designs/gospel-internet/src/centeredSet.js'
const fail = []
const ok = (label, cond, detail = '') => {
if (cond) console.log(` ✓ ${label}${detail ? ` — ${detail}` : ''}`)
else { console.log(` ✗ ${label}${detail ? ` — ${detail}` : ''}`); fail.push(label) }
}
console.log('GOSPEL-SET-CHARTS')
// ── bounded set ─────────────────────────────────────────────────────────────
const bg = [{ id: 'a', reached: true }, { id: 'b', reached: false }, { id: 'c' }, { id: 'd', reached: null }]
const B = layoutBoundedSet(bg, { r: 100 })
const bt = boundedSetTally(B)
ok('bounded: reached inside, unreached outside, unknown neither', bt.inside === 1 && bt.outside === 1 && bt.unknown === 2,
`in ${bt.inside} out ${bt.outside} unknown ${bt.unknown}`)
ok('bounded: unknown is NOT folded into outside', bt.outside === 1 && bt.unknown === 2)
ok('bounded: total is conserved', bt.total === bg.length)
const dist = (p) => Math.hypot(p.x, p.y)
ok('bounded: nothing sits ON the boundary', dist(B.inside[0]) < 100 && dist(B.outside[0]) > 100,
`in r=${dist(B.inside[0]).toFixed(1)} out r=${dist(B.outside[0]).toFixed(1)}`)
ok('bounded: deterministic between renders', layoutBoundedSet(bg, { r: 100 }).inside[0].x === B.inside[0].x)
// ── centered set ────────────────────────────────────────────────────────────
const cg = [{ id: 'a', ring: 1, neighborId: 'r1' }, { id: 'b', ring: RING_MAX, neighborId: 'r2' },
{ id: 'c', ring: RING_REACHED }, { id: 'd' }, { id: 'e', ring: RING_MAX + 1 }, { id: 'f', ring: 5 }]
const C = layoutCenteredSet(cg)
const ct = ringTally(C)
ok('centered: valid rings placed, unusable ones unplaced, reached at the centre',
ct.placed === 3 && ct.unplaced === 2 && ct.centre === 1,
`placed ${ct.placed} centre ${ct.centre} unplaced ${ct.unplaced}`)
ok('centered: a missing ring is NOT defaulted onto the outer ring',
C.placed.filter((p) => p.ring === RING_MAX).length === 1)
ok('centered: every unplaced group carries a reason', C.unplaced.every((u) => typeof u.reason === 'string' && u.reason))
ok('centered: rings are ordered inward to outward', ringRadii().length === RING_MAX &&
C.placed.find((p) => p.ring === 1).r < C.placed.find((p) => p.ring === RING_MAX).r)
const p1 = C.placed.find((p) => p.ring === 1)
ok('centered: a point lies exactly on its own ring', Math.abs(Math.hypot(p1.x, p1.y) - p1.r) < 1e-9)
const arrow = inwardArrow(C.placed.find((p) => p.ring === RING_MAX))
ok('centered: the inward arrow stops at innerR and does not cover the cross',
arrow && Math.abs(Math.hypot(arrow.x2, arrow.y2) - 26) < 1e-9, `ends at r=${Math.hypot(arrow.x2, arrow.y2).toFixed(2)}`)
ok('centered: no recorded neighbour means NO arrow, not a guessed one', inwardArrow({ x: 10, y: 0 }) === null)
ok('centered: deterministic between renders', layoutCenteredSet(cg).placed[0].x === C.placed[0].x)
// the three ring states are three different facts and must not collapse into one another
const sg = [{ id: 'p', ring: 1, neighborId: 'r' }, { id: 'reached', ring: RING_REACHED },
{ id: 'norel', ring: RING_NO_RELATION }, { id: 'noring' }]
const S = layoutCenteredSet(sg)
const st = ringTally(S)
ok('centered: reached (ring 0) is the CENTRE, not unplaced', st.centre === 1 && S.centre[0].id === 'reached')
ok('centered: no-relation (ring -1) is unplaced with its OWN reason',
S.unplaced.find((u) => u.id === 'norel')?.reason === 'no relation found')
ok('centered: the three states stay apart', st.placed === 1 && st.centre === 1 && st.unplaced === 2,
`placed ${st.placed} centre ${st.centre} unplaced ${st.unplaced}`)
ok('centered: a reached group reaches NO ring', [...st.counts.values()].reduce((a, b) => a + b, 0) === st.placed)
if (fail.length) {
console.log(`\nGOSPEL-SET-CHARTS: FAIL — ${fail.length}: ${fail.join(', ')}`)
process.exit(1)
}
console.log('\nGOSPEL-SET-CHARTS: PASS — both layouts pinned on the properties that fail silently')