#!/usr/bin/env node // PROCITY Lane F — R41 §41.6 THE DENY-LIST GATE (ruling 3 becomes repo law, enforced). // // node tools/qa/r41_denylist.mjs [--verbose] // // ROUND41 ruling 3, verbatim: these never enter `web/` under any circumstance — // • character_kit_modular/exports/bodies/** (44 bodies, 30 named -nsfw, 465k tris, broken bboxes) // • any anatomy/ dir // • elsa_coronation_hair_wig_kh3.glb (Disney/Square Enix IP rip) // • daphne_sexy.glb (unverified rip) // • ~/Documents/mocaponline/** (vendor demo packs, licence unread — includes Epic's // EULA-bound SK_Mannequin.fbx, named in the ruling) // // This is a LICENCE/IP gate, so it is deliberately blunt in two directions at once: // // 1. PATHS — no file under web/ may sit at, or under, a banned path component. // 2. BYTES — no file under web/ may CONTAIN a banned name, in any encoding this repo uses. The scan // reads every byte of every file, binaries included, and that is the point: a GLB carries its // source mesh/node names inside the JSON chunk, and a manifest carries the path it was published // from. "Nothing references it" is a claim about text; "nothing contains it" is a claim about the // bytes that ship, and the second is the one a licence audit asks. // // THE CONTROL RUNS EVERY TIME. A deny-list that has never fired is indistinguishable from a deny-list // with a typo in its regex — the R25 vacuous-gate law. So the gate plants a real banned reference in a // real file under web/, asserts the scan goes RED on it, removes it, and asserts the scan goes GREEN // again. Both arms print. The plant is removed in a `finally`, and its name is asserted absent before // the run starts (so a crashed earlier run cannot poison the tree silently). // // Exit 0 = clean · 1 = a banned reference is in web/, or the control failed to fire. import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); const WEB = path.join(ROOT, 'web'); const VERBOSE = process.argv.includes('--verbose'); const C = { red: '\x1b[31m', grn: '\x1b[32m', yel: '\x1b[33m', dim: '\x1b[2m', bold: '\x1b[1m', off: '\x1b[0m' }; let fails = 0; const OK = (m) => console.log(` ${C.grn}✓${C.off} ${m}`); const FAIL = (m) => { console.log(` ${C.red}✗${C.off} ${m}`); fails++; }; const NOTE = (m) => console.log(` ${C.yel}·${C.off} ${m}`); const head = (m) => console.log(`\n${C.bold}${m}${C.off}`); // ── the list ───────────────────────────────────────────────────────────────────────────────────── // `token`: a byte sequence that must not appear in any file under web/ (matched case-insensitively). // `component`: a path component that must not appear in any path under web/. const BANNED = [ { token: 'character_kit_modular', why: 'ruling 3 — the 44 bodies (30 -nsfw, 465k tris, 0x0x0 bboxes)' }, { token: 'exports/bodies/', why: 'ruling 3 — the bodies export dir, by its own path' }, { token: 'elsa_coronation_hair_wig_kh3', why: 'ruling 3 — Disney/Square Enix IP rip' }, { token: 'daphne_sexy', why: 'ruling 3 — unverified rip' }, { token: 'mocaponline', why: 'ruling 3 — vendor demo packs, licence unread' }, { token: 'SK_Mannequin', why: "ruling 3 — Epic's EULA-bound mannequin, named in the mocaponline clause" }, { token: 'anatomy/', why: 'ruling 3 — any anatomy/ dir' }, ]; const BANNED_COMPONENTS = ['anatomy', 'character_kit_modular', 'mocaponline']; // ── the scan ───────────────────────────────────────────────────────────────────────────────────── function walk(dir, out = []) { for (const e of fs.readdirSync(dir, { withFileTypes: true })) { const p = path.join(dir, e.name); if (e.isSymbolicLink()) { out.push({ p, link: true }); continue; } if (e.isDirectory()) walk(p, out); else if (e.isFile()) out.push({ p, link: false }); } return out; } // Case-insensitive byte search. Tokens are pure ASCII, so lower-casing the haystack bytes in place is // exact — no encoding assumption, works on UTF-8 text and on GLB/webp/jpg binaries alike. const needles = BANNED.map((b) => ({ ...b, bytes: Buffer.from(b.token.toLowerCase(), 'latin1') })); function scanBytes(buf) { const lower = Buffer.from(buf); // copy, then fold A-Z → a-z in place for (let i = 0; i < lower.length; i++) { const c = lower[i]; if (c >= 65 && c <= 90) lower[i] = c + 32; } const hits = []; for (const n of needles) if (lower.includes(n.bytes)) hits.push(n); return hits; } function scan() { const files = walk(WEB); const hits = []; let bytes = 0, links = 0; for (const f of files) { const rel = path.relative(ROOT, f.p); const comps = rel.split(path.sep); for (const c of BANNED_COMPONENTS) { if (comps.some((x) => x.toLowerCase() === c)) hits.push({ rel, kind: 'path', token: c, why: `banned path component "${c}"` }); } if (f.link) { // a symlink is a path, not bytes — its TARGET is the risk links++; let t = ''; try { t = fs.readlinkSync(f.p); } catch { /* dangling */ } for (const h of scanBytes(Buffer.from(t, 'utf8'))) hits.push({ rel, kind: 'symlink', token: h.token, why: `${h.why} (symlink → ${t})` }); continue; } let buf; try { buf = fs.readFileSync(f.p); } catch { continue; } bytes += buf.length; for (const h of scanBytes(buf)) hits.push({ rel, kind: 'bytes', token: h.token, why: h.why }); } return { files: files.length, bytes, links, hits }; } // ── run ────────────────────────────────────────────────────────────────────────────────────────── console.log(`${C.bold}PROCITY R41 §41.6 — DENY-LIST COMPLIANCE (ruling 3)${C.off} ${C.dim}web/ · paths + bytes${C.off}`); head('1. THE TREE — every file under web/, every byte, against the seven banned names'); const CONTROL_FILE = path.join(WEB, 'assets', '.r41_denylist_control.tmp.json'); if (fs.existsSync(CONTROL_FILE)) { fs.rmSync(CONTROL_FILE); NOTE(`removed a stale control plant from an earlier crashed run: ${path.relative(ROOT, CONTROL_FILE)}`); } const t0 = Date.now(); const clean = scan(); const ms = Date.now() - t0; NOTE(`${clean.files} files · ${(clean.bytes / 1048576).toFixed(1)} MiB · ${clean.links} symlink(s) · ${ms} ms`); if (clean.hits.length === 0) { OK(`0 banned references under web/ — ${BANNED.length} names × ${clean.files} files, paths AND bytes`); } else { for (const h of clean.hits) FAIL(`${h.rel} — ${h.kind}: "${h.token}" (${h.why})`); } if (VERBOSE) for (const b of BANNED) console.log(` ${C.dim}· ${b.token} — ${b.why}${C.off}`); head('2. THE CONTROL — plant a real banned reference, the gate must go RED, then GREEN again'); let planted = false; try { fs.mkdirSync(path.dirname(CONTROL_FILE), { recursive: true }); // A plausible plant, not a strawman: this is exactly the shape a wildcard copy would leave behind — // a manifest row naming the source it was published from. fs.writeFileSync(CONTROL_FILE, JSON.stringify({ _control: 'R41 deny-list gate control plant — deleted by the gate that wrote it', assets: [{ file: 'body_07.glb', source: '~/Documents/character_kit_modular/exports/bodies/body_07.glb' }], }, null, 1)); planted = true; const red = scan(); const caught = red.hits.filter((h) => h.rel.includes('.r41_denylist_control.tmp')); if (caught.length >= 2) OK(`RED arm fired: the plant is caught on ${caught.length} names (${caught.map((h) => `"${h.token}"`).join(', ')})`); else FAIL(`RED arm did NOT fire as expected — ${caught.length} hit(s) on the plant; the scan is vacuous`); } finally { if (planted) fs.rmSync(CONTROL_FILE, { force: true }); } const after = scan(); if (after.hits.length === clean.hits.length) OK(`GREEN arm restored: ${after.hits.length} hit(s), identical to the pre-control scan — the plant left nothing behind`); else FAIL(`the tree did not return to its pre-control state (${clean.hits.length} → ${after.hits.length} hits)`); console.log(''); if (fails === 0) { console.log(`${C.grn}● PASS${C.off} — web/ is deny-list clean, and the deny-list is not vacuous`); process.exit(0); } console.log(`${C.red}● FAIL${C.off} — ${fails} problem(s) above`); process.exit(1);