qa.sh --strict: 18 passed · 0 failed · 0 warn · 0 skipped. selfcheck 157,647/157,647, fingerprint 0x5f76e76 unmoved. No tag — John's playtest session rules the epoch. THE FIVE ASKS. [C] wardrobe wired via exported WARDROBE_BASE + stockBaseFor(shop), preloaded inside the existing STOCK_REAL gate: ON under ?stock=real (52 pack items, 69 garment ids on rendered meshes, 1 atlas, exactly 2 requests) and CHEAPER (59->55 draws, 111->100 textures); OFF is what ships by default, A/B'd against a reverted control tree — default arm identical on every counter (160 URLs same hash, 131/18,018 street, 59/28,505 interior). [D] three gates wired (r41_shots.py writes to TMPDIR so gate runs never rewrite committed shots). [E] clips_verify wired — 46 clips / 6 groups / 3,498,124 B. [B's bookmark finding] CONFIRMED AND WIDER: street_noon, shopfront_detail, crossroads_busy AND market_square all give 3 distinct hashes across 3 boots; only night_neon is stable. RULING: no gate pins a screenshot byte-hash on any bookmark — everything R41 asserts is a counter. Filed to B: one await document.fonts.ready in buildings.js. [C's drawSweep] CONFIRMED INDEPENDENTLY with F's own sweep (never calls C's): GLB-on worst 123 @ dept/auto. The phantom control (one stale room left in scene) reads 197 — +74 on every reading — decomposing R39/R40's '188' as 116 real + ~72 phantom. NEW GATES, controls demonstrated. r41_denylist.mjs (ruling 3): 738 files / 197.3 MiB scanned by path AND bytes, 7 banned names, 0 hits; the control plants a banned manifest row every run -> RED on 2 names -> removed -> GREEN. r41_integration.py: wardrobe both arms · interior <=350 on both instruments · ?noassets=1 zero across six fetch classes over a 3-shop walk (control fetches 5/6) · ?clips=0 142 URLs and ?classic=1 125 URLs with zero R41 cargo. BUDGETS. Street: noon 282 · NIGHT 291/120,093 — margin 9, INTACT · classic 269 byte-exact · ?clips=0 NIGHT 291, delta 0. No lane spent a street draw. Interior: true pre-R41 116, R41 123 @ dept/auto, margin 227; over 33 real shops worst 110; pub 49 quiet / 77 gig night. FILED BACK: flags_check's _enter_record_shop went RED on 8 smokes because §41.4's bins put every one of the first six record-shop counters inside 2.6 m — widened to the whole open set, verified not a game defect. smoke_djdance read _actions[0] and D's per-instance clip swapping made the mixer multi-action, so a healthy dancer was called a stall — now follows the clip by name. D's r41_shots.py browse arm is deterministically red here (re-enters the winner after a ~3 s/candidate scan, by which time the occupant has left) — wired WARN-level with the reason at the call site, fix handed to D. F's own r39_transmission.py had an unmeasured 30 s goto default that flaked once under a full strict run; pinned to 90 s. 11 proof frames in docs/shots/laneF_r41/ with sidecars: street_postures (5 rigs, 3 clips playing) against its ?clips=0 control (1 clip) · street_lean · browse_interior · pub_furnished + the honest kit before/after pair · record_dj_booth · opshop_wardrobe on/off · credits_panel with ODbL on screen. Every humanoid frame carries its R10 human-sized line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
147 lines
8.8 KiB
JavaScript
147 lines
8.8 KiB
JavaScript
#!/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);
|