44c5a79424
Six agents in parallel, and the two city packs independently reported the same blocker: `focusRegions` and `coarseFactor` existed on the `City` type and nothing implemented them. Uniform lattices would have been 2.9M points for Southern California and 3.7M for the expanded bay. Both packs were unloadable as written. `buildAxis` is the answer, and it is honest about its limits: refinement is per axis, not per rectangle, so a focus region sharpens its whole row *and* its whole column. Two regions at opposite corners refine nearly everything between them. Measured, not guessed — the bay went 0.53M points with one region and 1.64M with three, for detail nobody is looking at from a board this wide. One region each, coarse factor ten, and the builds land at 3.8 s and 2.3 s. Then three things that were only ever right because San Francisco was the only city. `maxDistance: 340` and a 170-unit shadow box were constants tuned for a 230-unit board; the bay is 1003 units across and the camera physically could not retreat far enough to frame it. Fog distances were scene units pinned to the same assumption. And `minVisibilityM` defaulted to 4.5 km of honest weather, which over ninety-four kilometres of bay correctly hides three quarters of it — the night view was a black rectangle for a completely reasonable reason. All three now derive from the board. The moon is a real ephemeris and its light is a deliberate lie: 1.15, against a physical ratio of one to four hundred thousand. What is being reproduced is what a moonlit night looks like on a screen in a lit room. The CI gate caught itself, which is the part worth keeping. Port 8431 was already held by a server from an earlier session, so the boot check polled a healthy stranger while the process it started died on EADDRINUSE. It now refuses to run rather than pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
161 lines
5.4 KiB
JavaScript
161 lines
5.4 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* The no-binary-art gate: `src/**` holds no committed binary art, ever.
|
|
*
|
|
* This is the check that backs the licensing argument in ARCHITECTURE.md §3 —
|
|
* every mesh is a function composing cached unit primitives and every texture is
|
|
* drawn on a 2D canvas from seeded noise, which is what gives the repo zero
|
|
* asset-licensing exposure. A committed `.glb` or `.png` is a claim about
|
|
* provenance that nobody in CI can verify, so the answer is that they do not
|
|
* arrive at all.
|
|
*
|
|
* Two things about the scope, both of them corrections of an earlier design that
|
|
* CONTRACT.md §7 calls out by name.
|
|
*
|
|
* The enumeration is `git ls-files` and never a filesystem walk. A self-hoster
|
|
* is told, by CONTRIBUTING.md and by the office packs, to drop their own
|
|
* legally-clean art into `public/props/` and friends; a walk would find it and
|
|
* fail their build over files this repo does not distribute and has no opinion
|
|
* about. Tracked files are the only files a licence claim can be made about, so
|
|
* tracked files are the only files enumerated. If git is missing, this exits
|
|
* non-zero rather than falling back to a walk — a gate that quietly changes what
|
|
* it measures is worse than one that stops.
|
|
*
|
|
* And it is strict over `src/**` only. That is where the procedural-assets
|
|
* promise lives. `public/props/`, `public/kits/`, `public/offices/` and `docs/`
|
|
* are hard-exempt below and git-ignored besides: they are self-hoster space.
|
|
*
|
|
* Runs by hand as `node scripts/check-no-binaries.mjs` from anywhere in the
|
|
* working tree.
|
|
*/
|
|
|
|
import { execFileSync } from "node:child_process";
|
|
|
|
// ---- The rule ----
|
|
|
|
/**
|
|
* Art and binary-asset extensions. Lowercased at the comparison, so a `.PNG`
|
|
* off a Windows checkout is caught too.
|
|
*/
|
|
const DENIED_EXTENSIONS = new Set([
|
|
".glb",
|
|
".gltf",
|
|
".png",
|
|
".jpg",
|
|
".jpeg",
|
|
".webp",
|
|
".hdr",
|
|
".exr",
|
|
".ttf",
|
|
".otf",
|
|
".woff",
|
|
".woff2",
|
|
".mp3",
|
|
".wav",
|
|
".fbx",
|
|
".obj",
|
|
]);
|
|
|
|
/** The pathspec handed to `git ls-files`. Everything outside it is unexamined. */
|
|
const SCOPE = "src";
|
|
|
|
/**
|
|
* Self-hoster space, exempt even if something puts it inside the scope later.
|
|
* These are repo-relative prefixes, matched against the paths git reports.
|
|
*/
|
|
const EXEMPT_PREFIXES = [
|
|
"public/props/",
|
|
"public/kits/",
|
|
"public/offices/",
|
|
"docs/",
|
|
];
|
|
|
|
// ---- Enumeration ----
|
|
|
|
function repoRoot() {
|
|
try {
|
|
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
}).trim();
|
|
} catch {
|
|
fail(
|
|
"could not ask git for the repo root.",
|
|
"This check enumerates tracked files with `git ls-files` and deliberately has no",
|
|
"filesystem-walk fallback, because a walk would fail a self-hoster's build over",
|
|
"their own untracked art. Run it inside a git checkout.",
|
|
);
|
|
}
|
|
}
|
|
|
|
function trackedFiles(root, pathspec) {
|
|
const out = execFileSync("git", ["ls-files", "-z", "--", pathspec], {
|
|
cwd: root,
|
|
encoding: "utf8",
|
|
// A NUL-separated listing of a repo this size is tiny, but the default 1 MB
|
|
// ceiling is close enough to be worth not thinking about again.
|
|
maxBuffer: 64 * 1024 * 1024,
|
|
});
|
|
return out.split("\0").filter((path) => path !== "");
|
|
}
|
|
|
|
/**
|
|
* The extension, lowercased, or the empty string. A leading dot with no stem —
|
|
* `src/.keep` — is a dotfile and has no extension, which is why this compares
|
|
* against the last slash rather than reaching for `path.extname`.
|
|
*/
|
|
function extensionOf(path) {
|
|
const dot = path.lastIndexOf(".");
|
|
const slash = path.lastIndexOf("/");
|
|
if (dot <= slash + 1) return "";
|
|
return path.slice(dot).toLowerCase();
|
|
}
|
|
|
|
function isExempt(path) {
|
|
return EXEMPT_PREFIXES.some((prefix) => path.startsWith(prefix));
|
|
}
|
|
|
|
// ---- Reporting ----
|
|
|
|
function fail(...lines) {
|
|
console.error(`\ncheck-no-binaries: FAIL — ${lines[0]}\n`);
|
|
for (const line of lines.slice(1)) console.error(line);
|
|
console.error("");
|
|
process.exit(1);
|
|
}
|
|
|
|
// ---- Run ----
|
|
|
|
const root = repoRoot();
|
|
const tracked = trackedFiles(root, SCOPE);
|
|
const offenders = tracked
|
|
.filter((path) => !isExempt(path))
|
|
.filter((path) => DENIED_EXTENSIONS.has(extensionOf(path)));
|
|
|
|
if (offenders.length > 0) {
|
|
const listed = offenders.map((path) => ` ${path} (${extensionOf(path)})`);
|
|
fail(
|
|
`${offenders.length} binary asset${offenders.length === 1 ? "" : "s"} tracked under ${SCOPE}/.`,
|
|
...listed,
|
|
"",
|
|
"Apache 2.0 is a promise that everything in this repo is ours to give away, and a",
|
|
"committed binary asset is a provenance claim no reviewer can check. Assets here are",
|
|
"procedural TypeScript: meshes compose cached unit primitives, textures are drawn on a",
|
|
"2D canvas from seeded noise. See ARCHITECTURE.md §3 and CONTRACT.md §3.",
|
|
"",
|
|
"If this is your own art for your own deployment, it does not belong in src/ at all —",
|
|
`put it under ${EXEMPT_PREFIXES.join(", ")}, which are git-ignored self-hoster space, and`,
|
|
"this check will never see it.",
|
|
);
|
|
}
|
|
|
|
console.log(
|
|
`check-no-binaries: ok — ${tracked.length} tracked file${tracked.length === 1 ? "" : "s"} under ` +
|
|
`${SCOPE}/, none matching ${DENIED_EXTENSIONS.size} denied extensions.`,
|
|
);
|
|
console.log(
|
|
` enumerated with \`git ls-files\`, so untracked local art is invisible to this check.`,
|
|
);
|
|
console.log(` denied: ${[...DENIED_EXTENSIONS].join(" ")}`);
|
|
console.log(` exempt: ${EXEMPT_PREFIXES.join(" ")}`);
|