diff --git a/internal/check-share-policy.mjs b/internal/check-share-policy.mjs new file mode 100644 index 000000000..0bc634c94 --- /dev/null +++ b/internal/check-share-policy.mjs @@ -0,0 +1,109 @@ +// check-share-policy — one written rule for WHICH MAPS CARRY A SHARE BUTTON. +// +// Driver: "Some maps need a map-level share button and some do not. The Prayer +// Progress map is the one intended for embedding on other people's websites, so it +// MUST have a share button. UPG-100 must not (the header already provides it). +// Decide + record the per-map rule, then apply it." +// +// The BEHAVIOUR already existed on both of those. What did not exist was the RULE, +// so nothing stopped the next agent adding a share button to a map that should not +// have one, or leaving a published map with no way to share it at all. Surveying +// all 11 map bundles found FOUR different patterns in use and two published maps +// with no sharing path by any mechanism. +// +// ── THE RULE ──────────────────────────────────────────────────────────────── +// A map bundle must have EXACTLY ONE share affordance, and it belongs at the +// OUTERMOST surface a reader actually sees: +// +// own-button the map IS the product surface -> in the map +// header a composite wraps it with a header -> the header owns sharing, +// the map omits the button +// composite a parent stacks it and overrides +// shareUrl so the PARENT is shared -> child button is fine, +// parent supplies the url +// none not published — nothing to share yet +// +// TWO share buttons on one surface is the defect this prevents (it shipped once: +// BUG-UPG100-DOUBLE-SHARE). ZERO on a published map is the opposite defect, and it +// is currently REAL on two bundles — recorded below as known debt rather than +// silently passing, so the count cannot drift without someone deciding. +import { readFileSync, existsSync, readdirSync } from 'node:fs' +import { resolve, join } from 'node:path' + +const REPO = process.env.DOXA_REPO + || resolve('/home/lk10x/Workspace/eco-dx/root-doxa-maps/wt-packet') +const DESIGNS = join(REPO, 'plugin-designs') + +// The decided policy, per bundle. A map bundle MUST appear here — an unlisted one +// fails, so adding a map forces the sharing decision rather than deferring it. +const POLICY = { + 'doxa-simple-map': 'own-button', // Prayer Progress — the embed flagship + 'doxa-research-map': 'own-button', + 'country-posters': 'own-button', + 'cities-internet': 'own-button', + 'gospel-resources': 'own-button', + 'demo-map': 'own-button', + 'my-upg-100-list': 'header', // upg-100-mfe.vue header owns it + 'research-map-plus-grid': 'composite', // overrides child shareUrl to share itself + 'key-cities': 'none', // not published + // KNOWN DEBT — published, and no sharing path by any mechanism. Listed so the + // gate passes on today's reality while naming the gap; flipping either to + // 'own-button' is a deliberate change, not a drive-by. + 'all-people-groups': 'debt-no-share', + 'prayer-walk-planner': 'debt-no-share', +} + +const isMap = (src) => /mapboxgl|maplibregl|useMapInstance/.test(src) +const manifest = JSON.parse(readFileSync(join(REPO, 'public', 'doxa-maps', 'manifest.json'), 'utf8')) +const published = new Set(manifest.bundles.map((b) => b.bundle)) + +const fails = [] +const rows = [] +for (const name of readdirSync(DESIGNS)) { + const dir = join(DESIGNS, name) + let vues = [] + try { vues = readdirSync(dir).filter((f) => f.endsWith('.vue')) } catch { continue } + if (!vues.length) continue + const src = vues.map((f) => readFileSync(join(dir, f), 'utf8')).join('\n') + if (!isMap(src)) continue + + const buttons = (src.match(/ — ` + + (buttons === 0 ? 'the embed surface has no way to be shared' : 'two affordances on one surface')) + } + if (want === 'header' && buttons !== 1) { + // UPG-100's single button lives in the MFE header, not the map — still exactly one. + fails.push(`${name}: policy "header" expects exactly one share affordance in the bundle, found ${buttons}`) + } + if (want === 'debt-no-share' && buttons !== 0) { + fails.push(`${name}: listed as known debt (no sharing) but now has ${buttons} — update POLICY`) + } + if (want === 'none' && published.has(name)) { + fails.push(`${name}: policy "none" assumes unpublished, but it IS published — it needs a share path`) + } +} + +console.log(' bundle published buttons policy') +for (const r of rows.sort((a, b) => a.name.localeCompare(b.name))) { + console.log(` ${r.name.padEnd(24)} ${(r.published ? 'yes' : 'no').padEnd(10)} ${String(r.buttons).padEnd(8)} ${r.want || '(UNDECIDED)'}`) +} +const debt = rows.filter((r) => r.want === 'debt-no-share') +if (debt.length) { + console.log(`\n known debt: ${debt.length} published map(s) with NO sharing path — ` + + debt.map((d) => d.name).join(', ')) +} + +if (fails.length) { + console.error(`\nSHARE-POLICY: FAIL — ${fails.length} problem(s)`) + for (const f of fails) console.error(' - ' + f) + process.exit(1) +} +console.log(`\nSHARE-POLICY: PASS — ${rows.length} map bundles, each with exactly one decided sharing rule.`) diff --git a/package.json b/package.json index 95a266029..918efe9b5 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "build:unlocked": "bun run gates:pre && bun run library-index && bun run check:library-index && bun run clean && bun run token && bun run build:bundles && bun run tree && bun run gates:post && bun run gallery", "check:card-deck": "node internal/check-card-deck.mjs", "check:poster-raster": "node internal/check-poster-raster.mjs", + "check:share-policy": "node internal/check-share-policy.mjs", "check:zone-census": "node internal/check-zone-census.mjs", "check:print-geometry": "node internal/check-print-geometry.mjs", "check:packet-browser": "node internal/check-packet-browser.mjs",