Phase 2: Find and Rules

Both tabs were rendering Explore. They now take the whole screen, as the
reference does by hiding the unit on each: a list or a page of prose
wants the room more than the panel does. Picking a search result jumps
to Explore with that combo lit, which is the reference's data-go.

The rules text is extracted verbatim rather than retyped or reworded.
Two bugs on the way there, both worth recording:

- RULES is a template literal that itself contains template literals
  inside a ${} interpolation, so the bracket scanner used for the combo
  table stops at the first inner backtick. The nine title and body pairs
  are matched out of the source instead, with a count assertion so drift
  fails loudly rather than silently dropping a rule.
- PADMAP was read with g() but never added to the list of declarations
  evaluated into the vm, so it was simply not in scope.

The extractor now asserts 9 rules and 4 pad-map rows alongside the 159
combos.

141 tests, typecheck clean. Both tabs verified by rendering them, Find
with a seeded query since headless Firefox cannot type.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-17 10:49:50 +03:00
parent b875342864
commit 6d1f219e8a
7 changed files with 467 additions and 1 deletions

View File

@@ -35,7 +35,18 @@ function literalAfter(name) {
throw new Error(`unbalanced literal: ${name}`);
}
const NAMES = ["L", "PL", "GROUPS", "GNAME", "NOLATCH", "MODORDER", "RAW", "CTX", "CTXHINT"];
const NAMES = [
"L",
"PL",
"GROUPS",
"GNAME",
"NOLATCH",
"MODORDER",
"RAW",
"CTX",
"CTXHINT",
"PADMAP",
];
const ctx = vm.createContext({});
for (const n of NAMES) vm.runInContext(`var ${n} = ${literalAfter(n)};`, ctx);
// The reference adds the 16 pad labels in a loop right after declaring L.
@@ -73,7 +84,34 @@ writeFileSync(
`export const CTXHINT: Record<ContextCode, string> = ${j(g("CTXHINT"))};\n`
);
// The nine rules live in a template literal that itself contains template
// literals inside a ${...} interpolation, so the bracket scanner above would
// stop at the first inner backtick. Match the pairs out of the source instead.
const rulePairs = [...src.matchAll(/<h3>(.*?)<\/h3><p>(.*?)<\/p>/gs)].map((m) => ({
title: m[1].trim(),
body: m[2].trim(),
}));
if (rulePairs.length !== 9) {
throw new Error(`expected 9 rules, found ${rulePairs.length}`);
}
const padMap = g("PADMAP").map(([label, pads]) => ({ label, pads }));
if (padMap.length !== 4) {
throw new Error(`expected 4 pad-map rows, found ${padMap.length}`);
}
writeFileSync(
"src/domain/rules.ts",
header("The Rules tab: nine rules, and the SHIFT + pad map.") +
`export interface Rule {\n readonly title: string;\n readonly body: string;\n}\n\n` +
`export interface PadMapRow {\n readonly label: string;\n readonly pads: readonly string[];\n}\n\n` +
`export const RULES: readonly Rule[] = ${j(rulePairs)};\n\n` +
`export const PAD_MAP: readonly PadMapRow[] = ${j(padMap)};\n`
);
const raw = g("RAW");
console.log(`rules ${rulePairs.length}`);
console.log(`pad-map rows ${padMap.length}`);
console.log(`RAW entries ${raw.length}`);
console.log(`control labels ${Object.keys(g("L")).length}`);
console.log(`contexts ${Object.keys(g("CTX")).join(", ")}`);