Surface rounds deal three structures, draw wireframes, and anchor comps on a reference screenshot

- concept-seed --scope surface deals three grounded-list indices (dice-picked,
  primary leads) instead of one: a single card is not a choice, and the
  no-lineup rule stays direction-only, where it was written for worlds
- serve-question renders a new per-card wireframe field as a layout schematic
  in the media slot: the code-led channel's visualization, no image
  generation needed, no card back, no salience weight
- generate-image gains --ref (repeatable): routes through the edits endpoint
  with input images, so an established world's comp inherits identity from a
  captured screenshot of a real page instead of a prose paraphrase; tested
  against impeccable.style, where the reference-anchored comp reproduced the
  live site's chrome and the prose-only comp drifted
- new-work rung two rewritten around the dealt hand: lock-in is the
  approval, a locked comp builds comp-led and discharges the visualize.md
  three-option round, a locked wireframe builds code-led; visualize.md
  records the exemption and the reference-image discipline, including the
  reference-leak caveat (chrome carries, the reference page's content
  does not)

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-08-12 16:28:15 -04:00
co-authored by Claude Fable 5
parent 89368a2430
commit ac4c3200db
6 changed files with 162 additions and 23 deletions
+41 -11
View File
@@ -338,6 +338,20 @@ export function renderConceptSeed({
};
const indexSalt = reroll === 0 ? 'index' : `index:reroll-${reroll}`;
const buildIndex = 3 + Math.floor(unit(indexSalt) * (candidateCount - 2)); // 3..candidateCount
// Surface scope deals a hand of three grounded structures: one card is not
// a choice, and the full ranked list would hand selection back to the
// model's taste. The dice pick all three; the primary index leads. The
// no-lineup rule stays direction-only, where it was written for worlds.
const dealtIndices = [buildIndex];
for (let draw = 0; scope === 'surface' && dealtIndices.length < Math.min(3, candidateCount); draw += 1) {
const idx = 1 + Math.floor(unit(`${indexSalt}:deal-${draw}`) * candidateCount);
if (!dealtIndices.includes(idx)) dealtIndices.push(idx);
if (draw > 64) { // hash repeats cannot stall the deal
for (let fill = 1; dealtIndices.length < Math.min(3, candidateCount); fill += 1) {
if (!dealtIndices.includes(fill)) dealtIndices.push(fill);
}
}
}
// Local catalog first (private repo, evals, tests), then the roll API,
// then a degraded assignment-only seed. The assigned index is pure local
@@ -411,12 +425,22 @@ export function renderConceptSeed({
on named factual grounds, when the assignment cannot carry the product's
truth or task; taste is never grounds.`
: `After ordering the task's grounded structural candidates by resonance,
build candidate ${buildIndex} of your own grounded list; the assignment never
points at a challenger. The assignment is the roll, not a suggestion.
In an attended run, present the assigned structure and offer re-roll; never
present a ranked lineup to choose from. Re-roll yourself only when the
assignment fails audience identification or product clarity on named
factual grounds.`;
deal candidates ${dealtIndices.join(', ')} of your own grounded list to the
table; index ${buildIndex} leads, and the deal never points at a challenger.
The deal is the roll, not a suggestion: the dice decide which structures
reach the user, so the ranking rut stays broken while the user still gets a
real choice, and the full ranked list stays yours. In an attended run,
present the three dealt structures as full cards of equal salience, the
lead carrying kicker THE ROLL, with steer and re-roll, and let the user
lock one in; the world is already settled, so this choice is composition.
Visualize every dealt card: with image generation available and no
standing code-led preference, declare a comp per card and generate after
serving, lead first; otherwise author each card's wireframe field (see
serve-question --schema) and the page draws the schematic. Locking a card
approves its comp: a surface round that put three visualized structures on
the table replaces the three-option comp round in visualize.md. Re-roll
yourself only when every dealt structure fails audience identification or
product clarity on named factual grounds.`;
const challengerInstruction = scope === 'direction'
? `Fuse each challenger before judging it: the challenger supplies the form
@@ -490,7 +514,7 @@ assigned index is suspended this round and the user picks; seed key ${key}.
`
: '';
return `${degradedHeader}
${degradedRegister}ASSIGNED INDEX: ${buildIndex}
${degradedRegister}${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`}
${promotedInstruction}
The assignment exists to refuse the model's ranking rut, never to outrank
the user or the brief. Never expose assignment metadata in user-facing labels.
@@ -514,8 +538,11 @@ channel: when a browser can open, present the direction on the decision page
the no-browser fallback.
${authorityInstruction}
A user- or brief-pinned decision beats the roll, always.
ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate
${buildIndex} of your own grounded list; seed key ${key}.
${scope === 'direction'
? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate
${buildIndex} of your own grounded list; seed key ${key}.`
: `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index
${buildIndex} leads. Present all three dealt structures; seed key ${key}.`}
`;
}
@@ -595,7 +622,7 @@ rivals to your habitual layout, and keep only what makes this product clearer.${
or IMPECCABLE_NO_TELEMETRY is set.\n`
: '';
const assignedBlock = register === null
? `ASSIGNED INDEX: ${buildIndex}
? `${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`}
${promotedInstruction}
The assignment exists to refuse the model's ranking rut, never to outrank
the user or the brief. Never expose assignment metadata in user-facing labels.`
@@ -621,8 +648,11 @@ craft bar, the finish level and commitment the build is expected to reach,
never as a mockup to copy; your surface serves this product, not that render.
`;
const restated = register === null
? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate
? (scope === 'direction'
? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate
${buildIndex} of your own grounded list; seed key ${key}.`
: `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index
${buildIndex} leads. Present all three dealt structures; seed key ${key}.`)
: `REGISTER (restated for truncated readers): ${register}, user-requested; the
assigned index is suspended this round; seed key ${key}.`;
return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision)
+43 -6
View File
@@ -10,6 +10,11 @@
*
* node generate-image.mjs --prompt "..." --out mock.png [--size 1536x1024] [--quality medium]
* node generate-image.mjs --prompt-file prompt.txt --out mock.png
* node generate-image.mjs --prompt "..." --out mock.png --ref screenshot.png [--ref more.png]
*
* --ref anchors generation on input image(s) via the edits endpoint: pass a
* captured screenshot of a representative existing page when comping a new
* surface for an established world, so the identity comes from the real UI.
*/
import fs from 'node:fs';
import zlib from 'node:zlib';
@@ -212,12 +217,44 @@ if (!prompt || !out) {
}
const size = arg('size', '1536x1024');
const quality = arg('quality', 'medium');
// Reference images (--ref, repeatable): route through the edits endpoint,
// which accepts input images. This is how a comp for an established world
// inherits the real UI's identity from a captured screenshot instead of a
// prose paraphrase of it; the prompt then describes the NEW surface and the
// reference carries palette, type, and component character.
const refs = (() => {
const found = [];
for (let i = 0; i < process.argv.length; i += 1) {
if (process.argv[i] === '--ref' && process.argv[i + 1] && !process.argv[i + 1].startsWith('--')) found.push(process.argv[i + 1]);
}
return found;
})();
const response = await fetch('https://api.openai.com/v1/images/generations', {
method: 'POST',
headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' },
body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }),
});
let response;
if (refs.length) {
const form = new FormData();
form.append('model', 'gpt-image-2');
form.append('prompt', prompt);
form.append('size', size);
form.append('quality', quality);
form.append('n', '1');
for (const ref of refs) {
const bytes = fs.readFileSync(ref);
const type = ref.endsWith('.png') ? 'image/png' : ref.endsWith('.webp') ? 'image/webp' : 'image/jpeg';
form.append('image[]', new Blob([bytes], { type }), ref.split('/').pop());
}
response = await fetch('https://api.openai.com/v1/images/edits', {
method: 'POST',
headers: { Authorization: `Bearer ${key}` },
body: form,
});
} else {
response = await fetch('https://api.openai.com/v1/images/generations', {
method: 'POST',
headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' },
body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }),
});
}
if (!response.ok) {
console.error(`generate-image: API error ${response.status}: ${(await response.text()).slice(0, 300)}`);
process.exit(1);
@@ -235,6 +272,6 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64'));
try {
const { spawnSync } = await import('node:child_process');
spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' });
fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2' }, null, 2));
fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2));
} catch { /* embedding is best-effort */ }
console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`);
+35 -3
View File
@@ -185,7 +185,7 @@ if (hasFlag('schema')) {
canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', palette: ['#ffffff', '#111827', '#2563eb'], materials: ['clean grid', 'product photography'], viewport: 'The arrangement a visitor expects, at full craft.', risk: 'Indistinguishable from the competition by design.', comp: '.impeccable/mocks/decision/canon.webp' },
steer: true,
}, null, 2));
console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdicts call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLES PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challengers system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. The comp slot carries the cards full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register <value> for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.');
console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdicts call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLES PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challengers system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. A card may instead declare "wireframe" ({"cols":12,"rows":10,"regions":[{"label":"nav rail","x":0,"y":0,"w":3,"h":10,"accent":true}]}): the page draws it as a layout schematic in the media slot; surface-scope rounds use it on code-led builds, it never counts toward salience, and the card keeps its full read on the front. The comp slot carries the cards full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register <value> for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.');
process.exit(0);
}
@@ -472,13 +472,36 @@ function page() {
}
return '';
};
// Wireframe media: a code-led card's layout schematic, authored as grid
// regions in the payload and drawn by the page; boxes and labels, no art.
// It fills the media slot only when the card has no imagery, and it never
// counts toward salience or earns a card back: the full read stays on the
// front, exactly like a text-only card.
const wire = (option) => {
const frame = option.wireframe;
if (!frame || !Array.isArray(frame.regions) || !frame.regions.length || media(option) || demoted(option)) return '';
const cols = Number(frame.cols) > 0 ? Number(frame.cols) : 12;
const rows = Number(frame.rows) > 0 ? Number(frame.rows) : 10;
const pct = (n, total) => `${Math.max(0, Math.min(100, (n / total) * 100)).toFixed(2)}%`;
const cells = frame.regions.slice(0, 12).map((region) => {
const x = Number(region.x) || 0;
const y = Number(region.y) || 0;
const w = Math.max(Number(region.w) || 1, 0.5);
const h = Math.max(Number(region.h) || 1, 0.5);
return `<div class="wire-region${region.accent ? ' accent' : ''}" style="left:${pct(x, cols)};top:${pct(y, rows)};width:${pct(w, cols)};height:${pct(h, rows)}"><span>${esc(region.label || '')}</span></div>`;
}).join('');
return `<div class="media wire" role="img" aria-label="Layout schematic">
<div class="wire-field">${cells}</div>
<p class="media-label">layout</p>
</div>`;
};
const chooseLabel = (option) => option.isCanon ? 'Play it straight' : demoted(option) ? 'Adopt anyway' : 'Build this';
const cards = options.map((option, index) => `
<article class="card${option.isCanon ? ' canon' : ''}${demoted(option) ? ' declined' : ''}" style="--fan:${index === 0 ? '0deg' : (index % 2 ? '1.4deg' : '-1.2deg')};--deal:${index * 90}ms" data-id="${esc(option.id)}">
<div class="card-inner">
<div class="face front${index === 0 ? ' lead' : ''}${media(option) ? '' : ' text-only'}">
<div class="face front${index === 0 ? ' lead' : ''}${(media(option) || wire(option)) ? '' : ' text-only'}">
${option.kicker ? `<span class="kicker">${esc(option.kicker)}</span>` : demoted(option) ? '<span class="kicker declined-k">Declined</span>' : option.isCanon ? '<span class="kicker standing">The standing door</span>' : ''}
${media(option)}
${media(option) || wire(option)}
<div class="body">
${option.lineage ? `<p class="tier">${esc(option.lineage)}</p>` : ''}
<h2>${esc(option.label)}</h2>
@@ -710,6 +733,15 @@ function page() {
.kicker.declined-k { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); }
.card.declined button.choose { background: transparent; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); font-size: .85rem; padding: 8px 22px; }
.card.declined button.choose:hover { background: var(--ks-graphite-2); border-color: var(--ks-text-muted); }
/* Wireframe media: the code-led schematic. Quiet boxes in the card's own
chrome; uniform salience across cards by construction, so it needs no
parity rules. */
.media.wire { background: var(--ks-lacquer); border-bottom: 1px solid var(--ks-rule); }
.wire-field { position: absolute; inset: 12px 12px 26px; }
.wire-region { position: absolute; border: 1px solid oklch(78% 0 0 / 0.26); border-radius: 3px; background: oklch(78% 0 0 / 0.05); display: flex; align-items: center; justify-content: center; overflow: hidden; }
.wire-region span { font-family: var(--ks-mono); font-size: .55rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ks-text-faint); text-align: center; padding: 2px 4px; }
.wire-region.accent { border-color: oklch(84% 0.19 80.46 / 0.5); background: oklch(84% 0.19 80.46 / 0.06); }
.wire-region.accent span { color: var(--ks-kinpaku-rich); }
/* Thumb-scale inspiration: present, labeled, zoomable, and incapable of
outshouting a text-only assigned card. */
.inspo { position: relative; flex: none; margin: 2px 0; width: 104px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; cursor: zoom-in; background: var(--ks-lacquer); }