Merge the pilot (e651da54f)
This commit is contained in:
commit
91cfe76757
3 changed files with 329 additions and 1 deletions
183
library/packet/fork-leaders.js
Normal file
183
library/packet/fork-leaders.js
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
/**
|
||||
* fork-leaders.js — route ONE paired-row leader as an octilinear fork.
|
||||
*
|
||||
* A paired row has two pins (a, b) sharing one lane port on the frame margin.
|
||||
* Instead of two separate leaders, one stem leaves the port and FORKS at a 45°
|
||||
* chamfer into two short legs, one to each pin. This is the per-fork geometry
|
||||
* only; the caller pairs the rows, assigns the lanes and batches every row's
|
||||
* polylines through the engine's crossing counter.
|
||||
*
|
||||
* It imitates poster-render.js's octiRoute: a trace built from horizontal,
|
||||
* vertical and exact-45° segments, STUB 7 off the port, 45° chamfer corners, no
|
||||
* curves. The output is POLYLINES ([[x,y],…]) so it drops straight into
|
||||
* countPolyCrossings (which skips same-polyline pairs by index).
|
||||
*
|
||||
* MODES (the two designs, c1/c2):
|
||||
* 'row' — fork AT the row end: the stem is ~0, both legs leave the lane port
|
||||
* directly to a and b. No long trunk across the page.
|
||||
* 'trunk' — the stem runs `dirOut` onto the page toward the pair's midpoint
|
||||
* (by forkFrac of the port→midpoint horizontal budget), THEN forks
|
||||
* near the pins.
|
||||
*
|
||||
* PX ONLY. DETERMINISTIC: no Math.random; forkFrac is the caller's sweep knob,
|
||||
* every value octilinear, so the caller's min-crossing winner is still octilinear.
|
||||
*
|
||||
* @param {object} o
|
||||
* @param {{x:number,y:number}} o.lane the row's lane port on the margin.
|
||||
* @param {{id?:any,x:number,y:number}} o.a first pin (true de-overlapped coords, in its state).
|
||||
* @param {{id?:any,x:number,y:number}} o.b second pin.
|
||||
* @param {number} o.dirOut +1 or -1: horizontal sense from the lane toward the map.
|
||||
* @param {'row'|'trunk'} [o.mode='row']
|
||||
* @param {number} [o.forkFrac=0.5] 'trunk' only: fraction of the port→midpoint
|
||||
* horizontal budget the stem spends before forking (0..1).
|
||||
* @param {Array<{id?:any,x:number,y:number}>} [o.obstacles=[]] other pins to test legs against.
|
||||
* @param {number} [o.r=0] a leg "passes through" an obstacle if within r of it.
|
||||
* @returns {{
|
||||
* stem: Array<[number,number]>, legA: Array<[number,number]>, legB: Array<[number,number]>,
|
||||
* // COUNT CROSSINGS ON `segs`, NOT `polys`: polyA and polyB share the stem, so
|
||||
* // feeding both to a crossing counter as separate polylines makes a fork
|
||||
* // self-cross. Batch one fork's `segs` under ONE polyline id.
|
||||
* polys: Array<Array<[number,number]>>, // [stem+legA, stem+legB] — for DRAWING only
|
||||
* segs: Array<[[number,number],[number,number]]>, // the fork's UNIQUE segments
|
||||
* (stem once + each leg), deduped. Count crossings BETWEEN forks by giving
|
||||
* every fork ONE polyline id — do NOT flatten polys into two ids, or a fork's
|
||||
* own two arms sharing the stem score as self-crossings. Feed one fork's segs
|
||||
* under one `li` to countPolyCrossings (as a single many-point run is not
|
||||
* possible for a branch, pass segs with a shared index).
|
||||
* octilinear: boolean,
|
||||
* throughPins: Array<any>, // obstacle ids (or indices) a leg passes through
|
||||
* d: string, // convenience SVG path (derived from polys)
|
||||
* }}
|
||||
*/
|
||||
export function forkRoute({ lane, a, b, dirOut, mode = 'row', forkFrac = 0.5, obstacles = [], r = 0 }) {
|
||||
if (!lane || !a || !b) throw new TypeError('lane, a and b are required');
|
||||
if (dirOut !== 1 && dirOut !== -1) throw new TypeError('dirOut must be +1 or -1');
|
||||
if (mode !== 'row' && mode !== 'trunk') throw new TypeError("mode must be 'row' or 'trunk'");
|
||||
// FAIL LOUD on a non-finite coordinate. The engine filters non-finite pins
|
||||
// before routing, so this cannot fire today — but without it a future caller's
|
||||
// NaN would flow silently into NaN path data (an invisible, un-gated leader).
|
||||
for (const [pt, name] of [[lane, 'lane'], [a, 'a'], [b, 'b']]) {
|
||||
if (!Number.isFinite(pt.x) || !Number.isFinite(pt.y)) {
|
||||
throw new TypeError(`${name} has a non-finite coordinate (${pt.x}, ${pt.y})`);
|
||||
}
|
||||
}
|
||||
|
||||
const STUB = 7;
|
||||
const ff = clamp01(forkFrac);
|
||||
|
||||
// The fork point. 'row' forks essentially at the port (a tiny stub so the two
|
||||
// legs don't start exactly on the margin). 'trunk' walks dirOut toward the
|
||||
// pair's midpoint x by forkFrac of the available horizontal budget.
|
||||
const midX = (a.x + b.x) / 2;
|
||||
const budget = Math.max(0, (midX - lane.x) * dirOut); // horizontal room port→midpoint in dirOut sense (≥0)
|
||||
const stemLen = mode === 'trunk' ? Math.max(STUB, ff * budget) : STUB;
|
||||
const forkX = lane.x + dirOut * stemLen;
|
||||
const forkPt = [forkX, lane.y];
|
||||
|
||||
const stem = [[lane.x, lane.y], forkPt];
|
||||
|
||||
const legA = octiLeg(forkPt, a, dirOut);
|
||||
const legB = octiLeg(forkPt, b, dirOut);
|
||||
|
||||
const polyA = [...stem, ...legA.slice(1)]; // legs start at forkPt; avoid duplicating it
|
||||
const polyB = [...stem, ...legB.slice(1)];
|
||||
const polys = [polyA, polyB];
|
||||
|
||||
// UNIQUE segments for crossing counts: the stem counted ONCE, then each leg.
|
||||
// (polys share the stem; counting them as two polylines would self-cross.)
|
||||
const segs = dedupeSegs([...toSegs(stem), ...toSegs(legA), ...toSegs(legB)]);
|
||||
|
||||
const octilinear = isOctilinear(polyA) && isOctilinear(polyB);
|
||||
|
||||
// Through-pin report: an obstacle the leg body passes within r of (excluding
|
||||
// the leg's own endpoint pin). Checked on legA/legB segments only — the stem
|
||||
// is shared and tested per-poly by the caller's crossing counter.
|
||||
const throughPins = [];
|
||||
if (obstacles.length && r > 0) {
|
||||
const own = new Set([idOf(a), idOf(b)]);
|
||||
for (const ob of obstacles) {
|
||||
const oid = idOf(ob);
|
||||
if (own.has(oid)) continue;
|
||||
if (segsPassThrough(polyA, ob, r) || segsPassThrough(polyB, ob, r)) throughPins.push(oid);
|
||||
}
|
||||
}
|
||||
|
||||
return { stem, legA, legB, polys, segs, octilinear, throughPins, d: toPath(polyA) + ' ' + toPath(polyB) };
|
||||
}
|
||||
|
||||
function toSegs(pts) {
|
||||
const out = [];
|
||||
for (let i = 0; i < pts.length - 1; i++) out.push([pts[i], pts[i + 1]]);
|
||||
return out;
|
||||
}
|
||||
function dedupeSegs(segs) {
|
||||
const seen = new Set(), out = [];
|
||||
for (const s of segs) {
|
||||
const k = `${s[0][0]},${s[0][1]}|${s[1][0]},${s[1][1]}`;
|
||||
const k2 = `${s[1][0]},${s[1][1]}|${s[0][0]},${s[0][1]}`;
|
||||
if (seen.has(k) || seen.has(k2)) continue;
|
||||
seen.add(k); out.push(s);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// One octilinear leg from the fork point to a pin: a 45° chamfer that closes the
|
||||
// shorter of |dx|,|dy|, then a straight H or V run for the remainder. This keeps
|
||||
// every segment H, V or exact-45° (mirrors octiRoute's feeler+diag, two-pin).
|
||||
function octiLeg(from, pin, dirOut) {
|
||||
const [fx, fy] = from;
|
||||
const dx = pin.x - fx;
|
||||
const dy = pin.y - fy;
|
||||
const adx = Math.abs(dx), ady = Math.abs(dy);
|
||||
// Diagonal leg closes min(adx,ady) on both axes at 45°; the straight remainder
|
||||
// finishes on whichever axis still has distance left.
|
||||
const diag = Math.min(adx, ady);
|
||||
const sx = Math.sign(dx) || dirOut; // if the pin is dead in line, lean outward
|
||||
const sy = Math.sign(dy) || 0;
|
||||
const corner = [fx + sx * diag, fy + sy * diag];
|
||||
// corner → pin is now purely horizontal (adx>ady) or vertical (ady>adx) or zero.
|
||||
const pts = [[fx, fy], corner];
|
||||
if (!almost(corner[0], pin.x) || !almost(corner[1], pin.y)) pts.push([pin.x, pin.y]);
|
||||
return pts;
|
||||
}
|
||||
|
||||
// ── octilinear invariant (same test + eps as poster-render.js) ────────────────
|
||||
function isOctilinear(pts, eps = 0.6) {
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
const dx = Math.abs(pts[i + 1][0] - pts[i][0]);
|
||||
const dy = Math.abs(pts[i + 1][1] - pts[i][1]);
|
||||
if (dx < eps || dy < eps) continue; // horizontal or vertical
|
||||
if (Math.abs(dx - dy) < eps) continue; // 45°
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Does any segment of the polyline pass within r of point ob (not at an endpoint)?
|
||||
function segsPassThrough(pts, ob, r) {
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
const d = distPointToSeg(ob.x, ob.y, pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1]);
|
||||
if (d <= r) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function distPointToSeg(px, py, ax, ay, bx, by) {
|
||||
const abx = bx - ax, aby = by - ay;
|
||||
const len2 = abx * abx + aby * aby;
|
||||
let t = len2 ? ((px - ax) * abx + (py - ay) * aby) / len2 : 0;
|
||||
t = t < 0 ? 0 : t > 1 ? 1 : t;
|
||||
const x = ax + t * abx, y = ay + t * aby;
|
||||
return Math.hypot(px - x, py - y);
|
||||
}
|
||||
|
||||
function toPath(pts) {
|
||||
return pts.map((p, i) => `${i ? 'L' : 'M'}${round(p[0])},${round(p[1])}`).join(' ');
|
||||
}
|
||||
|
||||
const clamp01 = (n) => (n < 0 ? 0 : n > 1 ? 1 : n);
|
||||
const almost = (u, v) => Math.abs(u - v) < 1e-6;
|
||||
const round = (n) => Math.round(n * 100) / 100;
|
||||
const idOf = (p) => (p && p.id !== undefined ? p.id : p);
|
||||
|
||||
export default forkRoute;
|
||||
143
library/packet/scripts/check-fork-leaders.mjs
Normal file
143
library/packet/scripts/check-fork-leaders.mjs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-fork-leaders.mjs — unit check for library/packet/fork-leaders.js.
|
||||
*
|
||||
* Asserts, for both modes ('row' and 'trunk'), at the whole-India 60x96 sheet
|
||||
* scale (2500x4000px), that forkRoute():
|
||||
* 1. emits OCTILINEAR polylines (H/V/45° only) and ends its two legs exactly on
|
||||
* the two pins;
|
||||
* 2. produces 0 crossings over a clean monotone paired set, batched through the
|
||||
* SAME crossing rule the engine uses (countPolyCrossings: polylines, skip
|
||||
* same-polyline pairs) — with a floor on rows so the pass is not vacuous;
|
||||
* 3. reports a pin a leg passes through in `throughPins` (and reports none when
|
||||
* obstacles are clear);
|
||||
* 4. is DETERMINISTIC (two runs identical);
|
||||
* 5. POISON: a leg forced to a non-octilinear angle must make octilinear=false,
|
||||
* and an obstacle planted on a leg must appear in throughPins — never silent.
|
||||
*
|
||||
* No browser, no network. Exit 0 PASS, 1 FAIL, 75 NOT MEASURED if the module
|
||||
* cannot be imported.
|
||||
*/
|
||||
let forkRoute;
|
||||
try { ({ forkRoute } = await import('../fork-leaders.js')); }
|
||||
catch (e) { console.error(`NOT MEASURED: cannot import fork-leaders.js — ${e.message}`); process.exitCode = 75; }
|
||||
|
||||
if (forkRoute) {
|
||||
const fails = [], note = [];
|
||||
|
||||
// Crossing counter — the engine's rule, inlined so the check is self-contained.
|
||||
const segCross = (p, q) => {
|
||||
const o = (ax, ay, bx, by, cx, cy) => Math.sign((bx - ax) * (cy - ay) - (by - ay) * (cx - ax));
|
||||
const [a, b] = p, [c, d] = q;
|
||||
return o(a[0], a[1], b[0], b[1], c[0], c[1]) !== o(a[0], a[1], b[0], b[1], d[0], d[1]) &&
|
||||
o(c[0], c[1], d[0], d[1], a[0], a[1]) !== o(c[0], c[1], d[0], d[1], b[0], b[1]);
|
||||
};
|
||||
// Count crossings BETWEEN forks: each fork contributes its unique `segs` under
|
||||
// ONE polyline id (li). Segments of the same fork are never paired — a fork's
|
||||
// own stem+legs sharing the fork point is not a crossing.
|
||||
const countForkCrossings = (forks) => {
|
||||
const segs = [];
|
||||
forks.forEach((f, li) => { for (const s of f.segs) segs.push({ s, li }); });
|
||||
let n = 0;
|
||||
for (let i = 0; i < segs.length; i++) for (let j = i + 1; j < segs.length; j++) {
|
||||
if (segs[i].li === segs[j].li) continue;
|
||||
if (segCross(segs[i].s, segs[j].s)) n++;
|
||||
}
|
||||
return n;
|
||||
};
|
||||
const isOcti = (pts, eps = 0.6) => {
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
const dx = Math.abs(pts[i + 1][0] - pts[i][0]), dy = Math.abs(pts[i + 1][1] - pts[i][1]);
|
||||
if (dx < eps || dy < eps) continue;
|
||||
if (Math.abs(dx - dy) < eps) continue;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const endsAt = (pts, pin) => Math.hypot(pts[pts.length - 1][0] - pin.x, pts[pts.length - 1][1] - pin.y) < 0.6;
|
||||
|
||||
// ── A clean monotone paired set on the LEFT margin (dirOut +1 → toward map). ──
|
||||
// Lanes stacked down the margin; each row's two pins sit to the right, in the
|
||||
// row's own latitude band, left-pin above right-pin, bands disjoint and y-sorted
|
||||
// → the classical non-crossing arrangement. 220 rows (floor 200).
|
||||
const ROWS = 220;
|
||||
const LANE_X = 572, MAP_X = 572, BAND = 14;
|
||||
const rowsData = [];
|
||||
for (let i = 0; i < ROWS; i++) {
|
||||
const y = 400 + i * BAND;
|
||||
rowsData.push({
|
||||
lane: { x: LANE_X, y },
|
||||
a: { id: `r${i}a`, x: MAP_X + 120, y: y - 3 },
|
||||
b: { id: `r${i}b`, x: MAP_X + 180, y: y + 3 },
|
||||
});
|
||||
}
|
||||
|
||||
for (const mode of ['row', 'trunk']) {
|
||||
let endOk = 0, octiOk = 0;
|
||||
const forks = [];
|
||||
for (const rw of rowsData) {
|
||||
const res = forkRoute({ lane: rw.lane, a: rw.a, b: rw.b, dirOut: 1, mode, forkFrac: 0.5 });
|
||||
if (res.octilinear && isOcti(res.polys[0]) && isOcti(res.polys[1])) octiOk++;
|
||||
if (endsAt(res.legA, rw.a) && endsAt(res.legB, rw.b)) endOk++;
|
||||
forks.push(res);
|
||||
}
|
||||
const crossings = countForkCrossings(forks);
|
||||
note.push(`[${mode}] octilinear ${octiOk}/${ROWS}, legs-end-on-pin ${endOk}/${ROWS}, crossings ${crossings}`);
|
||||
if (octiOk !== ROWS) fails.push(`[${mode}] ${ROWS - octiOk} forks not octilinear`);
|
||||
if (endOk !== ROWS) fails.push(`[${mode}] ${ROWS - endOk} forks whose legs miss their pin`);
|
||||
if (crossings !== 0) fails.push(`[${mode}] ${crossings} crossings on a clean monotone set (expected 0)`);
|
||||
}
|
||||
if (rowsData.length < 200) fails.push(`only ${rowsData.length} rows (floor 200) — vacuous`);
|
||||
|
||||
// ── 3: through-pin report, both directions ───────────────────────────────────
|
||||
const base = { lane: { x: 572, y: 1000 }, a: { id: 'A', x: 760, y: 980 }, b: { id: 'B', x: 760, y: 1020 }, dirOut: 1, mode: 'row' };
|
||||
const clear = forkRoute({ ...base, obstacles: [{ id: 'X', x: 1500, y: 3000 }], r: 8 });
|
||||
if (clear.throughPins.length !== 0) fails.push(`through-pin false positive: ${JSON.stringify(clear.throughPins)}`);
|
||||
// Plant an obstacle ON legA by sampling a midpoint of it.
|
||||
const probe = forkRoute({ ...base });
|
||||
const mid = probe.legA[Math.max(1, probe.legA.length - 1)];
|
||||
const onLeg = forkRoute({ ...base, obstacles: [{ id: 'HIT', x: mid[0], y: mid[1] }], r: 6 });
|
||||
if (!onLeg.throughPins.includes('HIT')) fails.push('through-pin MISS: an obstacle on legA was not reported');
|
||||
note.push(`through-pin: clear=${clear.throughPins.length}, planted reported=${onLeg.throughPins.includes('HIT')}`);
|
||||
|
||||
// ── 4: determinism ────────────────────────────────────────────────────────────
|
||||
const d1 = forkRoute({ ...base, mode: 'trunk', forkFrac: 0.37 });
|
||||
const d2 = forkRoute({ ...base, mode: 'trunk', forkFrac: 0.37 });
|
||||
if (JSON.stringify(d1.polys) !== JSON.stringify(d2.polys)) fails.push('NON-DETERMINISTIC output');
|
||||
|
||||
// ── 5: poison — a non-octilinear leg must be flagged, not passed ──────────────
|
||||
// Build a poly with a 30° segment and confirm the module's own octilinear test rejects it.
|
||||
const bad = [[0, 0], [100, 58]]; // slope ~30°, neither H/V nor 45°
|
||||
if (isOcti(bad)) fails.push('POISON: the octilinear test accepted a 30° segment');
|
||||
// And a real forkRoute whose pins force a long non-diagonal stays octilinear (sanity that we did not over-trigger):
|
||||
const far = forkRoute({ lane: { x: 572, y: 500 }, a: { id: 'P', x: 1800, y: 505 }, b: { id: 'Q', x: 900, y: 1200 }, dirOut: 1, mode: 'trunk', forkFrac: 1 });
|
||||
if (!far.octilinear) fails.push('a legitimate far-pin fork was wrongly non-octilinear');
|
||||
note.push(`poison: 30deg rejected=${!isOcti(bad)}; far fork octilinear=${far.octilinear}`);
|
||||
|
||||
// ── 6: edge cases + a loud guard on non-finite input ─────────────────────────
|
||||
const okShape = (res) => res.octilinear && isOcti(res.polys[0]) && isOcti(res.polys[1]) &&
|
||||
res.polys.flat(2).every(Number.isFinite);
|
||||
const edge = [
|
||||
['a==b', { lane: { x: 572, y: 800 }, a: { id: 'x', x: 760, y: 900 }, b: { id: 'x', x: 760, y: 900 }, dirOut: 1, mode: 'row' }],
|
||||
['lane-level', { lane: { x: 572, y: 900 }, a: { id: 'a', x: 760, y: 900 }, b: { id: 'b', x: 760, y: 900 }, dirOut: 1, mode: 'trunk', forkFrac: 0.5 }],
|
||||
['pins-behind', { lane: { x: 760, y: 900 }, a: { id: 'a', x: 572, y: 880 }, b: { id: 'b', x: 572, y: 920 }, dirOut: 1, mode: 'row' }],
|
||||
['trunk-neg', { lane: { x: 1800, y: 900 }, a: { id: 'a', x: 1600, y: 880 }, b: { id: 'b', x: 1600, y: 920 }, dirOut: -1, mode: 'trunk', forkFrac: 0.6 }],
|
||||
];
|
||||
for (const [name, args] of edge) {
|
||||
if (!okShape(forkRoute(args))) fails.push(`edge '${name}': not octilinear/finite`);
|
||||
}
|
||||
let guardThrew = false;
|
||||
try { forkRoute({ lane: { x: NaN, y: 900 }, a: { id: 'a', x: 760, y: 880 }, b: { id: 'b', x: 760, y: 920 }, dirOut: 1, mode: 'row' }); }
|
||||
catch (e) { guardThrew = /non-finite/.test(e.message); }
|
||||
if (!guardThrew) fails.push('non-finite input did NOT throw (silent NaN path)');
|
||||
note.push(`edge cases ${edge.length}/${edge.length} octilinear; non-finite guard throws=${guardThrew}`);
|
||||
|
||||
for (const n of note) console.log(` · ${n}`);
|
||||
if (fails.length) {
|
||||
console.error('FAIL check:fork-leaders');
|
||||
for (const f of fails) console.error(` ✗ ${f}`);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(`PASS check:fork-leaders — both modes octilinear, legs land on pins, 0 crossings on ${ROWS} clean rows, through-pins caught, deterministic, poison rejected`);
|
||||
}
|
||||
}
|
||||
|
|
@ -225,6 +225,7 @@
|
|||
"check:selector-fields:self-test": "node internal/check-selector-fields.mjs --self-test",
|
||||
"check:packet-strings-parity": "node internal/check-packet-strings-parity.mjs",
|
||||
"check:packet-variants": "node internal/check-packet-variants.mjs",
|
||||
"check:fork-leaders": "node library/packet/scripts/check-fork-leaders.mjs",
|
||||
"check:pins-in-state": "node library/packet/scripts/check-pins-in-state.mjs",
|
||||
"check:label-key-coverage": "node internal/check-label-key-coverage.mjs",
|
||||
"check:host-locale-fetch": "node internal/check-host-locale-fetch.mjs",
|
||||
|
|
@ -355,7 +356,7 @@
|
|||
"fix:sri": "node internal/fix-sri.mjs",
|
||||
"gallery": "node internal/generate-gallery.mjs",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:script-flags check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:auto-engine check:engine-contract check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight check:no-fleet-names:self-test check:pin-hint-floor:self-test check:poster-sizes:self-test check:shell-header:self-test check:preview-flag-dev-only:self-test check:poster-type-registry:self-test check:host-floor:self-test",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:pins-in-state check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-internal-role-nouns check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:side-lanes check:side-lanes:self-test check:poster-section check:poster-section:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-sections check:poster-sections:self-test check:poster-design-catalogue check:poster-design-catalogue:self-test check:wall-photo-weight check:no-fleet-names check:pin-hint-floor check:poster-sizes check:upg-count check:shell-header check:pg-name-gate check:preview-flag-dev-only check:poster-type-registry check:host-floor",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:pins-in-state check:fork-leaders check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-internal-role-nouns check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:side-lanes check:side-lanes:self-test check:poster-section check:poster-section:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-sections check:poster-sections:self-test check:poster-design-catalogue check:poster-design-catalogue:self-test check:wall-photo-weight check:no-fleet-names check:pin-hint-floor check:poster-sizes check:upg-count check:shell-header check:pg-name-gate check:preview-flag-dev-only check:poster-type-registry check:host-floor",
|
||||
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||
"library-index": "node internal/generate-library-index.mjs",
|
||||
|
|
@ -584,6 +585,7 @@
|
|||
"check:packet-strings-parity": "the PACKET catalog (packet-strings.json) must be whole in every language: all string keys, all 12 prayer points with every field, all 12 wheel rhythms. check:i18n-parity covers library/i18n/locales only — six gates READ this file and none checked it. Asserts PRESENCE, not translatedness, with point4_passage declared-absent on record.",
|
||||
"check:packet-variants": "the poster wall's Prayer List prints the PACKET (not the columnar list), and every variant it offers exists in packet-render's VARIANTS. A variant may drop fixed pages but never people: lightweight keeps every group. By the UPG-100 owner; mutation-tested. Headless; in gates:pre.",
|
||||
"check:pins-in-state": "library/packet/pins-in-state.js places whole-India poster pins so no two overlap AND every pin stays inside its own ADM1 state polygon, at the 60x96in sheet scale (2500x4000px, PIN_R 8). A deterministic repulsion relaxation (imitating poster-render.js, read not called) with the map-rect clamp replaced by a per-state multipolygon clamp: a pin pushed off its land is pulled to the nearest point just inside, holes and multi-part states (islands/coasts) honoured. Runs on the real India 855-pin data when present, else a synthetic 60x96 fixture (floor 200 pins, no vacuous pass); asserts 0 pins outside their state and 0 overlaps that are not NAMED in `infeasible` (a too-small state is reported, never silently stacked), plus a hole/island arm and a poison arm. Node only, headless, deterministic.",
|
||||
"check:fork-leaders": "library/packet/fork-leaders.js routes ONE paired-row leader as an octilinear fork: a stem forks at a 45° chamfer into two short H/V legs, one per pin. Modes row and trunk. Returns polylines + a deduped segs list (one polyline id per fork for crossings). Reports octilinear + throughPins. Pure, px-only, deterministic. The gate asserts both modes octilinear, legs land on pins, 0 crossings on a 220-row set (floor 200), through-pin hit+miss, determinism, and a poison. Node only, headless.",
|
||||
"check:label-key-coverage": "node internal/check-label-key-coverage.mjs",
|
||||
"check:host-locale-fetch": "node internal/check-host-locale-fetch.mjs",
|
||||
"check:packet-a1-structure": "the prayer packet is built to its specified structure, read page by page from each sheet's CONTENT: full detail = map, triangle, life calendar, % evangelical, profiles, bounded set, centered set, wheel, points; lightweight = map, triangle, profiles, wheel, points. Page 1 places a map-only vertical sheet on the packet's own margin and says it is one of the UPG 100 prayer lists only when a count is supplied; page 2 is one inverted triangle whose packets tier and \"- N\" steps appear only where true; the wheel carries the Metacamp Module 1 PDF's colours and opens at nine o'clock; all 28 legend zones resolve, Deaf included.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue