Hero gate: a passed plate is placed material; plate rows travel on state; low-detail text with held structure is drift; wait long on plate generation

Also records the third sweep in COMP-FIDELITY.md (branch +7 to +18 points over main on three niches, sol).

AI-assisted (Claude Code).
This commit is contained in:
Paul Bakaus
2026-08-28 06:13:59 +05:00
committed by Abdul Wahab
parent 99f73390db
commit caf9744763
4 changed files with 71 additions and 5 deletions
+21 -1
View File
@@ -115,4 +115,24 @@ Why the machinery was skipped, in order of blame:
Read the earlier "exec cut, packet C, ask names the phased build" row and this sweep's four phased samples together: same model, same comp, and the phased build lands 10-15 points above the one-write build every time it actually runs. The open question is not whether the phases help but whether a resumed session enters them; that is a packet property, and it is now validated at cut time.
Next: re-cut 07, 05, 11 on the fixed branch skill (state.json verified in each packet), sol-only rerun at n=3 (~$40), then opus if the sol delta holds.
## Third sweep (2026-08-17, gpt-5.6-sol, re-cut packets carrying state.json, n=2-3): the phases run, and they win
Packets re-cut with the phase state reconstructed at the pick (replay now re-executes `build-phase.mjs` verbs and the OpenAI worker pulls the sandbox's `.impeccable/build/` before close). Baseline = main skill via `IMPECCABLE_SKILL_DIR`, branch = this branch's dist. About $60.
| Niche | main (n=3) | branch (n=2) | branch hero |
|---|---|---|---|
| 05-experimental-album | 53%, 53%, 56% (all contradicted) | **66%, 77%** (drift) | 77%, 78% |
| 07-vintage-moto-forum | 46%, 47% (contradicted) | **62%, 64%** (drift; hero open at 63/68%) | 63%, 68% |
| 11-analytics-dashboard | 66%, 61%, 66% (drift) | **72%, 71%** (drift) | 76%, 80% |
Every branch sample ran the phase machine (`state.json` at hero or later); every main sample resumed at `comps` and wrote the page in one pass. Mean delta on comp-diff overall: 05 +18, 07 +17, 11 +7. Branch runs cost 2-4x (turn cap at 100-110 on four of six; wall clock 60 min).
What the traces said, and what changed from them:
- **Font ranking without a browser was a dead end.** Sessions spent 6-10 turns installing Playwright or hand-typed a `chosen` face into spec.json. Now: the catalog index has an all-caps render (`48c`), non-text faces are excluded, the distance carries a gross width/weight gap, and with no browser `--rank` records the catalog's nearest face; the gate refuses a `chosen` it did not stamp. The eval worker lends its Playwright to the sandbox.
- **Painted material filed as chrome.** 07's exploded carburetor and rack drawing were `chrome` regions "drawn in code", then scored missing at the hero with no fix available. `comp-spec` now refuses a code kind whose note names painted material.
- **A passed plate is placed material.** With the plates referenced and passing the plates gate, comp-diff at the region box still called them `missing` on detail (the plate's carburetor is not the comp's carburetor at pixel level). The hero now scores a passed plate on placement (present in the box, at the box) and says the box as numbers.
- **The control ink-box veto blocked a full-width bar six times** ("1376x87 vs 1382x102"), unmovable by any edit. It now applies only to discrete controls.
- **Plate generation polled turn by turn** (8 `write_stdin` empty polls per plate); the NEXT line now says to wait long.
Not yet measured: the last three fixes landed after this sweep. Next: rerun sol at n=3 on the same packets to confirm the delta holds with fewer turns, then opus.
+28 -2
View File
@@ -400,7 +400,30 @@ export function gateHero(state, { buildPath = HERO_REPRO, specPath = SPEC_PATH,
if (inkPresent) { r.verdict = 'drift'; return false; }
return true;
});
for (const r of missing) reasons.push(`region ${r.id} is missing (detail ${(r.score.detail * 100).toFixed(0)}%, structure ${(r.score.structure * 100).toFixed(0)}%): the comp shows material the build does not`);
// A plate that passed the plates gate and is referenced by the page is
// placed material, not missing material: comp-diff at the region box
// re-litigates the plate's content (an exploded diagram of a different
// carburetor scores 'missing' on detail against the comp's), and no CSS
// edit can move that score. What the hero owes for a passed plate is its
// placement: material present in the box, at the box. Say that as a box.
const passedPlate = (id) => state.plates && state.plates[id] && state.plates[id].status === 'ok' && (state.plates[id].score == null || state.plates[id].score >= PLATE_MIN);
const placementNotes = [];
for (const r of report.regions) {
if (!(r.kind === 'plate' || r.kind === 'image') || !passedPlate(r.id)) continue;
if (r.verdict !== 'missing' && r.verdict !== 'contradicted') continue;
const present = r.score.detailRaw != null ? r.score.detailRaw >= 0.3 : r.score.detail >= 0.3;
if (!present) continue; // nothing drawn there: still missing
r.verdict = 'drift';
r.placed = true;
if (r.inkBox && r.inkBox.comp && r.inkBox.build) {
const c = r.inkBox.comp, b = r.inkBox.build;
const off = Math.abs(b.w - c.w) > c.w * 0.2 || Math.abs(b.h - c.h) > c.h * 0.2 || Math.abs(b.x - c.x) > c.w * 0.15 || Math.abs(b.y - c.y) > c.h * 0.15;
if (off) placementNotes.push(`plate ${r.id} is placed but not at the comp's box: its ink spans ${c.w}x${c.h}px at (${c.x},${c.y}) in the comp region and ${b.w}x${b.h}px at (${b.x},${b.y}) in the build; size and position the <img> to the spec box (object-fit: cover), not to the surrounding layout`);
}
}
const missingAfter = missing.filter((r) => r.verdict === 'missing');
for (const r of missingAfter) reasons.push(`region ${r.id} is missing (detail ${(r.score.detail * 100).toFixed(0)}%, structure ${(r.score.structure * 100).toFixed(0)}%): the comp shows material the build does not`);
for (const n of placementNotes) reasons.push(n);
const contradicted = report.regions.filter((r) => r.verdict === 'contradicted');
// A contradicted plate, image, or text region is the wrong page whatever
// the mean says; chrome and controls get the one-third allowance.
@@ -559,6 +582,9 @@ export function advance(state, { force = false, reason = null, gateOpts = {} } =
const gate = runGate(state, phase, gateOpts);
const { plates: _p, ...gateRecord } = gate;
p.gate = { ...gateRecord, at: now() };
// the plates gate's per-plate scores are what the hero gate reads to tell
// a placed plate from a missing one, so they travel on the state
if (phase === 'plates' && Array.isArray(gate.plates)) state.plates = Object.fromEntries(gate.plates.map((pl) => [pl.id, { status: pl.status, score: pl.score, size: pl.size }]));
if (!gate.ok && force && !forceAllowed(reason)) {
p.status = 'open';
return { ok: false, phase, reasons: [...gate.reasons, `--force refused: "${reason || ''}" does not quote the user downgrading the comp. A single-file deliverable, a missing tool, or difficulty is not a reason; embed the plate as a data URI, produce it with the harness image tool, or ask the user.`], gate };
@@ -584,7 +610,7 @@ export function nextInstruction(state) {
switch (state.phase) {
case 'comps': return `Comp round for the chosen direction${state.direction ? ` (seed ${state.direction})` : ''}: read reference/visualize.md, generate three compositional comps of the requested surface at its own viewport into ${MOCKS_DIR}/ (each with a prompt sidecar), put them in front of the user, and set "approved": true in the chosen comp's sidecar. Then build-phase.mjs advance. No page code before this closes.`;
case 'spec': return `Measure the comp: node comp-spec.mjs --comp ${state.comp} --grid, open ${path.join(BUILD_DIR, 'comp-grid.png')}, write regions.json (every illustration, photo, texture as its own plate region; every text block its own text region), run comp-spec.mjs --comp ${state.comp} --regions regions.json. Then measure the type: node font-match.mjs --measure <id> for each text region (cap height, width class, weight class) and font-match.mjs --rank <lead text region> --text "<its first words>" to choose the headline face by metrics (the USE line is the CSS; with no browser it records the catalog's nearest face, which is the choice; do not install one, and do not write a chosen face into the spec by hand). Then build-phase.mjs advance.`;
case 'plates': return 'Produce every plate in the spec (comp-spec.mjs --print lists them). Illustrations, photos, figures: comp-spec.mjs --crop <id>, then generate-image.mjs --plate <id> (or the harness image tool with the crop as reference and the comp-spec plate prompt). A line drawing or figure on flat ground is keyed to alpha automatically (PLATE-CHROMA): place it with a plain <img> over the page\'s own ground, never on a second paper. An opaque plate whose ground differs from the page goes in with mix-blend-mode: multiply. Textures (paper, cloth, grain): do not generate first; crop a clean patch of the comp region (comp-spec.mjs --crop <id> --raw, then cut a patch free of ink), mirror-tile it to the plate size, and save it as the plate; generate only when no clean patch exists. The gate scores a texture against its whole region box, so a texture region should be drawn around clean ground (a sample cell), not around the ink it sits under; the page tiles it wherever the material goes. Then build-phase.mjs advance. Write no page code before this passes.';
case 'plates': return 'Produce every plate in the spec (comp-spec.mjs --print lists them). Illustrations, photos, figures: comp-spec.mjs --crop <id>, then generate-image.mjs --plate <id> (or the harness image tool with the crop as reference and the comp-spec plate prompt). A generation takes 30 to 90 seconds: run it with a long wait (a 90 s yield, or all plates in one command joined with &&) rather than polling an open session turn after turn. A line drawing or figure on flat ground is keyed to alpha automatically (PLATE-CHROMA): place it with a plain <img> over the page\'s own ground, never on a second paper. An opaque plate whose ground differs from the page goes in with mix-blend-mode: multiply. Textures (paper, cloth, grain): do not generate first; crop a clean patch of the comp region (comp-spec.mjs --crop <id> --raw, then cut a patch free of ink), mirror-tile it to the plate size, and save it as the plate; generate only when no clean patch exists. The gate scores a texture against its whole region box, so a texture region should be drawn around clean ground (a sample cell), not around the ink it sits under; the page tiles it wherever the material goes. Then build-phase.mjs advance. Write no page code before this passes.';
case 'hero': return `Build only the first viewport at ${state.breakpoint || 'the comp size'}. Copy the comp's words verbatim in this phase (headline, labels, table cells, footer): the user approved that comp with those words, and rewriting is a later, stated decision, never a silent one here. Set every text region's font-size from its measured cap height and its face from the ranking. Plates first: place every plate at its spec box (comp-spec.mjs --print lists boxes as percentages of the viewport) with object-fit: cover before writing a line of text or a control, capture into ${HERO_REPRO}, and run build-phase.mjs record hero (not advance) once so you see the plate regions read as match before text exists; then lay the semantic layer (text, controls, rules) over the plates from the spec's palette and boxes, capture, advance. When it fails, open the region crops it lists first, in order, then fix; do not build past the hero until it passes.`;
case 'sections': return 'Build the remaining sections inside the spec system (same corner language, rules, and palette; nothing the comp does not show). The hero passed with the comp\'s words verbatim; from here, content beyond the comp is yours to author at full fidelity, and any change to words the comp showed is a stated decision in your report, never silent. Then build-phase.mjs advance.';
case 'motion': return 'Add the signature interaction, reveals, and motion. Then build-phase.mjs advance.';
+8 -1
View File
@@ -138,7 +138,14 @@ export function verdictFor(s, kind = null) {
// there, not that a thin strip sits a few pixels off: require the build's
// own energy to be near zero relative to the comp (rawScore, before the
// added-detail penalty), and drift for a mere misalignment.
if (!painted && s.detail < 0.35 && s.structure < 0.6) return (s.detailRaw != null && s.detailRaw < 0.2) ? 'missing' : 'contradicted';
if (!painted && s.detail < 0.35 && s.structure < 0.6) {
if (s.detailRaw != null && s.detailRaw < 0.2) return 'missing';
// low detail with structure and palette both holding is grain the build
// renders flatter (a spine of rotated type on textured red), not a
// different composition
if (s.structure >= 0.5 && s.color >= 0.5) return 'drift';
return 'contradicted';
}
if (s.detail < 0.35 && s.structure < 0.6) return 'missing';
// Structure is the one thing a wrong-but-busy region cannot fake: noise,
// a mirrored crop, a swapped column, a tile shuffle all keep color and
+14 -1
View File
@@ -241,6 +241,19 @@ describe('build-phase state machine (CLI)', () => {
assert.match(res.stdout, /GATE HERO FAILED/);
assert.match(res.stdout, /region art is missing/);
assert.ok(fs.existsSync(path.join(dir, '.impeccable', 'review', 'diff', 'hero', 'side-by-side.png')));
// the plates gate recorded the plate on the state
let st = JSON.parse(fs.readFileSync(path.join(dir, '.impeccable', 'build', 'state.json'), 'utf8'));
assert.ok(st.plates && st.plates.art && st.plates.art.status === 'ok', 'plate row travels on the state');
// a passed plate that IS drawn in the box (a different noise field, so its
// content re-scores low) is placed material: the hero says placement,
// never 'missing', and only when the box is off
const placed = createImage(comp.width, comp.height, [240, 237, 226, 255]);
fillRect(placed, 0, 0, comp.width, 40, [19, 33, 48, 255]);
const rnd2 = lcg(11);
for (let y = 40; y < 200; y++) for (let x = 320; x < 640; x++) { const v = 100 + Math.floor(rnd2() * 140); const q = (y * comp.width + x) * 4; placed.data[q] = v; placed.data[q + 1] = v; placed.data[q + 2] = v; }
fs.writeFileSync(path.join(dir, '.impeccable', 'review', 'hero-repro.png'), encodePng(placed));
res = run(PHASE_SCRIPT, ['advance'], dir);
assert.doesNotMatch(res.stdout, /region art is missing/);
// faithful: the comp shifted by a few px, captured at 1.5x width
const shifted = createImage(comp.width, comp.height, [240, 237, 226, 255]);
blit(shifted, comp, 3, 2);
@@ -249,7 +262,7 @@ describe('build-phase state machine (CLI)', () => {
assert.equal(res.status, 0, res.stdout);
assert.match(res.stdout, /ADVANCED hero -> sections/);
const state = JSON.parse(fs.readFileSync(path.join(dir, '.impeccable', 'build', 'state.json'), 'utf8'));
assert.equal(state.phases.hero.attempts, 3);
assert.equal(state.phases.hero.attempts, 4);
assert.ok(state.phases.hero.gate.score >= 0.72);
});