From 7c32d42055e525befa1090c1b1705467a1163f45 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 23:27:30 +0000 Subject: [PATCH] Sync generated provider output --- .../impeccable/scripts/concept-seed.mjs | 54 +++++++++++-------- .../scripts/lib/composition-catalog.mjs | 6 +-- .../scripts/lib/concept-catalog.mjs | 18 +++---- .../impeccable/scripts/lib/roll-selection.mjs | 18 ++++--- .../impeccable/scripts/concept-seed.mjs | 54 +++++++++++-------- .../scripts/lib/composition-catalog.mjs | 6 +-- .../scripts/lib/concept-catalog.mjs | 18 +++---- .../impeccable/scripts/lib/roll-selection.mjs | 18 ++++--- .../impeccable/scripts/concept-seed.mjs | 54 +++++++++++-------- .../scripts/lib/composition-catalog.mjs | 6 +-- .../scripts/lib/concept-catalog.mjs | 18 +++---- .../impeccable/scripts/lib/roll-selection.mjs | 18 ++++--- .../impeccable/scripts/concept-seed.mjs | 54 +++++++++++-------- .../scripts/lib/composition-catalog.mjs | 6 +-- .../scripts/lib/concept-catalog.mjs | 18 +++---- .../impeccable/scripts/lib/roll-selection.mjs | 18 ++++--- .../impeccable/scripts/concept-seed.mjs | 54 +++++++++++-------- .../scripts/lib/composition-catalog.mjs | 6 +-- .../scripts/lib/concept-catalog.mjs | 18 +++---- .../impeccable/scripts/lib/roll-selection.mjs | 18 ++++--- .../impeccable/scripts/concept-seed.mjs | 54 +++++++++++-------- .../scripts/lib/composition-catalog.mjs | 6 +-- .../scripts/lib/concept-catalog.mjs | 18 +++---- .../impeccable/scripts/lib/roll-selection.mjs | 18 ++++--- .../impeccable/scripts/concept-seed.mjs | 54 +++++++++++-------- .../scripts/lib/composition-catalog.mjs | 6 +-- .../scripts/lib/concept-catalog.mjs | 18 +++---- .../impeccable/scripts/lib/roll-selection.mjs | 18 ++++--- .../impeccable/scripts/concept-seed.mjs | 54 +++++++++++-------- .../scripts/lib/composition-catalog.mjs | 6 +-- .../scripts/lib/concept-catalog.mjs | 18 +++---- .../impeccable/scripts/lib/roll-selection.mjs | 18 ++++--- .../impeccable/scripts/concept-seed.mjs | 54 +++++++++++-------- .../scripts/lib/composition-catalog.mjs | 6 +-- .../scripts/lib/concept-catalog.mjs | 18 +++---- .../impeccable/scripts/lib/roll-selection.mjs | 18 ++++--- .../impeccable/scripts/concept-seed.mjs | 54 +++++++++++-------- .../scripts/lib/composition-catalog.mjs | 6 +-- .../scripts/lib/concept-catalog.mjs | 18 +++---- .../impeccable/scripts/lib/roll-selection.mjs | 18 ++++--- .../impeccable/scripts/concept-seed.mjs | 54 +++++++++++-------- .../scripts/lib/composition-catalog.mjs | 6 +-- .../scripts/lib/concept-catalog.mjs | 18 +++---- .../impeccable/scripts/lib/roll-selection.mjs | 18 ++++--- .../impeccable/scripts/concept-seed.mjs | 54 +++++++++++-------- .../scripts/lib/composition-catalog.mjs | 6 +-- .../scripts/lib/concept-catalog.mjs | 18 +++---- .../impeccable/scripts/lib/roll-selection.mjs | 18 ++++--- .../impeccable/scripts/concept-seed.mjs | 54 +++++++++++-------- .../scripts/lib/composition-catalog.mjs | 6 +-- .../scripts/lib/concept-catalog.mjs | 18 +++---- .../impeccable/scripts/lib/roll-selection.mjs | 18 ++++--- .../impeccable/scripts/concept-seed.mjs | 54 +++++++++++-------- .../scripts/lib/composition-catalog.mjs | 6 +-- .../scripts/lib/concept-catalog.mjs | 18 +++---- .../impeccable/scripts/lib/roll-selection.mjs | 18 ++++--- .../impeccable/scripts/concept-seed.mjs | 54 +++++++++++-------- .../scripts/lib/composition-catalog.mjs | 6 +-- .../scripts/lib/concept-catalog.mjs | 18 +++---- .../impeccable/scripts/lib/roll-selection.mjs | 18 ++++--- 60 files changed, 810 insertions(+), 630 deletions(-) diff --git a/.agents/skills/impeccable/scripts/concept-seed.mjs b/.agents/skills/impeccable/scripts/concept-seed.mjs index 00a2f73fe..6efb7d643 100644 --- a/.agents/skills/impeccable/scripts/concept-seed.mjs +++ b/.agents/skills/impeccable/scripts/concept-seed.mjs @@ -29,7 +29,7 @@ * win over thin categories, which is the intended shape. * - RE-ROLL (--reroll ): 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 staging. One base key therefore + * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself @@ -43,8 +43,8 @@ * node scripts/concept-seed.mjs --chosen --from --scope direction * * --mode names the requested surface's mode (persuade, operate, read, - * experience) so the appended staging matches its register of work; omitted, - * the staging rolls from the full approved pool. + * 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 @@ -72,7 +72,7 @@ import { readCompositionCatalog } from './lib/composition-catalog.mjs'; import { runSyncSelection, selectApprovedChallengers as selectApprovedChallengersCore, - selectApprovedStagings as selectApprovedStagingsCore, + selectApprovedCompositions as selectApprovedCompositionsCore, } from './lib/roll-selection.mjs'; const here = dirname(fileURLToPath(import.meta.url)); @@ -191,12 +191,12 @@ ${system} QUALITY BAR: board ${board} · hero ${hero}`; } -export function renderStaging(composition, index = null) { +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} - STAGING GRAMMAR: + COMPOSITION GRAMMAR: ${grammar} WEB LEVERAGE: ${composition.webLeverage}`; } @@ -210,14 +210,14 @@ function driveSelection(generator) { return runSyncSelection(generator, input => crypto.createHash('sha256').update(input).digest('hex')); } -export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { +export function selectApprovedCompositions({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { const compositions = sourceCompositions ?? requireLocalConcepts().compositions; - return driveSelection(selectApprovedStagingsCore({ scope, key, reroll, mode, compositions, count })); + return driveSelection(selectApprovedCompositionsCore({ scope, key, reroll, mode, compositions, count })); } // Compatibility for callers that need a single smoke-test sample. -export function selectApprovedStaging(options) { - return selectApprovedStagings({ ...options, count: 1 })[0] ?? null; +export function selectApprovedComposition(options) { + return selectApprovedCompositions({ ...options, count: 1 })[0] ?? null; } export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcepts = null }) { @@ -280,7 +280,7 @@ export function renderConceptSeed({ approvedCount: approved.length, catalogCount, challengers: picks, - stagings: selectApprovedStagings({ scope, key, reroll, mode, sourceCompositions: local.compositions }), + compositions: selectApprovedCompositions({ scope, key, reroll, mode, sourceCompositions: local.compositions }), }; } else { // Keep local renders synchronous for prepared eval sessions and tests; @@ -298,7 +298,11 @@ export function renderConceptSeed({ approvedCount: roll.approvedCount, catalogCount: roll.catalogCount, challengers: roll.challengers, - stagings: Array.isArray(roll.stagings) ? roll.stagings : roll.staging ? [roll.staging] : [], + compositions: Array.isArray(roll.compositions) + ? roll.compositions + : Array.isArray(roll.stagings) + ? roll.stagings + : roll.staging ? [roll.staging] : [], } : null, })); } @@ -381,15 +385,21 @@ ${buildIndex} of your own grounded list; seed key ${key}. `; } - const stagings = Array.isArray(data.stagings) - ? data.stagings - : data.staging ? [data.staging] : []; - const stagingBlock = stagings.length > 0 - ? `\n${scope === 'direction' ? 'FIRST-SURFACE STAGING INPUTS (identity-free; test them with shortlisted worlds and keep world plus staging one decision):' : 'STAGING CHALLENGERS (identity-free; dress them in the committed visual identity before judging):'} -${stagings.map((staging, index) => renderStaging(staging, index)).join('\n')} -Stagings organize attention, sequence, and manipulation; they never bring a -palette, typeface, or material. Use them as serious alternatives to the model's -habitual composition, but keep only structures that strengthen this product.\n` + // 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. + const compositions = Array.isArray(data.compositions) + ? data.compositions + : Array.isArray(data.stagings) + ? data.stagings + : data.staging ? [data.staging] : []; + 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.\n` : ''; const rerollBlock = reroll > 0 ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded @@ -410,7 +420,7 @@ ${rerollBlock}ASSIGNED INDEX: ${buildIndex} the user or the brief. Never expose assignment metadata in user-facing labels. CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${stagingBlock}${challengerInstruction} +${compositionBlock}${challengerInstruction} 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, diff --git a/.agents/skills/impeccable/scripts/lib/composition-catalog.mjs b/.agents/skills/impeccable/scripts/lib/composition-catalog.mjs index 0b0c2b5fa..349cb4913 100644 --- a/.agents/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.agents/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -2,7 +2,7 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; import { CONCEPT_STATUSES, normalizeConceptForm } from './concept-catalog.mjs'; -// Catalog B: stagings rather than styles. A composition organizes attention, +// Catalog B: compositions rather than styles. A composition organizes attention, // sequence, or manipulation on a surface and must survive being dressed in // any committed visual identity; it deliberately carries no palette or type // half. Surface-scope seeds draw from here (plus catalog A duals); direction @@ -15,8 +15,8 @@ export const COMPOSITION_GRAMMAR_PREFIXES = [ 'Adaptation:', ]; -// Surfaces align with the skill's modes: a persuade staging and an operate -// staging are different species, and read/experience surfaces get their own. +// Surfaces align with the skill's modes: a persuade composition and an operate +// composition are different species, and read/experience surfaces get their own. export const COMPOSITION_SURFACES = new Set(['persuade', 'operate', 'read', 'experience']); export function compositionContentHash(composition) { diff --git a/.agents/skills/impeccable/scripts/lib/concept-catalog.mjs b/.agents/skills/impeccable/scripts/lib/concept-catalog.mjs index 5e4a24e3c..42c828c74 100644 --- a/.agents/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.agents/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -1,11 +1,12 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; +import { WELL_TIERS } from './roll-selection.mjs'; export const CONCEPT_STATUSES = new Set(['approved', 'rejected']); // What a concept is actually strong at. Worlds carry a durable visual // identity (their palette/type half is the magnet); compositions carry a -// staging or interaction idea (their topology half is the magnet) that can be +// composition or interaction idea (their topology half is the magnet) that can be // dressed in any committed identity; duals fuse both inseparably. Direction // seeds draw world|dual, surface seeds draw composition|dual. export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); @@ -15,7 +16,13 @@ export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); // atmosphere worlds need the largest translation step. Every seed roll draws // one challenger from each tier so at least one directly-usable graphic // system is always on the table. -export const WELL_TIERS = ['graphic', 'interaction', 'atmosphere']; +// Defined in roll-selection.mjs, the dependency-free leaf both the seeder and +// the roll API import. It cannot depend on this file: this one reads the +// filesystem, and a Pages Function must not pull node:fs into its bundle. +// Imported and re-exported rather than re-exported alone: a bare +// `export { X } from` does not bind X in this module's own scope, and +// validateConceptCatalog needs it. +export { WELL_TIERS }; const WEB_LEVERAGE_RE = /(?:\b3d\b|\badaptive\b|\banimat(?:e|ed|ion)\b|\bapi\b|\baria\b|\baudio\b|\bautomated?\b|\bbarcode\b|\bbroadcastchannel\b|\bbrowser\b|\bcamera\b|canvas\b|\bcaption\b|\bcollaborat(?:e|ive|ion)\b|\bcompar(?:e|ison)\b|\bcomput(?:e|ed|ation)\b|\bcomputer[- ]vision\b|\bconstraint[- ]solving\b|\bcryptographic?\b|\bcss\b|\bdeep[- ]link(?:ing)?\b|\bdirect manipulation\b|\bdom\b|\bdrag\b|\bfilter\b|\bfocus\b|\bgenerative\b|\bgeolocat(?:e|ed|ion)\b|\bgesture\b|\bgpu\b|\bgraph\b|\bhistory\b|\bindexeddb\b|\binteractive\b|\bintersectionobserver\b|\bkeyboard\b|\blive\b|\blocal\b|\bmicrophone\b|\bmotion\b|\bmultiplayer\b|\bnative\b|\bnotification\b|\boffline\b|\bpersonaliz(?:e|ed|ation)\b|\bplayable\b|\bpointer\b|\bprocedural\b|\bprovenance\b|\breal[- ]?time\b|\bresizeobserver\b|\bresponsive\b|\breveal\b|\bscrub\b|\bsearch\b|\bsearchparams\b|\bsensor\b|\bserver[- ]sent\b|\bservice worker\b|\bshader\b|\bsimulat(?:e|ed|ion|or)\b|\bspatial\b|\bstate\b|\bstream(?:ing)?\b|\bsvg\b|\bsynchroniz(?:e|ed|ation)\b|\btimeline\b|\btouch\b|\burl|\bvideo\b|\bweb(?:gl|socket|vtt)?\b|\bworker\b|\bzoom\b)/i; export const SYSTEM_PREFIXES = [ @@ -320,10 +327,3 @@ export function approvedPoolRevision(concepts) { return crypto.createHash('sha256').update(payload).digest('hex').slice(0, 12); } -export function deterministicRank(items, input, idFor = item => item.id) { - return [...items].sort((a, b) => { - const scoreA = crypto.createHash('sha256').update(`${input}:${idFor(a)}`).digest('hex'); - const scoreB = crypto.createHash('sha256').update(`${input}:${idFor(b)}`).digest('hex'); - return scoreB.localeCompare(scoreA) || idFor(a).localeCompare(idFor(b)); - }); -} diff --git a/.agents/skills/impeccable/scripts/lib/roll-selection.mjs b/.agents/skills/impeccable/scripts/lib/roll-selection.mjs index c6829dd9e..f579029e4 100644 --- a/.agents/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.agents/skills/impeccable/scripts/lib/roll-selection.mjs @@ -97,7 +97,7 @@ function compositionTickets(pool) { export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = null, concepts }) { const approved = concepts.filter(concept => concept.status === 'approved'); // Direction chooses a durable identity, so it draws worlds; surface designs - // one page inside a committed identity, so it draws stagings. Duals serve + // one page inside a committed identity, so it draws compositions. Duals serve // both. A tier with no matching-strength approvals falls back to its full // approved pool rather than starving the roll. const wanted = scope === 'direction' @@ -176,12 +176,12 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = } /** - * Three identity-free staging inputs from an explicit approved pool. + * Three identity-free composition inputs from an explicit approved pool. * Drive with runSyncSelection or runAsyncSelection. * * One input was too weak a counterweight to a model's habitual page skeleton: * it became a single optional flourish beside six identity challengers rather - * than a real search over composition. Distinct staging families are preferred + * than a real search over composition. Distinct composition families are preferred * so a roll tests materially different hierarchy, sequence, and interaction * laws. Cross-mode fallback would make the input misleading, so an absent mode * returns nothing rather than borrowing. Re-rolls exclude every earlier set @@ -196,11 +196,11 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = * @param {number} [options.count] * @returns {Generator} */ -export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { - // Stagings honour the same breadth gate as worlds: one too specific to serve +export function* selectApprovedCompositions({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { + // Compositions honour the same breadth gate as worlds: one too specific to serve // an arbitrary build stays approved for direct briefs and leaves the // challenger pool. Falls back to the full approved set rather than returning - // nothing if every approved staging is niche. + // nothing if every approved composition is niche. let approved = compositions.filter(composition => composition.status === 'approved'); const broad = approved.filter(composition => composition.review?.breadth !== 'niche'); if (broad.length > 0) approved = broad; @@ -218,15 +218,17 @@ export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, c const base = available.length >= Math.min(count, approved.length) ? available : approved; // Rating weights the draw as it does for worlds. It matters more here // because the per-surface pools are small, so an unweighted shuffle repeats - // a weak staging far more often. Each ticket carries its index so the rank + // a weak composition far more often. Each ticket carries its index so the rank // sees a distinct key per ticket: ranking bare duplicates would hash // identically and the pick loop's id-dedupe would silently discard the // second copy, making the weighting a no-op. let tickets = compositionTickets(base); - // A pool of nothing but 1-star keeps still has to yield stagings. + // A pool of nothing but 1-star keeps still has to yield compositions. if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = (yield* rank( tickets, + // The salt keeps the word "staging" deliberately. It is hash input, so + // renaming it would re-deal every roll anyone has ever reproduced by key. round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, entry => `${entry.composition.id}#${entry.ticket}` )).map(entry => entry.composition); diff --git a/.claude/skills/impeccable/scripts/concept-seed.mjs b/.claude/skills/impeccable/scripts/concept-seed.mjs index 00a2f73fe..6efb7d643 100644 --- a/.claude/skills/impeccable/scripts/concept-seed.mjs +++ b/.claude/skills/impeccable/scripts/concept-seed.mjs @@ -29,7 +29,7 @@ * win over thin categories, which is the intended shape. * - RE-ROLL (--reroll ): 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 staging. One base key therefore + * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself @@ -43,8 +43,8 @@ * node scripts/concept-seed.mjs --chosen --from --scope direction * * --mode names the requested surface's mode (persuade, operate, read, - * experience) so the appended staging matches its register of work; omitted, - * the staging rolls from the full approved pool. + * 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 @@ -72,7 +72,7 @@ import { readCompositionCatalog } from './lib/composition-catalog.mjs'; import { runSyncSelection, selectApprovedChallengers as selectApprovedChallengersCore, - selectApprovedStagings as selectApprovedStagingsCore, + selectApprovedCompositions as selectApprovedCompositionsCore, } from './lib/roll-selection.mjs'; const here = dirname(fileURLToPath(import.meta.url)); @@ -191,12 +191,12 @@ ${system} QUALITY BAR: board ${board} · hero ${hero}`; } -export function renderStaging(composition, index = null) { +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} - STAGING GRAMMAR: + COMPOSITION GRAMMAR: ${grammar} WEB LEVERAGE: ${composition.webLeverage}`; } @@ -210,14 +210,14 @@ function driveSelection(generator) { return runSyncSelection(generator, input => crypto.createHash('sha256').update(input).digest('hex')); } -export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { +export function selectApprovedCompositions({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { const compositions = sourceCompositions ?? requireLocalConcepts().compositions; - return driveSelection(selectApprovedStagingsCore({ scope, key, reroll, mode, compositions, count })); + return driveSelection(selectApprovedCompositionsCore({ scope, key, reroll, mode, compositions, count })); } // Compatibility for callers that need a single smoke-test sample. -export function selectApprovedStaging(options) { - return selectApprovedStagings({ ...options, count: 1 })[0] ?? null; +export function selectApprovedComposition(options) { + return selectApprovedCompositions({ ...options, count: 1 })[0] ?? null; } export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcepts = null }) { @@ -280,7 +280,7 @@ export function renderConceptSeed({ approvedCount: approved.length, catalogCount, challengers: picks, - stagings: selectApprovedStagings({ scope, key, reroll, mode, sourceCompositions: local.compositions }), + compositions: selectApprovedCompositions({ scope, key, reroll, mode, sourceCompositions: local.compositions }), }; } else { // Keep local renders synchronous for prepared eval sessions and tests; @@ -298,7 +298,11 @@ export function renderConceptSeed({ approvedCount: roll.approvedCount, catalogCount: roll.catalogCount, challengers: roll.challengers, - stagings: Array.isArray(roll.stagings) ? roll.stagings : roll.staging ? [roll.staging] : [], + compositions: Array.isArray(roll.compositions) + ? roll.compositions + : Array.isArray(roll.stagings) + ? roll.stagings + : roll.staging ? [roll.staging] : [], } : null, })); } @@ -381,15 +385,21 @@ ${buildIndex} of your own grounded list; seed key ${key}. `; } - const stagings = Array.isArray(data.stagings) - ? data.stagings - : data.staging ? [data.staging] : []; - const stagingBlock = stagings.length > 0 - ? `\n${scope === 'direction' ? 'FIRST-SURFACE STAGING INPUTS (identity-free; test them with shortlisted worlds and keep world plus staging one decision):' : 'STAGING CHALLENGERS (identity-free; dress them in the committed visual identity before judging):'} -${stagings.map((staging, index) => renderStaging(staging, index)).join('\n')} -Stagings organize attention, sequence, and manipulation; they never bring a -palette, typeface, or material. Use them as serious alternatives to the model's -habitual composition, but keep only structures that strengthen this product.\n` + // 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. + const compositions = Array.isArray(data.compositions) + ? data.compositions + : Array.isArray(data.stagings) + ? data.stagings + : data.staging ? [data.staging] : []; + 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.\n` : ''; const rerollBlock = reroll > 0 ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded @@ -410,7 +420,7 @@ ${rerollBlock}ASSIGNED INDEX: ${buildIndex} the user or the brief. Never expose assignment metadata in user-facing labels. CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${stagingBlock}${challengerInstruction} +${compositionBlock}${challengerInstruction} 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, diff --git a/.claude/skills/impeccable/scripts/lib/composition-catalog.mjs b/.claude/skills/impeccable/scripts/lib/composition-catalog.mjs index 0b0c2b5fa..349cb4913 100644 --- a/.claude/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.claude/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -2,7 +2,7 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; import { CONCEPT_STATUSES, normalizeConceptForm } from './concept-catalog.mjs'; -// Catalog B: stagings rather than styles. A composition organizes attention, +// Catalog B: compositions rather than styles. A composition organizes attention, // sequence, or manipulation on a surface and must survive being dressed in // any committed visual identity; it deliberately carries no palette or type // half. Surface-scope seeds draw from here (plus catalog A duals); direction @@ -15,8 +15,8 @@ export const COMPOSITION_GRAMMAR_PREFIXES = [ 'Adaptation:', ]; -// Surfaces align with the skill's modes: a persuade staging and an operate -// staging are different species, and read/experience surfaces get their own. +// Surfaces align with the skill's modes: a persuade composition and an operate +// composition are different species, and read/experience surfaces get their own. export const COMPOSITION_SURFACES = new Set(['persuade', 'operate', 'read', 'experience']); export function compositionContentHash(composition) { diff --git a/.claude/skills/impeccable/scripts/lib/concept-catalog.mjs b/.claude/skills/impeccable/scripts/lib/concept-catalog.mjs index 5e4a24e3c..42c828c74 100644 --- a/.claude/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.claude/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -1,11 +1,12 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; +import { WELL_TIERS } from './roll-selection.mjs'; export const CONCEPT_STATUSES = new Set(['approved', 'rejected']); // What a concept is actually strong at. Worlds carry a durable visual // identity (their palette/type half is the magnet); compositions carry a -// staging or interaction idea (their topology half is the magnet) that can be +// composition or interaction idea (their topology half is the magnet) that can be // dressed in any committed identity; duals fuse both inseparably. Direction // seeds draw world|dual, surface seeds draw composition|dual. export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); @@ -15,7 +16,13 @@ export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); // atmosphere worlds need the largest translation step. Every seed roll draws // one challenger from each tier so at least one directly-usable graphic // system is always on the table. -export const WELL_TIERS = ['graphic', 'interaction', 'atmosphere']; +// Defined in roll-selection.mjs, the dependency-free leaf both the seeder and +// the roll API import. It cannot depend on this file: this one reads the +// filesystem, and a Pages Function must not pull node:fs into its bundle. +// Imported and re-exported rather than re-exported alone: a bare +// `export { X } from` does not bind X in this module's own scope, and +// validateConceptCatalog needs it. +export { WELL_TIERS }; const WEB_LEVERAGE_RE = /(?:\b3d\b|\badaptive\b|\banimat(?:e|ed|ion)\b|\bapi\b|\baria\b|\baudio\b|\bautomated?\b|\bbarcode\b|\bbroadcastchannel\b|\bbrowser\b|\bcamera\b|canvas\b|\bcaption\b|\bcollaborat(?:e|ive|ion)\b|\bcompar(?:e|ison)\b|\bcomput(?:e|ed|ation)\b|\bcomputer[- ]vision\b|\bconstraint[- ]solving\b|\bcryptographic?\b|\bcss\b|\bdeep[- ]link(?:ing)?\b|\bdirect manipulation\b|\bdom\b|\bdrag\b|\bfilter\b|\bfocus\b|\bgenerative\b|\bgeolocat(?:e|ed|ion)\b|\bgesture\b|\bgpu\b|\bgraph\b|\bhistory\b|\bindexeddb\b|\binteractive\b|\bintersectionobserver\b|\bkeyboard\b|\blive\b|\blocal\b|\bmicrophone\b|\bmotion\b|\bmultiplayer\b|\bnative\b|\bnotification\b|\boffline\b|\bpersonaliz(?:e|ed|ation)\b|\bplayable\b|\bpointer\b|\bprocedural\b|\bprovenance\b|\breal[- ]?time\b|\bresizeobserver\b|\bresponsive\b|\breveal\b|\bscrub\b|\bsearch\b|\bsearchparams\b|\bsensor\b|\bserver[- ]sent\b|\bservice worker\b|\bshader\b|\bsimulat(?:e|ed|ion|or)\b|\bspatial\b|\bstate\b|\bstream(?:ing)?\b|\bsvg\b|\bsynchroniz(?:e|ed|ation)\b|\btimeline\b|\btouch\b|\burl|\bvideo\b|\bweb(?:gl|socket|vtt)?\b|\bworker\b|\bzoom\b)/i; export const SYSTEM_PREFIXES = [ @@ -320,10 +327,3 @@ export function approvedPoolRevision(concepts) { return crypto.createHash('sha256').update(payload).digest('hex').slice(0, 12); } -export function deterministicRank(items, input, idFor = item => item.id) { - return [...items].sort((a, b) => { - const scoreA = crypto.createHash('sha256').update(`${input}:${idFor(a)}`).digest('hex'); - const scoreB = crypto.createHash('sha256').update(`${input}:${idFor(b)}`).digest('hex'); - return scoreB.localeCompare(scoreA) || idFor(a).localeCompare(idFor(b)); - }); -} diff --git a/.claude/skills/impeccable/scripts/lib/roll-selection.mjs b/.claude/skills/impeccable/scripts/lib/roll-selection.mjs index c6829dd9e..f579029e4 100644 --- a/.claude/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.claude/skills/impeccable/scripts/lib/roll-selection.mjs @@ -97,7 +97,7 @@ function compositionTickets(pool) { export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = null, concepts }) { const approved = concepts.filter(concept => concept.status === 'approved'); // Direction chooses a durable identity, so it draws worlds; surface designs - // one page inside a committed identity, so it draws stagings. Duals serve + // one page inside a committed identity, so it draws compositions. Duals serve // both. A tier with no matching-strength approvals falls back to its full // approved pool rather than starving the roll. const wanted = scope === 'direction' @@ -176,12 +176,12 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = } /** - * Three identity-free staging inputs from an explicit approved pool. + * Three identity-free composition inputs from an explicit approved pool. * Drive with runSyncSelection or runAsyncSelection. * * One input was too weak a counterweight to a model's habitual page skeleton: * it became a single optional flourish beside six identity challengers rather - * than a real search over composition. Distinct staging families are preferred + * than a real search over composition. Distinct composition families are preferred * so a roll tests materially different hierarchy, sequence, and interaction * laws. Cross-mode fallback would make the input misleading, so an absent mode * returns nothing rather than borrowing. Re-rolls exclude every earlier set @@ -196,11 +196,11 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = * @param {number} [options.count] * @returns {Generator} */ -export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { - // Stagings honour the same breadth gate as worlds: one too specific to serve +export function* selectApprovedCompositions({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { + // Compositions honour the same breadth gate as worlds: one too specific to serve // an arbitrary build stays approved for direct briefs and leaves the // challenger pool. Falls back to the full approved set rather than returning - // nothing if every approved staging is niche. + // nothing if every approved composition is niche. let approved = compositions.filter(composition => composition.status === 'approved'); const broad = approved.filter(composition => composition.review?.breadth !== 'niche'); if (broad.length > 0) approved = broad; @@ -218,15 +218,17 @@ export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, c const base = available.length >= Math.min(count, approved.length) ? available : approved; // Rating weights the draw as it does for worlds. It matters more here // because the per-surface pools are small, so an unweighted shuffle repeats - // a weak staging far more often. Each ticket carries its index so the rank + // a weak composition far more often. Each ticket carries its index so the rank // sees a distinct key per ticket: ranking bare duplicates would hash // identically and the pick loop's id-dedupe would silently discard the // second copy, making the weighting a no-op. let tickets = compositionTickets(base); - // A pool of nothing but 1-star keeps still has to yield stagings. + // A pool of nothing but 1-star keeps still has to yield compositions. if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = (yield* rank( tickets, + // The salt keeps the word "staging" deliberately. It is hash input, so + // renaming it would re-deal every roll anyone has ever reproduced by key. round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, entry => `${entry.composition.id}#${entry.ticket}` )).map(entry => entry.composition); diff --git a/.cursor/skills/impeccable/scripts/concept-seed.mjs b/.cursor/skills/impeccable/scripts/concept-seed.mjs index 00a2f73fe..6efb7d643 100644 --- a/.cursor/skills/impeccable/scripts/concept-seed.mjs +++ b/.cursor/skills/impeccable/scripts/concept-seed.mjs @@ -29,7 +29,7 @@ * win over thin categories, which is the intended shape. * - RE-ROLL (--reroll ): 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 staging. One base key therefore + * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself @@ -43,8 +43,8 @@ * node scripts/concept-seed.mjs --chosen --from --scope direction * * --mode names the requested surface's mode (persuade, operate, read, - * experience) so the appended staging matches its register of work; omitted, - * the staging rolls from the full approved pool. + * 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 @@ -72,7 +72,7 @@ import { readCompositionCatalog } from './lib/composition-catalog.mjs'; import { runSyncSelection, selectApprovedChallengers as selectApprovedChallengersCore, - selectApprovedStagings as selectApprovedStagingsCore, + selectApprovedCompositions as selectApprovedCompositionsCore, } from './lib/roll-selection.mjs'; const here = dirname(fileURLToPath(import.meta.url)); @@ -191,12 +191,12 @@ ${system} QUALITY BAR: board ${board} · hero ${hero}`; } -export function renderStaging(composition, index = null) { +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} - STAGING GRAMMAR: + COMPOSITION GRAMMAR: ${grammar} WEB LEVERAGE: ${composition.webLeverage}`; } @@ -210,14 +210,14 @@ function driveSelection(generator) { return runSyncSelection(generator, input => crypto.createHash('sha256').update(input).digest('hex')); } -export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { +export function selectApprovedCompositions({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { const compositions = sourceCompositions ?? requireLocalConcepts().compositions; - return driveSelection(selectApprovedStagingsCore({ scope, key, reroll, mode, compositions, count })); + return driveSelection(selectApprovedCompositionsCore({ scope, key, reroll, mode, compositions, count })); } // Compatibility for callers that need a single smoke-test sample. -export function selectApprovedStaging(options) { - return selectApprovedStagings({ ...options, count: 1 })[0] ?? null; +export function selectApprovedComposition(options) { + return selectApprovedCompositions({ ...options, count: 1 })[0] ?? null; } export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcepts = null }) { @@ -280,7 +280,7 @@ export function renderConceptSeed({ approvedCount: approved.length, catalogCount, challengers: picks, - stagings: selectApprovedStagings({ scope, key, reroll, mode, sourceCompositions: local.compositions }), + compositions: selectApprovedCompositions({ scope, key, reroll, mode, sourceCompositions: local.compositions }), }; } else { // Keep local renders synchronous for prepared eval sessions and tests; @@ -298,7 +298,11 @@ export function renderConceptSeed({ approvedCount: roll.approvedCount, catalogCount: roll.catalogCount, challengers: roll.challengers, - stagings: Array.isArray(roll.stagings) ? roll.stagings : roll.staging ? [roll.staging] : [], + compositions: Array.isArray(roll.compositions) + ? roll.compositions + : Array.isArray(roll.stagings) + ? roll.stagings + : roll.staging ? [roll.staging] : [], } : null, })); } @@ -381,15 +385,21 @@ ${buildIndex} of your own grounded list; seed key ${key}. `; } - const stagings = Array.isArray(data.stagings) - ? data.stagings - : data.staging ? [data.staging] : []; - const stagingBlock = stagings.length > 0 - ? `\n${scope === 'direction' ? 'FIRST-SURFACE STAGING INPUTS (identity-free; test them with shortlisted worlds and keep world plus staging one decision):' : 'STAGING CHALLENGERS (identity-free; dress them in the committed visual identity before judging):'} -${stagings.map((staging, index) => renderStaging(staging, index)).join('\n')} -Stagings organize attention, sequence, and manipulation; they never bring a -palette, typeface, or material. Use them as serious alternatives to the model's -habitual composition, but keep only structures that strengthen this product.\n` + // 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. + const compositions = Array.isArray(data.compositions) + ? data.compositions + : Array.isArray(data.stagings) + ? data.stagings + : data.staging ? [data.staging] : []; + 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.\n` : ''; const rerollBlock = reroll > 0 ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded @@ -410,7 +420,7 @@ ${rerollBlock}ASSIGNED INDEX: ${buildIndex} the user or the brief. Never expose assignment metadata in user-facing labels. CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${stagingBlock}${challengerInstruction} +${compositionBlock}${challengerInstruction} 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, diff --git a/.cursor/skills/impeccable/scripts/lib/composition-catalog.mjs b/.cursor/skills/impeccable/scripts/lib/composition-catalog.mjs index 0b0c2b5fa..349cb4913 100644 --- a/.cursor/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.cursor/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -2,7 +2,7 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; import { CONCEPT_STATUSES, normalizeConceptForm } from './concept-catalog.mjs'; -// Catalog B: stagings rather than styles. A composition organizes attention, +// Catalog B: compositions rather than styles. A composition organizes attention, // sequence, or manipulation on a surface and must survive being dressed in // any committed visual identity; it deliberately carries no palette or type // half. Surface-scope seeds draw from here (plus catalog A duals); direction @@ -15,8 +15,8 @@ export const COMPOSITION_GRAMMAR_PREFIXES = [ 'Adaptation:', ]; -// Surfaces align with the skill's modes: a persuade staging and an operate -// staging are different species, and read/experience surfaces get their own. +// Surfaces align with the skill's modes: a persuade composition and an operate +// composition are different species, and read/experience surfaces get their own. export const COMPOSITION_SURFACES = new Set(['persuade', 'operate', 'read', 'experience']); export function compositionContentHash(composition) { diff --git a/.cursor/skills/impeccable/scripts/lib/concept-catalog.mjs b/.cursor/skills/impeccable/scripts/lib/concept-catalog.mjs index 5e4a24e3c..42c828c74 100644 --- a/.cursor/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.cursor/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -1,11 +1,12 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; +import { WELL_TIERS } from './roll-selection.mjs'; export const CONCEPT_STATUSES = new Set(['approved', 'rejected']); // What a concept is actually strong at. Worlds carry a durable visual // identity (their palette/type half is the magnet); compositions carry a -// staging or interaction idea (their topology half is the magnet) that can be +// composition or interaction idea (their topology half is the magnet) that can be // dressed in any committed identity; duals fuse both inseparably. Direction // seeds draw world|dual, surface seeds draw composition|dual. export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); @@ -15,7 +16,13 @@ export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); // atmosphere worlds need the largest translation step. Every seed roll draws // one challenger from each tier so at least one directly-usable graphic // system is always on the table. -export const WELL_TIERS = ['graphic', 'interaction', 'atmosphere']; +// Defined in roll-selection.mjs, the dependency-free leaf both the seeder and +// the roll API import. It cannot depend on this file: this one reads the +// filesystem, and a Pages Function must not pull node:fs into its bundle. +// Imported and re-exported rather than re-exported alone: a bare +// `export { X } from` does not bind X in this module's own scope, and +// validateConceptCatalog needs it. +export { WELL_TIERS }; const WEB_LEVERAGE_RE = /(?:\b3d\b|\badaptive\b|\banimat(?:e|ed|ion)\b|\bapi\b|\baria\b|\baudio\b|\bautomated?\b|\bbarcode\b|\bbroadcastchannel\b|\bbrowser\b|\bcamera\b|canvas\b|\bcaption\b|\bcollaborat(?:e|ive|ion)\b|\bcompar(?:e|ison)\b|\bcomput(?:e|ed|ation)\b|\bcomputer[- ]vision\b|\bconstraint[- ]solving\b|\bcryptographic?\b|\bcss\b|\bdeep[- ]link(?:ing)?\b|\bdirect manipulation\b|\bdom\b|\bdrag\b|\bfilter\b|\bfocus\b|\bgenerative\b|\bgeolocat(?:e|ed|ion)\b|\bgesture\b|\bgpu\b|\bgraph\b|\bhistory\b|\bindexeddb\b|\binteractive\b|\bintersectionobserver\b|\bkeyboard\b|\blive\b|\blocal\b|\bmicrophone\b|\bmotion\b|\bmultiplayer\b|\bnative\b|\bnotification\b|\boffline\b|\bpersonaliz(?:e|ed|ation)\b|\bplayable\b|\bpointer\b|\bprocedural\b|\bprovenance\b|\breal[- ]?time\b|\bresizeobserver\b|\bresponsive\b|\breveal\b|\bscrub\b|\bsearch\b|\bsearchparams\b|\bsensor\b|\bserver[- ]sent\b|\bservice worker\b|\bshader\b|\bsimulat(?:e|ed|ion|or)\b|\bspatial\b|\bstate\b|\bstream(?:ing)?\b|\bsvg\b|\bsynchroniz(?:e|ed|ation)\b|\btimeline\b|\btouch\b|\burl|\bvideo\b|\bweb(?:gl|socket|vtt)?\b|\bworker\b|\bzoom\b)/i; export const SYSTEM_PREFIXES = [ @@ -320,10 +327,3 @@ export function approvedPoolRevision(concepts) { return crypto.createHash('sha256').update(payload).digest('hex').slice(0, 12); } -export function deterministicRank(items, input, idFor = item => item.id) { - return [...items].sort((a, b) => { - const scoreA = crypto.createHash('sha256').update(`${input}:${idFor(a)}`).digest('hex'); - const scoreB = crypto.createHash('sha256').update(`${input}:${idFor(b)}`).digest('hex'); - return scoreB.localeCompare(scoreA) || idFor(a).localeCompare(idFor(b)); - }); -} diff --git a/.cursor/skills/impeccable/scripts/lib/roll-selection.mjs b/.cursor/skills/impeccable/scripts/lib/roll-selection.mjs index c6829dd9e..f579029e4 100644 --- a/.cursor/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.cursor/skills/impeccable/scripts/lib/roll-selection.mjs @@ -97,7 +97,7 @@ function compositionTickets(pool) { export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = null, concepts }) { const approved = concepts.filter(concept => concept.status === 'approved'); // Direction chooses a durable identity, so it draws worlds; surface designs - // one page inside a committed identity, so it draws stagings. Duals serve + // one page inside a committed identity, so it draws compositions. Duals serve // both. A tier with no matching-strength approvals falls back to its full // approved pool rather than starving the roll. const wanted = scope === 'direction' @@ -176,12 +176,12 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = } /** - * Three identity-free staging inputs from an explicit approved pool. + * Three identity-free composition inputs from an explicit approved pool. * Drive with runSyncSelection or runAsyncSelection. * * One input was too weak a counterweight to a model's habitual page skeleton: * it became a single optional flourish beside six identity challengers rather - * than a real search over composition. Distinct staging families are preferred + * than a real search over composition. Distinct composition families are preferred * so a roll tests materially different hierarchy, sequence, and interaction * laws. Cross-mode fallback would make the input misleading, so an absent mode * returns nothing rather than borrowing. Re-rolls exclude every earlier set @@ -196,11 +196,11 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = * @param {number} [options.count] * @returns {Generator} */ -export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { - // Stagings honour the same breadth gate as worlds: one too specific to serve +export function* selectApprovedCompositions({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { + // Compositions honour the same breadth gate as worlds: one too specific to serve // an arbitrary build stays approved for direct briefs and leaves the // challenger pool. Falls back to the full approved set rather than returning - // nothing if every approved staging is niche. + // nothing if every approved composition is niche. let approved = compositions.filter(composition => composition.status === 'approved'); const broad = approved.filter(composition => composition.review?.breadth !== 'niche'); if (broad.length > 0) approved = broad; @@ -218,15 +218,17 @@ export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, c const base = available.length >= Math.min(count, approved.length) ? available : approved; // Rating weights the draw as it does for worlds. It matters more here // because the per-surface pools are small, so an unweighted shuffle repeats - // a weak staging far more often. Each ticket carries its index so the rank + // a weak composition far more often. Each ticket carries its index so the rank // sees a distinct key per ticket: ranking bare duplicates would hash // identically and the pick loop's id-dedupe would silently discard the // second copy, making the weighting a no-op. let tickets = compositionTickets(base); - // A pool of nothing but 1-star keeps still has to yield stagings. + // A pool of nothing but 1-star keeps still has to yield compositions. if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = (yield* rank( tickets, + // The salt keeps the word "staging" deliberately. It is hash input, so + // renaming it would re-deal every roll anyone has ever reproduced by key. round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, entry => `${entry.composition.id}#${entry.ticket}` )).map(entry => entry.composition); diff --git a/.gemini/skills/impeccable/scripts/concept-seed.mjs b/.gemini/skills/impeccable/scripts/concept-seed.mjs index 00a2f73fe..6efb7d643 100644 --- a/.gemini/skills/impeccable/scripts/concept-seed.mjs +++ b/.gemini/skills/impeccable/scripts/concept-seed.mjs @@ -29,7 +29,7 @@ * win over thin categories, which is the intended shape. * - RE-ROLL (--reroll ): 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 staging. One base key therefore + * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself @@ -43,8 +43,8 @@ * node scripts/concept-seed.mjs --chosen --from --scope direction * * --mode names the requested surface's mode (persuade, operate, read, - * experience) so the appended staging matches its register of work; omitted, - * the staging rolls from the full approved pool. + * 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 @@ -72,7 +72,7 @@ import { readCompositionCatalog } from './lib/composition-catalog.mjs'; import { runSyncSelection, selectApprovedChallengers as selectApprovedChallengersCore, - selectApprovedStagings as selectApprovedStagingsCore, + selectApprovedCompositions as selectApprovedCompositionsCore, } from './lib/roll-selection.mjs'; const here = dirname(fileURLToPath(import.meta.url)); @@ -191,12 +191,12 @@ ${system} QUALITY BAR: board ${board} · hero ${hero}`; } -export function renderStaging(composition, index = null) { +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} - STAGING GRAMMAR: + COMPOSITION GRAMMAR: ${grammar} WEB LEVERAGE: ${composition.webLeverage}`; } @@ -210,14 +210,14 @@ function driveSelection(generator) { return runSyncSelection(generator, input => crypto.createHash('sha256').update(input).digest('hex')); } -export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { +export function selectApprovedCompositions({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { const compositions = sourceCompositions ?? requireLocalConcepts().compositions; - return driveSelection(selectApprovedStagingsCore({ scope, key, reroll, mode, compositions, count })); + return driveSelection(selectApprovedCompositionsCore({ scope, key, reroll, mode, compositions, count })); } // Compatibility for callers that need a single smoke-test sample. -export function selectApprovedStaging(options) { - return selectApprovedStagings({ ...options, count: 1 })[0] ?? null; +export function selectApprovedComposition(options) { + return selectApprovedCompositions({ ...options, count: 1 })[0] ?? null; } export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcepts = null }) { @@ -280,7 +280,7 @@ export function renderConceptSeed({ approvedCount: approved.length, catalogCount, challengers: picks, - stagings: selectApprovedStagings({ scope, key, reroll, mode, sourceCompositions: local.compositions }), + compositions: selectApprovedCompositions({ scope, key, reroll, mode, sourceCompositions: local.compositions }), }; } else { // Keep local renders synchronous for prepared eval sessions and tests; @@ -298,7 +298,11 @@ export function renderConceptSeed({ approvedCount: roll.approvedCount, catalogCount: roll.catalogCount, challengers: roll.challengers, - stagings: Array.isArray(roll.stagings) ? roll.stagings : roll.staging ? [roll.staging] : [], + compositions: Array.isArray(roll.compositions) + ? roll.compositions + : Array.isArray(roll.stagings) + ? roll.stagings + : roll.staging ? [roll.staging] : [], } : null, })); } @@ -381,15 +385,21 @@ ${buildIndex} of your own grounded list; seed key ${key}. `; } - const stagings = Array.isArray(data.stagings) - ? data.stagings - : data.staging ? [data.staging] : []; - const stagingBlock = stagings.length > 0 - ? `\n${scope === 'direction' ? 'FIRST-SURFACE STAGING INPUTS (identity-free; test them with shortlisted worlds and keep world plus staging one decision):' : 'STAGING CHALLENGERS (identity-free; dress them in the committed visual identity before judging):'} -${stagings.map((staging, index) => renderStaging(staging, index)).join('\n')} -Stagings organize attention, sequence, and manipulation; they never bring a -palette, typeface, or material. Use them as serious alternatives to the model's -habitual composition, but keep only structures that strengthen this product.\n` + // 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. + const compositions = Array.isArray(data.compositions) + ? data.compositions + : Array.isArray(data.stagings) + ? data.stagings + : data.staging ? [data.staging] : []; + 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.\n` : ''; const rerollBlock = reroll > 0 ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded @@ -410,7 +420,7 @@ ${rerollBlock}ASSIGNED INDEX: ${buildIndex} the user or the brief. Never expose assignment metadata in user-facing labels. CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${stagingBlock}${challengerInstruction} +${compositionBlock}${challengerInstruction} 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, diff --git a/.gemini/skills/impeccable/scripts/lib/composition-catalog.mjs b/.gemini/skills/impeccable/scripts/lib/composition-catalog.mjs index 0b0c2b5fa..349cb4913 100644 --- a/.gemini/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.gemini/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -2,7 +2,7 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; import { CONCEPT_STATUSES, normalizeConceptForm } from './concept-catalog.mjs'; -// Catalog B: stagings rather than styles. A composition organizes attention, +// Catalog B: compositions rather than styles. A composition organizes attention, // sequence, or manipulation on a surface and must survive being dressed in // any committed visual identity; it deliberately carries no palette or type // half. Surface-scope seeds draw from here (plus catalog A duals); direction @@ -15,8 +15,8 @@ export const COMPOSITION_GRAMMAR_PREFIXES = [ 'Adaptation:', ]; -// Surfaces align with the skill's modes: a persuade staging and an operate -// staging are different species, and read/experience surfaces get their own. +// Surfaces align with the skill's modes: a persuade composition and an operate +// composition are different species, and read/experience surfaces get their own. export const COMPOSITION_SURFACES = new Set(['persuade', 'operate', 'read', 'experience']); export function compositionContentHash(composition) { diff --git a/.gemini/skills/impeccable/scripts/lib/concept-catalog.mjs b/.gemini/skills/impeccable/scripts/lib/concept-catalog.mjs index 5e4a24e3c..42c828c74 100644 --- a/.gemini/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.gemini/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -1,11 +1,12 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; +import { WELL_TIERS } from './roll-selection.mjs'; export const CONCEPT_STATUSES = new Set(['approved', 'rejected']); // What a concept is actually strong at. Worlds carry a durable visual // identity (their palette/type half is the magnet); compositions carry a -// staging or interaction idea (their topology half is the magnet) that can be +// composition or interaction idea (their topology half is the magnet) that can be // dressed in any committed identity; duals fuse both inseparably. Direction // seeds draw world|dual, surface seeds draw composition|dual. export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); @@ -15,7 +16,13 @@ export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); // atmosphere worlds need the largest translation step. Every seed roll draws // one challenger from each tier so at least one directly-usable graphic // system is always on the table. -export const WELL_TIERS = ['graphic', 'interaction', 'atmosphere']; +// Defined in roll-selection.mjs, the dependency-free leaf both the seeder and +// the roll API import. It cannot depend on this file: this one reads the +// filesystem, and a Pages Function must not pull node:fs into its bundle. +// Imported and re-exported rather than re-exported alone: a bare +// `export { X } from` does not bind X in this module's own scope, and +// validateConceptCatalog needs it. +export { WELL_TIERS }; const WEB_LEVERAGE_RE = /(?:\b3d\b|\badaptive\b|\banimat(?:e|ed|ion)\b|\bapi\b|\baria\b|\baudio\b|\bautomated?\b|\bbarcode\b|\bbroadcastchannel\b|\bbrowser\b|\bcamera\b|canvas\b|\bcaption\b|\bcollaborat(?:e|ive|ion)\b|\bcompar(?:e|ison)\b|\bcomput(?:e|ed|ation)\b|\bcomputer[- ]vision\b|\bconstraint[- ]solving\b|\bcryptographic?\b|\bcss\b|\bdeep[- ]link(?:ing)?\b|\bdirect manipulation\b|\bdom\b|\bdrag\b|\bfilter\b|\bfocus\b|\bgenerative\b|\bgeolocat(?:e|ed|ion)\b|\bgesture\b|\bgpu\b|\bgraph\b|\bhistory\b|\bindexeddb\b|\binteractive\b|\bintersectionobserver\b|\bkeyboard\b|\blive\b|\blocal\b|\bmicrophone\b|\bmotion\b|\bmultiplayer\b|\bnative\b|\bnotification\b|\boffline\b|\bpersonaliz(?:e|ed|ation)\b|\bplayable\b|\bpointer\b|\bprocedural\b|\bprovenance\b|\breal[- ]?time\b|\bresizeobserver\b|\bresponsive\b|\breveal\b|\bscrub\b|\bsearch\b|\bsearchparams\b|\bsensor\b|\bserver[- ]sent\b|\bservice worker\b|\bshader\b|\bsimulat(?:e|ed|ion|or)\b|\bspatial\b|\bstate\b|\bstream(?:ing)?\b|\bsvg\b|\bsynchroniz(?:e|ed|ation)\b|\btimeline\b|\btouch\b|\burl|\bvideo\b|\bweb(?:gl|socket|vtt)?\b|\bworker\b|\bzoom\b)/i; export const SYSTEM_PREFIXES = [ @@ -320,10 +327,3 @@ export function approvedPoolRevision(concepts) { return crypto.createHash('sha256').update(payload).digest('hex').slice(0, 12); } -export function deterministicRank(items, input, idFor = item => item.id) { - return [...items].sort((a, b) => { - const scoreA = crypto.createHash('sha256').update(`${input}:${idFor(a)}`).digest('hex'); - const scoreB = crypto.createHash('sha256').update(`${input}:${idFor(b)}`).digest('hex'); - return scoreB.localeCompare(scoreA) || idFor(a).localeCompare(idFor(b)); - }); -} diff --git a/.gemini/skills/impeccable/scripts/lib/roll-selection.mjs b/.gemini/skills/impeccable/scripts/lib/roll-selection.mjs index c6829dd9e..f579029e4 100644 --- a/.gemini/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.gemini/skills/impeccable/scripts/lib/roll-selection.mjs @@ -97,7 +97,7 @@ function compositionTickets(pool) { export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = null, concepts }) { const approved = concepts.filter(concept => concept.status === 'approved'); // Direction chooses a durable identity, so it draws worlds; surface designs - // one page inside a committed identity, so it draws stagings. Duals serve + // one page inside a committed identity, so it draws compositions. Duals serve // both. A tier with no matching-strength approvals falls back to its full // approved pool rather than starving the roll. const wanted = scope === 'direction' @@ -176,12 +176,12 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = } /** - * Three identity-free staging inputs from an explicit approved pool. + * Three identity-free composition inputs from an explicit approved pool. * Drive with runSyncSelection or runAsyncSelection. * * One input was too weak a counterweight to a model's habitual page skeleton: * it became a single optional flourish beside six identity challengers rather - * than a real search over composition. Distinct staging families are preferred + * than a real search over composition. Distinct composition families are preferred * so a roll tests materially different hierarchy, sequence, and interaction * laws. Cross-mode fallback would make the input misleading, so an absent mode * returns nothing rather than borrowing. Re-rolls exclude every earlier set @@ -196,11 +196,11 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = * @param {number} [options.count] * @returns {Generator} */ -export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { - // Stagings honour the same breadth gate as worlds: one too specific to serve +export function* selectApprovedCompositions({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { + // Compositions honour the same breadth gate as worlds: one too specific to serve // an arbitrary build stays approved for direct briefs and leaves the // challenger pool. Falls back to the full approved set rather than returning - // nothing if every approved staging is niche. + // nothing if every approved composition is niche. let approved = compositions.filter(composition => composition.status === 'approved'); const broad = approved.filter(composition => composition.review?.breadth !== 'niche'); if (broad.length > 0) approved = broad; @@ -218,15 +218,17 @@ export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, c const base = available.length >= Math.min(count, approved.length) ? available : approved; // Rating weights the draw as it does for worlds. It matters more here // because the per-surface pools are small, so an unweighted shuffle repeats - // a weak staging far more often. Each ticket carries its index so the rank + // a weak composition far more often. Each ticket carries its index so the rank // sees a distinct key per ticket: ranking bare duplicates would hash // identically and the pick loop's id-dedupe would silently discard the // second copy, making the weighting a no-op. let tickets = compositionTickets(base); - // A pool of nothing but 1-star keeps still has to yield stagings. + // A pool of nothing but 1-star keeps still has to yield compositions. if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = (yield* rank( tickets, + // The salt keeps the word "staging" deliberately. It is hash input, so + // renaming it would re-deal every roll anyone has ever reproduced by key. round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, entry => `${entry.composition.id}#${entry.ticket}` )).map(entry => entry.composition); diff --git a/.github/skills/impeccable/scripts/concept-seed.mjs b/.github/skills/impeccable/scripts/concept-seed.mjs index 00a2f73fe..6efb7d643 100644 --- a/.github/skills/impeccable/scripts/concept-seed.mjs +++ b/.github/skills/impeccable/scripts/concept-seed.mjs @@ -29,7 +29,7 @@ * win over thin categories, which is the intended shape. * - RE-ROLL (--reroll ): 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 staging. One base key therefore + * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself @@ -43,8 +43,8 @@ * node scripts/concept-seed.mjs --chosen --from --scope direction * * --mode names the requested surface's mode (persuade, operate, read, - * experience) so the appended staging matches its register of work; omitted, - * the staging rolls from the full approved pool. + * 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 @@ -72,7 +72,7 @@ import { readCompositionCatalog } from './lib/composition-catalog.mjs'; import { runSyncSelection, selectApprovedChallengers as selectApprovedChallengersCore, - selectApprovedStagings as selectApprovedStagingsCore, + selectApprovedCompositions as selectApprovedCompositionsCore, } from './lib/roll-selection.mjs'; const here = dirname(fileURLToPath(import.meta.url)); @@ -191,12 +191,12 @@ ${system} QUALITY BAR: board ${board} · hero ${hero}`; } -export function renderStaging(composition, index = null) { +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} - STAGING GRAMMAR: + COMPOSITION GRAMMAR: ${grammar} WEB LEVERAGE: ${composition.webLeverage}`; } @@ -210,14 +210,14 @@ function driveSelection(generator) { return runSyncSelection(generator, input => crypto.createHash('sha256').update(input).digest('hex')); } -export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { +export function selectApprovedCompositions({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { const compositions = sourceCompositions ?? requireLocalConcepts().compositions; - return driveSelection(selectApprovedStagingsCore({ scope, key, reroll, mode, compositions, count })); + return driveSelection(selectApprovedCompositionsCore({ scope, key, reroll, mode, compositions, count })); } // Compatibility for callers that need a single smoke-test sample. -export function selectApprovedStaging(options) { - return selectApprovedStagings({ ...options, count: 1 })[0] ?? null; +export function selectApprovedComposition(options) { + return selectApprovedCompositions({ ...options, count: 1 })[0] ?? null; } export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcepts = null }) { @@ -280,7 +280,7 @@ export function renderConceptSeed({ approvedCount: approved.length, catalogCount, challengers: picks, - stagings: selectApprovedStagings({ scope, key, reroll, mode, sourceCompositions: local.compositions }), + compositions: selectApprovedCompositions({ scope, key, reroll, mode, sourceCompositions: local.compositions }), }; } else { // Keep local renders synchronous for prepared eval sessions and tests; @@ -298,7 +298,11 @@ export function renderConceptSeed({ approvedCount: roll.approvedCount, catalogCount: roll.catalogCount, challengers: roll.challengers, - stagings: Array.isArray(roll.stagings) ? roll.stagings : roll.staging ? [roll.staging] : [], + compositions: Array.isArray(roll.compositions) + ? roll.compositions + : Array.isArray(roll.stagings) + ? roll.stagings + : roll.staging ? [roll.staging] : [], } : null, })); } @@ -381,15 +385,21 @@ ${buildIndex} of your own grounded list; seed key ${key}. `; } - const stagings = Array.isArray(data.stagings) - ? data.stagings - : data.staging ? [data.staging] : []; - const stagingBlock = stagings.length > 0 - ? `\n${scope === 'direction' ? 'FIRST-SURFACE STAGING INPUTS (identity-free; test them with shortlisted worlds and keep world plus staging one decision):' : 'STAGING CHALLENGERS (identity-free; dress them in the committed visual identity before judging):'} -${stagings.map((staging, index) => renderStaging(staging, index)).join('\n')} -Stagings organize attention, sequence, and manipulation; they never bring a -palette, typeface, or material. Use them as serious alternatives to the model's -habitual composition, but keep only structures that strengthen this product.\n` + // 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. + const compositions = Array.isArray(data.compositions) + ? data.compositions + : Array.isArray(data.stagings) + ? data.stagings + : data.staging ? [data.staging] : []; + 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.\n` : ''; const rerollBlock = reroll > 0 ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded @@ -410,7 +420,7 @@ ${rerollBlock}ASSIGNED INDEX: ${buildIndex} the user or the brief. Never expose assignment metadata in user-facing labels. CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${stagingBlock}${challengerInstruction} +${compositionBlock}${challengerInstruction} 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, diff --git a/.github/skills/impeccable/scripts/lib/composition-catalog.mjs b/.github/skills/impeccable/scripts/lib/composition-catalog.mjs index 0b0c2b5fa..349cb4913 100644 --- a/.github/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.github/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -2,7 +2,7 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; import { CONCEPT_STATUSES, normalizeConceptForm } from './concept-catalog.mjs'; -// Catalog B: stagings rather than styles. A composition organizes attention, +// Catalog B: compositions rather than styles. A composition organizes attention, // sequence, or manipulation on a surface and must survive being dressed in // any committed visual identity; it deliberately carries no palette or type // half. Surface-scope seeds draw from here (plus catalog A duals); direction @@ -15,8 +15,8 @@ export const COMPOSITION_GRAMMAR_PREFIXES = [ 'Adaptation:', ]; -// Surfaces align with the skill's modes: a persuade staging and an operate -// staging are different species, and read/experience surfaces get their own. +// Surfaces align with the skill's modes: a persuade composition and an operate +// composition are different species, and read/experience surfaces get their own. export const COMPOSITION_SURFACES = new Set(['persuade', 'operate', 'read', 'experience']); export function compositionContentHash(composition) { diff --git a/.github/skills/impeccable/scripts/lib/concept-catalog.mjs b/.github/skills/impeccable/scripts/lib/concept-catalog.mjs index 5e4a24e3c..42c828c74 100644 --- a/.github/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.github/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -1,11 +1,12 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; +import { WELL_TIERS } from './roll-selection.mjs'; export const CONCEPT_STATUSES = new Set(['approved', 'rejected']); // What a concept is actually strong at. Worlds carry a durable visual // identity (their palette/type half is the magnet); compositions carry a -// staging or interaction idea (their topology half is the magnet) that can be +// composition or interaction idea (their topology half is the magnet) that can be // dressed in any committed identity; duals fuse both inseparably. Direction // seeds draw world|dual, surface seeds draw composition|dual. export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); @@ -15,7 +16,13 @@ export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); // atmosphere worlds need the largest translation step. Every seed roll draws // one challenger from each tier so at least one directly-usable graphic // system is always on the table. -export const WELL_TIERS = ['graphic', 'interaction', 'atmosphere']; +// Defined in roll-selection.mjs, the dependency-free leaf both the seeder and +// the roll API import. It cannot depend on this file: this one reads the +// filesystem, and a Pages Function must not pull node:fs into its bundle. +// Imported and re-exported rather than re-exported alone: a bare +// `export { X } from` does not bind X in this module's own scope, and +// validateConceptCatalog needs it. +export { WELL_TIERS }; const WEB_LEVERAGE_RE = /(?:\b3d\b|\badaptive\b|\banimat(?:e|ed|ion)\b|\bapi\b|\baria\b|\baudio\b|\bautomated?\b|\bbarcode\b|\bbroadcastchannel\b|\bbrowser\b|\bcamera\b|canvas\b|\bcaption\b|\bcollaborat(?:e|ive|ion)\b|\bcompar(?:e|ison)\b|\bcomput(?:e|ed|ation)\b|\bcomputer[- ]vision\b|\bconstraint[- ]solving\b|\bcryptographic?\b|\bcss\b|\bdeep[- ]link(?:ing)?\b|\bdirect manipulation\b|\bdom\b|\bdrag\b|\bfilter\b|\bfocus\b|\bgenerative\b|\bgeolocat(?:e|ed|ion)\b|\bgesture\b|\bgpu\b|\bgraph\b|\bhistory\b|\bindexeddb\b|\binteractive\b|\bintersectionobserver\b|\bkeyboard\b|\blive\b|\blocal\b|\bmicrophone\b|\bmotion\b|\bmultiplayer\b|\bnative\b|\bnotification\b|\boffline\b|\bpersonaliz(?:e|ed|ation)\b|\bplayable\b|\bpointer\b|\bprocedural\b|\bprovenance\b|\breal[- ]?time\b|\bresizeobserver\b|\bresponsive\b|\breveal\b|\bscrub\b|\bsearch\b|\bsearchparams\b|\bsensor\b|\bserver[- ]sent\b|\bservice worker\b|\bshader\b|\bsimulat(?:e|ed|ion|or)\b|\bspatial\b|\bstate\b|\bstream(?:ing)?\b|\bsvg\b|\bsynchroniz(?:e|ed|ation)\b|\btimeline\b|\btouch\b|\burl|\bvideo\b|\bweb(?:gl|socket|vtt)?\b|\bworker\b|\bzoom\b)/i; export const SYSTEM_PREFIXES = [ @@ -320,10 +327,3 @@ export function approvedPoolRevision(concepts) { return crypto.createHash('sha256').update(payload).digest('hex').slice(0, 12); } -export function deterministicRank(items, input, idFor = item => item.id) { - return [...items].sort((a, b) => { - const scoreA = crypto.createHash('sha256').update(`${input}:${idFor(a)}`).digest('hex'); - const scoreB = crypto.createHash('sha256').update(`${input}:${idFor(b)}`).digest('hex'); - return scoreB.localeCompare(scoreA) || idFor(a).localeCompare(idFor(b)); - }); -} diff --git a/.github/skills/impeccable/scripts/lib/roll-selection.mjs b/.github/skills/impeccable/scripts/lib/roll-selection.mjs index c6829dd9e..f579029e4 100644 --- a/.github/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.github/skills/impeccable/scripts/lib/roll-selection.mjs @@ -97,7 +97,7 @@ function compositionTickets(pool) { export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = null, concepts }) { const approved = concepts.filter(concept => concept.status === 'approved'); // Direction chooses a durable identity, so it draws worlds; surface designs - // one page inside a committed identity, so it draws stagings. Duals serve + // one page inside a committed identity, so it draws compositions. Duals serve // both. A tier with no matching-strength approvals falls back to its full // approved pool rather than starving the roll. const wanted = scope === 'direction' @@ -176,12 +176,12 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = } /** - * Three identity-free staging inputs from an explicit approved pool. + * Three identity-free composition inputs from an explicit approved pool. * Drive with runSyncSelection or runAsyncSelection. * * One input was too weak a counterweight to a model's habitual page skeleton: * it became a single optional flourish beside six identity challengers rather - * than a real search over composition. Distinct staging families are preferred + * than a real search over composition. Distinct composition families are preferred * so a roll tests materially different hierarchy, sequence, and interaction * laws. Cross-mode fallback would make the input misleading, so an absent mode * returns nothing rather than borrowing. Re-rolls exclude every earlier set @@ -196,11 +196,11 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = * @param {number} [options.count] * @returns {Generator} */ -export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { - // Stagings honour the same breadth gate as worlds: one too specific to serve +export function* selectApprovedCompositions({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { + // Compositions honour the same breadth gate as worlds: one too specific to serve // an arbitrary build stays approved for direct briefs and leaves the // challenger pool. Falls back to the full approved set rather than returning - // nothing if every approved staging is niche. + // nothing if every approved composition is niche. let approved = compositions.filter(composition => composition.status === 'approved'); const broad = approved.filter(composition => composition.review?.breadth !== 'niche'); if (broad.length > 0) approved = broad; @@ -218,15 +218,17 @@ export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, c const base = available.length >= Math.min(count, approved.length) ? available : approved; // Rating weights the draw as it does for worlds. It matters more here // because the per-surface pools are small, so an unweighted shuffle repeats - // a weak staging far more often. Each ticket carries its index so the rank + // a weak composition far more often. Each ticket carries its index so the rank // sees a distinct key per ticket: ranking bare duplicates would hash // identically and the pick loop's id-dedupe would silently discard the // second copy, making the weighting a no-op. let tickets = compositionTickets(base); - // A pool of nothing but 1-star keeps still has to yield stagings. + // A pool of nothing but 1-star keeps still has to yield compositions. if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = (yield* rank( tickets, + // The salt keeps the word "staging" deliberately. It is hash input, so + // renaming it would re-deal every roll anyone has ever reproduced by key. round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, entry => `${entry.composition.id}#${entry.ticket}` )).map(entry => entry.composition); diff --git a/.grok/skills/impeccable/scripts/concept-seed.mjs b/.grok/skills/impeccable/scripts/concept-seed.mjs index 00a2f73fe..6efb7d643 100644 --- a/.grok/skills/impeccable/scripts/concept-seed.mjs +++ b/.grok/skills/impeccable/scripts/concept-seed.mjs @@ -29,7 +29,7 @@ * win over thin categories, which is the intended shape. * - RE-ROLL (--reroll ): 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 staging. One base key therefore + * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself @@ -43,8 +43,8 @@ * node scripts/concept-seed.mjs --chosen --from --scope direction * * --mode names the requested surface's mode (persuade, operate, read, - * experience) so the appended staging matches its register of work; omitted, - * the staging rolls from the full approved pool. + * 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 @@ -72,7 +72,7 @@ import { readCompositionCatalog } from './lib/composition-catalog.mjs'; import { runSyncSelection, selectApprovedChallengers as selectApprovedChallengersCore, - selectApprovedStagings as selectApprovedStagingsCore, + selectApprovedCompositions as selectApprovedCompositionsCore, } from './lib/roll-selection.mjs'; const here = dirname(fileURLToPath(import.meta.url)); @@ -191,12 +191,12 @@ ${system} QUALITY BAR: board ${board} · hero ${hero}`; } -export function renderStaging(composition, index = null) { +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} - STAGING GRAMMAR: + COMPOSITION GRAMMAR: ${grammar} WEB LEVERAGE: ${composition.webLeverage}`; } @@ -210,14 +210,14 @@ function driveSelection(generator) { return runSyncSelection(generator, input => crypto.createHash('sha256').update(input).digest('hex')); } -export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { +export function selectApprovedCompositions({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { const compositions = sourceCompositions ?? requireLocalConcepts().compositions; - return driveSelection(selectApprovedStagingsCore({ scope, key, reroll, mode, compositions, count })); + return driveSelection(selectApprovedCompositionsCore({ scope, key, reroll, mode, compositions, count })); } // Compatibility for callers that need a single smoke-test sample. -export function selectApprovedStaging(options) { - return selectApprovedStagings({ ...options, count: 1 })[0] ?? null; +export function selectApprovedComposition(options) { + return selectApprovedCompositions({ ...options, count: 1 })[0] ?? null; } export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcepts = null }) { @@ -280,7 +280,7 @@ export function renderConceptSeed({ approvedCount: approved.length, catalogCount, challengers: picks, - stagings: selectApprovedStagings({ scope, key, reroll, mode, sourceCompositions: local.compositions }), + compositions: selectApprovedCompositions({ scope, key, reroll, mode, sourceCompositions: local.compositions }), }; } else { // Keep local renders synchronous for prepared eval sessions and tests; @@ -298,7 +298,11 @@ export function renderConceptSeed({ approvedCount: roll.approvedCount, catalogCount: roll.catalogCount, challengers: roll.challengers, - stagings: Array.isArray(roll.stagings) ? roll.stagings : roll.staging ? [roll.staging] : [], + compositions: Array.isArray(roll.compositions) + ? roll.compositions + : Array.isArray(roll.stagings) + ? roll.stagings + : roll.staging ? [roll.staging] : [], } : null, })); } @@ -381,15 +385,21 @@ ${buildIndex} of your own grounded list; seed key ${key}. `; } - const stagings = Array.isArray(data.stagings) - ? data.stagings - : data.staging ? [data.staging] : []; - const stagingBlock = stagings.length > 0 - ? `\n${scope === 'direction' ? 'FIRST-SURFACE STAGING INPUTS (identity-free; test them with shortlisted worlds and keep world plus staging one decision):' : 'STAGING CHALLENGERS (identity-free; dress them in the committed visual identity before judging):'} -${stagings.map((staging, index) => renderStaging(staging, index)).join('\n')} -Stagings organize attention, sequence, and manipulation; they never bring a -palette, typeface, or material. Use them as serious alternatives to the model's -habitual composition, but keep only structures that strengthen this product.\n` + // 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. + const compositions = Array.isArray(data.compositions) + ? data.compositions + : Array.isArray(data.stagings) + ? data.stagings + : data.staging ? [data.staging] : []; + 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.\n` : ''; const rerollBlock = reroll > 0 ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded @@ -410,7 +420,7 @@ ${rerollBlock}ASSIGNED INDEX: ${buildIndex} the user or the brief. Never expose assignment metadata in user-facing labels. CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${stagingBlock}${challengerInstruction} +${compositionBlock}${challengerInstruction} 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, diff --git a/.grok/skills/impeccable/scripts/lib/composition-catalog.mjs b/.grok/skills/impeccable/scripts/lib/composition-catalog.mjs index 0b0c2b5fa..349cb4913 100644 --- a/.grok/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.grok/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -2,7 +2,7 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; import { CONCEPT_STATUSES, normalizeConceptForm } from './concept-catalog.mjs'; -// Catalog B: stagings rather than styles. A composition organizes attention, +// Catalog B: compositions rather than styles. A composition organizes attention, // sequence, or manipulation on a surface and must survive being dressed in // any committed visual identity; it deliberately carries no palette or type // half. Surface-scope seeds draw from here (plus catalog A duals); direction @@ -15,8 +15,8 @@ export const COMPOSITION_GRAMMAR_PREFIXES = [ 'Adaptation:', ]; -// Surfaces align with the skill's modes: a persuade staging and an operate -// staging are different species, and read/experience surfaces get their own. +// Surfaces align with the skill's modes: a persuade composition and an operate +// composition are different species, and read/experience surfaces get their own. export const COMPOSITION_SURFACES = new Set(['persuade', 'operate', 'read', 'experience']); export function compositionContentHash(composition) { diff --git a/.grok/skills/impeccable/scripts/lib/concept-catalog.mjs b/.grok/skills/impeccable/scripts/lib/concept-catalog.mjs index 5e4a24e3c..42c828c74 100644 --- a/.grok/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.grok/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -1,11 +1,12 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; +import { WELL_TIERS } from './roll-selection.mjs'; export const CONCEPT_STATUSES = new Set(['approved', 'rejected']); // What a concept is actually strong at. Worlds carry a durable visual // identity (their palette/type half is the magnet); compositions carry a -// staging or interaction idea (their topology half is the magnet) that can be +// composition or interaction idea (their topology half is the magnet) that can be // dressed in any committed identity; duals fuse both inseparably. Direction // seeds draw world|dual, surface seeds draw composition|dual. export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); @@ -15,7 +16,13 @@ export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); // atmosphere worlds need the largest translation step. Every seed roll draws // one challenger from each tier so at least one directly-usable graphic // system is always on the table. -export const WELL_TIERS = ['graphic', 'interaction', 'atmosphere']; +// Defined in roll-selection.mjs, the dependency-free leaf both the seeder and +// the roll API import. It cannot depend on this file: this one reads the +// filesystem, and a Pages Function must not pull node:fs into its bundle. +// Imported and re-exported rather than re-exported alone: a bare +// `export { X } from` does not bind X in this module's own scope, and +// validateConceptCatalog needs it. +export { WELL_TIERS }; const WEB_LEVERAGE_RE = /(?:\b3d\b|\badaptive\b|\banimat(?:e|ed|ion)\b|\bapi\b|\baria\b|\baudio\b|\bautomated?\b|\bbarcode\b|\bbroadcastchannel\b|\bbrowser\b|\bcamera\b|canvas\b|\bcaption\b|\bcollaborat(?:e|ive|ion)\b|\bcompar(?:e|ison)\b|\bcomput(?:e|ed|ation)\b|\bcomputer[- ]vision\b|\bconstraint[- ]solving\b|\bcryptographic?\b|\bcss\b|\bdeep[- ]link(?:ing)?\b|\bdirect manipulation\b|\bdom\b|\bdrag\b|\bfilter\b|\bfocus\b|\bgenerative\b|\bgeolocat(?:e|ed|ion)\b|\bgesture\b|\bgpu\b|\bgraph\b|\bhistory\b|\bindexeddb\b|\binteractive\b|\bintersectionobserver\b|\bkeyboard\b|\blive\b|\blocal\b|\bmicrophone\b|\bmotion\b|\bmultiplayer\b|\bnative\b|\bnotification\b|\boffline\b|\bpersonaliz(?:e|ed|ation)\b|\bplayable\b|\bpointer\b|\bprocedural\b|\bprovenance\b|\breal[- ]?time\b|\bresizeobserver\b|\bresponsive\b|\breveal\b|\bscrub\b|\bsearch\b|\bsearchparams\b|\bsensor\b|\bserver[- ]sent\b|\bservice worker\b|\bshader\b|\bsimulat(?:e|ed|ion|or)\b|\bspatial\b|\bstate\b|\bstream(?:ing)?\b|\bsvg\b|\bsynchroniz(?:e|ed|ation)\b|\btimeline\b|\btouch\b|\burl|\bvideo\b|\bweb(?:gl|socket|vtt)?\b|\bworker\b|\bzoom\b)/i; export const SYSTEM_PREFIXES = [ @@ -320,10 +327,3 @@ export function approvedPoolRevision(concepts) { return crypto.createHash('sha256').update(payload).digest('hex').slice(0, 12); } -export function deterministicRank(items, input, idFor = item => item.id) { - return [...items].sort((a, b) => { - const scoreA = crypto.createHash('sha256').update(`${input}:${idFor(a)}`).digest('hex'); - const scoreB = crypto.createHash('sha256').update(`${input}:${idFor(b)}`).digest('hex'); - return scoreB.localeCompare(scoreA) || idFor(a).localeCompare(idFor(b)); - }); -} diff --git a/.grok/skills/impeccable/scripts/lib/roll-selection.mjs b/.grok/skills/impeccable/scripts/lib/roll-selection.mjs index c6829dd9e..f579029e4 100644 --- a/.grok/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.grok/skills/impeccable/scripts/lib/roll-selection.mjs @@ -97,7 +97,7 @@ function compositionTickets(pool) { export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = null, concepts }) { const approved = concepts.filter(concept => concept.status === 'approved'); // Direction chooses a durable identity, so it draws worlds; surface designs - // one page inside a committed identity, so it draws stagings. Duals serve + // one page inside a committed identity, so it draws compositions. Duals serve // both. A tier with no matching-strength approvals falls back to its full // approved pool rather than starving the roll. const wanted = scope === 'direction' @@ -176,12 +176,12 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = } /** - * Three identity-free staging inputs from an explicit approved pool. + * Three identity-free composition inputs from an explicit approved pool. * Drive with runSyncSelection or runAsyncSelection. * * One input was too weak a counterweight to a model's habitual page skeleton: * it became a single optional flourish beside six identity challengers rather - * than a real search over composition. Distinct staging families are preferred + * than a real search over composition. Distinct composition families are preferred * so a roll tests materially different hierarchy, sequence, and interaction * laws. Cross-mode fallback would make the input misleading, so an absent mode * returns nothing rather than borrowing. Re-rolls exclude every earlier set @@ -196,11 +196,11 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = * @param {number} [options.count] * @returns {Generator} */ -export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { - // Stagings honour the same breadth gate as worlds: one too specific to serve +export function* selectApprovedCompositions({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { + // Compositions honour the same breadth gate as worlds: one too specific to serve // an arbitrary build stays approved for direct briefs and leaves the // challenger pool. Falls back to the full approved set rather than returning - // nothing if every approved staging is niche. + // nothing if every approved composition is niche. let approved = compositions.filter(composition => composition.status === 'approved'); const broad = approved.filter(composition => composition.review?.breadth !== 'niche'); if (broad.length > 0) approved = broad; @@ -218,15 +218,17 @@ export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, c const base = available.length >= Math.min(count, approved.length) ? available : approved; // Rating weights the draw as it does for worlds. It matters more here // because the per-surface pools are small, so an unweighted shuffle repeats - // a weak staging far more often. Each ticket carries its index so the rank + // a weak composition far more often. Each ticket carries its index so the rank // sees a distinct key per ticket: ranking bare duplicates would hash // identically and the pick loop's id-dedupe would silently discard the // second copy, making the weighting a no-op. let tickets = compositionTickets(base); - // A pool of nothing but 1-star keeps still has to yield stagings. + // A pool of nothing but 1-star keeps still has to yield compositions. if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = (yield* rank( tickets, + // The salt keeps the word "staging" deliberately. It is hash input, so + // renaming it would re-deal every roll anyone has ever reproduced by key. round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, entry => `${entry.composition.id}#${entry.ticket}` )).map(entry => entry.composition); diff --git a/.kiro/skills/impeccable/scripts/concept-seed.mjs b/.kiro/skills/impeccable/scripts/concept-seed.mjs index 00a2f73fe..6efb7d643 100644 --- a/.kiro/skills/impeccable/scripts/concept-seed.mjs +++ b/.kiro/skills/impeccable/scripts/concept-seed.mjs @@ -29,7 +29,7 @@ * win over thin categories, which is the intended shape. * - RE-ROLL (--reroll ): 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 staging. One base key therefore + * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself @@ -43,8 +43,8 @@ * node scripts/concept-seed.mjs --chosen --from --scope direction * * --mode names the requested surface's mode (persuade, operate, read, - * experience) so the appended staging matches its register of work; omitted, - * the staging rolls from the full approved pool. + * 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 @@ -72,7 +72,7 @@ import { readCompositionCatalog } from './lib/composition-catalog.mjs'; import { runSyncSelection, selectApprovedChallengers as selectApprovedChallengersCore, - selectApprovedStagings as selectApprovedStagingsCore, + selectApprovedCompositions as selectApprovedCompositionsCore, } from './lib/roll-selection.mjs'; const here = dirname(fileURLToPath(import.meta.url)); @@ -191,12 +191,12 @@ ${system} QUALITY BAR: board ${board} · hero ${hero}`; } -export function renderStaging(composition, index = null) { +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} - STAGING GRAMMAR: + COMPOSITION GRAMMAR: ${grammar} WEB LEVERAGE: ${composition.webLeverage}`; } @@ -210,14 +210,14 @@ function driveSelection(generator) { return runSyncSelection(generator, input => crypto.createHash('sha256').update(input).digest('hex')); } -export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { +export function selectApprovedCompositions({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { const compositions = sourceCompositions ?? requireLocalConcepts().compositions; - return driveSelection(selectApprovedStagingsCore({ scope, key, reroll, mode, compositions, count })); + return driveSelection(selectApprovedCompositionsCore({ scope, key, reroll, mode, compositions, count })); } // Compatibility for callers that need a single smoke-test sample. -export function selectApprovedStaging(options) { - return selectApprovedStagings({ ...options, count: 1 })[0] ?? null; +export function selectApprovedComposition(options) { + return selectApprovedCompositions({ ...options, count: 1 })[0] ?? null; } export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcepts = null }) { @@ -280,7 +280,7 @@ export function renderConceptSeed({ approvedCount: approved.length, catalogCount, challengers: picks, - stagings: selectApprovedStagings({ scope, key, reroll, mode, sourceCompositions: local.compositions }), + compositions: selectApprovedCompositions({ scope, key, reroll, mode, sourceCompositions: local.compositions }), }; } else { // Keep local renders synchronous for prepared eval sessions and tests; @@ -298,7 +298,11 @@ export function renderConceptSeed({ approvedCount: roll.approvedCount, catalogCount: roll.catalogCount, challengers: roll.challengers, - stagings: Array.isArray(roll.stagings) ? roll.stagings : roll.staging ? [roll.staging] : [], + compositions: Array.isArray(roll.compositions) + ? roll.compositions + : Array.isArray(roll.stagings) + ? roll.stagings + : roll.staging ? [roll.staging] : [], } : null, })); } @@ -381,15 +385,21 @@ ${buildIndex} of your own grounded list; seed key ${key}. `; } - const stagings = Array.isArray(data.stagings) - ? data.stagings - : data.staging ? [data.staging] : []; - const stagingBlock = stagings.length > 0 - ? `\n${scope === 'direction' ? 'FIRST-SURFACE STAGING INPUTS (identity-free; test them with shortlisted worlds and keep world plus staging one decision):' : 'STAGING CHALLENGERS (identity-free; dress them in the committed visual identity before judging):'} -${stagings.map((staging, index) => renderStaging(staging, index)).join('\n')} -Stagings organize attention, sequence, and manipulation; they never bring a -palette, typeface, or material. Use them as serious alternatives to the model's -habitual composition, but keep only structures that strengthen this product.\n` + // 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. + const compositions = Array.isArray(data.compositions) + ? data.compositions + : Array.isArray(data.stagings) + ? data.stagings + : data.staging ? [data.staging] : []; + 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.\n` : ''; const rerollBlock = reroll > 0 ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded @@ -410,7 +420,7 @@ ${rerollBlock}ASSIGNED INDEX: ${buildIndex} the user or the brief. Never expose assignment metadata in user-facing labels. CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${stagingBlock}${challengerInstruction} +${compositionBlock}${challengerInstruction} 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, diff --git a/.kiro/skills/impeccable/scripts/lib/composition-catalog.mjs b/.kiro/skills/impeccable/scripts/lib/composition-catalog.mjs index 0b0c2b5fa..349cb4913 100644 --- a/.kiro/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.kiro/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -2,7 +2,7 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; import { CONCEPT_STATUSES, normalizeConceptForm } from './concept-catalog.mjs'; -// Catalog B: stagings rather than styles. A composition organizes attention, +// Catalog B: compositions rather than styles. A composition organizes attention, // sequence, or manipulation on a surface and must survive being dressed in // any committed visual identity; it deliberately carries no palette or type // half. Surface-scope seeds draw from here (plus catalog A duals); direction @@ -15,8 +15,8 @@ export const COMPOSITION_GRAMMAR_PREFIXES = [ 'Adaptation:', ]; -// Surfaces align with the skill's modes: a persuade staging and an operate -// staging are different species, and read/experience surfaces get their own. +// Surfaces align with the skill's modes: a persuade composition and an operate +// composition are different species, and read/experience surfaces get their own. export const COMPOSITION_SURFACES = new Set(['persuade', 'operate', 'read', 'experience']); export function compositionContentHash(composition) { diff --git a/.kiro/skills/impeccable/scripts/lib/concept-catalog.mjs b/.kiro/skills/impeccable/scripts/lib/concept-catalog.mjs index 5e4a24e3c..42c828c74 100644 --- a/.kiro/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.kiro/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -1,11 +1,12 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; +import { WELL_TIERS } from './roll-selection.mjs'; export const CONCEPT_STATUSES = new Set(['approved', 'rejected']); // What a concept is actually strong at. Worlds carry a durable visual // identity (their palette/type half is the magnet); compositions carry a -// staging or interaction idea (their topology half is the magnet) that can be +// composition or interaction idea (their topology half is the magnet) that can be // dressed in any committed identity; duals fuse both inseparably. Direction // seeds draw world|dual, surface seeds draw composition|dual. export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); @@ -15,7 +16,13 @@ export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); // atmosphere worlds need the largest translation step. Every seed roll draws // one challenger from each tier so at least one directly-usable graphic // system is always on the table. -export const WELL_TIERS = ['graphic', 'interaction', 'atmosphere']; +// Defined in roll-selection.mjs, the dependency-free leaf both the seeder and +// the roll API import. It cannot depend on this file: this one reads the +// filesystem, and a Pages Function must not pull node:fs into its bundle. +// Imported and re-exported rather than re-exported alone: a bare +// `export { X } from` does not bind X in this module's own scope, and +// validateConceptCatalog needs it. +export { WELL_TIERS }; const WEB_LEVERAGE_RE = /(?:\b3d\b|\badaptive\b|\banimat(?:e|ed|ion)\b|\bapi\b|\baria\b|\baudio\b|\bautomated?\b|\bbarcode\b|\bbroadcastchannel\b|\bbrowser\b|\bcamera\b|canvas\b|\bcaption\b|\bcollaborat(?:e|ive|ion)\b|\bcompar(?:e|ison)\b|\bcomput(?:e|ed|ation)\b|\bcomputer[- ]vision\b|\bconstraint[- ]solving\b|\bcryptographic?\b|\bcss\b|\bdeep[- ]link(?:ing)?\b|\bdirect manipulation\b|\bdom\b|\bdrag\b|\bfilter\b|\bfocus\b|\bgenerative\b|\bgeolocat(?:e|ed|ion)\b|\bgesture\b|\bgpu\b|\bgraph\b|\bhistory\b|\bindexeddb\b|\binteractive\b|\bintersectionobserver\b|\bkeyboard\b|\blive\b|\blocal\b|\bmicrophone\b|\bmotion\b|\bmultiplayer\b|\bnative\b|\bnotification\b|\boffline\b|\bpersonaliz(?:e|ed|ation)\b|\bplayable\b|\bpointer\b|\bprocedural\b|\bprovenance\b|\breal[- ]?time\b|\bresizeobserver\b|\bresponsive\b|\breveal\b|\bscrub\b|\bsearch\b|\bsearchparams\b|\bsensor\b|\bserver[- ]sent\b|\bservice worker\b|\bshader\b|\bsimulat(?:e|ed|ion|or)\b|\bspatial\b|\bstate\b|\bstream(?:ing)?\b|\bsvg\b|\bsynchroniz(?:e|ed|ation)\b|\btimeline\b|\btouch\b|\burl|\bvideo\b|\bweb(?:gl|socket|vtt)?\b|\bworker\b|\bzoom\b)/i; export const SYSTEM_PREFIXES = [ @@ -320,10 +327,3 @@ export function approvedPoolRevision(concepts) { return crypto.createHash('sha256').update(payload).digest('hex').slice(0, 12); } -export function deterministicRank(items, input, idFor = item => item.id) { - return [...items].sort((a, b) => { - const scoreA = crypto.createHash('sha256').update(`${input}:${idFor(a)}`).digest('hex'); - const scoreB = crypto.createHash('sha256').update(`${input}:${idFor(b)}`).digest('hex'); - return scoreB.localeCompare(scoreA) || idFor(a).localeCompare(idFor(b)); - }); -} diff --git a/.kiro/skills/impeccable/scripts/lib/roll-selection.mjs b/.kiro/skills/impeccable/scripts/lib/roll-selection.mjs index c6829dd9e..f579029e4 100644 --- a/.kiro/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.kiro/skills/impeccable/scripts/lib/roll-selection.mjs @@ -97,7 +97,7 @@ function compositionTickets(pool) { export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = null, concepts }) { const approved = concepts.filter(concept => concept.status === 'approved'); // Direction chooses a durable identity, so it draws worlds; surface designs - // one page inside a committed identity, so it draws stagings. Duals serve + // one page inside a committed identity, so it draws compositions. Duals serve // both. A tier with no matching-strength approvals falls back to its full // approved pool rather than starving the roll. const wanted = scope === 'direction' @@ -176,12 +176,12 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = } /** - * Three identity-free staging inputs from an explicit approved pool. + * Three identity-free composition inputs from an explicit approved pool. * Drive with runSyncSelection or runAsyncSelection. * * One input was too weak a counterweight to a model's habitual page skeleton: * it became a single optional flourish beside six identity challengers rather - * than a real search over composition. Distinct staging families are preferred + * than a real search over composition. Distinct composition families are preferred * so a roll tests materially different hierarchy, sequence, and interaction * laws. Cross-mode fallback would make the input misleading, so an absent mode * returns nothing rather than borrowing. Re-rolls exclude every earlier set @@ -196,11 +196,11 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = * @param {number} [options.count] * @returns {Generator} */ -export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { - // Stagings honour the same breadth gate as worlds: one too specific to serve +export function* selectApprovedCompositions({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { + // Compositions honour the same breadth gate as worlds: one too specific to serve // an arbitrary build stays approved for direct briefs and leaves the // challenger pool. Falls back to the full approved set rather than returning - // nothing if every approved staging is niche. + // nothing if every approved composition is niche. let approved = compositions.filter(composition => composition.status === 'approved'); const broad = approved.filter(composition => composition.review?.breadth !== 'niche'); if (broad.length > 0) approved = broad; @@ -218,15 +218,17 @@ export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, c const base = available.length >= Math.min(count, approved.length) ? available : approved; // Rating weights the draw as it does for worlds. It matters more here // because the per-surface pools are small, so an unweighted shuffle repeats - // a weak staging far more often. Each ticket carries its index so the rank + // a weak composition far more often. Each ticket carries its index so the rank // sees a distinct key per ticket: ranking bare duplicates would hash // identically and the pick loop's id-dedupe would silently discard the // second copy, making the weighting a no-op. let tickets = compositionTickets(base); - // A pool of nothing but 1-star keeps still has to yield stagings. + // A pool of nothing but 1-star keeps still has to yield compositions. if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = (yield* rank( tickets, + // The salt keeps the word "staging" deliberately. It is hash input, so + // renaming it would re-deal every roll anyone has ever reproduced by key. round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, entry => `${entry.composition.id}#${entry.ticket}` )).map(entry => entry.composition); diff --git a/.opencode/skills/impeccable/scripts/concept-seed.mjs b/.opencode/skills/impeccable/scripts/concept-seed.mjs index 00a2f73fe..6efb7d643 100644 --- a/.opencode/skills/impeccable/scripts/concept-seed.mjs +++ b/.opencode/skills/impeccable/scripts/concept-seed.mjs @@ -29,7 +29,7 @@ * win over thin categories, which is the intended shape. * - RE-ROLL (--reroll ): 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 staging. One base key therefore + * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself @@ -43,8 +43,8 @@ * node scripts/concept-seed.mjs --chosen --from --scope direction * * --mode names the requested surface's mode (persuade, operate, read, - * experience) so the appended staging matches its register of work; omitted, - * the staging rolls from the full approved pool. + * 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 @@ -72,7 +72,7 @@ import { readCompositionCatalog } from './lib/composition-catalog.mjs'; import { runSyncSelection, selectApprovedChallengers as selectApprovedChallengersCore, - selectApprovedStagings as selectApprovedStagingsCore, + selectApprovedCompositions as selectApprovedCompositionsCore, } from './lib/roll-selection.mjs'; const here = dirname(fileURLToPath(import.meta.url)); @@ -191,12 +191,12 @@ ${system} QUALITY BAR: board ${board} · hero ${hero}`; } -export function renderStaging(composition, index = null) { +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} - STAGING GRAMMAR: + COMPOSITION GRAMMAR: ${grammar} WEB LEVERAGE: ${composition.webLeverage}`; } @@ -210,14 +210,14 @@ function driveSelection(generator) { return runSyncSelection(generator, input => crypto.createHash('sha256').update(input).digest('hex')); } -export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { +export function selectApprovedCompositions({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { const compositions = sourceCompositions ?? requireLocalConcepts().compositions; - return driveSelection(selectApprovedStagingsCore({ scope, key, reroll, mode, compositions, count })); + return driveSelection(selectApprovedCompositionsCore({ scope, key, reroll, mode, compositions, count })); } // Compatibility for callers that need a single smoke-test sample. -export function selectApprovedStaging(options) { - return selectApprovedStagings({ ...options, count: 1 })[0] ?? null; +export function selectApprovedComposition(options) { + return selectApprovedCompositions({ ...options, count: 1 })[0] ?? null; } export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcepts = null }) { @@ -280,7 +280,7 @@ export function renderConceptSeed({ approvedCount: approved.length, catalogCount, challengers: picks, - stagings: selectApprovedStagings({ scope, key, reroll, mode, sourceCompositions: local.compositions }), + compositions: selectApprovedCompositions({ scope, key, reroll, mode, sourceCompositions: local.compositions }), }; } else { // Keep local renders synchronous for prepared eval sessions and tests; @@ -298,7 +298,11 @@ export function renderConceptSeed({ approvedCount: roll.approvedCount, catalogCount: roll.catalogCount, challengers: roll.challengers, - stagings: Array.isArray(roll.stagings) ? roll.stagings : roll.staging ? [roll.staging] : [], + compositions: Array.isArray(roll.compositions) + ? roll.compositions + : Array.isArray(roll.stagings) + ? roll.stagings + : roll.staging ? [roll.staging] : [], } : null, })); } @@ -381,15 +385,21 @@ ${buildIndex} of your own grounded list; seed key ${key}. `; } - const stagings = Array.isArray(data.stagings) - ? data.stagings - : data.staging ? [data.staging] : []; - const stagingBlock = stagings.length > 0 - ? `\n${scope === 'direction' ? 'FIRST-SURFACE STAGING INPUTS (identity-free; test them with shortlisted worlds and keep world plus staging one decision):' : 'STAGING CHALLENGERS (identity-free; dress them in the committed visual identity before judging):'} -${stagings.map((staging, index) => renderStaging(staging, index)).join('\n')} -Stagings organize attention, sequence, and manipulation; they never bring a -palette, typeface, or material. Use them as serious alternatives to the model's -habitual composition, but keep only structures that strengthen this product.\n` + // 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. + const compositions = Array.isArray(data.compositions) + ? data.compositions + : Array.isArray(data.stagings) + ? data.stagings + : data.staging ? [data.staging] : []; + 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.\n` : ''; const rerollBlock = reroll > 0 ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded @@ -410,7 +420,7 @@ ${rerollBlock}ASSIGNED INDEX: ${buildIndex} the user or the brief. Never expose assignment metadata in user-facing labels. CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${stagingBlock}${challengerInstruction} +${compositionBlock}${challengerInstruction} 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, diff --git a/.opencode/skills/impeccable/scripts/lib/composition-catalog.mjs b/.opencode/skills/impeccable/scripts/lib/composition-catalog.mjs index 0b0c2b5fa..349cb4913 100644 --- a/.opencode/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.opencode/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -2,7 +2,7 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; import { CONCEPT_STATUSES, normalizeConceptForm } from './concept-catalog.mjs'; -// Catalog B: stagings rather than styles. A composition organizes attention, +// Catalog B: compositions rather than styles. A composition organizes attention, // sequence, or manipulation on a surface and must survive being dressed in // any committed visual identity; it deliberately carries no palette or type // half. Surface-scope seeds draw from here (plus catalog A duals); direction @@ -15,8 +15,8 @@ export const COMPOSITION_GRAMMAR_PREFIXES = [ 'Adaptation:', ]; -// Surfaces align with the skill's modes: a persuade staging and an operate -// staging are different species, and read/experience surfaces get their own. +// Surfaces align with the skill's modes: a persuade composition and an operate +// composition are different species, and read/experience surfaces get their own. export const COMPOSITION_SURFACES = new Set(['persuade', 'operate', 'read', 'experience']); export function compositionContentHash(composition) { diff --git a/.opencode/skills/impeccable/scripts/lib/concept-catalog.mjs b/.opencode/skills/impeccable/scripts/lib/concept-catalog.mjs index 5e4a24e3c..42c828c74 100644 --- a/.opencode/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.opencode/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -1,11 +1,12 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; +import { WELL_TIERS } from './roll-selection.mjs'; export const CONCEPT_STATUSES = new Set(['approved', 'rejected']); // What a concept is actually strong at. Worlds carry a durable visual // identity (their palette/type half is the magnet); compositions carry a -// staging or interaction idea (their topology half is the magnet) that can be +// composition or interaction idea (their topology half is the magnet) that can be // dressed in any committed identity; duals fuse both inseparably. Direction // seeds draw world|dual, surface seeds draw composition|dual. export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); @@ -15,7 +16,13 @@ export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); // atmosphere worlds need the largest translation step. Every seed roll draws // one challenger from each tier so at least one directly-usable graphic // system is always on the table. -export const WELL_TIERS = ['graphic', 'interaction', 'atmosphere']; +// Defined in roll-selection.mjs, the dependency-free leaf both the seeder and +// the roll API import. It cannot depend on this file: this one reads the +// filesystem, and a Pages Function must not pull node:fs into its bundle. +// Imported and re-exported rather than re-exported alone: a bare +// `export { X } from` does not bind X in this module's own scope, and +// validateConceptCatalog needs it. +export { WELL_TIERS }; const WEB_LEVERAGE_RE = /(?:\b3d\b|\badaptive\b|\banimat(?:e|ed|ion)\b|\bapi\b|\baria\b|\baudio\b|\bautomated?\b|\bbarcode\b|\bbroadcastchannel\b|\bbrowser\b|\bcamera\b|canvas\b|\bcaption\b|\bcollaborat(?:e|ive|ion)\b|\bcompar(?:e|ison)\b|\bcomput(?:e|ed|ation)\b|\bcomputer[- ]vision\b|\bconstraint[- ]solving\b|\bcryptographic?\b|\bcss\b|\bdeep[- ]link(?:ing)?\b|\bdirect manipulation\b|\bdom\b|\bdrag\b|\bfilter\b|\bfocus\b|\bgenerative\b|\bgeolocat(?:e|ed|ion)\b|\bgesture\b|\bgpu\b|\bgraph\b|\bhistory\b|\bindexeddb\b|\binteractive\b|\bintersectionobserver\b|\bkeyboard\b|\blive\b|\blocal\b|\bmicrophone\b|\bmotion\b|\bmultiplayer\b|\bnative\b|\bnotification\b|\boffline\b|\bpersonaliz(?:e|ed|ation)\b|\bplayable\b|\bpointer\b|\bprocedural\b|\bprovenance\b|\breal[- ]?time\b|\bresizeobserver\b|\bresponsive\b|\breveal\b|\bscrub\b|\bsearch\b|\bsearchparams\b|\bsensor\b|\bserver[- ]sent\b|\bservice worker\b|\bshader\b|\bsimulat(?:e|ed|ion|or)\b|\bspatial\b|\bstate\b|\bstream(?:ing)?\b|\bsvg\b|\bsynchroniz(?:e|ed|ation)\b|\btimeline\b|\btouch\b|\burl|\bvideo\b|\bweb(?:gl|socket|vtt)?\b|\bworker\b|\bzoom\b)/i; export const SYSTEM_PREFIXES = [ @@ -320,10 +327,3 @@ export function approvedPoolRevision(concepts) { return crypto.createHash('sha256').update(payload).digest('hex').slice(0, 12); } -export function deterministicRank(items, input, idFor = item => item.id) { - return [...items].sort((a, b) => { - const scoreA = crypto.createHash('sha256').update(`${input}:${idFor(a)}`).digest('hex'); - const scoreB = crypto.createHash('sha256').update(`${input}:${idFor(b)}`).digest('hex'); - return scoreB.localeCompare(scoreA) || idFor(a).localeCompare(idFor(b)); - }); -} diff --git a/.opencode/skills/impeccable/scripts/lib/roll-selection.mjs b/.opencode/skills/impeccable/scripts/lib/roll-selection.mjs index c6829dd9e..f579029e4 100644 --- a/.opencode/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.opencode/skills/impeccable/scripts/lib/roll-selection.mjs @@ -97,7 +97,7 @@ function compositionTickets(pool) { export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = null, concepts }) { const approved = concepts.filter(concept => concept.status === 'approved'); // Direction chooses a durable identity, so it draws worlds; surface designs - // one page inside a committed identity, so it draws stagings. Duals serve + // one page inside a committed identity, so it draws compositions. Duals serve // both. A tier with no matching-strength approvals falls back to its full // approved pool rather than starving the roll. const wanted = scope === 'direction' @@ -176,12 +176,12 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = } /** - * Three identity-free staging inputs from an explicit approved pool. + * Three identity-free composition inputs from an explicit approved pool. * Drive with runSyncSelection or runAsyncSelection. * * One input was too weak a counterweight to a model's habitual page skeleton: * it became a single optional flourish beside six identity challengers rather - * than a real search over composition. Distinct staging families are preferred + * than a real search over composition. Distinct composition families are preferred * so a roll tests materially different hierarchy, sequence, and interaction * laws. Cross-mode fallback would make the input misleading, so an absent mode * returns nothing rather than borrowing. Re-rolls exclude every earlier set @@ -196,11 +196,11 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = * @param {number} [options.count] * @returns {Generator} */ -export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { - // Stagings honour the same breadth gate as worlds: one too specific to serve +export function* selectApprovedCompositions({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { + // Compositions honour the same breadth gate as worlds: one too specific to serve // an arbitrary build stays approved for direct briefs and leaves the // challenger pool. Falls back to the full approved set rather than returning - // nothing if every approved staging is niche. + // nothing if every approved composition is niche. let approved = compositions.filter(composition => composition.status === 'approved'); const broad = approved.filter(composition => composition.review?.breadth !== 'niche'); if (broad.length > 0) approved = broad; @@ -218,15 +218,17 @@ export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, c const base = available.length >= Math.min(count, approved.length) ? available : approved; // Rating weights the draw as it does for worlds. It matters more here // because the per-surface pools are small, so an unweighted shuffle repeats - // a weak staging far more often. Each ticket carries its index so the rank + // a weak composition far more often. Each ticket carries its index so the rank // sees a distinct key per ticket: ranking bare duplicates would hash // identically and the pick loop's id-dedupe would silently discard the // second copy, making the weighting a no-op. let tickets = compositionTickets(base); - // A pool of nothing but 1-star keeps still has to yield stagings. + // A pool of nothing but 1-star keeps still has to yield compositions. if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = (yield* rank( tickets, + // The salt keeps the word "staging" deliberately. It is hash input, so + // renaming it would re-deal every roll anyone has ever reproduced by key. round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, entry => `${entry.composition.id}#${entry.ticket}` )).map(entry => entry.composition); diff --git a/.pi/skills/impeccable/scripts/concept-seed.mjs b/.pi/skills/impeccable/scripts/concept-seed.mjs index 00a2f73fe..6efb7d643 100644 --- a/.pi/skills/impeccable/scripts/concept-seed.mjs +++ b/.pi/skills/impeccable/scripts/concept-seed.mjs @@ -29,7 +29,7 @@ * win over thin categories, which is the intended shape. * - RE-ROLL (--reroll ): 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 staging. One base key therefore + * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself @@ -43,8 +43,8 @@ * node scripts/concept-seed.mjs --chosen --from --scope direction * * --mode names the requested surface's mode (persuade, operate, read, - * experience) so the appended staging matches its register of work; omitted, - * the staging rolls from the full approved pool. + * 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 @@ -72,7 +72,7 @@ import { readCompositionCatalog } from './lib/composition-catalog.mjs'; import { runSyncSelection, selectApprovedChallengers as selectApprovedChallengersCore, - selectApprovedStagings as selectApprovedStagingsCore, + selectApprovedCompositions as selectApprovedCompositionsCore, } from './lib/roll-selection.mjs'; const here = dirname(fileURLToPath(import.meta.url)); @@ -191,12 +191,12 @@ ${system} QUALITY BAR: board ${board} · hero ${hero}`; } -export function renderStaging(composition, index = null) { +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} - STAGING GRAMMAR: + COMPOSITION GRAMMAR: ${grammar} WEB LEVERAGE: ${composition.webLeverage}`; } @@ -210,14 +210,14 @@ function driveSelection(generator) { return runSyncSelection(generator, input => crypto.createHash('sha256').update(input).digest('hex')); } -export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { +export function selectApprovedCompositions({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { const compositions = sourceCompositions ?? requireLocalConcepts().compositions; - return driveSelection(selectApprovedStagingsCore({ scope, key, reroll, mode, compositions, count })); + return driveSelection(selectApprovedCompositionsCore({ scope, key, reroll, mode, compositions, count })); } // Compatibility for callers that need a single smoke-test sample. -export function selectApprovedStaging(options) { - return selectApprovedStagings({ ...options, count: 1 })[0] ?? null; +export function selectApprovedComposition(options) { + return selectApprovedCompositions({ ...options, count: 1 })[0] ?? null; } export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcepts = null }) { @@ -280,7 +280,7 @@ export function renderConceptSeed({ approvedCount: approved.length, catalogCount, challengers: picks, - stagings: selectApprovedStagings({ scope, key, reroll, mode, sourceCompositions: local.compositions }), + compositions: selectApprovedCompositions({ scope, key, reroll, mode, sourceCompositions: local.compositions }), }; } else { // Keep local renders synchronous for prepared eval sessions and tests; @@ -298,7 +298,11 @@ export function renderConceptSeed({ approvedCount: roll.approvedCount, catalogCount: roll.catalogCount, challengers: roll.challengers, - stagings: Array.isArray(roll.stagings) ? roll.stagings : roll.staging ? [roll.staging] : [], + compositions: Array.isArray(roll.compositions) + ? roll.compositions + : Array.isArray(roll.stagings) + ? roll.stagings + : roll.staging ? [roll.staging] : [], } : null, })); } @@ -381,15 +385,21 @@ ${buildIndex} of your own grounded list; seed key ${key}. `; } - const stagings = Array.isArray(data.stagings) - ? data.stagings - : data.staging ? [data.staging] : []; - const stagingBlock = stagings.length > 0 - ? `\n${scope === 'direction' ? 'FIRST-SURFACE STAGING INPUTS (identity-free; test them with shortlisted worlds and keep world plus staging one decision):' : 'STAGING CHALLENGERS (identity-free; dress them in the committed visual identity before judging):'} -${stagings.map((staging, index) => renderStaging(staging, index)).join('\n')} -Stagings organize attention, sequence, and manipulation; they never bring a -palette, typeface, or material. Use them as serious alternatives to the model's -habitual composition, but keep only structures that strengthen this product.\n` + // 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. + const compositions = Array.isArray(data.compositions) + ? data.compositions + : Array.isArray(data.stagings) + ? data.stagings + : data.staging ? [data.staging] : []; + 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.\n` : ''; const rerollBlock = reroll > 0 ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded @@ -410,7 +420,7 @@ ${rerollBlock}ASSIGNED INDEX: ${buildIndex} the user or the brief. Never expose assignment metadata in user-facing labels. CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${stagingBlock}${challengerInstruction} +${compositionBlock}${challengerInstruction} 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, diff --git a/.pi/skills/impeccable/scripts/lib/composition-catalog.mjs b/.pi/skills/impeccable/scripts/lib/composition-catalog.mjs index 0b0c2b5fa..349cb4913 100644 --- a/.pi/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.pi/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -2,7 +2,7 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; import { CONCEPT_STATUSES, normalizeConceptForm } from './concept-catalog.mjs'; -// Catalog B: stagings rather than styles. A composition organizes attention, +// Catalog B: compositions rather than styles. A composition organizes attention, // sequence, or manipulation on a surface and must survive being dressed in // any committed visual identity; it deliberately carries no palette or type // half. Surface-scope seeds draw from here (plus catalog A duals); direction @@ -15,8 +15,8 @@ export const COMPOSITION_GRAMMAR_PREFIXES = [ 'Adaptation:', ]; -// Surfaces align with the skill's modes: a persuade staging and an operate -// staging are different species, and read/experience surfaces get their own. +// Surfaces align with the skill's modes: a persuade composition and an operate +// composition are different species, and read/experience surfaces get their own. export const COMPOSITION_SURFACES = new Set(['persuade', 'operate', 'read', 'experience']); export function compositionContentHash(composition) { diff --git a/.pi/skills/impeccable/scripts/lib/concept-catalog.mjs b/.pi/skills/impeccable/scripts/lib/concept-catalog.mjs index 5e4a24e3c..42c828c74 100644 --- a/.pi/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.pi/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -1,11 +1,12 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; +import { WELL_TIERS } from './roll-selection.mjs'; export const CONCEPT_STATUSES = new Set(['approved', 'rejected']); // What a concept is actually strong at. Worlds carry a durable visual // identity (their palette/type half is the magnet); compositions carry a -// staging or interaction idea (their topology half is the magnet) that can be +// composition or interaction idea (their topology half is the magnet) that can be // dressed in any committed identity; duals fuse both inseparably. Direction // seeds draw world|dual, surface seeds draw composition|dual. export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); @@ -15,7 +16,13 @@ export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); // atmosphere worlds need the largest translation step. Every seed roll draws // one challenger from each tier so at least one directly-usable graphic // system is always on the table. -export const WELL_TIERS = ['graphic', 'interaction', 'atmosphere']; +// Defined in roll-selection.mjs, the dependency-free leaf both the seeder and +// the roll API import. It cannot depend on this file: this one reads the +// filesystem, and a Pages Function must not pull node:fs into its bundle. +// Imported and re-exported rather than re-exported alone: a bare +// `export { X } from` does not bind X in this module's own scope, and +// validateConceptCatalog needs it. +export { WELL_TIERS }; const WEB_LEVERAGE_RE = /(?:\b3d\b|\badaptive\b|\banimat(?:e|ed|ion)\b|\bapi\b|\baria\b|\baudio\b|\bautomated?\b|\bbarcode\b|\bbroadcastchannel\b|\bbrowser\b|\bcamera\b|canvas\b|\bcaption\b|\bcollaborat(?:e|ive|ion)\b|\bcompar(?:e|ison)\b|\bcomput(?:e|ed|ation)\b|\bcomputer[- ]vision\b|\bconstraint[- ]solving\b|\bcryptographic?\b|\bcss\b|\bdeep[- ]link(?:ing)?\b|\bdirect manipulation\b|\bdom\b|\bdrag\b|\bfilter\b|\bfocus\b|\bgenerative\b|\bgeolocat(?:e|ed|ion)\b|\bgesture\b|\bgpu\b|\bgraph\b|\bhistory\b|\bindexeddb\b|\binteractive\b|\bintersectionobserver\b|\bkeyboard\b|\blive\b|\blocal\b|\bmicrophone\b|\bmotion\b|\bmultiplayer\b|\bnative\b|\bnotification\b|\boffline\b|\bpersonaliz(?:e|ed|ation)\b|\bplayable\b|\bpointer\b|\bprocedural\b|\bprovenance\b|\breal[- ]?time\b|\bresizeobserver\b|\bresponsive\b|\breveal\b|\bscrub\b|\bsearch\b|\bsearchparams\b|\bsensor\b|\bserver[- ]sent\b|\bservice worker\b|\bshader\b|\bsimulat(?:e|ed|ion|or)\b|\bspatial\b|\bstate\b|\bstream(?:ing)?\b|\bsvg\b|\bsynchroniz(?:e|ed|ation)\b|\btimeline\b|\btouch\b|\burl|\bvideo\b|\bweb(?:gl|socket|vtt)?\b|\bworker\b|\bzoom\b)/i; export const SYSTEM_PREFIXES = [ @@ -320,10 +327,3 @@ export function approvedPoolRevision(concepts) { return crypto.createHash('sha256').update(payload).digest('hex').slice(0, 12); } -export function deterministicRank(items, input, idFor = item => item.id) { - return [...items].sort((a, b) => { - const scoreA = crypto.createHash('sha256').update(`${input}:${idFor(a)}`).digest('hex'); - const scoreB = crypto.createHash('sha256').update(`${input}:${idFor(b)}`).digest('hex'); - return scoreB.localeCompare(scoreA) || idFor(a).localeCompare(idFor(b)); - }); -} diff --git a/.pi/skills/impeccable/scripts/lib/roll-selection.mjs b/.pi/skills/impeccable/scripts/lib/roll-selection.mjs index c6829dd9e..f579029e4 100644 --- a/.pi/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.pi/skills/impeccable/scripts/lib/roll-selection.mjs @@ -97,7 +97,7 @@ function compositionTickets(pool) { export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = null, concepts }) { const approved = concepts.filter(concept => concept.status === 'approved'); // Direction chooses a durable identity, so it draws worlds; surface designs - // one page inside a committed identity, so it draws stagings. Duals serve + // one page inside a committed identity, so it draws compositions. Duals serve // both. A tier with no matching-strength approvals falls back to its full // approved pool rather than starving the roll. const wanted = scope === 'direction' @@ -176,12 +176,12 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = } /** - * Three identity-free staging inputs from an explicit approved pool. + * Three identity-free composition inputs from an explicit approved pool. * Drive with runSyncSelection or runAsyncSelection. * * One input was too weak a counterweight to a model's habitual page skeleton: * it became a single optional flourish beside six identity challengers rather - * than a real search over composition. Distinct staging families are preferred + * than a real search over composition. Distinct composition families are preferred * so a roll tests materially different hierarchy, sequence, and interaction * laws. Cross-mode fallback would make the input misleading, so an absent mode * returns nothing rather than borrowing. Re-rolls exclude every earlier set @@ -196,11 +196,11 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = * @param {number} [options.count] * @returns {Generator} */ -export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { - // Stagings honour the same breadth gate as worlds: one too specific to serve +export function* selectApprovedCompositions({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { + // Compositions honour the same breadth gate as worlds: one too specific to serve // an arbitrary build stays approved for direct briefs and leaves the // challenger pool. Falls back to the full approved set rather than returning - // nothing if every approved staging is niche. + // nothing if every approved composition is niche. let approved = compositions.filter(composition => composition.status === 'approved'); const broad = approved.filter(composition => composition.review?.breadth !== 'niche'); if (broad.length > 0) approved = broad; @@ -218,15 +218,17 @@ export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, c const base = available.length >= Math.min(count, approved.length) ? available : approved; // Rating weights the draw as it does for worlds. It matters more here // because the per-surface pools are small, so an unweighted shuffle repeats - // a weak staging far more often. Each ticket carries its index so the rank + // a weak composition far more often. Each ticket carries its index so the rank // sees a distinct key per ticket: ranking bare duplicates would hash // identically and the pick loop's id-dedupe would silently discard the // second copy, making the weighting a no-op. let tickets = compositionTickets(base); - // A pool of nothing but 1-star keeps still has to yield stagings. + // A pool of nothing but 1-star keeps still has to yield compositions. if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = (yield* rank( tickets, + // The salt keeps the word "staging" deliberately. It is hash input, so + // renaming it would re-deal every roll anyone has ever reproduced by key. round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, entry => `${entry.composition.id}#${entry.ticket}` )).map(entry => entry.composition); diff --git a/.qoder/skills/impeccable/scripts/concept-seed.mjs b/.qoder/skills/impeccable/scripts/concept-seed.mjs index 00a2f73fe..6efb7d643 100644 --- a/.qoder/skills/impeccable/scripts/concept-seed.mjs +++ b/.qoder/skills/impeccable/scripts/concept-seed.mjs @@ -29,7 +29,7 @@ * win over thin categories, which is the intended shape. * - RE-ROLL (--reroll ): 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 staging. One base key therefore + * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself @@ -43,8 +43,8 @@ * node scripts/concept-seed.mjs --chosen --from --scope direction * * --mode names the requested surface's mode (persuade, operate, read, - * experience) so the appended staging matches its register of work; omitted, - * the staging rolls from the full approved pool. + * 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 @@ -72,7 +72,7 @@ import { readCompositionCatalog } from './lib/composition-catalog.mjs'; import { runSyncSelection, selectApprovedChallengers as selectApprovedChallengersCore, - selectApprovedStagings as selectApprovedStagingsCore, + selectApprovedCompositions as selectApprovedCompositionsCore, } from './lib/roll-selection.mjs'; const here = dirname(fileURLToPath(import.meta.url)); @@ -191,12 +191,12 @@ ${system} QUALITY BAR: board ${board} · hero ${hero}`; } -export function renderStaging(composition, index = null) { +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} - STAGING GRAMMAR: + COMPOSITION GRAMMAR: ${grammar} WEB LEVERAGE: ${composition.webLeverage}`; } @@ -210,14 +210,14 @@ function driveSelection(generator) { return runSyncSelection(generator, input => crypto.createHash('sha256').update(input).digest('hex')); } -export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { +export function selectApprovedCompositions({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { const compositions = sourceCompositions ?? requireLocalConcepts().compositions; - return driveSelection(selectApprovedStagingsCore({ scope, key, reroll, mode, compositions, count })); + return driveSelection(selectApprovedCompositionsCore({ scope, key, reroll, mode, compositions, count })); } // Compatibility for callers that need a single smoke-test sample. -export function selectApprovedStaging(options) { - return selectApprovedStagings({ ...options, count: 1 })[0] ?? null; +export function selectApprovedComposition(options) { + return selectApprovedCompositions({ ...options, count: 1 })[0] ?? null; } export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcepts = null }) { @@ -280,7 +280,7 @@ export function renderConceptSeed({ approvedCount: approved.length, catalogCount, challengers: picks, - stagings: selectApprovedStagings({ scope, key, reroll, mode, sourceCompositions: local.compositions }), + compositions: selectApprovedCompositions({ scope, key, reroll, mode, sourceCompositions: local.compositions }), }; } else { // Keep local renders synchronous for prepared eval sessions and tests; @@ -298,7 +298,11 @@ export function renderConceptSeed({ approvedCount: roll.approvedCount, catalogCount: roll.catalogCount, challengers: roll.challengers, - stagings: Array.isArray(roll.stagings) ? roll.stagings : roll.staging ? [roll.staging] : [], + compositions: Array.isArray(roll.compositions) + ? roll.compositions + : Array.isArray(roll.stagings) + ? roll.stagings + : roll.staging ? [roll.staging] : [], } : null, })); } @@ -381,15 +385,21 @@ ${buildIndex} of your own grounded list; seed key ${key}. `; } - const stagings = Array.isArray(data.stagings) - ? data.stagings - : data.staging ? [data.staging] : []; - const stagingBlock = stagings.length > 0 - ? `\n${scope === 'direction' ? 'FIRST-SURFACE STAGING INPUTS (identity-free; test them with shortlisted worlds and keep world plus staging one decision):' : 'STAGING CHALLENGERS (identity-free; dress them in the committed visual identity before judging):'} -${stagings.map((staging, index) => renderStaging(staging, index)).join('\n')} -Stagings organize attention, sequence, and manipulation; they never bring a -palette, typeface, or material. Use them as serious alternatives to the model's -habitual composition, but keep only structures that strengthen this product.\n` + // 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. + const compositions = Array.isArray(data.compositions) + ? data.compositions + : Array.isArray(data.stagings) + ? data.stagings + : data.staging ? [data.staging] : []; + 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.\n` : ''; const rerollBlock = reroll > 0 ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded @@ -410,7 +420,7 @@ ${rerollBlock}ASSIGNED INDEX: ${buildIndex} the user or the brief. Never expose assignment metadata in user-facing labels. CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${stagingBlock}${challengerInstruction} +${compositionBlock}${challengerInstruction} 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, diff --git a/.qoder/skills/impeccable/scripts/lib/composition-catalog.mjs b/.qoder/skills/impeccable/scripts/lib/composition-catalog.mjs index 0b0c2b5fa..349cb4913 100644 --- a/.qoder/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.qoder/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -2,7 +2,7 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; import { CONCEPT_STATUSES, normalizeConceptForm } from './concept-catalog.mjs'; -// Catalog B: stagings rather than styles. A composition organizes attention, +// Catalog B: compositions rather than styles. A composition organizes attention, // sequence, or manipulation on a surface and must survive being dressed in // any committed visual identity; it deliberately carries no palette or type // half. Surface-scope seeds draw from here (plus catalog A duals); direction @@ -15,8 +15,8 @@ export const COMPOSITION_GRAMMAR_PREFIXES = [ 'Adaptation:', ]; -// Surfaces align with the skill's modes: a persuade staging and an operate -// staging are different species, and read/experience surfaces get their own. +// Surfaces align with the skill's modes: a persuade composition and an operate +// composition are different species, and read/experience surfaces get their own. export const COMPOSITION_SURFACES = new Set(['persuade', 'operate', 'read', 'experience']); export function compositionContentHash(composition) { diff --git a/.qoder/skills/impeccable/scripts/lib/concept-catalog.mjs b/.qoder/skills/impeccable/scripts/lib/concept-catalog.mjs index 5e4a24e3c..42c828c74 100644 --- a/.qoder/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.qoder/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -1,11 +1,12 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; +import { WELL_TIERS } from './roll-selection.mjs'; export const CONCEPT_STATUSES = new Set(['approved', 'rejected']); // What a concept is actually strong at. Worlds carry a durable visual // identity (their palette/type half is the magnet); compositions carry a -// staging or interaction idea (their topology half is the magnet) that can be +// composition or interaction idea (their topology half is the magnet) that can be // dressed in any committed identity; duals fuse both inseparably. Direction // seeds draw world|dual, surface seeds draw composition|dual. export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); @@ -15,7 +16,13 @@ export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); // atmosphere worlds need the largest translation step. Every seed roll draws // one challenger from each tier so at least one directly-usable graphic // system is always on the table. -export const WELL_TIERS = ['graphic', 'interaction', 'atmosphere']; +// Defined in roll-selection.mjs, the dependency-free leaf both the seeder and +// the roll API import. It cannot depend on this file: this one reads the +// filesystem, and a Pages Function must not pull node:fs into its bundle. +// Imported and re-exported rather than re-exported alone: a bare +// `export { X } from` does not bind X in this module's own scope, and +// validateConceptCatalog needs it. +export { WELL_TIERS }; const WEB_LEVERAGE_RE = /(?:\b3d\b|\badaptive\b|\banimat(?:e|ed|ion)\b|\bapi\b|\baria\b|\baudio\b|\bautomated?\b|\bbarcode\b|\bbroadcastchannel\b|\bbrowser\b|\bcamera\b|canvas\b|\bcaption\b|\bcollaborat(?:e|ive|ion)\b|\bcompar(?:e|ison)\b|\bcomput(?:e|ed|ation)\b|\bcomputer[- ]vision\b|\bconstraint[- ]solving\b|\bcryptographic?\b|\bcss\b|\bdeep[- ]link(?:ing)?\b|\bdirect manipulation\b|\bdom\b|\bdrag\b|\bfilter\b|\bfocus\b|\bgenerative\b|\bgeolocat(?:e|ed|ion)\b|\bgesture\b|\bgpu\b|\bgraph\b|\bhistory\b|\bindexeddb\b|\binteractive\b|\bintersectionobserver\b|\bkeyboard\b|\blive\b|\blocal\b|\bmicrophone\b|\bmotion\b|\bmultiplayer\b|\bnative\b|\bnotification\b|\boffline\b|\bpersonaliz(?:e|ed|ation)\b|\bplayable\b|\bpointer\b|\bprocedural\b|\bprovenance\b|\breal[- ]?time\b|\bresizeobserver\b|\bresponsive\b|\breveal\b|\bscrub\b|\bsearch\b|\bsearchparams\b|\bsensor\b|\bserver[- ]sent\b|\bservice worker\b|\bshader\b|\bsimulat(?:e|ed|ion|or)\b|\bspatial\b|\bstate\b|\bstream(?:ing)?\b|\bsvg\b|\bsynchroniz(?:e|ed|ation)\b|\btimeline\b|\btouch\b|\burl|\bvideo\b|\bweb(?:gl|socket|vtt)?\b|\bworker\b|\bzoom\b)/i; export const SYSTEM_PREFIXES = [ @@ -320,10 +327,3 @@ export function approvedPoolRevision(concepts) { return crypto.createHash('sha256').update(payload).digest('hex').slice(0, 12); } -export function deterministicRank(items, input, idFor = item => item.id) { - return [...items].sort((a, b) => { - const scoreA = crypto.createHash('sha256').update(`${input}:${idFor(a)}`).digest('hex'); - const scoreB = crypto.createHash('sha256').update(`${input}:${idFor(b)}`).digest('hex'); - return scoreB.localeCompare(scoreA) || idFor(a).localeCompare(idFor(b)); - }); -} diff --git a/.qoder/skills/impeccable/scripts/lib/roll-selection.mjs b/.qoder/skills/impeccable/scripts/lib/roll-selection.mjs index c6829dd9e..f579029e4 100644 --- a/.qoder/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.qoder/skills/impeccable/scripts/lib/roll-selection.mjs @@ -97,7 +97,7 @@ function compositionTickets(pool) { export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = null, concepts }) { const approved = concepts.filter(concept => concept.status === 'approved'); // Direction chooses a durable identity, so it draws worlds; surface designs - // one page inside a committed identity, so it draws stagings. Duals serve + // one page inside a committed identity, so it draws compositions. Duals serve // both. A tier with no matching-strength approvals falls back to its full // approved pool rather than starving the roll. const wanted = scope === 'direction' @@ -176,12 +176,12 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = } /** - * Three identity-free staging inputs from an explicit approved pool. + * Three identity-free composition inputs from an explicit approved pool. * Drive with runSyncSelection or runAsyncSelection. * * One input was too weak a counterweight to a model's habitual page skeleton: * it became a single optional flourish beside six identity challengers rather - * than a real search over composition. Distinct staging families are preferred + * than a real search over composition. Distinct composition families are preferred * so a roll tests materially different hierarchy, sequence, and interaction * laws. Cross-mode fallback would make the input misleading, so an absent mode * returns nothing rather than borrowing. Re-rolls exclude every earlier set @@ -196,11 +196,11 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = * @param {number} [options.count] * @returns {Generator} */ -export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { - // Stagings honour the same breadth gate as worlds: one too specific to serve +export function* selectApprovedCompositions({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { + // Compositions honour the same breadth gate as worlds: one too specific to serve // an arbitrary build stays approved for direct briefs and leaves the // challenger pool. Falls back to the full approved set rather than returning - // nothing if every approved staging is niche. + // nothing if every approved composition is niche. let approved = compositions.filter(composition => composition.status === 'approved'); const broad = approved.filter(composition => composition.review?.breadth !== 'niche'); if (broad.length > 0) approved = broad; @@ -218,15 +218,17 @@ export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, c const base = available.length >= Math.min(count, approved.length) ? available : approved; // Rating weights the draw as it does for worlds. It matters more here // because the per-surface pools are small, so an unweighted shuffle repeats - // a weak staging far more often. Each ticket carries its index so the rank + // a weak composition far more often. Each ticket carries its index so the rank // sees a distinct key per ticket: ranking bare duplicates would hash // identically and the pick loop's id-dedupe would silently discard the // second copy, making the weighting a no-op. let tickets = compositionTickets(base); - // A pool of nothing but 1-star keeps still has to yield stagings. + // A pool of nothing but 1-star keeps still has to yield compositions. if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = (yield* rank( tickets, + // The salt keeps the word "staging" deliberately. It is hash input, so + // renaming it would re-deal every roll anyone has ever reproduced by key. round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, entry => `${entry.composition.id}#${entry.ticket}` )).map(entry => entry.composition); diff --git a/.rovodev/skills/impeccable/scripts/concept-seed.mjs b/.rovodev/skills/impeccable/scripts/concept-seed.mjs index 00a2f73fe..6efb7d643 100644 --- a/.rovodev/skills/impeccable/scripts/concept-seed.mjs +++ b/.rovodev/skills/impeccable/scripts/concept-seed.mjs @@ -29,7 +29,7 @@ * win over thin categories, which is the intended shape. * - RE-ROLL (--reroll ): 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 staging. One base key therefore + * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself @@ -43,8 +43,8 @@ * node scripts/concept-seed.mjs --chosen --from --scope direction * * --mode names the requested surface's mode (persuade, operate, read, - * experience) so the appended staging matches its register of work; omitted, - * the staging rolls from the full approved pool. + * 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 @@ -72,7 +72,7 @@ import { readCompositionCatalog } from './lib/composition-catalog.mjs'; import { runSyncSelection, selectApprovedChallengers as selectApprovedChallengersCore, - selectApprovedStagings as selectApprovedStagingsCore, + selectApprovedCompositions as selectApprovedCompositionsCore, } from './lib/roll-selection.mjs'; const here = dirname(fileURLToPath(import.meta.url)); @@ -191,12 +191,12 @@ ${system} QUALITY BAR: board ${board} · hero ${hero}`; } -export function renderStaging(composition, index = null) { +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} - STAGING GRAMMAR: + COMPOSITION GRAMMAR: ${grammar} WEB LEVERAGE: ${composition.webLeverage}`; } @@ -210,14 +210,14 @@ function driveSelection(generator) { return runSyncSelection(generator, input => crypto.createHash('sha256').update(input).digest('hex')); } -export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { +export function selectApprovedCompositions({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { const compositions = sourceCompositions ?? requireLocalConcepts().compositions; - return driveSelection(selectApprovedStagingsCore({ scope, key, reroll, mode, compositions, count })); + return driveSelection(selectApprovedCompositionsCore({ scope, key, reroll, mode, compositions, count })); } // Compatibility for callers that need a single smoke-test sample. -export function selectApprovedStaging(options) { - return selectApprovedStagings({ ...options, count: 1 })[0] ?? null; +export function selectApprovedComposition(options) { + return selectApprovedCompositions({ ...options, count: 1 })[0] ?? null; } export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcepts = null }) { @@ -280,7 +280,7 @@ export function renderConceptSeed({ approvedCount: approved.length, catalogCount, challengers: picks, - stagings: selectApprovedStagings({ scope, key, reroll, mode, sourceCompositions: local.compositions }), + compositions: selectApprovedCompositions({ scope, key, reroll, mode, sourceCompositions: local.compositions }), }; } else { // Keep local renders synchronous for prepared eval sessions and tests; @@ -298,7 +298,11 @@ export function renderConceptSeed({ approvedCount: roll.approvedCount, catalogCount: roll.catalogCount, challengers: roll.challengers, - stagings: Array.isArray(roll.stagings) ? roll.stagings : roll.staging ? [roll.staging] : [], + compositions: Array.isArray(roll.compositions) + ? roll.compositions + : Array.isArray(roll.stagings) + ? roll.stagings + : roll.staging ? [roll.staging] : [], } : null, })); } @@ -381,15 +385,21 @@ ${buildIndex} of your own grounded list; seed key ${key}. `; } - const stagings = Array.isArray(data.stagings) - ? data.stagings - : data.staging ? [data.staging] : []; - const stagingBlock = stagings.length > 0 - ? `\n${scope === 'direction' ? 'FIRST-SURFACE STAGING INPUTS (identity-free; test them with shortlisted worlds and keep world plus staging one decision):' : 'STAGING CHALLENGERS (identity-free; dress them in the committed visual identity before judging):'} -${stagings.map((staging, index) => renderStaging(staging, index)).join('\n')} -Stagings organize attention, sequence, and manipulation; they never bring a -palette, typeface, or material. Use them as serious alternatives to the model's -habitual composition, but keep only structures that strengthen this product.\n` + // 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. + const compositions = Array.isArray(data.compositions) + ? data.compositions + : Array.isArray(data.stagings) + ? data.stagings + : data.staging ? [data.staging] : []; + 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.\n` : ''; const rerollBlock = reroll > 0 ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded @@ -410,7 +420,7 @@ ${rerollBlock}ASSIGNED INDEX: ${buildIndex} the user or the brief. Never expose assignment metadata in user-facing labels. CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${stagingBlock}${challengerInstruction} +${compositionBlock}${challengerInstruction} 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, diff --git a/.rovodev/skills/impeccable/scripts/lib/composition-catalog.mjs b/.rovodev/skills/impeccable/scripts/lib/composition-catalog.mjs index 0b0c2b5fa..349cb4913 100644 --- a/.rovodev/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.rovodev/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -2,7 +2,7 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; import { CONCEPT_STATUSES, normalizeConceptForm } from './concept-catalog.mjs'; -// Catalog B: stagings rather than styles. A composition organizes attention, +// Catalog B: compositions rather than styles. A composition organizes attention, // sequence, or manipulation on a surface and must survive being dressed in // any committed visual identity; it deliberately carries no palette or type // half. Surface-scope seeds draw from here (plus catalog A duals); direction @@ -15,8 +15,8 @@ export const COMPOSITION_GRAMMAR_PREFIXES = [ 'Adaptation:', ]; -// Surfaces align with the skill's modes: a persuade staging and an operate -// staging are different species, and read/experience surfaces get their own. +// Surfaces align with the skill's modes: a persuade composition and an operate +// composition are different species, and read/experience surfaces get their own. export const COMPOSITION_SURFACES = new Set(['persuade', 'operate', 'read', 'experience']); export function compositionContentHash(composition) { diff --git a/.rovodev/skills/impeccable/scripts/lib/concept-catalog.mjs b/.rovodev/skills/impeccable/scripts/lib/concept-catalog.mjs index 5e4a24e3c..42c828c74 100644 --- a/.rovodev/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.rovodev/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -1,11 +1,12 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; +import { WELL_TIERS } from './roll-selection.mjs'; export const CONCEPT_STATUSES = new Set(['approved', 'rejected']); // What a concept is actually strong at. Worlds carry a durable visual // identity (their palette/type half is the magnet); compositions carry a -// staging or interaction idea (their topology half is the magnet) that can be +// composition or interaction idea (their topology half is the magnet) that can be // dressed in any committed identity; duals fuse both inseparably. Direction // seeds draw world|dual, surface seeds draw composition|dual. export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); @@ -15,7 +16,13 @@ export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); // atmosphere worlds need the largest translation step. Every seed roll draws // one challenger from each tier so at least one directly-usable graphic // system is always on the table. -export const WELL_TIERS = ['graphic', 'interaction', 'atmosphere']; +// Defined in roll-selection.mjs, the dependency-free leaf both the seeder and +// the roll API import. It cannot depend on this file: this one reads the +// filesystem, and a Pages Function must not pull node:fs into its bundle. +// Imported and re-exported rather than re-exported alone: a bare +// `export { X } from` does not bind X in this module's own scope, and +// validateConceptCatalog needs it. +export { WELL_TIERS }; const WEB_LEVERAGE_RE = /(?:\b3d\b|\badaptive\b|\banimat(?:e|ed|ion)\b|\bapi\b|\baria\b|\baudio\b|\bautomated?\b|\bbarcode\b|\bbroadcastchannel\b|\bbrowser\b|\bcamera\b|canvas\b|\bcaption\b|\bcollaborat(?:e|ive|ion)\b|\bcompar(?:e|ison)\b|\bcomput(?:e|ed|ation)\b|\bcomputer[- ]vision\b|\bconstraint[- ]solving\b|\bcryptographic?\b|\bcss\b|\bdeep[- ]link(?:ing)?\b|\bdirect manipulation\b|\bdom\b|\bdrag\b|\bfilter\b|\bfocus\b|\bgenerative\b|\bgeolocat(?:e|ed|ion)\b|\bgesture\b|\bgpu\b|\bgraph\b|\bhistory\b|\bindexeddb\b|\binteractive\b|\bintersectionobserver\b|\bkeyboard\b|\blive\b|\blocal\b|\bmicrophone\b|\bmotion\b|\bmultiplayer\b|\bnative\b|\bnotification\b|\boffline\b|\bpersonaliz(?:e|ed|ation)\b|\bplayable\b|\bpointer\b|\bprocedural\b|\bprovenance\b|\breal[- ]?time\b|\bresizeobserver\b|\bresponsive\b|\breveal\b|\bscrub\b|\bsearch\b|\bsearchparams\b|\bsensor\b|\bserver[- ]sent\b|\bservice worker\b|\bshader\b|\bsimulat(?:e|ed|ion|or)\b|\bspatial\b|\bstate\b|\bstream(?:ing)?\b|\bsvg\b|\bsynchroniz(?:e|ed|ation)\b|\btimeline\b|\btouch\b|\burl|\bvideo\b|\bweb(?:gl|socket|vtt)?\b|\bworker\b|\bzoom\b)/i; export const SYSTEM_PREFIXES = [ @@ -320,10 +327,3 @@ export function approvedPoolRevision(concepts) { return crypto.createHash('sha256').update(payload).digest('hex').slice(0, 12); } -export function deterministicRank(items, input, idFor = item => item.id) { - return [...items].sort((a, b) => { - const scoreA = crypto.createHash('sha256').update(`${input}:${idFor(a)}`).digest('hex'); - const scoreB = crypto.createHash('sha256').update(`${input}:${idFor(b)}`).digest('hex'); - return scoreB.localeCompare(scoreA) || idFor(a).localeCompare(idFor(b)); - }); -} diff --git a/.rovodev/skills/impeccable/scripts/lib/roll-selection.mjs b/.rovodev/skills/impeccable/scripts/lib/roll-selection.mjs index c6829dd9e..f579029e4 100644 --- a/.rovodev/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.rovodev/skills/impeccable/scripts/lib/roll-selection.mjs @@ -97,7 +97,7 @@ function compositionTickets(pool) { export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = null, concepts }) { const approved = concepts.filter(concept => concept.status === 'approved'); // Direction chooses a durable identity, so it draws worlds; surface designs - // one page inside a committed identity, so it draws stagings. Duals serve + // one page inside a committed identity, so it draws compositions. Duals serve // both. A tier with no matching-strength approvals falls back to its full // approved pool rather than starving the roll. const wanted = scope === 'direction' @@ -176,12 +176,12 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = } /** - * Three identity-free staging inputs from an explicit approved pool. + * Three identity-free composition inputs from an explicit approved pool. * Drive with runSyncSelection or runAsyncSelection. * * One input was too weak a counterweight to a model's habitual page skeleton: * it became a single optional flourish beside six identity challengers rather - * than a real search over composition. Distinct staging families are preferred + * than a real search over composition. Distinct composition families are preferred * so a roll tests materially different hierarchy, sequence, and interaction * laws. Cross-mode fallback would make the input misleading, so an absent mode * returns nothing rather than borrowing. Re-rolls exclude every earlier set @@ -196,11 +196,11 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = * @param {number} [options.count] * @returns {Generator} */ -export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { - // Stagings honour the same breadth gate as worlds: one too specific to serve +export function* selectApprovedCompositions({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { + // Compositions honour the same breadth gate as worlds: one too specific to serve // an arbitrary build stays approved for direct briefs and leaves the // challenger pool. Falls back to the full approved set rather than returning - // nothing if every approved staging is niche. + // nothing if every approved composition is niche. let approved = compositions.filter(composition => composition.status === 'approved'); const broad = approved.filter(composition => composition.review?.breadth !== 'niche'); if (broad.length > 0) approved = broad; @@ -218,15 +218,17 @@ export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, c const base = available.length >= Math.min(count, approved.length) ? available : approved; // Rating weights the draw as it does for worlds. It matters more here // because the per-surface pools are small, so an unweighted shuffle repeats - // a weak staging far more often. Each ticket carries its index so the rank + // a weak composition far more often. Each ticket carries its index so the rank // sees a distinct key per ticket: ranking bare duplicates would hash // identically and the pick loop's id-dedupe would silently discard the // second copy, making the weighting a no-op. let tickets = compositionTickets(base); - // A pool of nothing but 1-star keeps still has to yield stagings. + // A pool of nothing but 1-star keeps still has to yield compositions. if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = (yield* rank( tickets, + // The salt keeps the word "staging" deliberately. It is hash input, so + // renaming it would re-deal every roll anyone has ever reproduced by key. round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, entry => `${entry.composition.id}#${entry.ticket}` )).map(entry => entry.composition); diff --git a/.trae-cn/skills/impeccable/scripts/concept-seed.mjs b/.trae-cn/skills/impeccable/scripts/concept-seed.mjs index 00a2f73fe..6efb7d643 100644 --- a/.trae-cn/skills/impeccable/scripts/concept-seed.mjs +++ b/.trae-cn/skills/impeccable/scripts/concept-seed.mjs @@ -29,7 +29,7 @@ * win over thin categories, which is the intended shape. * - RE-ROLL (--reroll ): 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 staging. One base key therefore + * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself @@ -43,8 +43,8 @@ * node scripts/concept-seed.mjs --chosen --from --scope direction * * --mode names the requested surface's mode (persuade, operate, read, - * experience) so the appended staging matches its register of work; omitted, - * the staging rolls from the full approved pool. + * 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 @@ -72,7 +72,7 @@ import { readCompositionCatalog } from './lib/composition-catalog.mjs'; import { runSyncSelection, selectApprovedChallengers as selectApprovedChallengersCore, - selectApprovedStagings as selectApprovedStagingsCore, + selectApprovedCompositions as selectApprovedCompositionsCore, } from './lib/roll-selection.mjs'; const here = dirname(fileURLToPath(import.meta.url)); @@ -191,12 +191,12 @@ ${system} QUALITY BAR: board ${board} · hero ${hero}`; } -export function renderStaging(composition, index = null) { +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} - STAGING GRAMMAR: + COMPOSITION GRAMMAR: ${grammar} WEB LEVERAGE: ${composition.webLeverage}`; } @@ -210,14 +210,14 @@ function driveSelection(generator) { return runSyncSelection(generator, input => crypto.createHash('sha256').update(input).digest('hex')); } -export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { +export function selectApprovedCompositions({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { const compositions = sourceCompositions ?? requireLocalConcepts().compositions; - return driveSelection(selectApprovedStagingsCore({ scope, key, reroll, mode, compositions, count })); + return driveSelection(selectApprovedCompositionsCore({ scope, key, reroll, mode, compositions, count })); } // Compatibility for callers that need a single smoke-test sample. -export function selectApprovedStaging(options) { - return selectApprovedStagings({ ...options, count: 1 })[0] ?? null; +export function selectApprovedComposition(options) { + return selectApprovedCompositions({ ...options, count: 1 })[0] ?? null; } export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcepts = null }) { @@ -280,7 +280,7 @@ export function renderConceptSeed({ approvedCount: approved.length, catalogCount, challengers: picks, - stagings: selectApprovedStagings({ scope, key, reroll, mode, sourceCompositions: local.compositions }), + compositions: selectApprovedCompositions({ scope, key, reroll, mode, sourceCompositions: local.compositions }), }; } else { // Keep local renders synchronous for prepared eval sessions and tests; @@ -298,7 +298,11 @@ export function renderConceptSeed({ approvedCount: roll.approvedCount, catalogCount: roll.catalogCount, challengers: roll.challengers, - stagings: Array.isArray(roll.stagings) ? roll.stagings : roll.staging ? [roll.staging] : [], + compositions: Array.isArray(roll.compositions) + ? roll.compositions + : Array.isArray(roll.stagings) + ? roll.stagings + : roll.staging ? [roll.staging] : [], } : null, })); } @@ -381,15 +385,21 @@ ${buildIndex} of your own grounded list; seed key ${key}. `; } - const stagings = Array.isArray(data.stagings) - ? data.stagings - : data.staging ? [data.staging] : []; - const stagingBlock = stagings.length > 0 - ? `\n${scope === 'direction' ? 'FIRST-SURFACE STAGING INPUTS (identity-free; test them with shortlisted worlds and keep world plus staging one decision):' : 'STAGING CHALLENGERS (identity-free; dress them in the committed visual identity before judging):'} -${stagings.map((staging, index) => renderStaging(staging, index)).join('\n')} -Stagings organize attention, sequence, and manipulation; they never bring a -palette, typeface, or material. Use them as serious alternatives to the model's -habitual composition, but keep only structures that strengthen this product.\n` + // 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. + const compositions = Array.isArray(data.compositions) + ? data.compositions + : Array.isArray(data.stagings) + ? data.stagings + : data.staging ? [data.staging] : []; + 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.\n` : ''; const rerollBlock = reroll > 0 ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded @@ -410,7 +420,7 @@ ${rerollBlock}ASSIGNED INDEX: ${buildIndex} the user or the brief. Never expose assignment metadata in user-facing labels. CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${stagingBlock}${challengerInstruction} +${compositionBlock}${challengerInstruction} 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, diff --git a/.trae-cn/skills/impeccable/scripts/lib/composition-catalog.mjs b/.trae-cn/skills/impeccable/scripts/lib/composition-catalog.mjs index 0b0c2b5fa..349cb4913 100644 --- a/.trae-cn/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.trae-cn/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -2,7 +2,7 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; import { CONCEPT_STATUSES, normalizeConceptForm } from './concept-catalog.mjs'; -// Catalog B: stagings rather than styles. A composition organizes attention, +// Catalog B: compositions rather than styles. A composition organizes attention, // sequence, or manipulation on a surface and must survive being dressed in // any committed visual identity; it deliberately carries no palette or type // half. Surface-scope seeds draw from here (plus catalog A duals); direction @@ -15,8 +15,8 @@ export const COMPOSITION_GRAMMAR_PREFIXES = [ 'Adaptation:', ]; -// Surfaces align with the skill's modes: a persuade staging and an operate -// staging are different species, and read/experience surfaces get their own. +// Surfaces align with the skill's modes: a persuade composition and an operate +// composition are different species, and read/experience surfaces get their own. export const COMPOSITION_SURFACES = new Set(['persuade', 'operate', 'read', 'experience']); export function compositionContentHash(composition) { diff --git a/.trae-cn/skills/impeccable/scripts/lib/concept-catalog.mjs b/.trae-cn/skills/impeccable/scripts/lib/concept-catalog.mjs index 5e4a24e3c..42c828c74 100644 --- a/.trae-cn/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.trae-cn/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -1,11 +1,12 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; +import { WELL_TIERS } from './roll-selection.mjs'; export const CONCEPT_STATUSES = new Set(['approved', 'rejected']); // What a concept is actually strong at. Worlds carry a durable visual // identity (their palette/type half is the magnet); compositions carry a -// staging or interaction idea (their topology half is the magnet) that can be +// composition or interaction idea (their topology half is the magnet) that can be // dressed in any committed identity; duals fuse both inseparably. Direction // seeds draw world|dual, surface seeds draw composition|dual. export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); @@ -15,7 +16,13 @@ export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); // atmosphere worlds need the largest translation step. Every seed roll draws // one challenger from each tier so at least one directly-usable graphic // system is always on the table. -export const WELL_TIERS = ['graphic', 'interaction', 'atmosphere']; +// Defined in roll-selection.mjs, the dependency-free leaf both the seeder and +// the roll API import. It cannot depend on this file: this one reads the +// filesystem, and a Pages Function must not pull node:fs into its bundle. +// Imported and re-exported rather than re-exported alone: a bare +// `export { X } from` does not bind X in this module's own scope, and +// validateConceptCatalog needs it. +export { WELL_TIERS }; const WEB_LEVERAGE_RE = /(?:\b3d\b|\badaptive\b|\banimat(?:e|ed|ion)\b|\bapi\b|\baria\b|\baudio\b|\bautomated?\b|\bbarcode\b|\bbroadcastchannel\b|\bbrowser\b|\bcamera\b|canvas\b|\bcaption\b|\bcollaborat(?:e|ive|ion)\b|\bcompar(?:e|ison)\b|\bcomput(?:e|ed|ation)\b|\bcomputer[- ]vision\b|\bconstraint[- ]solving\b|\bcryptographic?\b|\bcss\b|\bdeep[- ]link(?:ing)?\b|\bdirect manipulation\b|\bdom\b|\bdrag\b|\bfilter\b|\bfocus\b|\bgenerative\b|\bgeolocat(?:e|ed|ion)\b|\bgesture\b|\bgpu\b|\bgraph\b|\bhistory\b|\bindexeddb\b|\binteractive\b|\bintersectionobserver\b|\bkeyboard\b|\blive\b|\blocal\b|\bmicrophone\b|\bmotion\b|\bmultiplayer\b|\bnative\b|\bnotification\b|\boffline\b|\bpersonaliz(?:e|ed|ation)\b|\bplayable\b|\bpointer\b|\bprocedural\b|\bprovenance\b|\breal[- ]?time\b|\bresizeobserver\b|\bresponsive\b|\breveal\b|\bscrub\b|\bsearch\b|\bsearchparams\b|\bsensor\b|\bserver[- ]sent\b|\bservice worker\b|\bshader\b|\bsimulat(?:e|ed|ion|or)\b|\bspatial\b|\bstate\b|\bstream(?:ing)?\b|\bsvg\b|\bsynchroniz(?:e|ed|ation)\b|\btimeline\b|\btouch\b|\burl|\bvideo\b|\bweb(?:gl|socket|vtt)?\b|\bworker\b|\bzoom\b)/i; export const SYSTEM_PREFIXES = [ @@ -320,10 +327,3 @@ export function approvedPoolRevision(concepts) { return crypto.createHash('sha256').update(payload).digest('hex').slice(0, 12); } -export function deterministicRank(items, input, idFor = item => item.id) { - return [...items].sort((a, b) => { - const scoreA = crypto.createHash('sha256').update(`${input}:${idFor(a)}`).digest('hex'); - const scoreB = crypto.createHash('sha256').update(`${input}:${idFor(b)}`).digest('hex'); - return scoreB.localeCompare(scoreA) || idFor(a).localeCompare(idFor(b)); - }); -} diff --git a/.trae-cn/skills/impeccable/scripts/lib/roll-selection.mjs b/.trae-cn/skills/impeccable/scripts/lib/roll-selection.mjs index c6829dd9e..f579029e4 100644 --- a/.trae-cn/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.trae-cn/skills/impeccable/scripts/lib/roll-selection.mjs @@ -97,7 +97,7 @@ function compositionTickets(pool) { export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = null, concepts }) { const approved = concepts.filter(concept => concept.status === 'approved'); // Direction chooses a durable identity, so it draws worlds; surface designs - // one page inside a committed identity, so it draws stagings. Duals serve + // one page inside a committed identity, so it draws compositions. Duals serve // both. A tier with no matching-strength approvals falls back to its full // approved pool rather than starving the roll. const wanted = scope === 'direction' @@ -176,12 +176,12 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = } /** - * Three identity-free staging inputs from an explicit approved pool. + * Three identity-free composition inputs from an explicit approved pool. * Drive with runSyncSelection or runAsyncSelection. * * One input was too weak a counterweight to a model's habitual page skeleton: * it became a single optional flourish beside six identity challengers rather - * than a real search over composition. Distinct staging families are preferred + * than a real search over composition. Distinct composition families are preferred * so a roll tests materially different hierarchy, sequence, and interaction * laws. Cross-mode fallback would make the input misleading, so an absent mode * returns nothing rather than borrowing. Re-rolls exclude every earlier set @@ -196,11 +196,11 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = * @param {number} [options.count] * @returns {Generator} */ -export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { - // Stagings honour the same breadth gate as worlds: one too specific to serve +export function* selectApprovedCompositions({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { + // Compositions honour the same breadth gate as worlds: one too specific to serve // an arbitrary build stays approved for direct briefs and leaves the // challenger pool. Falls back to the full approved set rather than returning - // nothing if every approved staging is niche. + // nothing if every approved composition is niche. let approved = compositions.filter(composition => composition.status === 'approved'); const broad = approved.filter(composition => composition.review?.breadth !== 'niche'); if (broad.length > 0) approved = broad; @@ -218,15 +218,17 @@ export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, c const base = available.length >= Math.min(count, approved.length) ? available : approved; // Rating weights the draw as it does for worlds. It matters more here // because the per-surface pools are small, so an unweighted shuffle repeats - // a weak staging far more often. Each ticket carries its index so the rank + // a weak composition far more often. Each ticket carries its index so the rank // sees a distinct key per ticket: ranking bare duplicates would hash // identically and the pick loop's id-dedupe would silently discard the // second copy, making the weighting a no-op. let tickets = compositionTickets(base); - // A pool of nothing but 1-star keeps still has to yield stagings. + // A pool of nothing but 1-star keeps still has to yield compositions. if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = (yield* rank( tickets, + // The salt keeps the word "staging" deliberately. It is hash input, so + // renaming it would re-deal every roll anyone has ever reproduced by key. round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, entry => `${entry.composition.id}#${entry.ticket}` )).map(entry => entry.composition); diff --git a/.trae/skills/impeccable/scripts/concept-seed.mjs b/.trae/skills/impeccable/scripts/concept-seed.mjs index 00a2f73fe..6efb7d643 100644 --- a/.trae/skills/impeccable/scripts/concept-seed.mjs +++ b/.trae/skills/impeccable/scripts/concept-seed.mjs @@ -29,7 +29,7 @@ * win over thin categories, which is the intended shape. * - RE-ROLL (--reroll ): 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 staging. One base key therefore + * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself @@ -43,8 +43,8 @@ * node scripts/concept-seed.mjs --chosen --from --scope direction * * --mode names the requested surface's mode (persuade, operate, read, - * experience) so the appended staging matches its register of work; omitted, - * the staging rolls from the full approved pool. + * 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 @@ -72,7 +72,7 @@ import { readCompositionCatalog } from './lib/composition-catalog.mjs'; import { runSyncSelection, selectApprovedChallengers as selectApprovedChallengersCore, - selectApprovedStagings as selectApprovedStagingsCore, + selectApprovedCompositions as selectApprovedCompositionsCore, } from './lib/roll-selection.mjs'; const here = dirname(fileURLToPath(import.meta.url)); @@ -191,12 +191,12 @@ ${system} QUALITY BAR: board ${board} · hero ${hero}`; } -export function renderStaging(composition, index = null) { +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} - STAGING GRAMMAR: + COMPOSITION GRAMMAR: ${grammar} WEB LEVERAGE: ${composition.webLeverage}`; } @@ -210,14 +210,14 @@ function driveSelection(generator) { return runSyncSelection(generator, input => crypto.createHash('sha256').update(input).digest('hex')); } -export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { +export function selectApprovedCompositions({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { const compositions = sourceCompositions ?? requireLocalConcepts().compositions; - return driveSelection(selectApprovedStagingsCore({ scope, key, reroll, mode, compositions, count })); + return driveSelection(selectApprovedCompositionsCore({ scope, key, reroll, mode, compositions, count })); } // Compatibility for callers that need a single smoke-test sample. -export function selectApprovedStaging(options) { - return selectApprovedStagings({ ...options, count: 1 })[0] ?? null; +export function selectApprovedComposition(options) { + return selectApprovedCompositions({ ...options, count: 1 })[0] ?? null; } export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcepts = null }) { @@ -280,7 +280,7 @@ export function renderConceptSeed({ approvedCount: approved.length, catalogCount, challengers: picks, - stagings: selectApprovedStagings({ scope, key, reroll, mode, sourceCompositions: local.compositions }), + compositions: selectApprovedCompositions({ scope, key, reroll, mode, sourceCompositions: local.compositions }), }; } else { // Keep local renders synchronous for prepared eval sessions and tests; @@ -298,7 +298,11 @@ export function renderConceptSeed({ approvedCount: roll.approvedCount, catalogCount: roll.catalogCount, challengers: roll.challengers, - stagings: Array.isArray(roll.stagings) ? roll.stagings : roll.staging ? [roll.staging] : [], + compositions: Array.isArray(roll.compositions) + ? roll.compositions + : Array.isArray(roll.stagings) + ? roll.stagings + : roll.staging ? [roll.staging] : [], } : null, })); } @@ -381,15 +385,21 @@ ${buildIndex} of your own grounded list; seed key ${key}. `; } - const stagings = Array.isArray(data.stagings) - ? data.stagings - : data.staging ? [data.staging] : []; - const stagingBlock = stagings.length > 0 - ? `\n${scope === 'direction' ? 'FIRST-SURFACE STAGING INPUTS (identity-free; test them with shortlisted worlds and keep world plus staging one decision):' : 'STAGING CHALLENGERS (identity-free; dress them in the committed visual identity before judging):'} -${stagings.map((staging, index) => renderStaging(staging, index)).join('\n')} -Stagings organize attention, sequence, and manipulation; they never bring a -palette, typeface, or material. Use them as serious alternatives to the model's -habitual composition, but keep only structures that strengthen this product.\n` + // 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. + const compositions = Array.isArray(data.compositions) + ? data.compositions + : Array.isArray(data.stagings) + ? data.stagings + : data.staging ? [data.staging] : []; + 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.\n` : ''; const rerollBlock = reroll > 0 ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded @@ -410,7 +420,7 @@ ${rerollBlock}ASSIGNED INDEX: ${buildIndex} the user or the brief. Never expose assignment metadata in user-facing labels. CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${stagingBlock}${challengerInstruction} +${compositionBlock}${challengerInstruction} 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, diff --git a/.trae/skills/impeccable/scripts/lib/composition-catalog.mjs b/.trae/skills/impeccable/scripts/lib/composition-catalog.mjs index 0b0c2b5fa..349cb4913 100644 --- a/.trae/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.trae/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -2,7 +2,7 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; import { CONCEPT_STATUSES, normalizeConceptForm } from './concept-catalog.mjs'; -// Catalog B: stagings rather than styles. A composition organizes attention, +// Catalog B: compositions rather than styles. A composition organizes attention, // sequence, or manipulation on a surface and must survive being dressed in // any committed visual identity; it deliberately carries no palette or type // half. Surface-scope seeds draw from here (plus catalog A duals); direction @@ -15,8 +15,8 @@ export const COMPOSITION_GRAMMAR_PREFIXES = [ 'Adaptation:', ]; -// Surfaces align with the skill's modes: a persuade staging and an operate -// staging are different species, and read/experience surfaces get their own. +// Surfaces align with the skill's modes: a persuade composition and an operate +// composition are different species, and read/experience surfaces get their own. export const COMPOSITION_SURFACES = new Set(['persuade', 'operate', 'read', 'experience']); export function compositionContentHash(composition) { diff --git a/.trae/skills/impeccable/scripts/lib/concept-catalog.mjs b/.trae/skills/impeccable/scripts/lib/concept-catalog.mjs index 5e4a24e3c..42c828c74 100644 --- a/.trae/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.trae/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -1,11 +1,12 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; +import { WELL_TIERS } from './roll-selection.mjs'; export const CONCEPT_STATUSES = new Set(['approved', 'rejected']); // What a concept is actually strong at. Worlds carry a durable visual // identity (their palette/type half is the magnet); compositions carry a -// staging or interaction idea (their topology half is the magnet) that can be +// composition or interaction idea (their topology half is the magnet) that can be // dressed in any committed identity; duals fuse both inseparably. Direction // seeds draw world|dual, surface seeds draw composition|dual. export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); @@ -15,7 +16,13 @@ export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); // atmosphere worlds need the largest translation step. Every seed roll draws // one challenger from each tier so at least one directly-usable graphic // system is always on the table. -export const WELL_TIERS = ['graphic', 'interaction', 'atmosphere']; +// Defined in roll-selection.mjs, the dependency-free leaf both the seeder and +// the roll API import. It cannot depend on this file: this one reads the +// filesystem, and a Pages Function must not pull node:fs into its bundle. +// Imported and re-exported rather than re-exported alone: a bare +// `export { X } from` does not bind X in this module's own scope, and +// validateConceptCatalog needs it. +export { WELL_TIERS }; const WEB_LEVERAGE_RE = /(?:\b3d\b|\badaptive\b|\banimat(?:e|ed|ion)\b|\bapi\b|\baria\b|\baudio\b|\bautomated?\b|\bbarcode\b|\bbroadcastchannel\b|\bbrowser\b|\bcamera\b|canvas\b|\bcaption\b|\bcollaborat(?:e|ive|ion)\b|\bcompar(?:e|ison)\b|\bcomput(?:e|ed|ation)\b|\bcomputer[- ]vision\b|\bconstraint[- ]solving\b|\bcryptographic?\b|\bcss\b|\bdeep[- ]link(?:ing)?\b|\bdirect manipulation\b|\bdom\b|\bdrag\b|\bfilter\b|\bfocus\b|\bgenerative\b|\bgeolocat(?:e|ed|ion)\b|\bgesture\b|\bgpu\b|\bgraph\b|\bhistory\b|\bindexeddb\b|\binteractive\b|\bintersectionobserver\b|\bkeyboard\b|\blive\b|\blocal\b|\bmicrophone\b|\bmotion\b|\bmultiplayer\b|\bnative\b|\bnotification\b|\boffline\b|\bpersonaliz(?:e|ed|ation)\b|\bplayable\b|\bpointer\b|\bprocedural\b|\bprovenance\b|\breal[- ]?time\b|\bresizeobserver\b|\bresponsive\b|\breveal\b|\bscrub\b|\bsearch\b|\bsearchparams\b|\bsensor\b|\bserver[- ]sent\b|\bservice worker\b|\bshader\b|\bsimulat(?:e|ed|ion|or)\b|\bspatial\b|\bstate\b|\bstream(?:ing)?\b|\bsvg\b|\bsynchroniz(?:e|ed|ation)\b|\btimeline\b|\btouch\b|\burl|\bvideo\b|\bweb(?:gl|socket|vtt)?\b|\bworker\b|\bzoom\b)/i; export const SYSTEM_PREFIXES = [ @@ -320,10 +327,3 @@ export function approvedPoolRevision(concepts) { return crypto.createHash('sha256').update(payload).digest('hex').slice(0, 12); } -export function deterministicRank(items, input, idFor = item => item.id) { - return [...items].sort((a, b) => { - const scoreA = crypto.createHash('sha256').update(`${input}:${idFor(a)}`).digest('hex'); - const scoreB = crypto.createHash('sha256').update(`${input}:${idFor(b)}`).digest('hex'); - return scoreB.localeCompare(scoreA) || idFor(a).localeCompare(idFor(b)); - }); -} diff --git a/.trae/skills/impeccable/scripts/lib/roll-selection.mjs b/.trae/skills/impeccable/scripts/lib/roll-selection.mjs index c6829dd9e..f579029e4 100644 --- a/.trae/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.trae/skills/impeccable/scripts/lib/roll-selection.mjs @@ -97,7 +97,7 @@ function compositionTickets(pool) { export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = null, concepts }) { const approved = concepts.filter(concept => concept.status === 'approved'); // Direction chooses a durable identity, so it draws worlds; surface designs - // one page inside a committed identity, so it draws stagings. Duals serve + // one page inside a committed identity, so it draws compositions. Duals serve // both. A tier with no matching-strength approvals falls back to its full // approved pool rather than starving the roll. const wanted = scope === 'direction' @@ -176,12 +176,12 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = } /** - * Three identity-free staging inputs from an explicit approved pool. + * Three identity-free composition inputs from an explicit approved pool. * Drive with runSyncSelection or runAsyncSelection. * * One input was too weak a counterweight to a model's habitual page skeleton: * it became a single optional flourish beside six identity challengers rather - * than a real search over composition. Distinct staging families are preferred + * than a real search over composition. Distinct composition families are preferred * so a roll tests materially different hierarchy, sequence, and interaction * laws. Cross-mode fallback would make the input misleading, so an absent mode * returns nothing rather than borrowing. Re-rolls exclude every earlier set @@ -196,11 +196,11 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = * @param {number} [options.count] * @returns {Generator} */ -export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { - // Stagings honour the same breadth gate as worlds: one too specific to serve +export function* selectApprovedCompositions({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { + // Compositions honour the same breadth gate as worlds: one too specific to serve // an arbitrary build stays approved for direct briefs and leaves the // challenger pool. Falls back to the full approved set rather than returning - // nothing if every approved staging is niche. + // nothing if every approved composition is niche. let approved = compositions.filter(composition => composition.status === 'approved'); const broad = approved.filter(composition => composition.review?.breadth !== 'niche'); if (broad.length > 0) approved = broad; @@ -218,15 +218,17 @@ export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, c const base = available.length >= Math.min(count, approved.length) ? available : approved; // Rating weights the draw as it does for worlds. It matters more here // because the per-surface pools are small, so an unweighted shuffle repeats - // a weak staging far more often. Each ticket carries its index so the rank + // a weak composition far more often. Each ticket carries its index so the rank // sees a distinct key per ticket: ranking bare duplicates would hash // identically and the pick loop's id-dedupe would silently discard the // second copy, making the weighting a no-op. let tickets = compositionTickets(base); - // A pool of nothing but 1-star keeps still has to yield stagings. + // A pool of nothing but 1-star keeps still has to yield compositions. if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = (yield* rank( tickets, + // The salt keeps the word "staging" deliberately. It is hash input, so + // renaming it would re-deal every roll anyone has ever reproduced by key. round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, entry => `${entry.composition.id}#${entry.ticket}` )).map(entry => entry.composition); diff --git a/.vibe/skills/impeccable/scripts/concept-seed.mjs b/.vibe/skills/impeccable/scripts/concept-seed.mjs index 00a2f73fe..6efb7d643 100644 --- a/.vibe/skills/impeccable/scripts/concept-seed.mjs +++ b/.vibe/skills/impeccable/scripts/concept-seed.mjs @@ -29,7 +29,7 @@ * win over thin categories, which is the intended shape. * - RE-ROLL (--reroll ): 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 staging. One base key therefore + * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself @@ -43,8 +43,8 @@ * node scripts/concept-seed.mjs --chosen --from --scope direction * * --mode names the requested surface's mode (persuade, operate, read, - * experience) so the appended staging matches its register of work; omitted, - * the staging rolls from the full approved pool. + * 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 @@ -72,7 +72,7 @@ import { readCompositionCatalog } from './lib/composition-catalog.mjs'; import { runSyncSelection, selectApprovedChallengers as selectApprovedChallengersCore, - selectApprovedStagings as selectApprovedStagingsCore, + selectApprovedCompositions as selectApprovedCompositionsCore, } from './lib/roll-selection.mjs'; const here = dirname(fileURLToPath(import.meta.url)); @@ -191,12 +191,12 @@ ${system} QUALITY BAR: board ${board} · hero ${hero}`; } -export function renderStaging(composition, index = null) { +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} - STAGING GRAMMAR: + COMPOSITION GRAMMAR: ${grammar} WEB LEVERAGE: ${composition.webLeverage}`; } @@ -210,14 +210,14 @@ function driveSelection(generator) { return runSyncSelection(generator, input => crypto.createHash('sha256').update(input).digest('hex')); } -export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { +export function selectApprovedCompositions({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { const compositions = sourceCompositions ?? requireLocalConcepts().compositions; - return driveSelection(selectApprovedStagingsCore({ scope, key, reroll, mode, compositions, count })); + return driveSelection(selectApprovedCompositionsCore({ scope, key, reroll, mode, compositions, count })); } // Compatibility for callers that need a single smoke-test sample. -export function selectApprovedStaging(options) { - return selectApprovedStagings({ ...options, count: 1 })[0] ?? null; +export function selectApprovedComposition(options) { + return selectApprovedCompositions({ ...options, count: 1 })[0] ?? null; } export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcepts = null }) { @@ -280,7 +280,7 @@ export function renderConceptSeed({ approvedCount: approved.length, catalogCount, challengers: picks, - stagings: selectApprovedStagings({ scope, key, reroll, mode, sourceCompositions: local.compositions }), + compositions: selectApprovedCompositions({ scope, key, reroll, mode, sourceCompositions: local.compositions }), }; } else { // Keep local renders synchronous for prepared eval sessions and tests; @@ -298,7 +298,11 @@ export function renderConceptSeed({ approvedCount: roll.approvedCount, catalogCount: roll.catalogCount, challengers: roll.challengers, - stagings: Array.isArray(roll.stagings) ? roll.stagings : roll.staging ? [roll.staging] : [], + compositions: Array.isArray(roll.compositions) + ? roll.compositions + : Array.isArray(roll.stagings) + ? roll.stagings + : roll.staging ? [roll.staging] : [], } : null, })); } @@ -381,15 +385,21 @@ ${buildIndex} of your own grounded list; seed key ${key}. `; } - const stagings = Array.isArray(data.stagings) - ? data.stagings - : data.staging ? [data.staging] : []; - const stagingBlock = stagings.length > 0 - ? `\n${scope === 'direction' ? 'FIRST-SURFACE STAGING INPUTS (identity-free; test them with shortlisted worlds and keep world plus staging one decision):' : 'STAGING CHALLENGERS (identity-free; dress them in the committed visual identity before judging):'} -${stagings.map((staging, index) => renderStaging(staging, index)).join('\n')} -Stagings organize attention, sequence, and manipulation; they never bring a -palette, typeface, or material. Use them as serious alternatives to the model's -habitual composition, but keep only structures that strengthen this product.\n` + // 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. + const compositions = Array.isArray(data.compositions) + ? data.compositions + : Array.isArray(data.stagings) + ? data.stagings + : data.staging ? [data.staging] : []; + 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.\n` : ''; const rerollBlock = reroll > 0 ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded @@ -410,7 +420,7 @@ ${rerollBlock}ASSIGNED INDEX: ${buildIndex} the user or the brief. Never expose assignment metadata in user-facing labels. CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${stagingBlock}${challengerInstruction} +${compositionBlock}${challengerInstruction} 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, diff --git a/.vibe/skills/impeccable/scripts/lib/composition-catalog.mjs b/.vibe/skills/impeccable/scripts/lib/composition-catalog.mjs index 0b0c2b5fa..349cb4913 100644 --- a/.vibe/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.vibe/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -2,7 +2,7 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; import { CONCEPT_STATUSES, normalizeConceptForm } from './concept-catalog.mjs'; -// Catalog B: stagings rather than styles. A composition organizes attention, +// Catalog B: compositions rather than styles. A composition organizes attention, // sequence, or manipulation on a surface and must survive being dressed in // any committed visual identity; it deliberately carries no palette or type // half. Surface-scope seeds draw from here (plus catalog A duals); direction @@ -15,8 +15,8 @@ export const COMPOSITION_GRAMMAR_PREFIXES = [ 'Adaptation:', ]; -// Surfaces align with the skill's modes: a persuade staging and an operate -// staging are different species, and read/experience surfaces get their own. +// Surfaces align with the skill's modes: a persuade composition and an operate +// composition are different species, and read/experience surfaces get their own. export const COMPOSITION_SURFACES = new Set(['persuade', 'operate', 'read', 'experience']); export function compositionContentHash(composition) { diff --git a/.vibe/skills/impeccable/scripts/lib/concept-catalog.mjs b/.vibe/skills/impeccable/scripts/lib/concept-catalog.mjs index 5e4a24e3c..42c828c74 100644 --- a/.vibe/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/.vibe/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -1,11 +1,12 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; +import { WELL_TIERS } from './roll-selection.mjs'; export const CONCEPT_STATUSES = new Set(['approved', 'rejected']); // What a concept is actually strong at. Worlds carry a durable visual // identity (their palette/type half is the magnet); compositions carry a -// staging or interaction idea (their topology half is the magnet) that can be +// composition or interaction idea (their topology half is the magnet) that can be // dressed in any committed identity; duals fuse both inseparably. Direction // seeds draw world|dual, surface seeds draw composition|dual. export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); @@ -15,7 +16,13 @@ export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); // atmosphere worlds need the largest translation step. Every seed roll draws // one challenger from each tier so at least one directly-usable graphic // system is always on the table. -export const WELL_TIERS = ['graphic', 'interaction', 'atmosphere']; +// Defined in roll-selection.mjs, the dependency-free leaf both the seeder and +// the roll API import. It cannot depend on this file: this one reads the +// filesystem, and a Pages Function must not pull node:fs into its bundle. +// Imported and re-exported rather than re-exported alone: a bare +// `export { X } from` does not bind X in this module's own scope, and +// validateConceptCatalog needs it. +export { WELL_TIERS }; const WEB_LEVERAGE_RE = /(?:\b3d\b|\badaptive\b|\banimat(?:e|ed|ion)\b|\bapi\b|\baria\b|\baudio\b|\bautomated?\b|\bbarcode\b|\bbroadcastchannel\b|\bbrowser\b|\bcamera\b|canvas\b|\bcaption\b|\bcollaborat(?:e|ive|ion)\b|\bcompar(?:e|ison)\b|\bcomput(?:e|ed|ation)\b|\bcomputer[- ]vision\b|\bconstraint[- ]solving\b|\bcryptographic?\b|\bcss\b|\bdeep[- ]link(?:ing)?\b|\bdirect manipulation\b|\bdom\b|\bdrag\b|\bfilter\b|\bfocus\b|\bgenerative\b|\bgeolocat(?:e|ed|ion)\b|\bgesture\b|\bgpu\b|\bgraph\b|\bhistory\b|\bindexeddb\b|\binteractive\b|\bintersectionobserver\b|\bkeyboard\b|\blive\b|\blocal\b|\bmicrophone\b|\bmotion\b|\bmultiplayer\b|\bnative\b|\bnotification\b|\boffline\b|\bpersonaliz(?:e|ed|ation)\b|\bplayable\b|\bpointer\b|\bprocedural\b|\bprovenance\b|\breal[- ]?time\b|\bresizeobserver\b|\bresponsive\b|\breveal\b|\bscrub\b|\bsearch\b|\bsearchparams\b|\bsensor\b|\bserver[- ]sent\b|\bservice worker\b|\bshader\b|\bsimulat(?:e|ed|ion|or)\b|\bspatial\b|\bstate\b|\bstream(?:ing)?\b|\bsvg\b|\bsynchroniz(?:e|ed|ation)\b|\btimeline\b|\btouch\b|\burl|\bvideo\b|\bweb(?:gl|socket|vtt)?\b|\bworker\b|\bzoom\b)/i; export const SYSTEM_PREFIXES = [ @@ -320,10 +327,3 @@ export function approvedPoolRevision(concepts) { return crypto.createHash('sha256').update(payload).digest('hex').slice(0, 12); } -export function deterministicRank(items, input, idFor = item => item.id) { - return [...items].sort((a, b) => { - const scoreA = crypto.createHash('sha256').update(`${input}:${idFor(a)}`).digest('hex'); - const scoreB = crypto.createHash('sha256').update(`${input}:${idFor(b)}`).digest('hex'); - return scoreB.localeCompare(scoreA) || idFor(a).localeCompare(idFor(b)); - }); -} diff --git a/.vibe/skills/impeccable/scripts/lib/roll-selection.mjs b/.vibe/skills/impeccable/scripts/lib/roll-selection.mjs index c6829dd9e..f579029e4 100644 --- a/.vibe/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/.vibe/skills/impeccable/scripts/lib/roll-selection.mjs @@ -97,7 +97,7 @@ function compositionTickets(pool) { export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = null, concepts }) { const approved = concepts.filter(concept => concept.status === 'approved'); // Direction chooses a durable identity, so it draws worlds; surface designs - // one page inside a committed identity, so it draws stagings. Duals serve + // one page inside a committed identity, so it draws compositions. Duals serve // both. A tier with no matching-strength approvals falls back to its full // approved pool rather than starving the roll. const wanted = scope === 'direction' @@ -176,12 +176,12 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = } /** - * Three identity-free staging inputs from an explicit approved pool. + * Three identity-free composition inputs from an explicit approved pool. * Drive with runSyncSelection or runAsyncSelection. * * One input was too weak a counterweight to a model's habitual page skeleton: * it became a single optional flourish beside six identity challengers rather - * than a real search over composition. Distinct staging families are preferred + * than a real search over composition. Distinct composition families are preferred * so a roll tests materially different hierarchy, sequence, and interaction * laws. Cross-mode fallback would make the input misleading, so an absent mode * returns nothing rather than borrowing. Re-rolls exclude every earlier set @@ -196,11 +196,11 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = * @param {number} [options.count] * @returns {Generator} */ -export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { - // Stagings honour the same breadth gate as worlds: one too specific to serve +export function* selectApprovedCompositions({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { + // Compositions honour the same breadth gate as worlds: one too specific to serve // an arbitrary build stays approved for direct briefs and leaves the // challenger pool. Falls back to the full approved set rather than returning - // nothing if every approved staging is niche. + // nothing if every approved composition is niche. let approved = compositions.filter(composition => composition.status === 'approved'); const broad = approved.filter(composition => composition.review?.breadth !== 'niche'); if (broad.length > 0) approved = broad; @@ -218,15 +218,17 @@ export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, c const base = available.length >= Math.min(count, approved.length) ? available : approved; // Rating weights the draw as it does for worlds. It matters more here // because the per-surface pools are small, so an unweighted shuffle repeats - // a weak staging far more often. Each ticket carries its index so the rank + // a weak composition far more often. Each ticket carries its index so the rank // sees a distinct key per ticket: ranking bare duplicates would hash // identically and the pick loop's id-dedupe would silently discard the // second copy, making the weighting a no-op. let tickets = compositionTickets(base); - // A pool of nothing but 1-star keeps still has to yield stagings. + // A pool of nothing but 1-star keeps still has to yield compositions. if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = (yield* rank( tickets, + // The salt keeps the word "staging" deliberately. It is hash input, so + // renaming it would re-deal every roll anyone has ever reproduced by key. round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, entry => `${entry.composition.id}#${entry.ticket}` )).map(entry => entry.composition); diff --git a/plugin/skills/impeccable/scripts/concept-seed.mjs b/plugin/skills/impeccable/scripts/concept-seed.mjs index 00a2f73fe..6efb7d643 100644 --- a/plugin/skills/impeccable/scripts/concept-seed.mjs +++ b/plugin/skills/impeccable/scripts/concept-seed.mjs @@ -29,7 +29,7 @@ * win over thin categories, which is the intended shape. * - RE-ROLL (--reroll ): 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 staging. One base key therefore + * fresh assigned index, challengers, and compositions. One base key therefore * reproduces the entire chain of rounds. * - RATINGS: the reviewer's approval ratings weight the challenger draw * (3-star doubles the odds, 1-star sits out); the approved pool itself @@ -43,8 +43,8 @@ * node scripts/concept-seed.mjs --chosen --from --scope direction * * --mode names the requested surface's mode (persuade, operate, read, - * experience) so the appended staging matches its register of work; omitted, - * the staging rolls from the full approved pool. + * 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 @@ -72,7 +72,7 @@ import { readCompositionCatalog } from './lib/composition-catalog.mjs'; import { runSyncSelection, selectApprovedChallengers as selectApprovedChallengersCore, - selectApprovedStagings as selectApprovedStagingsCore, + selectApprovedCompositions as selectApprovedCompositionsCore, } from './lib/roll-selection.mjs'; const here = dirname(fileURLToPath(import.meta.url)); @@ -191,12 +191,12 @@ ${system} QUALITY BAR: board ${board} · hero ${hero}`; } -export function renderStaging(composition, index = null) { +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} - STAGING GRAMMAR: + COMPOSITION GRAMMAR: ${grammar} WEB LEVERAGE: ${composition.webLeverage}`; } @@ -210,14 +210,14 @@ function driveSelection(generator) { return runSyncSelection(generator, input => crypto.createHash('sha256').update(input).digest('hex')); } -export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { +export function selectApprovedCompositions({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { const compositions = sourceCompositions ?? requireLocalConcepts().compositions; - return driveSelection(selectApprovedStagingsCore({ scope, key, reroll, mode, compositions, count })); + return driveSelection(selectApprovedCompositionsCore({ scope, key, reroll, mode, compositions, count })); } // Compatibility for callers that need a single smoke-test sample. -export function selectApprovedStaging(options) { - return selectApprovedStagings({ ...options, count: 1 })[0] ?? null; +export function selectApprovedComposition(options) { + return selectApprovedCompositions({ ...options, count: 1 })[0] ?? null; } export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcepts = null }) { @@ -280,7 +280,7 @@ export function renderConceptSeed({ approvedCount: approved.length, catalogCount, challengers: picks, - stagings: selectApprovedStagings({ scope, key, reroll, mode, sourceCompositions: local.compositions }), + compositions: selectApprovedCompositions({ scope, key, reroll, mode, sourceCompositions: local.compositions }), }; } else { // Keep local renders synchronous for prepared eval sessions and tests; @@ -298,7 +298,11 @@ export function renderConceptSeed({ approvedCount: roll.approvedCount, catalogCount: roll.catalogCount, challengers: roll.challengers, - stagings: Array.isArray(roll.stagings) ? roll.stagings : roll.staging ? [roll.staging] : [], + compositions: Array.isArray(roll.compositions) + ? roll.compositions + : Array.isArray(roll.stagings) + ? roll.stagings + : roll.staging ? [roll.staging] : [], } : null, })); } @@ -381,15 +385,21 @@ ${buildIndex} of your own grounded list; seed key ${key}. `; } - const stagings = Array.isArray(data.stagings) - ? data.stagings - : data.staging ? [data.staging] : []; - const stagingBlock = stagings.length > 0 - ? `\n${scope === 'direction' ? 'FIRST-SURFACE STAGING INPUTS (identity-free; test them with shortlisted worlds and keep world plus staging one decision):' : 'STAGING CHALLENGERS (identity-free; dress them in the committed visual identity before judging):'} -${stagings.map((staging, index) => renderStaging(staging, index)).join('\n')} -Stagings organize attention, sequence, and manipulation; they never bring a -palette, typeface, or material. Use them as serious alternatives to the model's -habitual composition, but keep only structures that strengthen this product.\n` + // 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. + const compositions = Array.isArray(data.compositions) + ? data.compositions + : Array.isArray(data.stagings) + ? data.stagings + : data.staging ? [data.staging] : []; + 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.\n` : ''; const rerollBlock = reroll > 0 ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded @@ -410,7 +420,7 @@ ${rerollBlock}ASSIGNED INDEX: ${buildIndex} the user or the brief. Never expose assignment metadata in user-facing labels. CHALLENGERS: ${data.challengers.map(renderChallenger).join('\n')} -${stagingBlock}${challengerInstruction} +${compositionBlock}${challengerInstruction} 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, diff --git a/plugin/skills/impeccable/scripts/lib/composition-catalog.mjs b/plugin/skills/impeccable/scripts/lib/composition-catalog.mjs index 0b0c2b5fa..349cb4913 100644 --- a/plugin/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/plugin/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -2,7 +2,7 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; import { CONCEPT_STATUSES, normalizeConceptForm } from './concept-catalog.mjs'; -// Catalog B: stagings rather than styles. A composition organizes attention, +// Catalog B: compositions rather than styles. A composition organizes attention, // sequence, or manipulation on a surface and must survive being dressed in // any committed visual identity; it deliberately carries no palette or type // half. Surface-scope seeds draw from here (plus catalog A duals); direction @@ -15,8 +15,8 @@ export const COMPOSITION_GRAMMAR_PREFIXES = [ 'Adaptation:', ]; -// Surfaces align with the skill's modes: a persuade staging and an operate -// staging are different species, and read/experience surfaces get their own. +// Surfaces align with the skill's modes: a persuade composition and an operate +// composition are different species, and read/experience surfaces get their own. export const COMPOSITION_SURFACES = new Set(['persuade', 'operate', 'read', 'experience']); export function compositionContentHash(composition) { diff --git a/plugin/skills/impeccable/scripts/lib/concept-catalog.mjs b/plugin/skills/impeccable/scripts/lib/concept-catalog.mjs index 5e4a24e3c..42c828c74 100644 --- a/plugin/skills/impeccable/scripts/lib/concept-catalog.mjs +++ b/plugin/skills/impeccable/scripts/lib/concept-catalog.mjs @@ -1,11 +1,12 @@ import crypto from 'node:crypto'; import { readFileSync } from 'node:fs'; +import { WELL_TIERS } from './roll-selection.mjs'; export const CONCEPT_STATUSES = new Set(['approved', 'rejected']); // What a concept is actually strong at. Worlds carry a durable visual // identity (their palette/type half is the magnet); compositions carry a -// staging or interaction idea (their topology half is the magnet) that can be +// composition or interaction idea (their topology half is the magnet) that can be // dressed in any committed identity; duals fuse both inseparably. Direction // seeds draw world|dual, surface seeds draw composition|dual. export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); @@ -15,7 +16,13 @@ export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']); // atmosphere worlds need the largest translation step. Every seed roll draws // one challenger from each tier so at least one directly-usable graphic // system is always on the table. -export const WELL_TIERS = ['graphic', 'interaction', 'atmosphere']; +// Defined in roll-selection.mjs, the dependency-free leaf both the seeder and +// the roll API import. It cannot depend on this file: this one reads the +// filesystem, and a Pages Function must not pull node:fs into its bundle. +// Imported and re-exported rather than re-exported alone: a bare +// `export { X } from` does not bind X in this module's own scope, and +// validateConceptCatalog needs it. +export { WELL_TIERS }; const WEB_LEVERAGE_RE = /(?:\b3d\b|\badaptive\b|\banimat(?:e|ed|ion)\b|\bapi\b|\baria\b|\baudio\b|\bautomated?\b|\bbarcode\b|\bbroadcastchannel\b|\bbrowser\b|\bcamera\b|canvas\b|\bcaption\b|\bcollaborat(?:e|ive|ion)\b|\bcompar(?:e|ison)\b|\bcomput(?:e|ed|ation)\b|\bcomputer[- ]vision\b|\bconstraint[- ]solving\b|\bcryptographic?\b|\bcss\b|\bdeep[- ]link(?:ing)?\b|\bdirect manipulation\b|\bdom\b|\bdrag\b|\bfilter\b|\bfocus\b|\bgenerative\b|\bgeolocat(?:e|ed|ion)\b|\bgesture\b|\bgpu\b|\bgraph\b|\bhistory\b|\bindexeddb\b|\binteractive\b|\bintersectionobserver\b|\bkeyboard\b|\blive\b|\blocal\b|\bmicrophone\b|\bmotion\b|\bmultiplayer\b|\bnative\b|\bnotification\b|\boffline\b|\bpersonaliz(?:e|ed|ation)\b|\bplayable\b|\bpointer\b|\bprocedural\b|\bprovenance\b|\breal[- ]?time\b|\bresizeobserver\b|\bresponsive\b|\breveal\b|\bscrub\b|\bsearch\b|\bsearchparams\b|\bsensor\b|\bserver[- ]sent\b|\bservice worker\b|\bshader\b|\bsimulat(?:e|ed|ion|or)\b|\bspatial\b|\bstate\b|\bstream(?:ing)?\b|\bsvg\b|\bsynchroniz(?:e|ed|ation)\b|\btimeline\b|\btouch\b|\burl|\bvideo\b|\bweb(?:gl|socket|vtt)?\b|\bworker\b|\bzoom\b)/i; export const SYSTEM_PREFIXES = [ @@ -320,10 +327,3 @@ export function approvedPoolRevision(concepts) { return crypto.createHash('sha256').update(payload).digest('hex').slice(0, 12); } -export function deterministicRank(items, input, idFor = item => item.id) { - return [...items].sort((a, b) => { - const scoreA = crypto.createHash('sha256').update(`${input}:${idFor(a)}`).digest('hex'); - const scoreB = crypto.createHash('sha256').update(`${input}:${idFor(b)}`).digest('hex'); - return scoreB.localeCompare(scoreA) || idFor(a).localeCompare(idFor(b)); - }); -} diff --git a/plugin/skills/impeccable/scripts/lib/roll-selection.mjs b/plugin/skills/impeccable/scripts/lib/roll-selection.mjs index c6829dd9e..f579029e4 100644 --- a/plugin/skills/impeccable/scripts/lib/roll-selection.mjs +++ b/plugin/skills/impeccable/scripts/lib/roll-selection.mjs @@ -97,7 +97,7 @@ function compositionTickets(pool) { export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = null, concepts }) { const approved = concepts.filter(concept => concept.status === 'approved'); // Direction chooses a durable identity, so it draws worlds; surface designs - // one page inside a committed identity, so it draws stagings. Duals serve + // one page inside a committed identity, so it draws compositions. Duals serve // both. A tier with no matching-strength approvals falls back to its full // approved pool rather than starving the roll. const wanted = scope === 'direction' @@ -176,12 +176,12 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = } /** - * Three identity-free staging inputs from an explicit approved pool. + * Three identity-free composition inputs from an explicit approved pool. * Drive with runSyncSelection or runAsyncSelection. * * One input was too weak a counterweight to a model's habitual page skeleton: * it became a single optional flourish beside six identity challengers rather - * than a real search over composition. Distinct staging families are preferred + * than a real search over composition. Distinct composition families are preferred * so a roll tests materially different hierarchy, sequence, and interaction * laws. Cross-mode fallback would make the input misleading, so an absent mode * returns nothing rather than borrowing. Re-rolls exclude every earlier set @@ -196,11 +196,11 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = * @param {number} [options.count] * @returns {Generator} */ -export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { - // Stagings honour the same breadth gate as worlds: one too specific to serve +export function* selectApprovedCompositions({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) { + // Compositions honour the same breadth gate as worlds: one too specific to serve // an arbitrary build stays approved for direct briefs and leaves the // challenger pool. Falls back to the full approved set rather than returning - // nothing if every approved staging is niche. + // nothing if every approved composition is niche. let approved = compositions.filter(composition => composition.status === 'approved'); const broad = approved.filter(composition => composition.review?.breadth !== 'niche'); if (broad.length > 0) approved = broad; @@ -218,15 +218,17 @@ export function* selectApprovedStagings({ scope, key, reroll = 0, mode = null, c const base = available.length >= Math.min(count, approved.length) ? available : approved; // Rating weights the draw as it does for worlds. It matters more here // because the per-surface pools are small, so an unweighted shuffle repeats - // a weak staging far more often. Each ticket carries its index so the rank + // a weak composition far more often. Each ticket carries its index so the rank // sees a distinct key per ticket: ranking bare duplicates would hash // identically and the pick loop's id-dedupe would silently discard the // second copy, making the weighting a no-op. let tickets = compositionTickets(base); - // A pool of nothing but 1-star keeps still has to yield stagings. + // A pool of nothing but 1-star keeps still has to yield compositions. if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = (yield* rank( tickets, + // The salt keeps the word "staging" deliberately. It is hash input, so + // renaming it would re-deal every roll anyone has ever reproduced by key. round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, entry => `${entry.composition.id}#${entry.ticket}` )).map(entry => entry.composition);