mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-16 16:16:32 +03:00
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:
co-authored by
Claude Fable 5
parent
89368a2430
commit
ac4c3200db
@@ -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)
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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 verdict’s 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 "IMPECCABLE’S 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 challenger’s 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 card’s 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 verdict’s 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 "IMPECCABLE’S 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 challenger’s 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 card’s 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); }
|
||||
|
||||
Reference in New Issue
Block a user