Split breadth from rating in the challenger and staging pools

Rating grades quality, breadth says whether a world can serve an
arbitrary build at all; while they shared one field, the only way to
hold a narrow world back was calling it marginal, which made excellent
but narrow unrecordable and corrupted the ratings as a calibration
signal for the next authoring round. Both axes now exclude
independently, either kind of hold keeps its approval for direct
briefs, an all-niche tier falls back rather than starving, and
stagings honour the same gate with the same fallback. Tests cover the
niche exclusion at strength, the fallback parity with marginal-only
tiers, and the staging gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-27 19:17:09 -07:00
co-authored by Claude Fable 5
parent d3c7b05a3e
commit ce4dcf9a93
2 changed files with 72 additions and 4 deletions
+17 -4
View File
@@ -206,7 +206,13 @@ ${grammar}
// 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;
// Stagings honour the same breadth gate as worlds: a staging 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 marked niche.
let approved = pool.filter(composition => composition.status === 'approved');
const broad = approved.filter(composition => composition.review?.breadth !== 'niche');
if (broad.length > 0) approved = broad;
if (approved.length === 0) return [];
if (mode) {
const matching = approved.filter(composition => composition.surface === mode);
@@ -271,12 +277,19 @@ export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcep
// graphic systems beside instrument languages and atmosphere worlds, with
// the second pick preferring a different family for diversity. Tier order
// in the rendered list is rolled too, to avoid positional bias.
// Approval ratings weight the draw: a 3-star world earns a second ticket
// (roughly double odds), a 1-star keeps its approval for direct briefs but
// leaves the challenger pool unless a tier has nothing else.
// Two separate axes, and both can exclude. Rating grades quality: a 3-star
// earns a second ticket, a 1-star marginal keep leaves the pool. Breadth says
// whether a world can serve an arbitrary build at all, so a niche world
// leaves the pool however good it is. Breadth was split out of rating because
// the only way to hold a narrow world back used to be calling it marginal,
// which made "excellent but narrow" unrecordable and corrupted the ratings as
// a calibration signal for the next authoring round.
const ticketsFor = pool => pool.flatMap(concept => {
const rating = concept.review?.rating;
if (rating === 1) return [];
// Two independent exclusions: a marginal world is too weak to challenge,
// a niche world too narrow. Either one keeps its approval for direct
// briefs and leaves the pool.
if (rating === 1 || concept.review?.breadth === 'niche') return [];
return rating === 3
? [{ concept, ticket: 0 }, { concept, ticket: 1 }]
: [{ concept, ticket: 0 }];