mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 22:26:38 +03:00
* Route the direction hand by verdict, add the pick card, enforce salience parity The decision round previously rendered every dealt challenger as an equal full card whatever the weighing said, so a world that fused poorly (an underwater world dealt to a flower shop) sat at the same visual weight as the assigned direction, and concept-level fusion had no surviving output. Three changes, all presentation-layer; the dice, the assignment, and the two-axis weighing are untouched: - Verdict routing: the weighing closes with wins / competitive / declined per challenger, decided before any borrowing. Declined challengers render demoted (narrow, quiet, catalog art as a labeled thumb, "Adopt anyway"), reordered to the end of the deck by the page itself, still adoptable, never silently dropped. Donations return as named "raised by" lines on the assigned card: a declined challenger donates ambition and system discipline, never its clothes. - The pick card: one card for the model's top-ranked grounded candidate when the dice assigned another, kicker MY PICK, honest familiarity risk on its face. One card, never a ranked list, never the lead position; the anti-menu rule survives with exactly this carve-out. - Salience parity: a card's imagery weight is capped by the assigned card's. With a text-only assigned card (no image generation in the harness), full-bleed catalog heroes demote to labeled thumbs, so what looks important is the verdict's call, never rendering luck. serve-question payload gains additive fields (verdict, kept, raised); old payloads render unchanged. concept-seed's rendered instructions carry the verdict/donation contract and the pick-card carve-out. Covered by two Playwright tests in the new-work e2e suite (verdict routing + parity). Design exploration and rationale were worked through with the maintainer; research grounding is impeccable.style/research lessons 3-5. AI-assisted change. Co-Authored-By: Claude Code <noreply@anthropic.com> * Add Safer/Bolder re-roll registers to the direction round The re-roll gains the user's steering wheel on the familiar-to-bold axis. The decision page renders two register buttons beside the plain re-roll (payload: reroll: { registers: ["safer", "bolder"] }; booleans still work), the answer carries the chosen register, and concept-seed gains --register. The design constraint that shaped the implementation: a register changes only what a round INSTRUCTS, never what it DEALT. The same key and reroll count reproduce the same deal whatever the register, so the exclusion chain never forks and the reproduction contract holds with no API change. - bolder: the dealt foreign forms become the whole hand, every challenger a full card; the first-dealt challenger leads (assignment by deal order, so the dice still choose). The pick card sits out; the canon stays. - safer: the round's dealt hand is spent unseen and stays excluded; the model presents its remaining conventional grounded candidates (at most three) plus the canon executed against named competitors. This is the one sanctioned lineup of the model's own ranked list, existing only by explicit user request. Works degraded (needs no catalog); bolder degrades to a plain grounded round, disclosed. Registers are user steering, never the model's to pre-select. Covered by a concept-seed unit test (same-deal invariant, validation) and a Playwright test (button, answer field, REGISTER directive). AI-assisted change. Co-Authored-By: Claude Code <noreply@anthropic.com> * Add the execution-contract round: comp-led or code-led, chosen after the direction The build previously went comp-led for everyone, silently: a generated comp led and the build chased it, which produces the boldest compositions and also the measured worst-of-both-worlds failure (ambitious design landed poorly, no motion, fix rounds after). Models already defect from it by quietly skipping comp generation, which is unsanctioned code-led with no contract to catch it. This makes the fork explicit and both paths defection-proof: - Comp-led: the comp is law and non-optional once chosen; visualize.md and the comp-is-king build phases run as today. - Code-led: no comp of this page, skipped by contract rather than drift. The QUALITY BAR boards still calibrate finish, and the ambition moves into the written direction contract (FIRST VIEWPORT plus a named signature interaction and motion grammar), audited by the finish reviewer in behavior. Not a discount on commitment. Placement: a second round on the same open table, right after the direction lands. Sketches stay in the direction round (they pick the world); comps are what code-led skips (they bind the composition). The chosen world sets the default lead; the user flips freely; a standing preference recorded in PRODUCT.md skips the round on later surfaces; with no image generation there is no fork, code-led is the only path. Mechanism: serve-question gains payload-level followup: true, which keeps the detached server alive after a pick (exactly like re-roll), swaps the page to the loading hand instead of goodbye, marks the answer with followup: true so --wait keeps the table, and prints a FOLLOWUP OPEN directive telling the agent to deliver the next round via --update. Covered by a Playwright test driving the full two-round flow. AI-assisted change. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix: address PR review bot findings - Degraded safer register no longer contradicts itself (greptile, Copilot, cursor): the degraded template previously said "the assigned index is suspended; the user picks" and then emitted ASSIGNED INDEX, the mandatory build instruction, and the restated footer anyway. The degraded safer path now suppresses the assignment machinery entirely, matching the non-degraded safer round, and restates the user-picks behavior for truncated readers instead. - A declined card's declared sketch no longer renders a full media face (Copilot): the renderer ignores sketch slots on declined cards outright, so a stray sketch cannot buy back the salience the verdict took away. - Bolder rounds no longer carry the generic weighing instruction (cursor): it measures against the assigned grounded direction, which the bolder register suspends; a leader-relative variant weighs the fused challengers against the first-dealt leader instead. All three pinned by new assertions in tests/concept-seed.test.mjs and tests/new-work-e2e.test.mjs. AI-assisted change. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix: followup never arms the loading hand in blocking serve mode cursor[bot] caught a client/server disagreement: the page interpolated its FOLLOWUP constant from the payload alone, so a followup: true payload served in blocking mode (no --start) would leave the browser on a loading hand that nothing resolves, since a blocking server exits on any pick and has no update channel. The page constant is now armed only when the server is detached, blocking rounds get the goodbye screen as before, and new-work.md states that followup belongs only on a detached round; blocking and structured-tool channels run the build-path round as its own second question. Pinned in tests/serve-question.test.mjs. AI-assisted change. Co-Authored-By: Claude Code <noreply@anthropic.com> * Add card-kind choice telemetry and the bolder routing disambiguation The choice ping previously fired only when a dealt catalog challenger won, so pick-share and canon-share had no denominator and the decision page's new spectrum could not be measured. The ping now fires once per resolved attended round on API-dealt rolls: --kind names which card class won (assigned / pick / challenger / canon), --chosen carries the catalog id only when a dealt challenger won, and --register rides along when the round came from a steered hand. Grounded candidates' names never leave the machine (the ping carries the kind alone), the legacy id-only shape stays valid, and DO_NOT_TRACK / IMPECCABLE_NO_TELEMETRY still disable the ping entirely. The seed's TELEMETRY block teaches the new invocation. Also the naming-collision guard: "bolder" said while a direction round is open routes to the Bolder hand register, never the bolder refinement command; one line each in bolder.md and new-work.md. The /api/chosen field additions land in a sister impeccable-site PR; the API ignores unknown fields meanwhile, so this is safe to ship first. AI-assisted change. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix: ping test survives a DO_NOT_TRACK shell cursor[bot]: the pingChosen unit test cleared only IMPECCABLE_NO_TELEMETRY, so a developer shell with DO_NOT_TRACK set failed the success-path assertions. The test now clears both, restores prior values in finally, and passes under DO_NOT_TRACK=1. AI-assisted change. Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: Claude Code <noreply@anthropic.com>
698 lines
36 KiB
JavaScript
698 lines
36 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* External concept seed: the dice half of new-work's complete-direction and
|
|
* established-world surface procedures.
|
|
*
|
|
* Before this script runs, the model retrieves cultural material and derives
|
|
* a grounded shortlist of complete candidate directions from it (see
|
|
* reference/new-work.md). Left alone, it then always builds its #1 —
|
|
* and a single model's resonance ranking is deterministic, so every run
|
|
* in a category ships the same one or two concepts. Measured: 30/35
|
|
* identical concepts across 16 prompt framings; the model cannot roll
|
|
* its own dice.
|
|
*
|
|
* This script rolls them from outside, the same trick that made the
|
|
* palette seed work:
|
|
* - ASSIGNED INDEX: which entry of the model's own resonance-ordered
|
|
* shortlist gets built. The assignment is the dice: it never chooses an
|
|
* ungrounded ingredient, it only refuses the argmax rut. Attended runs
|
|
* present the assigned direction and offer re-roll instead of a ranked
|
|
* lineup, because a lineup hands selection back to a taste function
|
|
* (model or user) and taste functions pick the safest card.
|
|
* - CHALLENGERS (6): outside forms from concept-ingredients.json, two from
|
|
* each challenger tier (graphic system, instrument language, atmosphere
|
|
* world), fused with the product first (challenger supplies form and
|
|
* system grammar, product supplies every fact, clarity wins conflicts),
|
|
* then weighed against the derived candidates on audience identification
|
|
* and product clarity. They win only when they beat the grounded list;
|
|
* measured behavior is that they lose to strong cultural material and
|
|
* win over thin categories, which is the intended shape.
|
|
* - RE-ROLL (--reroll <n>): round n of the same base key. The script
|
|
* recomputes what rounds 0..n-1 drew, excludes all of it, and rolls a
|
|
* fresh assigned index, challengers, and compositions. One base key therefore
|
|
* reproduces the entire chain of rounds.
|
|
* - REGISTER (--register safer|bolder): the user's steering on the
|
|
* familiar-to-bold axis, applied to a re-roll round. A register changes
|
|
* only what this round instructs, never what it dealt: the same key and
|
|
* reroll count reproduce the same deal whatever the register, so the
|
|
* exclusion chain never forks. bolder presents the dealt foreign forms
|
|
* as the whole hand (first-dealt leads, dice-assigned by deal order);
|
|
* safer spends the dealt hand unseen and presents the familiar register,
|
|
* the model's conventional grounded candidates plus the canon against
|
|
* named competitors, the one sanctioned lineup of the model's own list.
|
|
* Registers are user-requested, never pre-selected by the model.
|
|
* - RATINGS: the reviewer's approval ratings weight the challenger draw
|
|
* (3-star doubles the odds, 1-star sits out); the approved pool itself
|
|
* is unchanged.
|
|
*
|
|
* Usage:
|
|
* node scripts/concept-seed.mjs --scope direction --mode persuade
|
|
* node scripts/concept-seed.mjs --scope surface --mode operate --from <key>
|
|
* node scripts/concept-seed.mjs --scope surface --mode operate --grain flow
|
|
* node scripts/concept-seed.mjs --scope direction --candidate-count 6
|
|
* node scripts/concept-seed.mjs --scope direction --mode persuade --from <key> --reroll 1
|
|
* node scripts/concept-seed.mjs --scope direction --mode persuade --from <key> --reroll 1 --register bolder
|
|
* node scripts/concept-seed.mjs --chosen <challenger-id> --kind challenger --from <key> --scope direction
|
|
* node scripts/concept-seed.mjs --kind assigned --from <key> --scope direction
|
|
*
|
|
* --grain names how much of the product is in play: product, flow, view, or
|
|
* region. A docs site, an onboarding flow, a landing page and a data table are
|
|
* four different amounts of product and want different compositions. Grain is a
|
|
* preference: it deals matching compositions first and tops up from the rest of
|
|
* the register, and the rendered seed says how many actually matched so a
|
|
* borrowed structure is never mistaken for a supplied one.
|
|
*
|
|
* --platform names the delivery target (web, ios, android). Unlike grain this is
|
|
* a hard filter: a composition that needs hover or a pointer does not degrade on
|
|
* a phone, it stops working. --mode also gates which worlds are eligible, for
|
|
* worlds whose reviewer marked them as carrying only some modes.
|
|
*
|
|
* --mode names the requested surface's mode (persuade, operate, read,
|
|
* experience) so the appended compositions match its register of work; omitted,
|
|
* they roll from the full approved pool.
|
|
*
|
|
* Challenger data resolves in order: a local catalog directory (the private
|
|
* service repo, evals, and tests set IMPECCABLE_CATALOG_DIR), then the roll
|
|
* API at impeccable.style, then a degraded assignment-only seed when both are
|
|
* unavailable. The anonymous choice ping fires once per resolved attended
|
|
* round on API-dealt rolls: --kind names which card class won (assigned,
|
|
* pick, challenger, canon) so share metrics have a denominator, --chosen
|
|
* carries the catalog id when a dealt challenger won, and --register rides
|
|
* along when the round came from a steered hand. Grounded candidates' names
|
|
* never leave the machine. DO_NOT_TRACK or IMPECCABLE_NO_TELEMETRY disables
|
|
* the ping entirely.
|
|
*
|
|
* Env vars:
|
|
* IMPECCABLE_CONCEPT_SEED — same as --from; for reproducible eval runs.
|
|
* IMPECCABLE_CATALOG_DIR — directory holding the four catalog JSON files.
|
|
* IMPECCABLE_API_URL — roll API base (default https://impeccable.style/api).
|
|
* IMPECCABLE_NO_TELEMETRY — disables the choice ping (DO_NOT_TRACK also honored).
|
|
*/
|
|
|
|
import crypto from 'node:crypto';
|
|
import { dirname, join, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import {
|
|
approvedPoolRevision,
|
|
readConceptCatalog,
|
|
validateConceptCatalog,
|
|
WELL_TIERS,
|
|
} from './lib/concept-catalog.mjs';
|
|
import { readCompositionCatalog } from './lib/composition-catalog.mjs';
|
|
import {
|
|
COMPOSITION_GRAINS,
|
|
COMPOSITION_PLATFORMS,
|
|
runSyncSelection,
|
|
selectApprovedChallengers as selectApprovedChallengersCore,
|
|
selectApprovedCompositions as selectApprovedCompositionsCore,
|
|
} from './lib/roll-selection.mjs';
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
|
|
// Data resolution order: a local catalog (the private service repo, evals, and
|
|
// tests point IMPECCABLE_CATALOG_DIR at one), then the roll API, then a
|
|
// degraded assignment-only seed. The full catalog does not ship with the skill.
|
|
const CATALOG_DIR = process.env.IMPECCABLE_CATALOG_DIR || here;
|
|
const API_BASE = (process.env.IMPECCABLE_API_URL || 'https://impeccable.style/api').replace(/\/$/, '');
|
|
const API_TIMEOUT_MS = Number(process.env.IMPECCABLE_API_TIMEOUT || 4000);
|
|
// All API calls in one seed run share a single deadline so an unreachable
|
|
// network degrades after one timeout total, never one timeout per call.
|
|
let apiDeadline = null;
|
|
function apiBudgetMs() {
|
|
if (apiDeadline === null) apiDeadline = Date.now() + API_TIMEOUT_MS;
|
|
return Math.max(0, apiDeadline - Date.now());
|
|
}
|
|
|
|
const localStates = new Map();
|
|
function loadLocal(catalogDir = CATALOG_DIR) {
|
|
if (localStates.has(catalogDir)) return localStates.get(catalogDir);
|
|
let localState;
|
|
try {
|
|
const catalogState = readConceptCatalog(
|
|
join(catalogDir, 'concept-ingredients.json'),
|
|
join(catalogDir, 'concept-reviews.json')
|
|
);
|
|
const validation = validateConceptCatalog(catalogState.catalog, catalogState.reviewData);
|
|
if (validation.errors.length > 0) {
|
|
throw new Error(`invalid catalog: ${validation.errors.join('; ')}`);
|
|
}
|
|
const compositionState = readCompositionCatalog(
|
|
join(catalogDir, 'composition-ingredients.json'),
|
|
join(catalogDir, 'composition-reviews.json')
|
|
);
|
|
localState = {
|
|
concepts: catalogState.concepts,
|
|
compositions: compositionState.compositions,
|
|
};
|
|
} catch {
|
|
localState = null;
|
|
}
|
|
localStates.set(catalogDir, localState);
|
|
return localState;
|
|
}
|
|
|
|
function requireLocalConcepts() {
|
|
const local = loadLocal();
|
|
if (!local) {
|
|
throw new Error('concept-seed: no local catalog (set IMPECCABLE_CATALOG_DIR or pass sourceConcepts)');
|
|
}
|
|
return local;
|
|
}
|
|
|
|
async function fetchRoll({ scope, key, mode, grain, platform, reroll }) {
|
|
const params = new URLSearchParams({ scope, key, reroll: String(reroll) });
|
|
if (mode) params.set('mode', mode);
|
|
if (grain) params.set('grain', grain);
|
|
if (platform) params.set('platform', platform);
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), apiBudgetMs());
|
|
try {
|
|
// Race the budget explicitly: abort signals do not reliably cancel the
|
|
// TCP connect phase, so a blackholed route would otherwise stall ~10s.
|
|
const response = await Promise.race([
|
|
fetch(`${API_BASE}/roll?${params}`, { signal: controller.signal }),
|
|
new Promise(resolveTimeout => setTimeout(() => resolveTimeout(null), apiBudgetMs())),
|
|
]);
|
|
if (!response) return null;
|
|
if (!response.ok) return null;
|
|
const roll = await response.json();
|
|
if (!Array.isArray(roll.challengers) || roll.challengers.length === 0) return null;
|
|
return roll;
|
|
} catch {
|
|
return null;
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
function telemetryDisabled() {
|
|
return Boolean(process.env.IMPECCABLE_NO_TELEMETRY || process.env.DO_NOT_TRACK);
|
|
}
|
|
|
|
// Anonymous choice ping: one per resolved attended direction round. kind
|
|
// says which card class won (assigned / pick / challenger / canon), so
|
|
// pick-share and canon-share have a denominator; chosenId rides along only
|
|
// when a dealt catalog world won, and register only when the round came from
|
|
// a steered hand. Grounded candidates' names never leave the machine: they
|
|
// are derived from the user's project, so the ping carries the kind alone.
|
|
// Fire-and-forget; never fails the caller.
|
|
const PING_KINDS = new Set(['assigned', 'pick', 'challenger', 'canon']);
|
|
export async function pingChosen({ chosenId, key, scope, mode, kind, register }) {
|
|
if (telemetryDisabled()) return false;
|
|
if (kind && !PING_KINDS.has(kind)) return false;
|
|
if (register && register !== 'safer' && register !== 'bolder') return false;
|
|
// Legacy shape: a bare challenger id with no kind stays a valid ping.
|
|
if (!chosenId && !kind) return false;
|
|
if ((kind === 'challenger' || !kind) && !chosenId) return false;
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), apiBudgetMs());
|
|
try {
|
|
await fetch(`${API_BASE}/chosen`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
...(chosenId ? { chosenId } : {}),
|
|
key,
|
|
scope,
|
|
mode,
|
|
...(kind ? { kind } : {}),
|
|
...(register ? { register } : {}),
|
|
}),
|
|
signal: controller.signal,
|
|
});
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
const CARD_BASE = process.env.IMPECCABLE_CARD_BASE || 'https://impeccable.style/worlds/cards';
|
|
|
|
export function renderChallenger(concept, index) {
|
|
const system = concept.system.map(rule => ` - ${rule}`).join('\n');
|
|
const board = concept.cardBoard || `${CARD_BASE}/${concept.id}.webp`;
|
|
const hero = concept.cardHero || `${CARD_BASE}/${concept.id}-hero.webp`;
|
|
return ` ${index + 1}. ${concept.form}
|
|
SOURCE ID: ${concept.id}
|
|
CREATIVE SPARK: ${concept.spark}
|
|
SYSTEM GRAMMAR:
|
|
${system}
|
|
WEB LEVERAGE: ${concept.webLeverage}
|
|
QUALITY BAR: board ${board} · hero ${hero}`;
|
|
}
|
|
|
|
export function renderComposition(composition, index = null) {
|
|
const grammar = composition.grammar.map(rule => ` - ${rule}`).join('\n');
|
|
return ` ${index == null ? '' : `${index + 1}. `}${composition.form}
|
|
SOURCE ID: ${composition.id}
|
|
SPARK: ${composition.spark}
|
|
COMPOSITION GRAMMAR:
|
|
${grammar}
|
|
WEB LEVERAGE: ${composition.webLeverage}`;
|
|
}
|
|
|
|
// Selection itself lives in lib/roll-selection.mjs so this script and the roll
|
|
// API run one algorithm rather than two that drifted. These wrappers add only
|
|
// what is local to the skill: resolving the catalog when no pool is passed, and
|
|
// driving the generator with Node's synchronous hash, which keeps a local render
|
|
// synchronous for prepared eval sessions and tests.
|
|
function driveSelection(generator) {
|
|
return runSyncSelection(generator, input => crypto.createHash('sha256').update(input).digest('hex'));
|
|
}
|
|
|
|
export function dealCompositions({ scope, key, reroll = 0, mode = null, grain = null, platform = null, sourceCompositions = null, count = 3 }) {
|
|
const compositions = sourceCompositions ?? requireLocalConcepts().compositions;
|
|
return driveSelection(selectApprovedCompositionsCore({ scope, key, reroll, mode, grain, platform, compositions, count }));
|
|
}
|
|
|
|
// Array-returning form, which is what every caller wanted before the match
|
|
// report existed.
|
|
export function selectApprovedCompositions(options) {
|
|
return dealCompositions(options).picks;
|
|
}
|
|
|
|
// Compatibility for callers that need a single smoke-test sample.
|
|
export function selectApprovedComposition(options) {
|
|
return selectApprovedCompositions({ ...options, count: 1 })[0] ?? null;
|
|
}
|
|
|
|
export function selectApprovedChallengers({ scope, key, reroll = 0, mode = null, sourceConcepts = null }) {
|
|
const source = sourceConcepts ?? requireLocalConcepts().concepts;
|
|
const { approved, picks } = driveSelection(selectApprovedChallengersCore({ scope, key, reroll, mode, concepts: source }));
|
|
return {
|
|
approved,
|
|
picks,
|
|
poolRevision: approvedPoolRevision(source),
|
|
catalogCount: source.length,
|
|
};
|
|
}
|
|
|
|
const SEED_MODES = new Set(['persuade', 'operate', 'read', 'experience']);
|
|
|
|
export function renderConceptSeed({
|
|
scope = 'surface',
|
|
key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'),
|
|
reroll = 0,
|
|
register = null,
|
|
mode = null,
|
|
grain = null,
|
|
platform = null,
|
|
candidateCount = 7,
|
|
catalogDir = CATALOG_DIR,
|
|
_resolvedData = undefined,
|
|
} = {}) {
|
|
if (scope !== 'surface' && scope !== 'direction') {
|
|
throw new Error('concept-seed: --scope must be direction or surface');
|
|
}
|
|
if (!Number.isInteger(reroll) || reroll < 0) {
|
|
throw new Error('concept-seed: --reroll must be a non-negative integer');
|
|
}
|
|
if (register !== null && register !== 'safer' && register !== 'bolder') {
|
|
throw new Error('concept-seed: --register must be safer or bolder');
|
|
}
|
|
if (register !== null && reroll < 1) {
|
|
throw new Error('concept-seed: --register steers a re-roll round; pass --reroll <n> with it');
|
|
}
|
|
if (register !== null && scope !== 'direction') {
|
|
throw new Error('concept-seed: --register applies to direction rounds only');
|
|
}
|
|
if (mode !== null && !SEED_MODES.has(mode)) {
|
|
throw new Error('concept-seed: --mode must be persuade, operate, read, or experience');
|
|
}
|
|
// Grain needs no mode: how much of the product is in play is independent of
|
|
// which register of work it is.
|
|
if (grain !== null && !COMPOSITION_GRAINS.includes(grain)) {
|
|
throw new Error(`concept-seed: --grain must be one of ${COMPOSITION_GRAINS.join(', ')}`);
|
|
}
|
|
if (platform !== null && !COMPOSITION_PLATFORMS.includes(platform)) {
|
|
throw new Error(`concept-seed: --platform must be one of ${COMPOSITION_PLATFORMS.join(', ')}`);
|
|
}
|
|
if (!Number.isInteger(candidateCount) || candidateCount < 5 || candidateCount > 7) {
|
|
throw new Error('concept-seed: --candidate-count must be an integer from 5 to 7');
|
|
}
|
|
const unit = (salt) => {
|
|
const h = crypto.createHash('sha256').update(`${scope}:${salt}:${key}`).digest();
|
|
return h.readUInt32BE(0) / 0xffffffff;
|
|
};
|
|
const indexSalt = reroll === 0 ? 'index' : `index:reroll-${reroll}`;
|
|
const buildIndex = 3 + Math.floor(unit(indexSalt) * (candidateCount - 2)); // 3..candidateCount
|
|
|
|
// Local catalog first (private repo, evals, tests), then the roll API,
|
|
// then a degraded assignment-only seed. The assigned index is pure local
|
|
// math, so even a fully offline run keeps the anti-argmax mechanism.
|
|
let data = _resolvedData ?? null;
|
|
if (_resolvedData === undefined) {
|
|
const local = loadLocal(catalogDir);
|
|
if (local) {
|
|
const { approved, picks, poolRevision, catalogCount } = selectApprovedChallengers({
|
|
scope,
|
|
key,
|
|
reroll,
|
|
mode,
|
|
sourceConcepts: local.concepts,
|
|
});
|
|
data = {
|
|
source: 'local',
|
|
poolRevision,
|
|
approvedCount: approved.length,
|
|
catalogCount,
|
|
challengers: picks,
|
|
...(() => {
|
|
const dealt = dealCompositions({ scope, key, reroll, mode, grain, platform, sourceCompositions: local.compositions });
|
|
return { compositions: dealt.picks, compositionMatch: dealt.match };
|
|
})(),
|
|
};
|
|
} else {
|
|
// Keep local renders synchronous for prepared eval sessions and tests;
|
|
// installed skills without a bundled catalog resolve through the API.
|
|
return fetchRoll({ scope, key, mode, grain, platform, reroll }).then(roll => renderConceptSeed({
|
|
scope,
|
|
key,
|
|
reroll,
|
|
register,
|
|
mode,
|
|
grain,
|
|
platform,
|
|
candidateCount,
|
|
catalogDir,
|
|
_resolvedData: roll ? {
|
|
source: 'api',
|
|
poolRevision: roll.poolRevision,
|
|
approvedCount: roll.approvedCount,
|
|
catalogCount: roll.catalogCount,
|
|
challengers: roll.challengers,
|
|
compositions: Array.isArray(roll.compositions)
|
|
? roll.compositions
|
|
: Array.isArray(roll.stagings)
|
|
? roll.stagings
|
|
: roll.staging ? [roll.staging] : [],
|
|
} : null,
|
|
}));
|
|
}
|
|
}
|
|
|
|
const promotedInstruction = scope === 'direction'
|
|
? `After ordering the grounded directions 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: your top-ranked
|
|
direction is what every run would ship, so the script decides which grounded
|
|
direction gets built. Each direction joins a durable visual system to a
|
|
concrete expression for the requested first surface, decided as one. It must
|
|
survive the current task plus navigation, quiet and dense content,
|
|
interaction and state, and a substantially different future surface. In an
|
|
attended run, present the assigned direction fully committed and offer
|
|
re-roll. You may add ONE card for your top-ranked grounded candidate when
|
|
it is not the assigned direction, kicker MY PICK, with an honest risk line
|
|
naming its familiarity; one pick card, never a ranked lineup, and the pick
|
|
never takes the lead position. When the assignment IS your top candidate,
|
|
there is no pick card. Re-roll yourself only
|
|
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.`;
|
|
|
|
const challengerInstruction = scope === 'direction'
|
|
? `Fuse each challenger before judging it: the challenger supplies the form
|
|
and its system grammar, the product supplies every fact, and clarity wins
|
|
conflicts. Weigh the fused result against the assigned direction on exactly
|
|
two axes, audience identification and product clarity. Losing to strong
|
|
grounded material is a valid outcome; beating a thin or tool-monoculture
|
|
list is the point. A fused challenger that wins both axes becomes the build.
|
|
Close the weighing with a verdict per challenger, decided before any
|
|
borrowing is considered: wins (beats the assigned direction on both axes),
|
|
competitive (holds one axis), or declined (loses both). A declined
|
|
challenger is not spent: name the one discipline of its system the assigned
|
|
direction lacks, and raise the assigned direction to match before
|
|
presenting it. A donation transfers ambition and system discipline, never
|
|
the challenger's clothes; one world owns the page. Write each raise as its
|
|
own named line on the presented direction, and carry every verdict, kept
|
|
line, and raise into the decision page payload.`
|
|
: `A challenger wins only when its fused result beats the grounded list on
|
|
audience identification and product clarity. It may change task topology or
|
|
interaction, but never the committed visual identity.`;
|
|
|
|
const authorityInstruction = scope === 'direction'
|
|
? `PRODUCT.md and explicit incumbent brand commitments constrain every direction.
|
|
The seed never chooses exact colors, fonts, tokens, or a user preference, and
|
|
it never permits the world and first surface to be selected independently.`
|
|
: `PRODUCT.md and DESIGN.md constrain every surface candidate's identity
|
|
vocabulary; they do not cancel task-level composition. The seed never
|
|
authorizes a new palette, type system, material world, or unfamiliar control
|
|
behavior.`;
|
|
|
|
const richnessInstruction = `The CREATIVE SPARK is a complete visual system, not a theme or decorative
|
|
reference. Translate every supplied system rule into the product: palette and
|
|
material, type and composition, topology, controls and states, and adaptation.
|
|
Keep the source's visible character, scale, rhythm, and interaction instead of
|
|
reducing vivid grammar to generic nouns. When the source is already a credible
|
|
interface language, commit to it across navigation, content, controls, and
|
|
states. Otherwise keep a literal carrier only when it becomes functional.
|
|
Ambitious motion, spatial media, or interaction is welcome when it strengthens
|
|
the product without weakening semantics, performance, or fallback behavior.`;
|
|
|
|
if (!data) {
|
|
// A degraded roll can still serve the safer register, which needs no
|
|
// catalog at all: the assignment machinery is suppressed entirely, the
|
|
// same as the non-degraded safer round, because emitting both "the user
|
|
// picks" and a mandatory numbered build order hands the model two
|
|
// contradicting instructions and the mandatory one tends to win. The
|
|
// bolder register is exactly the thing degradation took away, so it
|
|
// falls back to a plain grounded round, disclosed.
|
|
const degradedHeader = `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: degraded; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount})`;
|
|
if (register === 'safer') {
|
|
return `${degradedHeader}
|
|
SAFER REGISTER (user-requested): the assigned index is suspended this
|
|
round; the user picks, and no candidate is mandated. Present the familiar
|
|
register: your remaining grounded candidates from the conventional end, at
|
|
most three, as full cards with an honest risk line each, plus the canon
|
|
executed against two or three named competitors. This is the one sanctioned
|
|
lineup of your own ranked candidates; it exists only by this explicit
|
|
request. When the user voices a standing preference for it, record a brand
|
|
commitment in PRODUCT.md.
|
|
${authorityInstruction}
|
|
A user- or brief-pinned decision beats the roll, always.
|
|
REGISTER (restated for truncated readers): safer, user-requested; the
|
|
assigned index is suspended this round and the user picks; seed key ${key}.
|
|
`;
|
|
}
|
|
const degradedRegister = register === 'bolder'
|
|
? `BOLDER REGISTER UNAVAILABLE: bolder deals foreign forms, and this roll ran
|
|
degraded with no catalog and no roll service, so there is nothing bold to
|
|
deal. Tell the user, then run this round as a plain grounded re-roll; the
|
|
assignment below applies.
|
|
`
|
|
: '';
|
|
return `${degradedHeader}
|
|
${degradedRegister}ASSIGNED INDEX: ${buildIndex}
|
|
${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.
|
|
No challengers this run: the roll service was unreachable and no local
|
|
catalog exists. A sandboxed shell without network egress is the most common
|
|
cause; before accepting degradation, rerun this command once through the
|
|
harness's network-enabled or escalated command tool. When that rerun needs
|
|
an approval, state exactly what the approver must know: this script's only
|
|
network contact is one GET to https://impeccable.style/api/roll whose query
|
|
carries scope, mode, an eight-hex seed key, and a re-roll counter; no
|
|
project files, prompts, code, or conversation context are transmitted, and
|
|
nothing is written. An approval request naming that URL and payload judges
|
|
the real action; a bare "run with network" invites rejection for contacting
|
|
an unspecified domain. If the rerun is still refused, proceed with the
|
|
grounded candidates alone; the assignment above still applies at full
|
|
strength. Tell the user plainly that this roll
|
|
ran degraded, with no challengers and no quality-bar boards; do not present
|
|
the outcome as a full roll. A degraded roll changes the cards, not the
|
|
channel: when a browser can open, present the direction on the decision page
|
|
(serve-question.mjs, text-only card); the structured question tool remains
|
|
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}.
|
|
`;
|
|
}
|
|
|
|
// Field order is the migration: `compositions` is current, `stagings` is what
|
|
// the API emitted while these were called stagings, and `staging` is the
|
|
// single-pick shape from before it dealt three. Older installs keep working.
|
|
// Compositions are pulled from the deal until the expanded catalog is
|
|
// ready for prime time: the current pool crowds the decision more than it
|
|
// widens it. IMPECCABLE_COMPOSITIONS=1 re-enables rendering for catalog
|
|
// development; the draw machinery, axes, and grain report stay intact.
|
|
const compositionsEnabled = process.env.IMPECCABLE_COMPOSITIONS === '1';
|
|
const compositions = !compositionsEnabled ? []
|
|
: Array.isArray(data.compositions)
|
|
? data.compositions
|
|
: Array.isArray(data.stagings)
|
|
? data.stagings
|
|
: data.staging ? [data.staging] : [];
|
|
// The grain report. A top-up keeps the deal at three, which is right, but it
|
|
// must not read as three on-target inputs: a flow request answered entirely by
|
|
// view-grain compositions means the model has to derive the flow's own
|
|
// structure and borrow only their sequence law. Silence here would reproduce
|
|
// the exact failure this axis exists to fix.
|
|
const match = data.compositionMatch ?? null;
|
|
const grainNote = (() => {
|
|
if (!match?.grain) return '';
|
|
if (match.grainAvailable === 0) {
|
|
return `\nNONE of these sit at the requested ${match.grain} grain, because the catalog holds no ${match.grain}-grain composition yet. Derive that structure yourself and borrow only their sequence and attention laws.`;
|
|
}
|
|
if (match.atGrain === 0) {
|
|
return `\nNONE of these sit at the requested ${match.grain} grain, though ${match.grainAvailable} exist; these were topped up from the rest of the register. Treat their structure as borrowed.`;
|
|
}
|
|
if (match.atGrain < compositions.length) {
|
|
return `\n${match.atGrain} of ${compositions.length} sit at the requested ${match.grain} grain; the rest were topped up from the register and their structure is borrowed.`;
|
|
}
|
|
return '';
|
|
})();
|
|
const compositionBlock = compositions.length > 0
|
|
? `\n${scope === 'direction' ? 'FIRST-SURFACE COMPOSITION INPUTS (identity-free; test them with shortlisted worlds and keep world plus composition one decision):' : 'COMPOSITION CHALLENGERS (identity-free; dress them in the committed visual identity before judging):'}
|
|
${compositions.map((composition, index) => renderComposition(composition, index)).join('\n')}
|
|
Each one asks the same question of this build: what is the cleverest way to
|
|
present, organize, or make interactive the problem in front of you? They carry
|
|
structure only, never a palette, typeface, or material. Treat them as serious
|
|
rivals to your habitual layout, and keep only what makes this product clearer.${grainNote}\n`
|
|
: '';
|
|
const rerollBlock = reroll > 0
|
|
? `RE-ROLL ROUND ${reroll}${register ? ` (${register.toUpperCase()} REGISTER, user-requested)` : ''}: every candidate presented in earlier rounds, grounded
|
|
and challenger alike, is eliminated and may not return reworded.${register ? '' : ` Derive
|
|
genuinely new grounded candidates from unexplored angles before judging
|
|
these fresh challengers.`}\n`
|
|
: '';
|
|
// A register swaps the round's presentation, never its deal: the assigned
|
|
// index and challenger fetch stay identical so the chain reproduces, and
|
|
// only the instructions change.
|
|
const saferBlock = `SAFER REGISTER: the user asked for the familiar end of the spectrum, so this
|
|
round's dealt hand is spent unseen, stays excluded from future rounds, and
|
|
is not printed. The assigned index is suspended this round; the user picks. Present the familiar register: your remaining grounded
|
|
candidates from the conventional end, at most three, as full cards with an
|
|
honest risk line each, plus the canon executed against two or three named
|
|
competitors. This is the one sanctioned lineup of your own ranked
|
|
candidates; it exists only by this explicit request. When the user voices a
|
|
standing preference for it, record a brand commitment in PRODUCT.md.`;
|
|
const bolderBlock = `BOLDER REGISTER: the user asked for foreign forms at full commitment, so no
|
|
grounded direction is presented this round and the assigned index is
|
|
suspended. The hand is every dealt challenger below, each fused with the
|
|
product and presented as a full card; the FIRST dealt challenger leads, an
|
|
assignment by deal order, so the dice still choose. Verdicts and donations
|
|
apply between the challengers, weighed against the leader. The pick card
|
|
sits out; the canon stays, as always.`;
|
|
const telemetryBlock = data.source === 'api'
|
|
? `TELEMETRY: after the user's choice resolves, rerun this script once with
|
|
--kind <assigned|pick|challenger|canon> --from ${key} --scope ${scope}${mode ? ` --mode ${mode}` : ''},
|
|
adding --chosen <challenger-id> when a dealt challenger won and keeping
|
|
--register <safer|bolder> when the resolved round came from a steered hand.
|
|
One ping per resolved attended round. The ping is anonymous, the card kind
|
|
plus the catalog id when one won; your grounded candidates' names never
|
|
leave the machine, and the ping is skipped automatically when DO_NOT_TRACK
|
|
or IMPECCABLE_NO_TELEMETRY is set.\n`
|
|
: '';
|
|
const assignedBlock = register === null
|
|
? `ASSIGNED INDEX: ${buildIndex}
|
|
${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.`
|
|
: register === 'safer' ? saferBlock : bolderBlock;
|
|
// A bolder round has no assigned grounded direction, so the generic
|
|
// weighing instruction (which measures against the assignment) would
|
|
// contradict the register; the bolder variant weighs against the leader.
|
|
const bolderChallengerInstruction = `Fuse each challenger before judging it: the challenger supplies the form
|
|
and its system grammar, the product supplies every fact, and clarity wins
|
|
conflicts. Weigh every fused challenger against the fused LEADER, the first
|
|
dealt, on exactly two axes, audience identification and product clarity;
|
|
verdicts and donations apply between the challengers, and one that beats
|
|
the leader on both axes presents as the hand's strongest alternate.`;
|
|
const roundChallengerInstruction = register === 'bolder' ? bolderChallengerInstruction : challengerInstruction;
|
|
const challengerSection = register === 'safer'
|
|
? ''
|
|
: `CHALLENGERS:
|
|
${data.challengers.map(renderChallenger).join('\n')}
|
|
${compositionBlock}${roundChallengerInstruction}
|
|
When you can view images, open the QUALITY BAR board and hero for any
|
|
challenger you weigh seriously and for the world you build. They exist as a
|
|
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
|
|
${buildIndex} of your own grounded list; 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)
|
|
${rerollBlock}${assignedBlock}
|
|
${challengerSection}${authorityInstruction}
|
|
${richnessInstruction}
|
|
${telemetryBlock}A user- or brief-pinned decision beats the roll, always.
|
|
${restated}
|
|
`;
|
|
}
|
|
|
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
const args = process.argv.slice(2);
|
|
const fromIdx = args.indexOf('--from');
|
|
const scopeIdx = args.indexOf('--scope');
|
|
const rerollIdx = args.indexOf('--reroll');
|
|
const registerIdx = args.indexOf('--register');
|
|
const modeIdx = args.indexOf('--mode');
|
|
const grainIdx = args.indexOf('--grain');
|
|
const platformIdx = args.indexOf('--platform');
|
|
const candidateCountIdx = args.indexOf('--candidate-count');
|
|
const chosenIdx = args.indexOf('--chosen');
|
|
const kindIdx = args.indexOf('--kind');
|
|
try {
|
|
if (chosenIdx !== -1 || kindIdx !== -1) {
|
|
// Choice ping: always exits 0, telemetry must never fail a design flow.
|
|
// --kind alone pings a non-challenger outcome (assigned/pick/canon);
|
|
// --chosen alone stays the legacy challenger-win ping.
|
|
const sent = await pingChosen({
|
|
chosenId: chosenIdx !== -1 ? args[chosenIdx + 1] : undefined,
|
|
key: fromIdx !== -1 ? args[fromIdx + 1] : undefined,
|
|
scope: scopeIdx !== -1 ? args[scopeIdx + 1] : undefined,
|
|
mode: modeIdx !== -1 ? args[modeIdx + 1] : undefined,
|
|
kind: kindIdx !== -1 ? args[kindIdx + 1] : undefined,
|
|
register: registerIdx !== -1 ? args[registerIdx + 1] : undefined,
|
|
});
|
|
process.stdout.write(sent ? 'choice recorded\n' : 'choice ping skipped\n');
|
|
} else {
|
|
// Mechanical init gate: prose alone does not keep a model from dealing
|
|
// before init, and fresh repos produced exactly that skip (the model
|
|
// rolled directions with no PRODUCT.md, so nothing grounded the fusion).
|
|
// The --chosen branch above stays ungated; telemetry never blocks.
|
|
const { loadContext } = await import('./context.mjs');
|
|
if (!loadContext(process.cwd()).hasProduct) {
|
|
process.stdout.write([
|
|
'NO_PRODUCT_MD: the dice stay in the cup until product truth exists.',
|
|
'Complete the init ask round and write PRODUCT.md first (reference/init.md), then re-run this exact command.',
|
|
'Challengers fuse their form with facts from PRODUCT.md; without it every direction is ungrounded.',
|
|
].join(' ') + '\n');
|
|
process.exit(1);
|
|
}
|
|
process.stdout.write(await renderConceptSeed({
|
|
scope: scopeIdx !== -1 ? args[scopeIdx + 1] : 'surface',
|
|
key: fromIdx !== -1
|
|
? args[fromIdx + 1]
|
|
: (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')),
|
|
reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0,
|
|
register: registerIdx !== -1 ? args[registerIdx + 1] : null,
|
|
mode: modeIdx !== -1 ? args[modeIdx + 1] : null,
|
|
grain: grainIdx !== -1 ? args[grainIdx + 1] : null,
|
|
platform: platformIdx !== -1 ? args[platformIdx + 1] : null,
|
|
candidateCount: candidateCountIdx !== -1 ? Number(args[candidateCountIdx + 1]) : 7,
|
|
}));
|
|
}
|
|
} catch (error) {
|
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
process.exitCode = 1;
|
|
}
|
|
// A raced-out fetch may still hold a socket; exit explicitly so the CLI
|
|
// never lingers on a dead network path after output is written.
|
|
process.exit(process.exitCode ?? 0);
|
|
}
|