143 lines
8.3 KiB
JavaScript
143 lines
8.3 KiB
JavaScript
// check-share-policy — one written rule for WHICH MAPS CARRY A SHARE BUTTON.
|
|
//
|
|
// the agent: "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.
|
|
//
|
|
// ⚠ WHAT THIS GATE CANNOT SEE — read this before trusting a green run.
|
|
// It counts <ShareButton> occurrences in SOURCE. A button that is present in source
|
|
// and RENDERS NOWHERE counts exactly the same as a working one. Measured twice:
|
|
// - mutation test 2026-09-24: gating all-people-groups' toolbar off left this gate
|
|
// reporting "1 button / own-button", green, with no button in the DOM;
|
|
// - for real, same day: prayer-walk-planner mounts its SFC directly as the custom
|
|
// element and so installed no i18n. ShareButton calls useI18n(), which THROWS in
|
|
// setup(), and a thrown setup() removes the shadow-DOM subtree SILENTLY. Five
|
|
// toolbar buttons rendered, the sixth did not exist, and this gate passed.
|
|
// smoke:share-all is the gate that catches that: it clicks the real control, follows
|
|
// the url, and asserts the SAME custom element reopens. Keep every published map in
|
|
// its list — this file decides the RULE, that one proves the BEHAVIOUR.
|
|
//
|
|
// ── 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 -> <ShareButton> 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, dirname } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { TREE_NAME } from './_tree-name.mjs'
|
|
|
|
// Same fix as check-zone-census.mjs, same reason. DOXA_REPO still wins; only the FALLBACK moved
|
|
// off an absolute path into one author's worktree. check:no-abs-paths rejects those outright — "a
|
|
// clone must work on any machine" — and it runs inside build:unlocked, so the hardcoded default
|
|
// turned the whole build red for every builder. This is the idiom that gate names in its own
|
|
// failure text, and the one every sibling script here uses.
|
|
const REPO = process.env.DOXA_REPO
|
|
|| resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
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': 'header', // country-posters-mfe header owns it
|
|
'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
|
|
'all-people-groups': 'own-button', // was debt-no-share; ShareButton added 2026-09-24
|
|
// ⚠ prayer-walk-planner took a SECOND LOOK before this flipped, and the reason is worth
|
|
// keeping: it builds a PERSONAL WALKING ROUTE from polygons the reader draws, so a share
|
|
// link that silently drops them arrives as an empty planner — a button that reliably
|
|
// produces a blank map is worse than none, because it LOOKS like sharing worked.
|
|
// That read as a semantics question needing a ruling. It was not: the profile stores
|
|
// polygons in localStorage and has NO BACKEND (its own file header says so), so a URL
|
|
// CANNOT carry a route. There is no design freedom — the shareable artefact is the
|
|
// planner itself, exactly as every other map shares its own page. The failure mode only
|
|
// exists if the link CLAIMS to carry the route, so the button says "tool" and the status
|
|
// strip tells the reader their zones stay on the device.
|
|
'prayer-walk-planner': 'own-button', // was debt-no-share; resolved 2026-09-24
|
|
}
|
|
|
|
const isMap = (src) => /mapboxgl|maplibregl|useMapInstance/.test(src)
|
|
const manifest = JSON.parse(readFileSync(join(REPO, 'public', TREE_NAME, '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(/<ShareButton/g) || []).length
|
|
const want = POLICY[name]
|
|
rows.push({ name, buttons, want, published: published.has(name) })
|
|
|
|
if (!want) {
|
|
fails.push(`${name}: a map bundle with no entry in POLICY — decide whether it shares, and record it`)
|
|
continue
|
|
}
|
|
if (want === 'own-button' && buttons !== 1) {
|
|
fails.push(`${name}: policy "own-button" but ${buttons} <ShareButton> — `
|
|
+ (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}`)
|
|
}
|
|
// ⚠ 'header' HAS A CONSEQUENCE FOR smoke:share-all, and it drifted once. The button is
|
|
// in the MFE SHELL, so the bundle's LEAF url correctly has none — and smoke:share-all
|
|
// was still pointing at the leaf for both header bundles, reporting 2 failures that
|
|
// were the policy working as designed (measured 2026-09-24: 10/12, both failures
|
|
// 'header'). When a bundle moves to 'header', its entry in smoke:share-all must move
|
|
// from <bundle>/<leaf>/ to <bundle>/<mfe>/ or the suite tests a surface the policy
|
|
// says should be empty.
|
|
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.`)
|