4bd8481be1
The owner asked twice why there are still three separate boards. The honest answer, and what this round executes: **it feels like three boards, but not because the scale jumps 20x — because the three packs draw three different Californias, and the loudest difference is that the mountains are four times taller on one of them.** **THE 20x HORIZONTAL SCALE JUMP IS INVISIBLE**, and measuring that collapsed the cost of this whole round. `World.project` is a uniform scale in x/z with no vertical term, and a uniform scale leaves a perspective image identical — so a camera carried across the seam on matched true-metre offsets draws a pixel-identical horizontal frame. 1,919 -> 94 m/unit costs nothing to look at. Rescaling was never the problem. A boot card, a tab strip and a 4.17x vertical deflation were. **THE PAUSE WAS MOSTLY FAKE.** A switch covered the screen for 1,715 ms but only 608 ms blocked the main thread; the page drew 46 of 69 frames with nothing to show, because the outgoing board had already been disposed. `mountCity` now retains it: the incoming board builds BEHIND a live, interactive picture, and `stage.setScene` fires only on completion. Measured across all six directions, three runs each — boot card yes -> **no**, opaque cover 726-1,415 ms -> **0**, blank frames 21-46 -> **exactly 1**, wall clock down 12-29%, blocked main thread down 15-47%. A return to a board already seen links **zero** shader programs and blocks **zero** milliseconds: 298-312 ms of camera flight where it was ~1,600 ms behind a card. Disposal had been throwing away the shader cache too — linkProgram ran 38, 59, 78, 109, 127 across five mounts and never reused one. **The transition is a fog dip, not a crossfade**, through the `setAerialFog` seam built last round. Every both-boards-live crossfade breaks a budget — ca+sf is 2,640,307 triangles against bay-area's 2,600,000 cap — and a fade never lands inside the harness's sample window, which is the "a cap you do not measure is a cap you do not have" failure this repo already argues against. The dip costs zero triangles and zero draw calls, and it hides the 4.17x deflation, the 4,025 m projection disagreement and the vanishing 2 km freeway symbols at once, because all three happen at maximum obscuration. It is also diegetic: a descent through haze. The first dip was wrong and the photograph caught it: collapsing to 6% of board SPAN turned the whole night frame into one flat field — the exact "turns the map off" failure the risk list named. Re-anchored to 70% of camera STAND-OFF, so the coastline survives and only the relief melts. **One ladder, one places list.** 26 authored chapters become 24 rungs sorted descending by STAND-OFF, not altitude — by altitude they interleave badly and altitude cannot tell a low oblique from a high plan. The three-board tab strip is off by default; the left column is now one scrolling list of all 24 rungs under three region headings that does not change when the board does. Only which row is lit changes. Label collisions are resolved in the ladder and never in a pack, so the 29 index-aimed capture guards are untouched. The minimap stops turning through 90 degrees between boards: every board is pinned to a rectangle with California's proportions. **SF and SoCal are not regressed**, and that was the acceptance that mattered: 95.9-98.8% of board pixels are delta-0 against a baseline hash-verified identical to what the live site serves, and every one of the 34-70 surviving pixels per frame is an aircraft or a hull. **A real defect found only by photograph:** `minimap.setMarkers()` had zero call sites. Every marker on every board was gone — the LA studio's door dot, the Bay Area's eight company markers — dropped when the minimap went per-board. Typecheck, tests, budgets and the console were all green with that bug in. Also fixed: two capture presets that lied. `look.mjs`'s `glyph-la` and `glyph-sf` claimed California chapter closeups and returned SoCal and Bay Area frames, because they aimed by chapter index and the indices had moved. Aiming is now by identity, with a guard test. NOT SHIPPED, DELIBERATELY: the pack merge. At Bay density it is 34.04M triangles, 13x the highest budget — dead, not a trade. At SoCal density it is 1.99M and fits today, and the price is San Francisco rendering at 164 m lots instead of 40 m, i.e. SF looking the way SoCal looks now. SF and SoCal carry every marketing still on the site. That is the owner's decision and it is worthless as an argument and decisive as a photograph, so it ships as a measurement artifact with a side-by-side still and is wired into nothing. The four data reconciliations that would make one world honest — one exaggeration rule, roads in metres, one projection centre, one coastline convention — are behind TERA_RECONCILE, default OFF. Tests 1,570 -> 1,651, server 295. All ten budget cells pass, no cap raised. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
472 lines
19 KiB
JavaScript
472 lines
19 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* The chapter list is a public interface. This is the check that says so.
|
|
*
|
|
* node scripts/check-chapter-identity.mjs # against dist/
|
|
* node scripts/check-chapter-identity.mjs --dist … # against another build
|
|
* node scripts/check-chapter-identity.mjs --write # re-cut the fixture (read the warning)
|
|
*
|
|
* ## What it protects, and why it had to exist before the ladder work
|
|
*
|
|
* Twenty-six index-aimed guards outside this file point a camera at a chapter by
|
|
* its **position in a list** and then assert its short label. Counted from the
|
|
* source rather than taken from a plan — comment lines that *quote* the pattern
|
|
* are excluded, which is where the "29" in the round's brief came from:
|
|
*
|
|
* - `scripts/brand-assets/shots.mjs` — 21, every one of them a still on
|
|
* lumbridgecorp.com right now.
|
|
* - `scripts/brand-assets/films.mjs` — 4.
|
|
* - `scripts/brand-assets/capture.mjs` — 1: the Open Graph card, which is what
|
|
* every link to the site renders as, and the most-seen frame in the product.
|
|
*
|
|
* Twenty-one of them aim at a board a `?city=` can reach — 12 at the Bay Area,
|
|
* 6 at California, 3 at SoCal — and this check covers those. The other five are
|
|
* office shots aiming at a studio's own view list, which no `?city=` selects.
|
|
*
|
|
* That pattern was chosen for a good reason and it is written up at
|
|
* `capture.mjs:70`: a card once shipped for a fortnight showing a chase camera
|
|
* on a freeway under the headline "Cities from above", because `keyboard.press("2")`
|
|
* is an unguarded index into pack data and the default board had changed under it.
|
|
* The label assertion was the fix. It is a *good* fix and it is not sufficient,
|
|
* for two reasons this repo can now demonstrate rather than argue:
|
|
*
|
|
* 1. **Labels are not unique across boards.** "Whole Board" is chapter 01 of the
|
|
* Bay Area *and* chapter 01 of SoCal; "The Valley" is Silicon Valley on one
|
|
* board and the San Fernando Valley on the other. A reorder that preserves
|
|
* labels passes every one of those twenty-six guards and shoots the wrong place.
|
|
* 2. **A guard on the chapter you clicked says nothing about the one you did
|
|
* not.** Several shots take no chapter at all and inherit whatever board the
|
|
* URL opened on, which is a fallback rather than an assertion.
|
|
*
|
|
* So this file snapshots the *identity* of every chapter on every board — its
|
|
* `data-view`, its position, its printed number and its short label — against a
|
|
* checked-in fixture, and fails on any drift. It is deliberately not a test of
|
|
* whether the chapters are *good*; it is a test of whether they are the same
|
|
* ones the marketing imagery was aimed at.
|
|
*
|
|
* ## It reads the DOM, not the packs
|
|
*
|
|
* A unit test over `src/cities/*.ts` would be cheaper and would miss the thing
|
|
* that actually breaks: the harnesses read `#chapters .chapter`, and what lands
|
|
* there is `chromeState.ts`'s `views` array after `main.ts` has decided which
|
|
* board is up. Number is taken from the pack *or* the ordinal, and the
|
|
* `data-view-index` written into the button is the index into that rendered
|
|
* list. Only the DOM knows all of it at once. `src/test/chapterIdentity.test.ts`
|
|
* carries the fast pack-level half of the same contract, including the negative
|
|
* case, so that a reorder fails in `npm test` in a second rather than here in a
|
|
* minute.
|
|
*
|
|
* ## --write
|
|
*
|
|
* Regenerating the fixture is how you record a deliberate change, and it is not
|
|
* a way to make this go green. Every drift it reports is a frame on
|
|
* lumbridgecorp.com that is about to become a picture of somewhere else, and the
|
|
* 26 aims above have to be re-pointed in the same commit — which means
|
|
* re-shooting the imagery, which is an owner decision, not a refactor.
|
|
*/
|
|
|
|
import { chromium } from "playwright";
|
|
import { createServer } from "node:http";
|
|
import { readFile, writeFile } from "node:fs/promises";
|
|
import { extname, join, normalize, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
const DEFAULT_FIXTURE = join(ROOT, "scripts", "fixtures", "chapter-identity.json");
|
|
const MIME = {
|
|
".html": "text/html; charset=utf-8",
|
|
".js": "text/javascript",
|
|
".css": "text/css",
|
|
".json": "application/json",
|
|
".png": "image/png",
|
|
".svg": "image/svg+xml",
|
|
".webp": "image/webp",
|
|
".webmanifest": "application/manifest+json",
|
|
};
|
|
|
|
const args = process.argv.slice(2);
|
|
const option = (name, fallback) => {
|
|
const at = args.indexOf(`--${name}`);
|
|
return at < 0 ? fallback : args[at + 1];
|
|
};
|
|
const has = (name) => args.includes(`--${name}`);
|
|
|
|
const DIST = resolve(option("dist", join(ROOT, "dist")));
|
|
const WRITE = has("write");
|
|
const READY_TIMEOUT_MS = Number(option("timeout-ms", "90000")) || 90_000;
|
|
/*
|
|
* `--fixture` exists so the failure can be watched.
|
|
*
|
|
* A guard nobody has seen fail is not a guard, and the only other way to see
|
|
* this one fail is to break the packs — on a shared tree, with two other
|
|
* workstreams in the same files. Point it at a deliberately scrambled copy
|
|
* instead:
|
|
*
|
|
* jq '.boards.sf.chapters |= (.[0:2] + [.[3], .[2]] + .[4:])' \
|
|
* scripts/fixtures/chapter-identity.json > /tmp/scrambled.json
|
|
* node scripts/check-chapter-identity.mjs --fixture /tmp/scrambled.json # must FAIL
|
|
*/
|
|
const FIXTURE = resolve(option("fixture", DEFAULT_FIXTURE));
|
|
|
|
/**
|
|
* The boards, by the `?city=` value that selects them.
|
|
*
|
|
* This list is itself part of the contract: `?city=` is the coordinate every
|
|
* capture harness and every deep link uses, and `main.ts` falls back to the
|
|
* first board rather than failing on an unknown id — so a board that quietly
|
|
* lost its id would not throw, it would silently serve California to every shot
|
|
* aimed at SoCal. The snapshot records which board answered, so that failure
|
|
* shows up as a mismatch instead of as a fortnight of wrong pictures.
|
|
*/
|
|
const CITY_PARAMS = ["california", "sf", "socal"];
|
|
|
|
async function serve() {
|
|
const server = createServer(async (req, res) => {
|
|
const url = new URL(req.url ?? "/", "http://local.invalid");
|
|
const json = (status, body) => {
|
|
res.writeHead(status, { "content-type": "application/json", "cache-control": "no-store" });
|
|
res.end(JSON.stringify(body));
|
|
};
|
|
if (url.pathname === "/api/v1/health") {
|
|
return json(200, {
|
|
auth: { mode: "jwt", entryUrl: "/login.html" },
|
|
sources: { weather: "none", flights: "none", satellites: "none", markers: "none" },
|
|
regions: [],
|
|
});
|
|
}
|
|
if (url.pathname === "/api/v1/session") {
|
|
return json(200, { authenticated: false, subject: null, passwordLogin: true, admin: false });
|
|
}
|
|
if (url.pathname.startsWith("/api/v1/")) return json(404, { error: "not_found" });
|
|
const requested = normalize(decodeURIComponent(url.pathname)).replace(/^(?:\.\.[/\\])+/, "");
|
|
for (const relative of [requested === "/" ? "/index.html" : requested, "/index.html"]) {
|
|
const target = resolve(DIST, `.${relative}`);
|
|
if (!target.startsWith(`${resolve(DIST)}/`)) continue;
|
|
try {
|
|
const body = await readFile(target);
|
|
res.writeHead(200, { "content-type": MIME[extname(target)] ?? "application/octet-stream" });
|
|
res.end(body);
|
|
return;
|
|
} catch {
|
|
/* fall through to the SPA entry */
|
|
}
|
|
}
|
|
res.writeHead(404).end("not found");
|
|
});
|
|
await new Promise((ok, fail) => {
|
|
server.once("error", fail);
|
|
server.listen(0, "127.0.0.1", ok);
|
|
});
|
|
const address = server.address();
|
|
if (!address || typeof address === "string") throw new Error("no TCP port");
|
|
return { server, port: address.port };
|
|
}
|
|
|
|
async function launch() {
|
|
const resolver = "--host-resolver-rules=MAP tera.lumbridgecorp.com 127.0.0.1";
|
|
const common = ["--no-sandbox", "--disable-dev-shm-usage", "--ignore-gpu-blocklist"];
|
|
const ladder = [
|
|
["vulkan", ["--use-gl=angle", "--use-angle=vulkan"]],
|
|
["swiftshader", ["--use-gl=angle", "--use-angle=swiftshader", "--enable-unsafe-swiftshader"]],
|
|
];
|
|
let last;
|
|
for (const [backend, flags] of ladder) {
|
|
try {
|
|
const browser = await chromium.launch({ channel: "chrome", args: [...common, resolver, ...flags] });
|
|
return { browser, backend };
|
|
} catch (error) {
|
|
last = error;
|
|
}
|
|
}
|
|
throw new Error(`Chrome launch failed: ${last instanceof Error ? last.message : String(last)}`);
|
|
}
|
|
|
|
/** Everything the DOM knows about the chapter strip on one board. */
|
|
async function snapshotBoard(browser, port, cityParam) {
|
|
const page = await browser.newPage();
|
|
try {
|
|
await page.goto(`http://tera.lumbridgecorp.com:${port}/?city=${cityParam}`, {
|
|
waitUntil: "networkidle",
|
|
timeout: READY_TIMEOUT_MS,
|
|
});
|
|
await page.waitForFunction(
|
|
() =>
|
|
document.getElementById("boot")?.hidden === true &&
|
|
document.querySelectorAll("#chapters .chapter").length > 0,
|
|
null,
|
|
{ timeout: READY_TIMEOUT_MS },
|
|
);
|
|
return await page.evaluate(() => {
|
|
const text = (node) => (node?.textContent ?? "").trim();
|
|
const chapters = [...document.querySelectorAll("#chapters .chapter")].map((button, index) => {
|
|
const spans = [...button.querySelectorAll("span")];
|
|
return {
|
|
index,
|
|
dataView: button.getAttribute("data-view"),
|
|
dataViewIndex: button.getAttribute("data-view-index"),
|
|
number: text(spans[0]),
|
|
shortLabel: text(spans[spans.length - 1]),
|
|
};
|
|
});
|
|
const boards = [...document.querySelectorAll("[data-board]")].map((button) => ({
|
|
id: button.getAttribute("data-board"),
|
|
label: text(button.querySelector(".board__name")) || text(button),
|
|
active: button.getAttribute("aria-pressed") === "true",
|
|
}));
|
|
return { chapters, boards, activeBoard: boards.find((b) => b.active)?.id ?? null };
|
|
});
|
|
} finally {
|
|
await page.close();
|
|
}
|
|
}
|
|
|
|
// ---- The aims this exists to protect --------------------------------------
|
|
//
|
|
// Read out of the capture harnesses rather than duplicated here, because a copy
|
|
// would rot in exactly the way this file is trying to stop. Nothing under
|
|
// `scripts/brand-assets/` is written to — this reads their source and reports
|
|
// which of their `chapter: N` / `expect: "…"` pairs no longer agree with the
|
|
// board they aim at.
|
|
|
|
async function readAims() {
|
|
const files = [
|
|
["scripts/brand-assets/shots.mjs", "shot"],
|
|
["scripts/brand-assets/films.mjs", "film"],
|
|
["scripts/brand-assets/capture.mjs", "og-card"],
|
|
];
|
|
const aims = [];
|
|
for (const [relative, kind] of files) {
|
|
let source;
|
|
try {
|
|
source = await readFile(join(ROOT, relative), "utf8");
|
|
} catch {
|
|
continue;
|
|
}
|
|
const lines = source.split("\n");
|
|
let id = null;
|
|
let door = null;
|
|
let city = null;
|
|
for (let i = 0; i < lines.length; i += 1) {
|
|
const line = lines[i];
|
|
if (/^\s*\*/.test(line)) continue; // a comment quoting one of these is not one
|
|
const idAt = /^\s*id:\s*"([^"]+)"/.exec(line);
|
|
if (idAt) {
|
|
id = idAt[1];
|
|
door = null;
|
|
city = null;
|
|
}
|
|
const doorAt = /^\s*door:\s*"([^"]+)"/.exec(line);
|
|
if (doorAt) door = doorAt[1];
|
|
const cityAt = /^\s*city:\s*"([^"]+)"/.exec(line);
|
|
if (cityAt) city = cityAt[1];
|
|
// `capture.mjs` does not carry a shot record — it calls `shootApp` with a
|
|
// URL. The Open Graph card is the single most-seen frame in the product
|
|
// (it is what every link to lumbridgecorp.com renders as), so its aim is
|
|
// read out of the URL rather than left uncovered.
|
|
const urlAt = /"https?:\/\/[^"]*[?&]city=([a-z-]+)[^"]*"/.exec(line);
|
|
if (urlAt) {
|
|
city = urlAt[1];
|
|
door = null;
|
|
const nameAt = /"([\w-]+\.png)"/.exec(line);
|
|
if (nameAt) id = nameAt[1];
|
|
}
|
|
const chapterAt = /^\s*chapter:\s*(\d+)/.exec(line);
|
|
if (!chapterAt) continue;
|
|
let expect = null;
|
|
for (let j = i + 1; j < Math.min(lines.length, i + 6); j += 1) {
|
|
const expectAt = /^\s*expect:\s*"([^"]+)"/.exec(lines[j]);
|
|
if (expectAt) {
|
|
expect = expectAt[1];
|
|
break;
|
|
}
|
|
}
|
|
aims.push({
|
|
file: relative,
|
|
line: i + 1,
|
|
kind,
|
|
id: id ?? "(unnamed)",
|
|
// An office shot has no `city:` — it aims at the studio's own view list,
|
|
// which this check does not cover and which no `?city=` can reach.
|
|
city: door === "office" ? null : city,
|
|
chapter: Number(chapterAt[1]),
|
|
expect,
|
|
});
|
|
}
|
|
}
|
|
return aims;
|
|
}
|
|
|
|
// ---- Run ------------------------------------------------------------------
|
|
|
|
const { server, port } = await serve();
|
|
const { browser, backend } = await launch();
|
|
let observed;
|
|
try {
|
|
observed = {};
|
|
for (const cityParam of CITY_PARAMS) {
|
|
observed[cityParam] = await snapshotBoard(browser, port, cityParam);
|
|
}
|
|
} finally {
|
|
await browser.close();
|
|
server.close();
|
|
}
|
|
|
|
const snapshot = {
|
|
takenAt: "bcac6aa",
|
|
note:
|
|
"The chapter strip as the capture harnesses see it. Regenerate with --write ONLY as " +
|
|
"part of a deliberate, owner-approved re-shoot: 26 index-aimed guards in " +
|
|
"scripts/brand-assets/** point at these positions, 21 of them at a board a ?city= " +
|
|
"reaches, and every one of them is a frame on lumbridgecorp.com.",
|
|
boards: Object.fromEntries(
|
|
CITY_PARAMS.map((cityParam) => [
|
|
cityParam,
|
|
{
|
|
activeBoard: observed[cityParam].activeBoard,
|
|
boardTabs: observed[cityParam].boards.map((board) => ({ id: board.id, label: board.label })),
|
|
chapters: observed[cityParam].chapters,
|
|
},
|
|
]),
|
|
),
|
|
};
|
|
|
|
if (WRITE) {
|
|
await writeFile(FIXTURE, `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
console.log(`check-chapter-identity: wrote ${FIXTURE}`);
|
|
console.log(
|
|
"check-chapter-identity: a re-cut fixture means the 26 aims in scripts/brand-assets/**\n" +
|
|
" are now pointed at whatever this build happens to render. Re-point them and re-shoot.",
|
|
);
|
|
process.exit(0);
|
|
}
|
|
|
|
let expected;
|
|
try {
|
|
expected = JSON.parse(await readFile(FIXTURE, "utf8"));
|
|
} catch (error) {
|
|
console.error(`check-chapter-identity: cannot read ${FIXTURE} — ${error}`);
|
|
console.error(" Take one with --write, from a build you have looked at.");
|
|
process.exit(1);
|
|
}
|
|
|
|
/*
|
|
* TWO SEVERITIES, AND THE LINE BETWEEN THEM IS "DOES A CAMERA AIM AT IT".
|
|
*
|
|
* `failures` is the chapter strip: `data-view`, position, printed number, short
|
|
* label, and how many there are. Every one of those is something a capture
|
|
* harness resolves a frame through, so drift there is a picture of somewhere
|
|
* else and stops the run.
|
|
*
|
|
* `notes` is the board chrome — the tab strip and which tab is pressed. That was
|
|
* a hard failure in the first version of this file and it fired within the hour,
|
|
* on the continuity work legitimately removing the three-tab strip. Nothing aims
|
|
* a camera at a tab: `?city=` selects the board and the chapter list is what
|
|
* proves which board answered, because the ids are unique per pack. So chrome
|
|
* drift is printed and does not fail.
|
|
*
|
|
* The silent-fallback case is still caught, and caught harder: `?city=sf`
|
|
* degrading to California renders California's six chapters, which is a
|
|
* count-and-id failure on every row.
|
|
*/
|
|
const failures = [];
|
|
const notes = [];
|
|
const field = (where, want, got, soft = false) => {
|
|
if (want === got) return;
|
|
(soft ? notes : failures).push(
|
|
`${where}: expected ${JSON.stringify(want)}, got ${JSON.stringify(got)}`,
|
|
);
|
|
};
|
|
|
|
for (const cityParam of CITY_PARAMS) {
|
|
const want = expected.boards?.[cityParam];
|
|
const got = snapshot.boards[cityParam];
|
|
if (!want) {
|
|
failures.push(`?city=${cityParam}: not in the fixture — a board appeared`);
|
|
continue;
|
|
}
|
|
field(`?city=${cityParam} pressed tab`, want.activeBoard, got.activeBoard, true);
|
|
field(
|
|
`?city=${cityParam} board tabs`,
|
|
want.boardTabs.map((tab) => `${tab.id}:${tab.label}`).join(" | "),
|
|
got.boardTabs.map((tab) => `${tab.id}:${tab.label}`).join(" | "),
|
|
true,
|
|
);
|
|
field(`?city=${cityParam} chapter count`, want.chapters.length, got.chapters.length);
|
|
const count = Math.min(want.chapters.length, got.chapters.length);
|
|
for (let i = 0; i < count; i += 1) {
|
|
for (const key of ["dataView", "dataViewIndex", "number", "shortLabel", "index"]) {
|
|
field(`?city=${cityParam} chapter[${i}].${key}`, want.chapters[i][key], got.chapters[i][key]);
|
|
}
|
|
}
|
|
}
|
|
for (const cityParam of Object.keys(expected.boards ?? {})) {
|
|
if (!CITY_PARAMS.includes(cityParam)) failures.push(`?city=${cityParam}: gone from this build`);
|
|
}
|
|
|
|
// Whether or not the fixture drifted, say which aims are currently right — a
|
|
// green fixture with a misaimed shot is exactly the state this repo was in at
|
|
// bcac6aa, and it was found with a photograph rather than with a check.
|
|
const aims = await readAims();
|
|
const misaimed = [];
|
|
for (const aim of aims) {
|
|
if (aim.city === null) continue;
|
|
const board = snapshot.boards[aim.city];
|
|
if (!board) {
|
|
misaimed.push(`${aim.file}:${aim.line} ${aim.id} — city:"${aim.city}" is not a board`);
|
|
continue;
|
|
}
|
|
const chapter = board.chapters[aim.chapter];
|
|
if (!chapter) {
|
|
misaimed.push(
|
|
`${aim.file}:${aim.line} ${aim.id} — chapter ${aim.chapter} does not exist on ${aim.city}`,
|
|
);
|
|
continue;
|
|
}
|
|
if (aim.expect !== null && chapter.shortLabel !== aim.expect) {
|
|
misaimed.push(
|
|
`${aim.file}:${aim.line} ${aim.id} — chapter ${aim.chapter} on ${aim.city} is ` +
|
|
`"${chapter.shortLabel}" (${chapter.dataView}), the aim says "${aim.expect}"`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const covered = aims.filter((aim) => aim.city !== null).length;
|
|
console.log(`check-chapter-identity — dist ${DIST} (${backend})`);
|
|
for (const cityParam of CITY_PARAMS) {
|
|
const board = snapshot.boards[cityParam];
|
|
console.log(
|
|
` ?city=${cityParam.padEnd(11)} ${String(board.chapters.length).padStart(2)} chapters ` +
|
|
board.chapters.map((chapter) => chapter.dataView).join(", "),
|
|
);
|
|
}
|
|
console.log(
|
|
` ${aims.length} index-aimed guards read from the capture harnesses; ` +
|
|
`${covered} of them aim at a board this check covers.`,
|
|
);
|
|
|
|
if (notes.length > 0) {
|
|
console.log("");
|
|
console.log("NOTE — the board chrome moved. Nothing aims a camera at it, so this is not a");
|
|
console.log("failure; re-cut the fixture with --write when the change is settled:");
|
|
for (const line of notes) console.log(` ${line}`);
|
|
}
|
|
if (misaimed.length > 0) {
|
|
console.log("");
|
|
console.log("MISAIMED — these guards would shoot a different frame than their comment claims:");
|
|
for (const line of misaimed) console.log(` ${line}`);
|
|
}
|
|
if (failures.length > 0) {
|
|
console.log("");
|
|
console.log(`FAIL — the chapter strip moved under ${failures.length} field(s):`);
|
|
for (const line of failures) console.log(` ${line}`);
|
|
console.log("");
|
|
console.log(
|
|
"Every one of these is a capture aim that now points somewhere else. Re-point the\n" +
|
|
"guards in scripts/brand-assets/**, re-shoot the imagery, and only then --write.",
|
|
);
|
|
}
|
|
|
|
const bad = failures.length + misaimed.length;
|
|
if (bad === 0) console.log("check-chapter-identity: OK");
|
|
process.exit(bad === 0 ? 0 : 1);
|