From 85fd6b4498cb4600ba30da0907348dc69b7b3aa9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2026 23:27:13 +0000 Subject: [PATCH] Sync generated provider output --- .../impeccable/scripts/concept-seed.mjs | 26 ++++++++++++++++--- .../scripts/lib/composition-catalog.mjs | 9 +++++++ .../impeccable/scripts/concept-seed.mjs | 26 ++++++++++++++++--- .../scripts/lib/composition-catalog.mjs | 9 +++++++ .../impeccable/scripts/concept-seed.mjs | 26 ++++++++++++++++--- .../scripts/lib/composition-catalog.mjs | 9 +++++++ .../impeccable/scripts/concept-seed.mjs | 26 ++++++++++++++++--- .../scripts/lib/composition-catalog.mjs | 9 +++++++ .../impeccable/scripts/concept-seed.mjs | 26 ++++++++++++++++--- .../scripts/lib/composition-catalog.mjs | 9 +++++++ .../impeccable/scripts/concept-seed.mjs | 26 ++++++++++++++++--- .../scripts/lib/composition-catalog.mjs | 9 +++++++ .../impeccable/scripts/concept-seed.mjs | 26 ++++++++++++++++--- .../scripts/lib/composition-catalog.mjs | 9 +++++++ .../impeccable/scripts/concept-seed.mjs | 26 ++++++++++++++++--- .../scripts/lib/composition-catalog.mjs | 9 +++++++ .../impeccable/scripts/concept-seed.mjs | 26 ++++++++++++++++--- .../scripts/lib/composition-catalog.mjs | 9 +++++++ .../impeccable/scripts/concept-seed.mjs | 26 ++++++++++++++++--- .../scripts/lib/composition-catalog.mjs | 9 +++++++ .../impeccable/scripts/concept-seed.mjs | 26 ++++++++++++++++--- .../scripts/lib/composition-catalog.mjs | 9 +++++++ .../impeccable/scripts/concept-seed.mjs | 26 ++++++++++++++++--- .../scripts/lib/composition-catalog.mjs | 9 +++++++ .../impeccable/scripts/concept-seed.mjs | 26 ++++++++++++++++--- .../scripts/lib/composition-catalog.mjs | 9 +++++++ .../impeccable/scripts/concept-seed.mjs | 26 ++++++++++++++++--- .../scripts/lib/composition-catalog.mjs | 9 +++++++ .../impeccable/scripts/concept-seed.mjs | 26 ++++++++++++++++--- .../scripts/lib/composition-catalog.mjs | 9 +++++++ 30 files changed, 480 insertions(+), 45 deletions(-) diff --git a/.agents/skills/impeccable/scripts/concept-seed.mjs b/.agents/skills/impeccable/scripts/concept-seed.mjs index 23339d0f3..abf9e2aba 100644 --- a/.agents/skills/impeccable/scripts/concept-seed.mjs +++ b/.agents/skills/impeccable/scripts/concept-seed.mjs @@ -219,14 +219,34 @@ export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, so if (matching.length === 0) return []; approved = matching; } + // Rating weights the draw exactly as it does for world challengers: a 3-star + // staging earns a second ticket, a 1-star marginal keep leaves the pool. This + // matters more here than for worlds because the per-surface pools are small, + // so an unweighted shuffle repeats a weak staging far more often. + // Each ticket carries its index so deterministicRank 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 weighting a no-op. + const ticketsFor = pool => pool.flatMap(composition => { + const rating = composition.review?.rating; + if (rating === 1) return []; + return rating === 3 + ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] + : [{ composition, ticket: 0 }]; + }); + const prior = new Set(); let picks = []; for (let round = 0; round <= reroll; round += 1) { const available = approved.filter(composition => !prior.has(composition.id)); + const base = available.length >= Math.min(count, approved.length) ? available : approved; + let tickets = ticketsFor(base); + // A pool of nothing but 1-star keeps still has to yield stagings. + if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = deterministicRank( - available.length >= Math.min(count, approved.length) ? available : approved, - round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}` - ); + tickets, + round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, + entry => `${entry.composition.id}#${entry.ticket}` + ).map(entry => entry.composition); const families = new Set(); picks = []; for (const composition of ranked) { diff --git a/.agents/skills/impeccable/scripts/lib/composition-catalog.mjs b/.agents/skills/impeccable/scripts/lib/composition-catalog.mjs index 6c959021a..0b0c2b5fa 100644 --- a/.agents/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.agents/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -149,6 +149,15 @@ export function validateCompositionCatalog(catalog, reviewData, { minimumTotal } errors.push(`composition review ${id} is stale: content changed since review`); } } + // Mirrors the concept catalog: an optional 1-3 grade on approved entries + // only, read as a calibration signal and used to weight challenger draws. + if (review?.rating !== undefined) { + if (![1, 2, 3].includes(review.rating)) { + errors.push(`review ${id} rating must be 1, 2, or 3`); + } else if (review.status !== 'approved') { + errors.push(`review ${id} rating only applies to approved compositions`); + } + } if (review?.note !== undefined && (typeof review.note !== 'string' || !review.note.trim() || review.note.length > 500)) { errors.push(`composition review ${id} note must be a non-empty string of 500 characters or fewer`); } diff --git a/.claude/skills/impeccable/scripts/concept-seed.mjs b/.claude/skills/impeccable/scripts/concept-seed.mjs index 23339d0f3..abf9e2aba 100644 --- a/.claude/skills/impeccable/scripts/concept-seed.mjs +++ b/.claude/skills/impeccable/scripts/concept-seed.mjs @@ -219,14 +219,34 @@ export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, so if (matching.length === 0) return []; approved = matching; } + // Rating weights the draw exactly as it does for world challengers: a 3-star + // staging earns a second ticket, a 1-star marginal keep leaves the pool. This + // matters more here than for worlds because the per-surface pools are small, + // so an unweighted shuffle repeats a weak staging far more often. + // Each ticket carries its index so deterministicRank 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 weighting a no-op. + const ticketsFor = pool => pool.flatMap(composition => { + const rating = composition.review?.rating; + if (rating === 1) return []; + return rating === 3 + ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] + : [{ composition, ticket: 0 }]; + }); + const prior = new Set(); let picks = []; for (let round = 0; round <= reroll; round += 1) { const available = approved.filter(composition => !prior.has(composition.id)); + const base = available.length >= Math.min(count, approved.length) ? available : approved; + let tickets = ticketsFor(base); + // A pool of nothing but 1-star keeps still has to yield stagings. + if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = deterministicRank( - available.length >= Math.min(count, approved.length) ? available : approved, - round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}` - ); + tickets, + round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, + entry => `${entry.composition.id}#${entry.ticket}` + ).map(entry => entry.composition); const families = new Set(); picks = []; for (const composition of ranked) { diff --git a/.claude/skills/impeccable/scripts/lib/composition-catalog.mjs b/.claude/skills/impeccable/scripts/lib/composition-catalog.mjs index 6c959021a..0b0c2b5fa 100644 --- a/.claude/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.claude/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -149,6 +149,15 @@ export function validateCompositionCatalog(catalog, reviewData, { minimumTotal } errors.push(`composition review ${id} is stale: content changed since review`); } } + // Mirrors the concept catalog: an optional 1-3 grade on approved entries + // only, read as a calibration signal and used to weight challenger draws. + if (review?.rating !== undefined) { + if (![1, 2, 3].includes(review.rating)) { + errors.push(`review ${id} rating must be 1, 2, or 3`); + } else if (review.status !== 'approved') { + errors.push(`review ${id} rating only applies to approved compositions`); + } + } if (review?.note !== undefined && (typeof review.note !== 'string' || !review.note.trim() || review.note.length > 500)) { errors.push(`composition review ${id} note must be a non-empty string of 500 characters or fewer`); } diff --git a/.cursor/skills/impeccable/scripts/concept-seed.mjs b/.cursor/skills/impeccable/scripts/concept-seed.mjs index 23339d0f3..abf9e2aba 100644 --- a/.cursor/skills/impeccable/scripts/concept-seed.mjs +++ b/.cursor/skills/impeccable/scripts/concept-seed.mjs @@ -219,14 +219,34 @@ export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, so if (matching.length === 0) return []; approved = matching; } + // Rating weights the draw exactly as it does for world challengers: a 3-star + // staging earns a second ticket, a 1-star marginal keep leaves the pool. This + // matters more here than for worlds because the per-surface pools are small, + // so an unweighted shuffle repeats a weak staging far more often. + // Each ticket carries its index so deterministicRank 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 weighting a no-op. + const ticketsFor = pool => pool.flatMap(composition => { + const rating = composition.review?.rating; + if (rating === 1) return []; + return rating === 3 + ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] + : [{ composition, ticket: 0 }]; + }); + const prior = new Set(); let picks = []; for (let round = 0; round <= reroll; round += 1) { const available = approved.filter(composition => !prior.has(composition.id)); + const base = available.length >= Math.min(count, approved.length) ? available : approved; + let tickets = ticketsFor(base); + // A pool of nothing but 1-star keeps still has to yield stagings. + if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = deterministicRank( - available.length >= Math.min(count, approved.length) ? available : approved, - round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}` - ); + tickets, + round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, + entry => `${entry.composition.id}#${entry.ticket}` + ).map(entry => entry.composition); const families = new Set(); picks = []; for (const composition of ranked) { diff --git a/.cursor/skills/impeccable/scripts/lib/composition-catalog.mjs b/.cursor/skills/impeccable/scripts/lib/composition-catalog.mjs index 6c959021a..0b0c2b5fa 100644 --- a/.cursor/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.cursor/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -149,6 +149,15 @@ export function validateCompositionCatalog(catalog, reviewData, { minimumTotal } errors.push(`composition review ${id} is stale: content changed since review`); } } + // Mirrors the concept catalog: an optional 1-3 grade on approved entries + // only, read as a calibration signal and used to weight challenger draws. + if (review?.rating !== undefined) { + if (![1, 2, 3].includes(review.rating)) { + errors.push(`review ${id} rating must be 1, 2, or 3`); + } else if (review.status !== 'approved') { + errors.push(`review ${id} rating only applies to approved compositions`); + } + } if (review?.note !== undefined && (typeof review.note !== 'string' || !review.note.trim() || review.note.length > 500)) { errors.push(`composition review ${id} note must be a non-empty string of 500 characters or fewer`); } diff --git a/.gemini/skills/impeccable/scripts/concept-seed.mjs b/.gemini/skills/impeccable/scripts/concept-seed.mjs index 23339d0f3..abf9e2aba 100644 --- a/.gemini/skills/impeccable/scripts/concept-seed.mjs +++ b/.gemini/skills/impeccable/scripts/concept-seed.mjs @@ -219,14 +219,34 @@ export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, so if (matching.length === 0) return []; approved = matching; } + // Rating weights the draw exactly as it does for world challengers: a 3-star + // staging earns a second ticket, a 1-star marginal keep leaves the pool. This + // matters more here than for worlds because the per-surface pools are small, + // so an unweighted shuffle repeats a weak staging far more often. + // Each ticket carries its index so deterministicRank 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 weighting a no-op. + const ticketsFor = pool => pool.flatMap(composition => { + const rating = composition.review?.rating; + if (rating === 1) return []; + return rating === 3 + ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] + : [{ composition, ticket: 0 }]; + }); + const prior = new Set(); let picks = []; for (let round = 0; round <= reroll; round += 1) { const available = approved.filter(composition => !prior.has(composition.id)); + const base = available.length >= Math.min(count, approved.length) ? available : approved; + let tickets = ticketsFor(base); + // A pool of nothing but 1-star keeps still has to yield stagings. + if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = deterministicRank( - available.length >= Math.min(count, approved.length) ? available : approved, - round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}` - ); + tickets, + round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, + entry => `${entry.composition.id}#${entry.ticket}` + ).map(entry => entry.composition); const families = new Set(); picks = []; for (const composition of ranked) { diff --git a/.gemini/skills/impeccable/scripts/lib/composition-catalog.mjs b/.gemini/skills/impeccable/scripts/lib/composition-catalog.mjs index 6c959021a..0b0c2b5fa 100644 --- a/.gemini/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.gemini/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -149,6 +149,15 @@ export function validateCompositionCatalog(catalog, reviewData, { minimumTotal } errors.push(`composition review ${id} is stale: content changed since review`); } } + // Mirrors the concept catalog: an optional 1-3 grade on approved entries + // only, read as a calibration signal and used to weight challenger draws. + if (review?.rating !== undefined) { + if (![1, 2, 3].includes(review.rating)) { + errors.push(`review ${id} rating must be 1, 2, or 3`); + } else if (review.status !== 'approved') { + errors.push(`review ${id} rating only applies to approved compositions`); + } + } if (review?.note !== undefined && (typeof review.note !== 'string' || !review.note.trim() || review.note.length > 500)) { errors.push(`composition review ${id} note must be a non-empty string of 500 characters or fewer`); } diff --git a/.github/skills/impeccable/scripts/concept-seed.mjs b/.github/skills/impeccable/scripts/concept-seed.mjs index 23339d0f3..abf9e2aba 100644 --- a/.github/skills/impeccable/scripts/concept-seed.mjs +++ b/.github/skills/impeccable/scripts/concept-seed.mjs @@ -219,14 +219,34 @@ export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, so if (matching.length === 0) return []; approved = matching; } + // Rating weights the draw exactly as it does for world challengers: a 3-star + // staging earns a second ticket, a 1-star marginal keep leaves the pool. This + // matters more here than for worlds because the per-surface pools are small, + // so an unweighted shuffle repeats a weak staging far more often. + // Each ticket carries its index so deterministicRank 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 weighting a no-op. + const ticketsFor = pool => pool.flatMap(composition => { + const rating = composition.review?.rating; + if (rating === 1) return []; + return rating === 3 + ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] + : [{ composition, ticket: 0 }]; + }); + const prior = new Set(); let picks = []; for (let round = 0; round <= reroll; round += 1) { const available = approved.filter(composition => !prior.has(composition.id)); + const base = available.length >= Math.min(count, approved.length) ? available : approved; + let tickets = ticketsFor(base); + // A pool of nothing but 1-star keeps still has to yield stagings. + if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = deterministicRank( - available.length >= Math.min(count, approved.length) ? available : approved, - round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}` - ); + tickets, + round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, + entry => `${entry.composition.id}#${entry.ticket}` + ).map(entry => entry.composition); const families = new Set(); picks = []; for (const composition of ranked) { diff --git a/.github/skills/impeccable/scripts/lib/composition-catalog.mjs b/.github/skills/impeccable/scripts/lib/composition-catalog.mjs index 6c959021a..0b0c2b5fa 100644 --- a/.github/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.github/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -149,6 +149,15 @@ export function validateCompositionCatalog(catalog, reviewData, { minimumTotal } errors.push(`composition review ${id} is stale: content changed since review`); } } + // Mirrors the concept catalog: an optional 1-3 grade on approved entries + // only, read as a calibration signal and used to weight challenger draws. + if (review?.rating !== undefined) { + if (![1, 2, 3].includes(review.rating)) { + errors.push(`review ${id} rating must be 1, 2, or 3`); + } else if (review.status !== 'approved') { + errors.push(`review ${id} rating only applies to approved compositions`); + } + } if (review?.note !== undefined && (typeof review.note !== 'string' || !review.note.trim() || review.note.length > 500)) { errors.push(`composition review ${id} note must be a non-empty string of 500 characters or fewer`); } diff --git a/.grok/skills/impeccable/scripts/concept-seed.mjs b/.grok/skills/impeccable/scripts/concept-seed.mjs index 23339d0f3..abf9e2aba 100644 --- a/.grok/skills/impeccable/scripts/concept-seed.mjs +++ b/.grok/skills/impeccable/scripts/concept-seed.mjs @@ -219,14 +219,34 @@ export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, so if (matching.length === 0) return []; approved = matching; } + // Rating weights the draw exactly as it does for world challengers: a 3-star + // staging earns a second ticket, a 1-star marginal keep leaves the pool. This + // matters more here than for worlds because the per-surface pools are small, + // so an unweighted shuffle repeats a weak staging far more often. + // Each ticket carries its index so deterministicRank 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 weighting a no-op. + const ticketsFor = pool => pool.flatMap(composition => { + const rating = composition.review?.rating; + if (rating === 1) return []; + return rating === 3 + ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] + : [{ composition, ticket: 0 }]; + }); + const prior = new Set(); let picks = []; for (let round = 0; round <= reroll; round += 1) { const available = approved.filter(composition => !prior.has(composition.id)); + const base = available.length >= Math.min(count, approved.length) ? available : approved; + let tickets = ticketsFor(base); + // A pool of nothing but 1-star keeps still has to yield stagings. + if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = deterministicRank( - available.length >= Math.min(count, approved.length) ? available : approved, - round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}` - ); + tickets, + round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, + entry => `${entry.composition.id}#${entry.ticket}` + ).map(entry => entry.composition); const families = new Set(); picks = []; for (const composition of ranked) { diff --git a/.grok/skills/impeccable/scripts/lib/composition-catalog.mjs b/.grok/skills/impeccable/scripts/lib/composition-catalog.mjs index 6c959021a..0b0c2b5fa 100644 --- a/.grok/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.grok/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -149,6 +149,15 @@ export function validateCompositionCatalog(catalog, reviewData, { minimumTotal } errors.push(`composition review ${id} is stale: content changed since review`); } } + // Mirrors the concept catalog: an optional 1-3 grade on approved entries + // only, read as a calibration signal and used to weight challenger draws. + if (review?.rating !== undefined) { + if (![1, 2, 3].includes(review.rating)) { + errors.push(`review ${id} rating must be 1, 2, or 3`); + } else if (review.status !== 'approved') { + errors.push(`review ${id} rating only applies to approved compositions`); + } + } if (review?.note !== undefined && (typeof review.note !== 'string' || !review.note.trim() || review.note.length > 500)) { errors.push(`composition review ${id} note must be a non-empty string of 500 characters or fewer`); } diff --git a/.kiro/skills/impeccable/scripts/concept-seed.mjs b/.kiro/skills/impeccable/scripts/concept-seed.mjs index 23339d0f3..abf9e2aba 100644 --- a/.kiro/skills/impeccable/scripts/concept-seed.mjs +++ b/.kiro/skills/impeccable/scripts/concept-seed.mjs @@ -219,14 +219,34 @@ export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, so if (matching.length === 0) return []; approved = matching; } + // Rating weights the draw exactly as it does for world challengers: a 3-star + // staging earns a second ticket, a 1-star marginal keep leaves the pool. This + // matters more here than for worlds because the per-surface pools are small, + // so an unweighted shuffle repeats a weak staging far more often. + // Each ticket carries its index so deterministicRank 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 weighting a no-op. + const ticketsFor = pool => pool.flatMap(composition => { + const rating = composition.review?.rating; + if (rating === 1) return []; + return rating === 3 + ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] + : [{ composition, ticket: 0 }]; + }); + const prior = new Set(); let picks = []; for (let round = 0; round <= reroll; round += 1) { const available = approved.filter(composition => !prior.has(composition.id)); + const base = available.length >= Math.min(count, approved.length) ? available : approved; + let tickets = ticketsFor(base); + // A pool of nothing but 1-star keeps still has to yield stagings. + if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = deterministicRank( - available.length >= Math.min(count, approved.length) ? available : approved, - round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}` - ); + tickets, + round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, + entry => `${entry.composition.id}#${entry.ticket}` + ).map(entry => entry.composition); const families = new Set(); picks = []; for (const composition of ranked) { diff --git a/.kiro/skills/impeccable/scripts/lib/composition-catalog.mjs b/.kiro/skills/impeccable/scripts/lib/composition-catalog.mjs index 6c959021a..0b0c2b5fa 100644 --- a/.kiro/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.kiro/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -149,6 +149,15 @@ export function validateCompositionCatalog(catalog, reviewData, { minimumTotal } errors.push(`composition review ${id} is stale: content changed since review`); } } + // Mirrors the concept catalog: an optional 1-3 grade on approved entries + // only, read as a calibration signal and used to weight challenger draws. + if (review?.rating !== undefined) { + if (![1, 2, 3].includes(review.rating)) { + errors.push(`review ${id} rating must be 1, 2, or 3`); + } else if (review.status !== 'approved') { + errors.push(`review ${id} rating only applies to approved compositions`); + } + } if (review?.note !== undefined && (typeof review.note !== 'string' || !review.note.trim() || review.note.length > 500)) { errors.push(`composition review ${id} note must be a non-empty string of 500 characters or fewer`); } diff --git a/.opencode/skills/impeccable/scripts/concept-seed.mjs b/.opencode/skills/impeccable/scripts/concept-seed.mjs index 23339d0f3..abf9e2aba 100644 --- a/.opencode/skills/impeccable/scripts/concept-seed.mjs +++ b/.opencode/skills/impeccable/scripts/concept-seed.mjs @@ -219,14 +219,34 @@ export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, so if (matching.length === 0) return []; approved = matching; } + // Rating weights the draw exactly as it does for world challengers: a 3-star + // staging earns a second ticket, a 1-star marginal keep leaves the pool. This + // matters more here than for worlds because the per-surface pools are small, + // so an unweighted shuffle repeats a weak staging far more often. + // Each ticket carries its index so deterministicRank 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 weighting a no-op. + const ticketsFor = pool => pool.flatMap(composition => { + const rating = composition.review?.rating; + if (rating === 1) return []; + return rating === 3 + ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] + : [{ composition, ticket: 0 }]; + }); + const prior = new Set(); let picks = []; for (let round = 0; round <= reroll; round += 1) { const available = approved.filter(composition => !prior.has(composition.id)); + const base = available.length >= Math.min(count, approved.length) ? available : approved; + let tickets = ticketsFor(base); + // A pool of nothing but 1-star keeps still has to yield stagings. + if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = deterministicRank( - available.length >= Math.min(count, approved.length) ? available : approved, - round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}` - ); + tickets, + round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, + entry => `${entry.composition.id}#${entry.ticket}` + ).map(entry => entry.composition); const families = new Set(); picks = []; for (const composition of ranked) { diff --git a/.opencode/skills/impeccable/scripts/lib/composition-catalog.mjs b/.opencode/skills/impeccable/scripts/lib/composition-catalog.mjs index 6c959021a..0b0c2b5fa 100644 --- a/.opencode/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.opencode/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -149,6 +149,15 @@ export function validateCompositionCatalog(catalog, reviewData, { minimumTotal } errors.push(`composition review ${id} is stale: content changed since review`); } } + // Mirrors the concept catalog: an optional 1-3 grade on approved entries + // only, read as a calibration signal and used to weight challenger draws. + if (review?.rating !== undefined) { + if (![1, 2, 3].includes(review.rating)) { + errors.push(`review ${id} rating must be 1, 2, or 3`); + } else if (review.status !== 'approved') { + errors.push(`review ${id} rating only applies to approved compositions`); + } + } if (review?.note !== undefined && (typeof review.note !== 'string' || !review.note.trim() || review.note.length > 500)) { errors.push(`composition review ${id} note must be a non-empty string of 500 characters or fewer`); } diff --git a/.pi/skills/impeccable/scripts/concept-seed.mjs b/.pi/skills/impeccable/scripts/concept-seed.mjs index 23339d0f3..abf9e2aba 100644 --- a/.pi/skills/impeccable/scripts/concept-seed.mjs +++ b/.pi/skills/impeccable/scripts/concept-seed.mjs @@ -219,14 +219,34 @@ export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, so if (matching.length === 0) return []; approved = matching; } + // Rating weights the draw exactly as it does for world challengers: a 3-star + // staging earns a second ticket, a 1-star marginal keep leaves the pool. This + // matters more here than for worlds because the per-surface pools are small, + // so an unweighted shuffle repeats a weak staging far more often. + // Each ticket carries its index so deterministicRank 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 weighting a no-op. + const ticketsFor = pool => pool.flatMap(composition => { + const rating = composition.review?.rating; + if (rating === 1) return []; + return rating === 3 + ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] + : [{ composition, ticket: 0 }]; + }); + const prior = new Set(); let picks = []; for (let round = 0; round <= reroll; round += 1) { const available = approved.filter(composition => !prior.has(composition.id)); + const base = available.length >= Math.min(count, approved.length) ? available : approved; + let tickets = ticketsFor(base); + // A pool of nothing but 1-star keeps still has to yield stagings. + if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = deterministicRank( - available.length >= Math.min(count, approved.length) ? available : approved, - round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}` - ); + tickets, + round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, + entry => `${entry.composition.id}#${entry.ticket}` + ).map(entry => entry.composition); const families = new Set(); picks = []; for (const composition of ranked) { diff --git a/.pi/skills/impeccable/scripts/lib/composition-catalog.mjs b/.pi/skills/impeccable/scripts/lib/composition-catalog.mjs index 6c959021a..0b0c2b5fa 100644 --- a/.pi/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.pi/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -149,6 +149,15 @@ export function validateCompositionCatalog(catalog, reviewData, { minimumTotal } errors.push(`composition review ${id} is stale: content changed since review`); } } + // Mirrors the concept catalog: an optional 1-3 grade on approved entries + // only, read as a calibration signal and used to weight challenger draws. + if (review?.rating !== undefined) { + if (![1, 2, 3].includes(review.rating)) { + errors.push(`review ${id} rating must be 1, 2, or 3`); + } else if (review.status !== 'approved') { + errors.push(`review ${id} rating only applies to approved compositions`); + } + } if (review?.note !== undefined && (typeof review.note !== 'string' || !review.note.trim() || review.note.length > 500)) { errors.push(`composition review ${id} note must be a non-empty string of 500 characters or fewer`); } diff --git a/.qoder/skills/impeccable/scripts/concept-seed.mjs b/.qoder/skills/impeccable/scripts/concept-seed.mjs index 23339d0f3..abf9e2aba 100644 --- a/.qoder/skills/impeccable/scripts/concept-seed.mjs +++ b/.qoder/skills/impeccable/scripts/concept-seed.mjs @@ -219,14 +219,34 @@ export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, so if (matching.length === 0) return []; approved = matching; } + // Rating weights the draw exactly as it does for world challengers: a 3-star + // staging earns a second ticket, a 1-star marginal keep leaves the pool. This + // matters more here than for worlds because the per-surface pools are small, + // so an unweighted shuffle repeats a weak staging far more often. + // Each ticket carries its index so deterministicRank 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 weighting a no-op. + const ticketsFor = pool => pool.flatMap(composition => { + const rating = composition.review?.rating; + if (rating === 1) return []; + return rating === 3 + ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] + : [{ composition, ticket: 0 }]; + }); + const prior = new Set(); let picks = []; for (let round = 0; round <= reroll; round += 1) { const available = approved.filter(composition => !prior.has(composition.id)); + const base = available.length >= Math.min(count, approved.length) ? available : approved; + let tickets = ticketsFor(base); + // A pool of nothing but 1-star keeps still has to yield stagings. + if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = deterministicRank( - available.length >= Math.min(count, approved.length) ? available : approved, - round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}` - ); + tickets, + round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, + entry => `${entry.composition.id}#${entry.ticket}` + ).map(entry => entry.composition); const families = new Set(); picks = []; for (const composition of ranked) { diff --git a/.qoder/skills/impeccable/scripts/lib/composition-catalog.mjs b/.qoder/skills/impeccable/scripts/lib/composition-catalog.mjs index 6c959021a..0b0c2b5fa 100644 --- a/.qoder/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.qoder/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -149,6 +149,15 @@ export function validateCompositionCatalog(catalog, reviewData, { minimumTotal } errors.push(`composition review ${id} is stale: content changed since review`); } } + // Mirrors the concept catalog: an optional 1-3 grade on approved entries + // only, read as a calibration signal and used to weight challenger draws. + if (review?.rating !== undefined) { + if (![1, 2, 3].includes(review.rating)) { + errors.push(`review ${id} rating must be 1, 2, or 3`); + } else if (review.status !== 'approved') { + errors.push(`review ${id} rating only applies to approved compositions`); + } + } if (review?.note !== undefined && (typeof review.note !== 'string' || !review.note.trim() || review.note.length > 500)) { errors.push(`composition review ${id} note must be a non-empty string of 500 characters or fewer`); } diff --git a/.rovodev/skills/impeccable/scripts/concept-seed.mjs b/.rovodev/skills/impeccable/scripts/concept-seed.mjs index 23339d0f3..abf9e2aba 100644 --- a/.rovodev/skills/impeccable/scripts/concept-seed.mjs +++ b/.rovodev/skills/impeccable/scripts/concept-seed.mjs @@ -219,14 +219,34 @@ export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, so if (matching.length === 0) return []; approved = matching; } + // Rating weights the draw exactly as it does for world challengers: a 3-star + // staging earns a second ticket, a 1-star marginal keep leaves the pool. This + // matters more here than for worlds because the per-surface pools are small, + // so an unweighted shuffle repeats a weak staging far more often. + // Each ticket carries its index so deterministicRank 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 weighting a no-op. + const ticketsFor = pool => pool.flatMap(composition => { + const rating = composition.review?.rating; + if (rating === 1) return []; + return rating === 3 + ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] + : [{ composition, ticket: 0 }]; + }); + const prior = new Set(); let picks = []; for (let round = 0; round <= reroll; round += 1) { const available = approved.filter(composition => !prior.has(composition.id)); + const base = available.length >= Math.min(count, approved.length) ? available : approved; + let tickets = ticketsFor(base); + // A pool of nothing but 1-star keeps still has to yield stagings. + if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = deterministicRank( - available.length >= Math.min(count, approved.length) ? available : approved, - round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}` - ); + tickets, + round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, + entry => `${entry.composition.id}#${entry.ticket}` + ).map(entry => entry.composition); const families = new Set(); picks = []; for (const composition of ranked) { diff --git a/.rovodev/skills/impeccable/scripts/lib/composition-catalog.mjs b/.rovodev/skills/impeccable/scripts/lib/composition-catalog.mjs index 6c959021a..0b0c2b5fa 100644 --- a/.rovodev/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.rovodev/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -149,6 +149,15 @@ export function validateCompositionCatalog(catalog, reviewData, { minimumTotal } errors.push(`composition review ${id} is stale: content changed since review`); } } + // Mirrors the concept catalog: an optional 1-3 grade on approved entries + // only, read as a calibration signal and used to weight challenger draws. + if (review?.rating !== undefined) { + if (![1, 2, 3].includes(review.rating)) { + errors.push(`review ${id} rating must be 1, 2, or 3`); + } else if (review.status !== 'approved') { + errors.push(`review ${id} rating only applies to approved compositions`); + } + } if (review?.note !== undefined && (typeof review.note !== 'string' || !review.note.trim() || review.note.length > 500)) { errors.push(`composition review ${id} note must be a non-empty string of 500 characters or fewer`); } diff --git a/.trae-cn/skills/impeccable/scripts/concept-seed.mjs b/.trae-cn/skills/impeccable/scripts/concept-seed.mjs index 23339d0f3..abf9e2aba 100644 --- a/.trae-cn/skills/impeccable/scripts/concept-seed.mjs +++ b/.trae-cn/skills/impeccable/scripts/concept-seed.mjs @@ -219,14 +219,34 @@ export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, so if (matching.length === 0) return []; approved = matching; } + // Rating weights the draw exactly as it does for world challengers: a 3-star + // staging earns a second ticket, a 1-star marginal keep leaves the pool. This + // matters more here than for worlds because the per-surface pools are small, + // so an unweighted shuffle repeats a weak staging far more often. + // Each ticket carries its index so deterministicRank 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 weighting a no-op. + const ticketsFor = pool => pool.flatMap(composition => { + const rating = composition.review?.rating; + if (rating === 1) return []; + return rating === 3 + ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] + : [{ composition, ticket: 0 }]; + }); + const prior = new Set(); let picks = []; for (let round = 0; round <= reroll; round += 1) { const available = approved.filter(composition => !prior.has(composition.id)); + const base = available.length >= Math.min(count, approved.length) ? available : approved; + let tickets = ticketsFor(base); + // A pool of nothing but 1-star keeps still has to yield stagings. + if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = deterministicRank( - available.length >= Math.min(count, approved.length) ? available : approved, - round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}` - ); + tickets, + round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, + entry => `${entry.composition.id}#${entry.ticket}` + ).map(entry => entry.composition); const families = new Set(); picks = []; for (const composition of ranked) { diff --git a/.trae-cn/skills/impeccable/scripts/lib/composition-catalog.mjs b/.trae-cn/skills/impeccable/scripts/lib/composition-catalog.mjs index 6c959021a..0b0c2b5fa 100644 --- a/.trae-cn/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.trae-cn/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -149,6 +149,15 @@ export function validateCompositionCatalog(catalog, reviewData, { minimumTotal } errors.push(`composition review ${id} is stale: content changed since review`); } } + // Mirrors the concept catalog: an optional 1-3 grade on approved entries + // only, read as a calibration signal and used to weight challenger draws. + if (review?.rating !== undefined) { + if (![1, 2, 3].includes(review.rating)) { + errors.push(`review ${id} rating must be 1, 2, or 3`); + } else if (review.status !== 'approved') { + errors.push(`review ${id} rating only applies to approved compositions`); + } + } if (review?.note !== undefined && (typeof review.note !== 'string' || !review.note.trim() || review.note.length > 500)) { errors.push(`composition review ${id} note must be a non-empty string of 500 characters or fewer`); } diff --git a/.trae/skills/impeccable/scripts/concept-seed.mjs b/.trae/skills/impeccable/scripts/concept-seed.mjs index 23339d0f3..abf9e2aba 100644 --- a/.trae/skills/impeccable/scripts/concept-seed.mjs +++ b/.trae/skills/impeccable/scripts/concept-seed.mjs @@ -219,14 +219,34 @@ export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, so if (matching.length === 0) return []; approved = matching; } + // Rating weights the draw exactly as it does for world challengers: a 3-star + // staging earns a second ticket, a 1-star marginal keep leaves the pool. This + // matters more here than for worlds because the per-surface pools are small, + // so an unweighted shuffle repeats a weak staging far more often. + // Each ticket carries its index so deterministicRank 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 weighting a no-op. + const ticketsFor = pool => pool.flatMap(composition => { + const rating = composition.review?.rating; + if (rating === 1) return []; + return rating === 3 + ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] + : [{ composition, ticket: 0 }]; + }); + const prior = new Set(); let picks = []; for (let round = 0; round <= reroll; round += 1) { const available = approved.filter(composition => !prior.has(composition.id)); + const base = available.length >= Math.min(count, approved.length) ? available : approved; + let tickets = ticketsFor(base); + // A pool of nothing but 1-star keeps still has to yield stagings. + if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = deterministicRank( - available.length >= Math.min(count, approved.length) ? available : approved, - round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}` - ); + tickets, + round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, + entry => `${entry.composition.id}#${entry.ticket}` + ).map(entry => entry.composition); const families = new Set(); picks = []; for (const composition of ranked) { diff --git a/.trae/skills/impeccable/scripts/lib/composition-catalog.mjs b/.trae/skills/impeccable/scripts/lib/composition-catalog.mjs index 6c959021a..0b0c2b5fa 100644 --- a/.trae/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.trae/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -149,6 +149,15 @@ export function validateCompositionCatalog(catalog, reviewData, { minimumTotal } errors.push(`composition review ${id} is stale: content changed since review`); } } + // Mirrors the concept catalog: an optional 1-3 grade on approved entries + // only, read as a calibration signal and used to weight challenger draws. + if (review?.rating !== undefined) { + if (![1, 2, 3].includes(review.rating)) { + errors.push(`review ${id} rating must be 1, 2, or 3`); + } else if (review.status !== 'approved') { + errors.push(`review ${id} rating only applies to approved compositions`); + } + } if (review?.note !== undefined && (typeof review.note !== 'string' || !review.note.trim() || review.note.length > 500)) { errors.push(`composition review ${id} note must be a non-empty string of 500 characters or fewer`); } diff --git a/.vibe/skills/impeccable/scripts/concept-seed.mjs b/.vibe/skills/impeccable/scripts/concept-seed.mjs index 23339d0f3..abf9e2aba 100644 --- a/.vibe/skills/impeccable/scripts/concept-seed.mjs +++ b/.vibe/skills/impeccable/scripts/concept-seed.mjs @@ -219,14 +219,34 @@ export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, so if (matching.length === 0) return []; approved = matching; } + // Rating weights the draw exactly as it does for world challengers: a 3-star + // staging earns a second ticket, a 1-star marginal keep leaves the pool. This + // matters more here than for worlds because the per-surface pools are small, + // so an unweighted shuffle repeats a weak staging far more often. + // Each ticket carries its index so deterministicRank 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 weighting a no-op. + const ticketsFor = pool => pool.flatMap(composition => { + const rating = composition.review?.rating; + if (rating === 1) return []; + return rating === 3 + ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] + : [{ composition, ticket: 0 }]; + }); + const prior = new Set(); let picks = []; for (let round = 0; round <= reroll; round += 1) { const available = approved.filter(composition => !prior.has(composition.id)); + const base = available.length >= Math.min(count, approved.length) ? available : approved; + let tickets = ticketsFor(base); + // A pool of nothing but 1-star keeps still has to yield stagings. + if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = deterministicRank( - available.length >= Math.min(count, approved.length) ? available : approved, - round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}` - ); + tickets, + round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, + entry => `${entry.composition.id}#${entry.ticket}` + ).map(entry => entry.composition); const families = new Set(); picks = []; for (const composition of ranked) { diff --git a/.vibe/skills/impeccable/scripts/lib/composition-catalog.mjs b/.vibe/skills/impeccable/scripts/lib/composition-catalog.mjs index 6c959021a..0b0c2b5fa 100644 --- a/.vibe/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/.vibe/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -149,6 +149,15 @@ export function validateCompositionCatalog(catalog, reviewData, { minimumTotal } errors.push(`composition review ${id} is stale: content changed since review`); } } + // Mirrors the concept catalog: an optional 1-3 grade on approved entries + // only, read as a calibration signal and used to weight challenger draws. + if (review?.rating !== undefined) { + if (![1, 2, 3].includes(review.rating)) { + errors.push(`review ${id} rating must be 1, 2, or 3`); + } else if (review.status !== 'approved') { + errors.push(`review ${id} rating only applies to approved compositions`); + } + } if (review?.note !== undefined && (typeof review.note !== 'string' || !review.note.trim() || review.note.length > 500)) { errors.push(`composition review ${id} note must be a non-empty string of 500 characters or fewer`); } diff --git a/plugin/skills/impeccable/scripts/concept-seed.mjs b/plugin/skills/impeccable/scripts/concept-seed.mjs index 23339d0f3..abf9e2aba 100644 --- a/plugin/skills/impeccable/scripts/concept-seed.mjs +++ b/plugin/skills/impeccable/scripts/concept-seed.mjs @@ -219,14 +219,34 @@ export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, so if (matching.length === 0) return []; approved = matching; } + // Rating weights the draw exactly as it does for world challengers: a 3-star + // staging earns a second ticket, a 1-star marginal keep leaves the pool. This + // matters more here than for worlds because the per-surface pools are small, + // so an unweighted shuffle repeats a weak staging far more often. + // Each ticket carries its index so deterministicRank 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 weighting a no-op. + const ticketsFor = pool => pool.flatMap(composition => { + const rating = composition.review?.rating; + if (rating === 1) return []; + return rating === 3 + ? [{ composition, ticket: 0 }, { composition, ticket: 1 }] + : [{ composition, ticket: 0 }]; + }); + const prior = new Set(); let picks = []; for (let round = 0; round <= reroll; round += 1) { const available = approved.filter(composition => !prior.has(composition.id)); + const base = available.length >= Math.min(count, approved.length) ? available : approved; + let tickets = ticketsFor(base); + // A pool of nothing but 1-star keeps still has to yield stagings. + if (tickets.length === 0) tickets = base.map(composition => ({ composition, ticket: 0 })); const ranked = deterministicRank( - available.length >= Math.min(count, approved.length) ? available : approved, - round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}` - ); + tickets, + round === 0 ? `${scope}:${key}:staging` : `${scope}:${key}:staging:reroll-${round}`, + entry => `${entry.composition.id}#${entry.ticket}` + ).map(entry => entry.composition); const families = new Set(); picks = []; for (const composition of ranked) { diff --git a/plugin/skills/impeccable/scripts/lib/composition-catalog.mjs b/plugin/skills/impeccable/scripts/lib/composition-catalog.mjs index 6c959021a..0b0c2b5fa 100644 --- a/plugin/skills/impeccable/scripts/lib/composition-catalog.mjs +++ b/plugin/skills/impeccable/scripts/lib/composition-catalog.mjs @@ -149,6 +149,15 @@ export function validateCompositionCatalog(catalog, reviewData, { minimumTotal } errors.push(`composition review ${id} is stale: content changed since review`); } } + // Mirrors the concept catalog: an optional 1-3 grade on approved entries + // only, read as a calibration signal and used to weight challenger draws. + if (review?.rating !== undefined) { + if (![1, 2, 3].includes(review.rating)) { + errors.push(`review ${id} rating must be 1, 2, or 3`); + } else if (review.status !== 'approved') { + errors.push(`review ${id} rating only applies to approved compositions`); + } + } if (review?.note !== undefined && (typeof review.note !== 'string' || !review.note.trim() || review.note.length > 500)) { errors.push(`composition review ${id} note must be a non-empty string of 500 characters or fewer`); }