Expand concept system: modes, ratings, re-roll, breadth strategy

Catalog: mode-aligned staging surfaces (persuade/operate/read/experience),
star ratings on approvals feeding challenger draw weights, family
retirements, authoring strategy and territory guide, rework and breadth
authoring rounds, composition mining from rejected worlds.

Seed: six challengers (two per tier), --reroll chains, --mode staging
filter, rating-weighted draws. New-work: Present/visualize/re-roll flow,
image-gen requirement, register-neutral vocabulary.

Pipeline: per-mode staging prompts with split frames, hero-from-board
reference generation, render-safety guards. Labs: ratings UI, unrated
filter, mode chips, composition approve-guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-20 22:10:10 -07:00
co-authored by Claude Fable 5
parent 144cee5c36
commit 7557935fdb
55 changed files with 24574 additions and 24025 deletions
+3 -1
View File
@@ -633,6 +633,8 @@ function generateCFConfig(buildDir) {
/docs/teach /docs/init 301
/anti-patterns /slop#catalog 301
/visual-mode /slop#see-it 301
/worlds /labs/worlds 301
/worlds/stagings /labs/stagings 301
/neon-mirai /neo-mirai/ 301
/neon-mirai/ /neo-mirai/ 301
/cases/neon-mirai /cases/neo-mirai 301
@@ -644,7 +646,7 @@ function generateCFConfig(buildDir) {
// Without this, the SPA fallback serves index.html for function routes
const routes = {
version: 1,
include: ['/api/download/*'],
include: ['/api/download/*', '/worlds/cards/*'],
exclude: [],
};
fs.writeFileSync(path.join(buildDir, '_routes.json'), JSON.stringify(routes, null, 2));
+353
View File
@@ -0,0 +1,353 @@
#!/usr/bin/env node
/**
* Generate design-system translation cards for the concept world catalog.
*
* Each card renders one world as the web design system it implies: palette
* swatches and material chips, a type specimen, core UI components, and one
* small screen composition. The prompt is built from the concept's authored
* Palette/material and Type/composition rules, so the card tests exactly the
* translation the catalog claims.
*
* Output: site/public/worlds/cards/<concept-id>.webp (16:9, 1K, WebP) plus
* manifest.json mapping id -> { hash, generatedAt }. hash is the concept's
* content hash (same fingerprint reviews pin), so an edited concept marks its
* card stale and a rerun regenerates only what changed.
*
* Two image kinds per concept:
* board (<id>.webp): the design-system specimen sheet (palette, type,
* components, phone composition).
* hero (<id>-hero.webp): a full-frame desktop landing page composed in the
* world's system; the 16:9 frame IS the viewport, proving the world holds
* at desktop scale.
*
* Usage:
* node scripts/generate-world-cards.mjs # all stale/missing
* node scripts/generate-world-cards.mjs --kind hero # board | hero | both
* node scripts/generate-world-cards.mjs --only <id[,id]> # specific concepts
* node scripts/generate-world-cards.mjs --limit 5 # first N stale jobs
* node scripts/generate-world-cards.mjs --force # ignore manifest
* node scripts/generate-world-cards.mjs --model pro # lite | pro | gpt
* node scripts/generate-world-cards.mjs --out <dir> # alternate output dir
*/
import { GoogleGenAI } from '@google/genai';
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import sharp from 'sharp';
import { conceptContentHash, readConceptCatalog } from '../skill/scripts/lib/concept-catalog.mjs';
import { compositionContentHash, readCompositionCatalog } from '../skill/scripts/lib/composition-catalog.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const CARD_DIR = join(ROOT, 'site', 'public', 'worlds', 'cards');
// gpt (the default) won the human calibration bake-off decisively; lite and
// pro remain for future comparisons.
const MODELS = {
lite: { kind: 'gemini', id: 'models/gemini-3.1-flash-lite-image', imageSize: '1K' },
pro: { kind: 'gemini', id: 'models/gemini-3-pro-image', imageSize: '2K' },
gpt: { kind: 'openai', id: 'gpt-image-2', size: '2048x1152', quality: 'high' },
};
const DEFAULT_MODEL = 'gpt';
const CONCURRENCY = 6;
const MAX_ATTEMPTS = 4;
function loadEnv() {
if (process.env.GEMINI_API_KEY || process.env.GOOGLE_CLOUD_API_KEY) return;
const envPath = join(ROOT, '.env');
if (!existsSync(envPath)) return;
for (const line of readFileSync(envPath, 'utf8').split('\n')) {
const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
if (match && !process.env[match[1]]) process.env[match[1]] = match[2].replace(/^"|"$/g, '');
}
}
function boardTitle(concept) {
return concept.form
.split(',')[0]
.replace(/^(a|an|the)\s+/i, '')
.replace(/\b\w/g, letter => letter.toUpperCase());
}
function buildHeroPrompt(concept) {
const palette = concept.system[0].replace(/^Palette\/material:\s*/, '');
const type = concept.system[1].replace(/^Type\/composition:\s*/, '');
return `A complete desktop landing page filling the entire 16:9 frame edge to edge, as if screenshotted at 1440 pixels wide: a slim top navigation with a wordmark and a few links, a hero section with a large headline, one supporting line, and a primary call-to-action button, and the top of the next content section just visible at the bottom edge. No browser chrome, no device mockup, no specimen-board framing, no caption: the page itself is the entire image.
The page is designed wholly inside this visual world: ${concept.form}.
Palette and materials: ${palette}
Typography and composition: ${type}
Atmosphere, never written on the page but carried into it: ${concept.spark}
Light the page like the world: its hour, light quality, and mood shape the page ground, surfaces, and imagery, not just the accent color. A nocturnal or interior world produces a genuinely dark, atmospherically lit page.
Invent a plausible fictional product or brand this world would naturally serve and write short realistic copy for it (plain punctuation, never an em dash): an invented name that reuses no brand, label, designer, or place name from the world description, a headline of at most eight words, one supporting sentence, and button labels. The result must read as a real, current, award-caliber website built from this world's laws: disciplined grid, aligned edges, believable interface details, generous intentional spacing. Not a poster, not a pastiche, a landing page.
The world must live in the interface itself, not only in imagery: the navigation, buttons, cards, dividers, and type voices are built from the world's materials, colors, textures, and lettering traditions. A photograph or illustration may appear as content, but a generic clean website wearing a themed hero image is a failure; a stranger shown only the page footer or a single button should still recognize the world.
Craft rules, all mandatory: every piece of interface copy is English, even when the world is Japanese, Arabic, or otherwise non-Latin in origin; at most one small non-Latin glyph may appear as a decorative motif, never in the wordmark, navigation, headlines, or body text, and when in doubt use none. Rendered materials must read premium and physically plausible: real metal, wood, paper, and glass with honest light, never plasticky gradient fakes of them. This is a product or editorial website, not a video-game HUD: no health bars, minimaps, or game-menu chrome unless the world itself is a game interface; when the world is screen- or game-born, art-direct it to contemporary award standard, never dated chrome. Vary the composition beyond the world's single most famous motif and refuse AI-cliche renderings of it (Matrix-style glyph rain, red recording dots); use the world's wider grammar.`;
}
function buildPrompt(concept) {
const palette = concept.system[0].replace(/^Palette\/material:\s*/, '');
const type = concept.system[1].replace(/^Type\/composition:\s*/, '');
return `A single flat design-system specimen board, the kind a design studio produces to prove a visual world translates into web and app design. The board is titled "${boardTitle(concept)}" and fills the whole image edge to edge: no desk, wall, binder clips, pins, or drop shadows around it.
The visual world being translated: ${concept.form}.
Mood reference for art direction only: ${concept.spark}
Board sections, left to right:
1. COLOR & MATERIAL column: five to seven labeled palette swatches plus two or three material texture chips, derived strictly from: ${palette}.
2. TYPOGRAPHY panel: a large display headline specimen and a short body-text block obeying: ${type}.
3. COMPONENTS panel: a primary button, a secondary button, a text input, one content card, and a small navigation bar, all styled by this exact system, with normal and active states.
4. One small phone-screen composition in the corner showing the system as a real app or landing screen.
The board ground and dividers take their tone from the world's palette. Precise, crisp, flat graphic rendering like a printed specimen sheet; not a photograph or illustration of the world itself, only the interface system it yields. The only text on the board is the title, short section labels, and brief invented specimen words; never transcribe any sentence from these instructions onto the board.`;
}
async function extractImage(interaction) {
if (interaction?.output_image?.data) return Buffer.from(interaction.output_image.data, 'base64');
for (const step of interaction?.steps || []) {
if (step.type !== 'model_output' || !step.content) continue;
for (const part of step.content) {
if (part.type === 'image' && part.data) return Buffer.from(part.data, 'base64');
}
}
return null;
}
// Sequence-heavy families render two consecutive beats as a split frame; a
// still image cannot show motion any other honest way.
const SEQUENCE_FAMILIES = new Set(['narrative-scroll', 'reveal-transform', 'interaction-physics']);
const MODE_FRAMES = {
persuade: 'an extraordinary marketing page at desktop scale',
operate: 'an extraordinary application workspace at desktop scale, real work visibly in progress',
read: 'an extraordinary documentation or long-form reading page at desktop scale, structure visibly serving comprehension',
experience: 'an extraordinary portfolio or showcase surface at desktop scale, the work itself owning the frame',
};
function buildStagingPrompt(composition) {
const modeFrame = MODE_FRAMES[composition.surface] || MODE_FRAMES.persuade;
const hierarchy = composition.grammar[0].replace(/^Staging\/hierarchy:\s*/, '');
const sequence = composition.grammar[1].replace(/^Sequence\/attention:\s*/, '');
const controls = composition.grammar[2].replace(/^Controls\/state:\s*/, '');
const capture = SEQUENCE_FAMILIES.has(composition.familyId)
? 'Render two consecutive beats of the same page as one hard-cut split frame, a thin seam dividing left and right halves like film frames seconds apart. The staging visibly advances between the beats: something mid-reveal on the left has landed on the right, attention has moved, state has changed.'
: composition.familyId === 'first-viewport'
? 'Render one cinematic frame of the opening screen at the most dramatic moment of its signature staging.'
: 'Render one cinematic frame of the page caught mid-sequence, never its resting opening state: elements between states, partial reveals, momentum readable in the composition itself.';
const copy = composition.surface === 'operate'
? 'Invent a plausible product with short realistic English copy (plain punctuation, never an em dash): an invented name and real-looking task content, labels, and data, never placeholder text.'
: 'Invent a plausible product with short realistic English copy (plain punctuation, never an em dash): an invented name, a headline of at most eight words, one supporting line, one committed action.';
return `${capture} The subject: ${modeFrame}, filling the entire 16:9 frame edge to edge. No browser chrome, no device mockup, no board framing, no caption.
The staging: ${composition.form}
What owns the frame: ${hierarchy}
The motion to make visible: ${sequence}
One interaction in play: ${controls}. Show exactly one control or element mid-gesture with its state visibly changing: a cursor in contact, a drag mid-flight, a value mid-update. Everything else holds still.
This must read as interactive, dynamic, fully realized web design that raises the bar: monumental scale contrast, fearless negative space, editorial typographic confidence, real art direction discipline, and web technology pushed as far as the staging implies (${composition.webLeverage}). Choose a product domain the staging serves best; when it is technical or game-adjacent, art-direct it to contemporary award standard, never dated game chrome or generic sci-fi mission styling, and never lean on cliche AI-generated motifs like recording indicators. Identity stays quiet so the staging is the star: a monochrome ground, one accent color, one strong typeface family, wielded with award-winning conviction, never as a wireframe. Only the elements this staging requires exist on the page: no toolbars, settings chrome, feature grids, or decorative widgets beyond the one interaction in play.
${copy} Never transcribe these instructions onto the image. The viewer should gasp at the design first, then understand the mechanism from what the frame implies.`;
}
function promptFor(concept, kind) {
if (kind === 'staging') return buildStagingPrompt(concept);
return kind === 'hero' ? buildHeroPrompt(concept) : buildPrompt(concept);
}
async function generateGemini(ai, model, concept, kind) {
const interaction = await ai.interactions.create({
model: model.id,
input: promptFor(concept, kind),
generation_config: {
temperature: 1,
top_p: 0.95,
thinking_level: 'low',
image_config: { image_size: model.imageSize, aspect_ratio: '16:9' },
},
response_modalities: ['image', 'text'],
});
return extractImage(interaction);
}
// Set by main() so hero generation can pick up the concept's board as a
// binding style reference (the board/hero pair must read as one system).
let BOARD_REFERENCE_DIR = null;
async function generateOpenAI(model, concept, kind) {
// Heroes are generated from the specimen board when it exists: the edits
// endpoint takes the board as reference so both images share one system.
if (kind === 'hero' && BOARD_REFERENCE_DIR) {
const boardPath = join(BOARD_REFERENCE_DIR, `${concept.id}.webp`);
if (existsSync(boardPath)) {
try {
const form = new FormData();
form.append('model', model.id);
form.append('image[]', new Blob([readFileSync(boardPath)], { type: 'image/webp' }), `${concept.id}.webp`);
form.append('prompt', `The attached image is this world's design-system specimen board. Treat its palette, materials, type voices, and component grammar as binding reference; the page you render must read as the same system. ${promptFor(concept, kind)}`);
form.append('size', model.size);
form.append('quality', model.quality);
const response = await fetch('https://api.openai.com/v1/images/edits', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
body: form,
});
const json = await response.json();
if (!response.ok) throw new Error(json.error?.message || `HTTP ${response.status}`);
const b64 = json.data?.[0]?.b64_json;
if (b64) return Buffer.from(b64, 'base64');
} catch (error) {
process.stderr.write(` board-reference hero failed for ${concept.id} (${error.message}); falling back to plain generation\n`);
}
}
}
const response = await fetch('https://api.openai.com/v1/images/generations', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: model.id,
prompt: promptFor(concept, kind),
size: model.size,
quality: model.quality,
}),
});
const json = await response.json();
if (!response.ok) throw new Error(json.error?.message || `HTTP ${response.status}`);
const b64 = json.data?.[0]?.b64_json;
return b64 ? Buffer.from(b64, 'base64') : null;
}
async function generateCard(ai, model, concept, kind) {
let lastError;
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
try {
const raw = model.kind === 'openai'
? await generateOpenAI(model, concept, kind)
: await generateGemini(ai, model, concept, kind);
if (!raw) throw new Error('no image part in response');
return await sharp(raw).webp({ quality: 90 }).toBuffer();
} catch (error) {
lastError = error;
const wait = attempt * 8000;
process.stderr.write(` retry ${concept.id} (${attempt}/${MAX_ATTEMPTS}): ${error.message}\n`);
await new Promise(resolve => setTimeout(resolve, wait));
}
}
throw lastError;
}
async function main() {
loadEnv();
const args = process.argv.slice(2);
const modelKey = args.includes('--model') ? args[args.indexOf('--model') + 1] : DEFAULT_MODEL;
const model = MODELS[modelKey];
if (!model) throw new Error(`unknown --model ${modelKey}; use ${Object.keys(MODELS).join(', ')}`);
const outDir = args.includes('--out') ? args[args.indexOf('--out') + 1] : CARD_DIR;
const manifestPath = join(outDir, 'manifest.json');
const only = args.includes('--only') ? args[args.indexOf('--only') + 1].split(',').filter(Boolean) : null;
const limit = args.includes('--limit') ? Number(args[args.indexOf('--limit') + 1]) : Infinity;
const force = args.includes('--force');
const kindArg = args.includes('--kind') ? args[args.indexOf('--kind') + 1] : 'both';
if (!['board', 'hero', 'both', 'staging'].includes(kindArg)) throw new Error(`unknown --kind ${kindArg}; use board, hero, both, staging`);
const kinds = kindArg === 'both' ? ['board', 'hero'] : [kindArg];
const isStaging = kindArg === 'staging';
const geminiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_CLOUD_API_KEY;
if (model.kind === 'gemini' && !geminiKey) throw new Error('set GEMINI_API_KEY or GOOGLE_CLOUD_API_KEY (repo .env)');
if (model.kind === 'openai' && !process.env.OPENAI_API_KEY) throw new Error('set OPENAI_API_KEY (repo .env)');
const ai = model.kind === 'gemini' ? new GoogleGenAI({ apiKey: geminiKey }) : null;
// Staging images render composition-catalog entries; boards and heroes
// render world-catalog concepts. Both share the manifest and output dir.
const concepts = isStaging
? readCompositionCatalog(
join(ROOT, 'skill', 'scripts', 'composition-ingredients.json'),
join(ROOT, 'skill', 'scripts', 'composition-reviews.json')
).compositions
: readConceptCatalog(
join(ROOT, 'skill', 'scripts', 'concept-ingredients.json'),
join(ROOT, 'skill', 'scripts', 'concept-reviews.json')
).concepts;
const hashOf = isStaging ? compositionContentHash : conceptContentHash;
mkdirSync(outDir, { recursive: true });
BOARD_REFERENCE_DIR = outDir;
const manifest = existsSync(manifestPath) ? JSON.parse(readFileSync(manifestPath, 'utf8')) : {};
const writeManifest = () => writeFileSync(
manifestPath,
`${JSON.stringify(Object.fromEntries(Object.entries(manifest).sort(([a], [b]) => a.localeCompare(b))), null, 2)}\n`
);
const fileFor = (concept, kind) => join(outDir, `${concept.id}${kind === 'hero' ? '-hero' : ''}.webp`);
const stampKey = kind => (kind === 'hero' ? 'heroGeneratedAt' : 'generatedAt');
const queue = [];
for (const concept of concepts) {
if (only && !only.includes(concept.id)) continue;
const hash = hashOf(concept);
const row = manifest[concept.id];
for (const kind of kinds) {
const needed = force
|| !existsSync(fileFor(concept, kind))
|| row?.hash !== hash
|| !row?.[stampKey(kind)];
if (needed || only) queue.push({ concept, kind });
}
}
queue.splice(limit);
if (only) {
const found = new Set(queue.map(job => job.concept.id));
const missing = only.filter(id => !found.has(id));
if (missing.length) throw new Error(`concept not found: ${missing.join(', ')}`);
}
process.stdout.write(`generating ${queue.length} images (${kinds.join('+')}) across ${concepts.length} concepts with ${modelKey} -> ${outDir}\n`);
let done = 0;
let failed = 0;
const worker = async () => {
while (queue.length > 0) {
const { concept, kind } = queue.shift();
try {
const webp = await generateCard(ai, model, concept, kind);
writeFileSync(fileFor(concept, kind), webp);
const row = manifest[concept.id] || {};
row.hash = hashOf(concept);
row.model = modelKey;
row[stampKey(kind)] = new Date().toISOString();
manifest[concept.id] = row;
writeManifest();
done += 1;
process.stdout.write(` [${done}] ${concept.id} ${kind} (${Math.round(webp.length / 1024)}KB)\n`);
} catch (error) {
failed += 1;
process.stderr.write(` FAILED ${concept.id} ${kind}: ${error.message}\n`);
}
}
};
await Promise.all(Array.from({ length: CONCURRENCY }, worker));
// Prune manifest entries for ids that left both catalogs (they share the
// manifest, so pruning must check the union).
const worldIds = readConceptCatalog(
join(ROOT, 'skill', 'scripts', 'concept-ingredients.json'),
join(ROOT, 'skill', 'scripts', 'concept-reviews.json')
).concepts.map(concept => concept.id);
const stagingIds = readCompositionCatalog(
join(ROOT, 'skill', 'scripts', 'composition-ingredients.json'),
join(ROOT, 'skill', 'scripts', 'composition-reviews.json')
).compositions.map(composition => composition.id);
const liveIds = new Set([...worldIds, ...stagingIds]);
for (const id of Object.keys(manifest)) {
if (!liveIds.has(id)) delete manifest[id];
}
writeManifest();
process.stdout.write(`done: ${done} generated, ${failed} failed\n`);
if (failed > 0) process.exitCode = 1;
}
main().catch(error => {
process.stderr.write(`${error.message}\n`);
process.exit(1);
});
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env node
/**
* Upload world design-system cards to the R2 bucket that backs
* functions/worlds/cards/[[file]].js. Skips files already uploaded at their
* current generation (tracked in a local .published.json sidecar), so routine
* runs only push what changed. Requires an authenticated wrangler (same auth
* as `bun run deploy`) and the bucket:
* wrangler r2 bucket create impeccable-world-cards
*
* Usage:
* bun run world-cards:publish # changed files only
* bun run world-cards:publish -- --force # everything
*/
import { execFile } from 'node:child_process';
import { readdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
const run = promisify(execFile);
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const CARD_DIR = join(ROOT, 'site', 'public', 'worlds', 'cards');
const STATE_PATH = join(CARD_DIR, '.published.json');
const BUCKET = 'impeccable-world-cards';
const CONCURRENCY = 6;
const force = process.argv.includes('--force');
const manifest = JSON.parse(readFileSync(join(CARD_DIR, 'manifest.json'), 'utf8'));
const state = existsSync(STATE_PATH) ? JSON.parse(readFileSync(STATE_PATH, 'utf8')) : {};
const files = readdirSync(CARD_DIR).filter(file => file.endsWith('.webp'));
const queue = files.filter(file => {
if (force) return true;
const id = file.replace(/(-hero)?\.webp$/, '');
const stamp = file.endsWith('-hero.webp') ? manifest[id]?.heroGeneratedAt : manifest[id]?.generatedAt;
return !stamp || state[file] !== stamp;
});
console.log(`publishing ${queue.length} of ${files.length} cards to r2://${BUCKET}`);
let done = 0;
let failed = 0;
async function upload(file) {
const contentType = 'image/webp';
await run('wrangler', [
'r2', 'object', 'put', `${BUCKET}/${file}`,
'--file', join(CARD_DIR, file),
'--content-type', contentType,
'--remote',
], { cwd: ROOT });
}
const worker = async () => {
while (queue.length > 0) {
const file = queue.shift();
try {
await upload(file);
const id = file.replace(/(-hero)?\.webp$/, '');
state[file] = (file.endsWith('-hero.webp') ? manifest[id]?.heroGeneratedAt : manifest[id]?.generatedAt) || new Date().toISOString();
writeFileSync(STATE_PATH, `${JSON.stringify(state, null, 2)}\n`);
done += 1;
if (done % 25 === 0 || queue.length === 0) console.log(` ${done} uploaded, ${queue.length} remaining`);
} catch (error) {
failed += 1;
console.error(` FAILED ${file}: ${error.message}`);
}
}
};
await Promise.all(Array.from({ length: CONCURRENCY }, worker));
try {
await run('wrangler', [
'r2', 'object', 'put', `${BUCKET}/manifest.json`,
'--file', join(CARD_DIR, 'manifest.json'),
'--content-type', 'application/json',
'--remote',
], { cwd: ROOT });
console.log(' manifest.json uploaded');
} catch (error) {
failed += 1;
console.error(` FAILED manifest.json: ${error.message}`);
}
console.log(`done: ${done} uploaded, ${failed} failed`);
if (failed > 0) process.exitCode = 1;
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env node
// World cards are served from R2 by functions/worlds/cards/[[file]].js.
// Astro copies site/public/worlds/cards (local generation output) into the
// static build; this strips it so deploys stay light and the Function owns
// the route. Runs as part of `bun run build`.
import { rmSync, existsSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const buildCards = join(dirname(fileURLToPath(import.meta.url)), '..', 'build', 'worlds', 'cards');
if (existsSync(buildCards)) {
rmSync(buildCards, { recursive: true });
console.log('✓ Stripped local world cards from build output (served from R2)');
}
+3 -2
View File
@@ -25,11 +25,11 @@ export const SUITES = {
triggers: [
...COMMON_INFRA_PATTERNS,
/^scripts\/(?!benchmark-detector|build-browser-detector|build-extension)/,
/^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/(cleanup-deprecated|concept-ingredients|concept-reviews|concept-seed|context|context-signals|critique-storage|design-parser|hook|impeccable-paths|is-generated|lib\/(concept-catalog|provider|surface-briefs|target-slug)|pin|surface-brief|validate-concept-catalog))/,
/^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/(cleanup-deprecated|composition-ingredients|composition-reviews|concept-ingredients|concept-reviews|concept-seed|context|context-signals|critique-storage|design-parser|hook|impeccable-paths|is-generated|lib\/(composition-catalog|concept-catalog|provider|surface-briefs|target-slug)|pin|surface-brief|validate-concept-catalog))/,
/^site\/(pages|content|components|layouts)\//,
/^README(\.npm)?\.md$/,
/^cli\/bin\//,
/^tests\/(build|cleanup-deprecated|cli-ignores|concept-seed|context|context-signals|critique-storage|design-parser|docs-integrity|github-sheriff|hook|hook-build|impeccable-paths|openai-plugin|pin|shiki-theme|skills-cli|slop-catalog|surface-brief|target-args|test-suites|theme|windows-path-fix|worlds-review-vite-plugin|zip)\.test\.(js|mjs)$/,
/^tests\/(build|cleanup-deprecated|cli-ignores|concept-seed|context|context-signals|critique-storage|design-parser|docs-integrity|github-sheriff|hook|hook-build|impeccable-paths|live-ui-lab|openai-plugin|pin|shiki-theme|skills-cli|slop-catalog|surface-brief|target-args|test-suites|theme|windows-path-fix|worlds-review-vite-plugin|zip)\.test\.(js|mjs)$/,
/^tests\/lib\//,
],
commands: [
@@ -63,6 +63,7 @@ export const SUITES = {
'tests/hook-build.test.mjs',
'tests/hook.test.mjs',
'tests/impeccable-paths.test.mjs',
'tests/live-ui-lab.test.mjs',
'tests/openai-plugin.test.mjs',
'tests/pin.test.mjs',
'tests/target-args.test.mjs',
+158 -4
View File
@@ -1,5 +1,18 @@
import { readFile, rename, writeFile } from 'node:fs/promises';
import path from 'node:path';
import {
conceptContentHash,
CONCEPT_STRENGTHS,
normalizeConceptForm,
SYSTEM_PREFIXES,
validateConceptCatalog,
validateConceptEntry,
WELL_TIERS,
} from '../skill/scripts/lib/concept-catalog.mjs';
import {
compositionContentHash,
validateCompositionCatalog,
} from '../skill/scripts/lib/composition-catalog.mjs';
const API_PATH = '/__impeccable/worlds';
const MAX_BODY_BYTES = 64 * 1024;
@@ -51,18 +64,84 @@ function findConcept(catalog, id) {
return null;
}
function assertApprovedFloor(catalog, reviewData) {
const approvedIds = new Set(
Object.entries(reviewData.reviews)
.filter(([, review]) => review.status === 'approved')
.map(([id]) => id)
);
const tierByWell = new Map((catalog.wells || []).map(well => [well.id, well.tier]));
const approvedTiers = new Set(
catalog.families
.filter(family => family.concepts.some(concept => approvedIds.has(concept.id)))
.map(family => tierByWell.get(family.well))
.filter(tier => WELL_TIERS.includes(tier))
);
if (approvedIds.size < 3 || approvedTiers.size < WELL_TIERS.length) {
throw new Error('At least three approved concepts covering every challenger tier must remain available to the challenger');
}
}
function assertValidCatalog(catalog, reviewData) {
const { errors } = validateConceptCatalog(catalog, reviewData, { requireApprovedMinimum: false });
if (errors.length > 0) throw new Error(errors[0]);
}
function validateTags(tags) {
return Array.isArray(tags)
&& tags.length === 3
&& tags.every(tag => typeof tag === 'string' && tag.trim().length >= 2 && tag.trim().length <= 40);
}
function validateSystem(system) {
return Array.isArray(system)
&& system.length === SYSTEM_PREFIXES.length
&& system.every((rule, index) =>
typeof rule === 'string'
&& rule.trim().length >= 12
&& rule.trim().length <= 180
&& rule.trim().startsWith(SYSTEM_PREFIXES[index])
);
}
export function worldsReviewPlugin({ root = process.cwd() } = {}) {
const catalogPath = path.join(root, 'skill', 'scripts', 'concept-ingredients.json');
const reviewsPath = path.join(root, 'skill', 'scripts', 'concept-reviews.json');
const compositionCatalogPath = path.join(root, 'skill', 'scripts', 'composition-ingredients.json');
const compositionReviewsPath = path.join(root, 'skill', 'scripts', 'composition-reviews.json');
let mutationQueue = Promise.resolve();
// Composition-catalog reviews share the review mechanics but none of the
// world catalog's floors or editing; v1 supports the review action only.
async function mutateComposition(body) {
if (body.action !== 'review') throw new Error('Compositions support the review action only');
if (!REVIEW_STATUSES.has(body.status)) throw new Error('Review status is invalid');
const note = typeof body.note === 'string' ? body.note.trim() : '';
if (note.length > 500) throw new Error('Review note must be 500 characters or fewer');
const catalog = await readJson(compositionCatalogPath);
const reviewData = await readJson(compositionReviewsPath);
const entry = (catalog.compositions || []).find(composition => composition.id === body.id);
if (!entry) throw new Error('Composition was not found');
if (body.status === 'pending') {
delete reviewData.reviews[body.id];
} else {
reviewData.reviews[body.id] = {
status: body.status,
reviewedBy: 'pbakaus',
reviewedAt: new Date().toISOString(),
formHash: compositionContentHash(entry),
...(note ? { note } : {}),
};
}
reviewData.reviews = Object.fromEntries(Object.entries(reviewData.reviews).sort(([a], [b]) => a.localeCompare(b)));
const { errors } = validateCompositionCatalog(catalog, reviewData);
if (errors.length > 0) throw new Error(errors[0]);
await writeJsonAtomic(compositionReviewsPath, reviewData);
return { id: body.id, status: body.status, review: reviewData.reviews[body.id] || null };
}
async function mutate(body) {
if (body.catalog === 'compositions') return mutateComposition(body);
const catalog = await readJson(catalogPath);
const reviewData = await readJson(reviewsPath);
const match = findConcept(catalog, body.id);
@@ -70,6 +149,16 @@ export function worldsReviewPlugin({ root = process.cwd() } = {}) {
if (body.action === 'review') {
if (!REVIEW_STATUSES.has(body.status)) throw new Error('Review status is invalid');
// Composition strength is a routing verdict, not an approvable type:
// every composition-typed world entry the reviewer processed was
// rejected, and approved stagings live in the composition catalog.
if (body.status === 'approved' && match.concept.strength === 'composition') {
throw new Error('Stagings live in the composition catalog; reject it here and it joins the mining queue');
}
const note = typeof body.note === 'string' ? body.note.trim() : '';
if (note.length > 500) throw new Error('Review note must be 500 characters or fewer');
const previousStatus = reviewData.reviews[body.id]?.status || 'pending';
const previousRating = reviewData.reviews[body.id]?.rating;
if (body.status === 'pending') {
delete reviewData.reviews[body.id];
} else {
@@ -77,22 +166,61 @@ export function worldsReviewPlugin({ root = process.cwd() } = {}) {
status: body.status,
reviewedBy: 'pbakaus',
reviewedAt: new Date().toISOString(),
formHash: conceptContentHash(match.concept),
...(note ? { note } : {}),
...(body.status === 'approved' && [1, 2, 3].includes(previousRating) ? { rating: previousRating } : {}),
};
}
if (previousStatus === 'approved' && body.status !== 'approved') assertApprovedFloor(catalog, reviewData);
reviewData.reviews = Object.fromEntries(Object.entries(reviewData.reviews).sort(([a], [b]) => a.localeCompare(b)));
assertValidCatalog(catalog, reviewData);
await writeJsonAtomic(reviewsPath, reviewData);
return { id: body.id, status: body.status, review: reviewData.reviews[body.id] || null };
}
if (body.action === 'strength') {
// Strength is curation metadata outside the content hash, so retyping a
// concept never invalidates its human review.
if (!CONCEPT_STRENGTHS.has(body.strength)) throw new Error('Strength must be world, composition, or dual');
match.concept.strength = body.strength;
catalog.catalogVersion = new Date().toISOString();
assertValidCatalog(catalog, reviewData);
await writeJsonAtomic(catalogPath, catalog);
return { id: body.id, strength: body.strength, catalogVersion: catalog.catalogVersion };
}
if (body.action === 'rate') {
// Rating grades an approved concept's strength (3 exceptional, 2 solid,
// 1 marginal keep) as a calibration signal. It lives on the review but
// stays outside the content hash and never changes status.
const review = reviewData.reviews[body.id];
if (review?.status !== 'approved') throw new Error('Rating only applies to approved concepts');
if (body.rating === null) {
delete review.rating;
} else if ([1, 2, 3].includes(body.rating)) {
review.rating = body.rating;
} else {
throw new Error('Rating must be 1, 2, or 3');
}
assertValidCatalog(catalog, reviewData);
await writeJsonAtomic(reviewsPath, reviewData);
return { id: body.id, rating: review.rating ?? null, review };
}
if (body.action === 'update') {
const form = typeof body.form === 'string' ? body.form.trim() : '';
const lineage = typeof body.lineage === 'string' ? body.lineage.trim() : '';
const targetFamily = catalog.families.find(family => family.id === body.familyId);
if (form.length < 12 || form.length > 600 || !form.includes(',')) {
throw new Error('Form must be 12600 characters and include inherited structure after a comma');
if (form.length < 40 || form.length > 360 || !form.includes(',')) {
throw new Error('Form must be 40360 characters and include inherited structure after a comma');
}
if (lineage.length < 2 || lineage.length > 160) throw new Error('Lineage must be 2160 characters');
if (lineage.length < 12 || lineage.length > 200) throw new Error('Lineage must be 12200 characters');
if (!validateTags(body.tags)) throw new Error('Exactly three structural tags are required');
if (!validateSystem(body.system)) throw new Error('Exactly five system grammar rules of 12180 characters are required');
const spark = typeof body.spark === 'string' ? body.spark.trim() : '';
if (spark.length < 80 || spark.length > 320) throw new Error('Creative spark must be 80320 characters');
const webLeverage = typeof body.webLeverage === 'string' ? body.webLeverage.trim() : '';
if (webLeverage.length < 20 || webLeverage.length > 240) throw new Error('Web leverage must be 20240 characters');
if (!targetFamily) throw new Error('Family was not found');
const updated = {
@@ -100,7 +228,19 @@ export function worldsReviewPlugin({ root = process.cwd() } = {}) {
form,
lineage,
tags: body.tags.map(tag => tag.trim()),
system: body.system.map(rule => rule.trim()),
spark,
webLeverage,
};
const existingForms = new Map();
for (const family of catalog.families) {
for (const concept of family.concepts) {
if (concept.id === body.id) continue;
existingForms.set(normalizeConceptForm(concept.form), concept.id);
}
}
const entryErrors = validateConceptEntry(updated, { existingForms });
if (entryErrors.length > 0) throw new Error(entryErrors[0]);
if (targetFamily.id === match.family.id) {
match.family.concepts[match.index] = updated;
} else {
@@ -109,8 +249,22 @@ export function worldsReviewPlugin({ root = process.cwd() } = {}) {
targetFamily.concepts.sort((a, b) => a.id.localeCompare(b.id));
}
catalog.catalogVersion = new Date().toISOString();
const previousStatus = reviewData.reviews[body.id]?.status || 'pending';
if (reviewData.reviews[body.id]) {
delete reviewData.reviews[body.id];
reviewData.reviews = Object.fromEntries(Object.entries(reviewData.reviews).sort(([a], [b]) => a.localeCompare(b)));
}
if (previousStatus === 'approved') assertApprovedFloor(catalog, reviewData);
assertValidCatalog(catalog, reviewData);
await writeJsonAtomic(catalogPath, catalog);
return { id: body.id, concept: updated, familyId: targetFamily.id };
if (previousStatus !== 'pending') await writeJsonAtomic(reviewsPath, reviewData);
return {
id: body.id,
concept: updated,
familyId: targetFamily.id,
status: 'pending',
catalogVersion: catalog.catalogVersion,
};
}
throw new Error('Action is invalid');