From b1735015a99b4470e43ca62338e49e5e7bba7851 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Tue, 21 Jul 2026 01:29:37 -0700 Subject: [PATCH] Deal three staging inputs per roll instead of one A single staging input was too weak a counterweight to the model's habitual page skeleton: beside six identity challengers it read as one optional flourish rather than a real search over composition. Roll three from distinct staging families so a roll tests materially different hierarchy, sequence, and interaction laws. selectApprovedStagings replaces the single-pick selector; the old selectApprovedStaging stays as a count-1 wrapper for smoke tests. Re-rolls exclude every earlier set, and an absent mode still returns nothing rather than falling back across modes. Co-Authored-By: Claude --- skill/scripts/concept-seed.mjs | 76 ++++++++++++++++++++++------------ tests/concept-seed.test.mjs | 16 ++++++- 2 files changed, 65 insertions(+), 27 deletions(-) diff --git a/skill/scripts/concept-seed.mjs b/skill/scripts/concept-seed.mjs index 415276f96..164e2a246 100644 --- a/skill/scripts/concept-seed.mjs +++ b/skill/scripts/concept-seed.mjs @@ -163,39 +163,60 @@ ${system} WEB LEVERAGE: ${concept.webLeverage}`; } -export function renderStaging(composition) { +export function renderStaging(composition, index = null) { const grammar = composition.grammar.map(rule => ` - ${rule}`).join('\n'); - return ` ${composition.form} + return ` ${index == null ? '' : `${index + 1}. `}${composition.form} SPARK: ${composition.spark} STAGING GRAMMAR: ${grammar} WEB LEVERAGE: ${composition.webLeverage}`; } -// One approved staging from the composition catalog, rolled deterministically. -// Returns null while the composition pool has no approved entry for the -// requested mode. Cross-mode fallback would turn an absent staging into a -// misleading one. A re-roll excludes earlier stagings until the pool runs out. -export function selectApprovedStaging({ scope, key, reroll = 0, mode = null, sourceCompositions = null }) { +// Three approved, identity-free staging inputs are rolled deterministically. +// 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. Prefer distinct staging families so a +// roll tests materially different hierarchy, sequence, and interaction laws. +// Cross-mode fallback would make the input misleading, so an absent mode still +// returns no staging. Re-rolls exclude every earlier set until the pool runs out. +export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) { const pool = sourceCompositions ?? requireLocalConcepts().compositions; let approved = pool.filter(composition => composition.status === 'approved'); - if (approved.length === 0) return null; + if (approved.length === 0) return []; if (mode) { const matching = approved.filter(composition => composition.surface === mode); - if (matching.length === 0) return null; + if (matching.length === 0) return []; approved = matching; } const prior = new Set(); - let pick = deterministicRank(approved, `${scope}:${key}:staging`)[0]; - for (let round = 1; round <= reroll; round += 1) { - prior.add(pick.id); - const pool = approved.filter(composition => !prior.has(composition.id)); - pick = deterministicRank( - pool.length > 0 ? pool : approved, - `${scope}:${key}:staging:reroll-${round}` - )[0]; + let picks = []; + for (let round = 0; round <= reroll; round += 1) { + const available = approved.filter(composition => !prior.has(composition.id)); + const ranked = deterministicRank( + available.length >= Math.min(count, approved.length) ? available : approved, + round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}` + ); + const families = new Set(); + picks = []; + for (const composition of ranked) { + const family = composition.familyId ?? composition.id; + if (families.has(family)) continue; + picks.push(composition); + families.add(family); + if (picks.length >= count) break; + } + for (const composition of ranked) { + if (picks.length >= count) break; + if (!picks.some(pick => pick.id === composition.id)) picks.push(composition); + } + if (round < reroll) picks.forEach(composition => prior.add(composition.id)); } - return pick; + return picks; +} + +// Compatibility for callers that need a single smoke-test sample. +export function selectApprovedStaging(options) { + return selectApprovedStagings({ ...options, count: 1 })[0] ?? null; } export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcepts = null }) { @@ -326,7 +347,7 @@ export function renderConceptSeed({ approvedCount: approved.length, catalogCount, challengers: picks, - staging: selectApprovedStaging({ scope, key, reroll, mode, sourceCompositions: local.compositions }), + stagings: selectApprovedStagings({ scope, key, reroll, mode, sourceCompositions: local.compositions }), }; } else { // Keep local renders synchronous for prepared eval sessions and tests; @@ -343,7 +364,7 @@ export function renderConceptSeed({ approvedCount: roll.approvedCount, catalogCount: roll.catalogCount, challengers: roll.challengers, - staging: roll.staging, + stagings: Array.isArray(roll.stagings) ? roll.stagings : roll.staging ? [roll.staging] : [], } : null, })); } @@ -418,12 +439,15 @@ A user- or brief-pinned decision beats the roll, always. `; } - const stagingBlock = data.staging - ? `\n${scope === 'direction' ? 'FIRST-SURFACE STAGING (identity-free; pair it with the chosen world and judge the pair as one decision):' : 'STAGING CHALLENGER (identity-free; dress it in the committed visual identity before judging):'} -${renderStaging(data.staging)} -A staging organizes attention, sequence, and manipulation; it never brings a -palette, typeface, or material. It competes on structure alone and loses to a -grounded structure that fits the product better.\n` + 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` : ''; const rerollBlock = reroll > 0 ? `RE-ROLL ROUND ${reroll}: every candidate presented in earlier rounds, grounded diff --git a/tests/concept-seed.test.mjs b/tests/concept-seed.test.mjs index 5f68548e6..b48105043 100644 --- a/tests/concept-seed.test.mjs +++ b/tests/concept-seed.test.mjs @@ -9,7 +9,7 @@ import { validateConceptEntry, } from '../skill/scripts/lib/concept-catalog.mjs'; import { readCompositionCatalog } from '../skill/scripts/lib/composition-catalog.mjs'; -import { renderChallenger, selectApprovedChallengers, selectApprovedStaging } from '../skill/scripts/concept-seed.mjs'; +import { renderChallenger, selectApprovedChallengers, selectApprovedStaging, selectApprovedStagings } from '../skill/scripts/concept-seed.mjs'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const SCRIPT = path.join(ROOT, 'skill', 'scripts', 'concept-seed.mjs'); @@ -103,6 +103,20 @@ describe('concept seed scopes', () => { assert.match(rendered.stdout, /FIRST-SURFACE STAGING/); }); + it('draws several staging inputs from distinct families when the approved pool allows it', () => { + const pool = [ + { id: 'a', familyId: 'first', surface: 'persuade', status: 'approved' }, + { id: 'b', familyId: 'scroll', surface: 'persuade', status: 'approved' }, + { id: 'c', familyId: 'physics', surface: 'persuade', status: 'approved' }, + { id: 'd', familyId: 'first', surface: 'persuade', status: 'approved' }, + { id: 'e', familyId: 'other', surface: 'operate', status: 'approved' }, + ]; + const picks = selectApprovedStagings({ scope: 'direction', key: 'several', mode: 'persuade', sourceCompositions: pool }); + assert.equal(picks.length, 3); + assert.equal(new Set(picks.map(pick => pick.familyId)).size, 3); + assert.equal(picks.every(pick => pick.surface === 'persuade'), true); + }); + it('validates the fixture catalog with the real gates', () => { const result = validateConceptCatalog(fixtureState.catalog, fixtureState.reviewData); assert.deepEqual(result.errors, []);