mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 22:26:38 +03:00
Sync generated provider output
This commit is contained in:
@@ -38,10 +38,17 @@
|
||||
* Usage:
|
||||
* node scripts/concept-seed.mjs --scope direction --mode persuade
|
||||
* node scripts/concept-seed.mjs --scope surface --mode operate --from <key>
|
||||
* node scripts/concept-seed.mjs --scope surface --mode operate --area onboarding-and-setup
|
||||
* node scripts/concept-seed.mjs --scope direction --candidate-count 6
|
||||
* node scripts/concept-seed.mjs --scope direction --mode persuade --from <key> --reroll 1
|
||||
* node scripts/concept-seed.mjs --chosen <challenger-id> --from <key> --scope direction
|
||||
*
|
||||
* --area names the area of concern inside that mode (an onboarding flow and a
|
||||
* settings page are both operate, and want different compositions). It needs
|
||||
* --mode, prefers compositions in that area, and tops up from the rest of the
|
||||
* surface rather than dealing fewer. --mode also gates which worlds are
|
||||
* eligible, for worlds whose reviewer marked them as carrying only some modes.
|
||||
*
|
||||
* --mode names the requested surface's mode (persuade, operate, read,
|
||||
* experience) so the appended compositions match its register of work; omitted,
|
||||
* they roll from the full approved pool.
|
||||
@@ -68,7 +75,7 @@ import {
|
||||
validateConceptCatalog,
|
||||
WELL_TIERS,
|
||||
} from './lib/concept-catalog.mjs';
|
||||
import { readCompositionCatalog } from './lib/composition-catalog.mjs';
|
||||
import { areasForSurface, readCompositionCatalog } from './lib/composition-catalog.mjs';
|
||||
import {
|
||||
runSyncSelection,
|
||||
selectApprovedChallengers as selectApprovedChallengersCore,
|
||||
@@ -127,9 +134,10 @@ function requireLocalConcepts() {
|
||||
return local;
|
||||
}
|
||||
|
||||
async function fetchRoll({ scope, key, mode, reroll }) {
|
||||
async function fetchRoll({ scope, key, mode, area, reroll }) {
|
||||
const params = new URLSearchParams({ scope, key, reroll: String(reroll) });
|
||||
if (mode) params.set('mode', mode);
|
||||
if (area) params.set('area', area);
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), apiBudgetMs());
|
||||
try {
|
||||
@@ -210,9 +218,9 @@ function driveSelection(generator) {
|
||||
return runSyncSelection(generator, input => crypto.createHash('sha256').update(input).digest('hex'));
|
||||
}
|
||||
|
||||
export function selectApprovedCompositions({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) {
|
||||
export function selectApprovedCompositions({ scope, key, reroll = 0, mode = null, area = null, sourceCompositions = null, count = 3 }) {
|
||||
const compositions = sourceCompositions ?? requireLocalConcepts().compositions;
|
||||
return driveSelection(selectApprovedCompositionsCore({ scope, key, reroll, mode, compositions, count }));
|
||||
return driveSelection(selectApprovedCompositionsCore({ scope, key, reroll, mode, area, compositions, count }));
|
||||
}
|
||||
|
||||
// Compatibility for callers that need a single smoke-test sample.
|
||||
@@ -220,9 +228,9 @@ export function selectApprovedComposition(options) {
|
||||
return selectApprovedCompositions({ ...options, count: 1 })[0] ?? null;
|
||||
}
|
||||
|
||||
export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcepts = null }) {
|
||||
export function selectApprovedChallengers({ scope, key, reroll = 0, mode = null, sourceConcepts = null }) {
|
||||
const source = sourceConcepts ?? requireLocalConcepts().concepts;
|
||||
const { approved, picks } = driveSelection(selectApprovedChallengersCore({ scope, key, reroll, concepts: source }));
|
||||
const { approved, picks } = driveSelection(selectApprovedChallengersCore({ scope, key, reroll, mode, concepts: source }));
|
||||
return {
|
||||
approved,
|
||||
picks,
|
||||
@@ -238,6 +246,7 @@ export function renderConceptSeed({
|
||||
key = process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex'),
|
||||
reroll = 0,
|
||||
mode = null,
|
||||
area = null,
|
||||
candidateCount = 7,
|
||||
catalogDir = CATALOG_DIR,
|
||||
_resolvedData = undefined,
|
||||
@@ -251,6 +260,15 @@ export function renderConceptSeed({
|
||||
if (mode !== null && !SEED_MODES.has(mode)) {
|
||||
throw new Error('concept-seed: --mode must be persuade, operate, read, or experience');
|
||||
}
|
||||
if (area !== null) {
|
||||
if (mode === null) {
|
||||
throw new Error('concept-seed: --area needs --mode, because areas are scoped to a surface');
|
||||
}
|
||||
const allowed = areasForSurface(mode);
|
||||
if (!allowed.includes(area)) {
|
||||
throw new Error(`concept-seed: --area must be one of the ${mode} areas (${allowed.join(', ')})`);
|
||||
}
|
||||
}
|
||||
if (!Number.isInteger(candidateCount) || candidateCount < 5 || candidateCount > 7) {
|
||||
throw new Error('concept-seed: --candidate-count must be an integer from 5 to 7');
|
||||
}
|
||||
@@ -272,6 +290,7 @@ export function renderConceptSeed({
|
||||
scope,
|
||||
key,
|
||||
reroll,
|
||||
mode,
|
||||
sourceConcepts: local.concepts,
|
||||
});
|
||||
data = {
|
||||
@@ -280,16 +299,17 @@ export function renderConceptSeed({
|
||||
approvedCount: approved.length,
|
||||
catalogCount,
|
||||
challengers: picks,
|
||||
compositions: selectApprovedCompositions({ scope, key, reroll, mode, sourceCompositions: local.compositions }),
|
||||
compositions: selectApprovedCompositions({ scope, key, reroll, mode, area, sourceCompositions: local.compositions }),
|
||||
};
|
||||
} else {
|
||||
// Keep local renders synchronous for prepared eval sessions and tests;
|
||||
// installed skills without a bundled catalog resolve through the API.
|
||||
return fetchRoll({ scope, key, mode, reroll }).then(roll => renderConceptSeed({
|
||||
return fetchRoll({ scope, key, mode, area, reroll }).then(roll => renderConceptSeed({
|
||||
scope,
|
||||
key,
|
||||
reroll,
|
||||
mode,
|
||||
area,
|
||||
candidateCount,
|
||||
catalogDir,
|
||||
_resolvedData: roll ? {
|
||||
@@ -439,6 +459,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
|
||||
const scopeIdx = args.indexOf('--scope');
|
||||
const rerollIdx = args.indexOf('--reroll');
|
||||
const modeIdx = args.indexOf('--mode');
|
||||
const areaIdx = args.indexOf('--area');
|
||||
const candidateCountIdx = args.indexOf('--candidate-count');
|
||||
const chosenIdx = args.indexOf('--chosen');
|
||||
try {
|
||||
@@ -472,6 +493,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
|
||||
: (process.env.IMPECCABLE_CONCEPT_SEED || crypto.randomBytes(4).toString('hex')),
|
||||
reroll: rerollIdx !== -1 ? Number(args[rerollIdx + 1]) : 0,
|
||||
mode: modeIdx !== -1 ? args[modeIdx + 1] : null,
|
||||
area: areaIdx !== -1 ? args[areaIdx + 1] : null,
|
||||
candidateCount: candidateCountIdx !== -1 ? Number(args[candidateCountIdx + 1]) : 7,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -19,6 +19,29 @@ export const COMPOSITION_GRAMMAR_PREFIXES = [
|
||||
// composition are different species, and read/experience surfaces get their own.
|
||||
export const COMPOSITION_SURFACES = new Set(['persuade', 'operate', 'read', 'experience']);
|
||||
|
||||
// Areas of concern, one level below surface. Surface alone is too coarse to deal
|
||||
// against: "operate" spans onboarding, settings, dashboards and editors, so a
|
||||
// build designing an onboarding flow could legitimately draw a settings
|
||||
// composition and the input would read as noise. Areas name the problem the
|
||||
// composition is about, not how it is built, which is what familyId already does.
|
||||
//
|
||||
// `area` is optional. An entry without one is eligible for any request in its
|
||||
// surface, so nothing has to be backfilled before this ships, and a request for
|
||||
// an area with a thin pool tops up from the rest of the surface rather than
|
||||
// dealing fewer.
|
||||
export const COMPOSITION_AREAS = {
|
||||
persuade: ['landing-hero', 'feature-argument', 'pricing-and-plans', 'proof-and-testimony', 'campaign-and-launch'],
|
||||
operate: ['onboarding-and-setup', 'dashboard-and-overview', 'records-and-tables', 'editor-and-canvas', 'settings-and-account', 'empty-and-failure'],
|
||||
read: ['long-form-article', 'reference-and-docs', 'index-and-archive', 'search-and-results'],
|
||||
experience: ['gallery-and-collection', 'player-and-timeline', 'space-and-map', 'play-and-toy'],
|
||||
};
|
||||
|
||||
export const ALL_COMPOSITION_AREAS = new Set(Object.values(COMPOSITION_AREAS).flat());
|
||||
|
||||
export function areasForSurface(surface) {
|
||||
return COMPOSITION_AREAS[surface] ?? [];
|
||||
}
|
||||
|
||||
export function compositionContentHash(composition) {
|
||||
const payload = [
|
||||
composition?.form ?? '',
|
||||
@@ -57,6 +80,16 @@ export function validateCompositionEntry(composition, { existingForms = new Map(
|
||||
if (!COMPOSITION_SURFACES.has(composition?.surface)) {
|
||||
errors.push(`composition ${id} needs a surface of ${[...COMPOSITION_SURFACES].join(', ')}`);
|
||||
}
|
||||
// Optional, but an area from the wrong surface is a mistake rather than a
|
||||
// looser tag: it would make the entry unreachable by every real request.
|
||||
if (composition?.area !== undefined && composition.area !== null) {
|
||||
const allowed = areasForSurface(composition.surface);
|
||||
if (!allowed.includes(composition.area)) {
|
||||
errors.push(
|
||||
`composition ${id} area "${composition.area}" is not one of the ${composition.surface} areas (${allowed.join(', ')})`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(composition?.tags)
|
||||
|| composition.tags.length !== 3
|
||||
|| composition.tags.some(tag => typeof tag !== 'string' || !tag.trim())) {
|
||||
|
||||
@@ -24,6 +24,13 @@ export const CONCEPT_STRENGTHS = new Set(['world', 'composition', 'dual']);
|
||||
// validateConceptCatalog needs it.
|
||||
export { WELL_TIERS };
|
||||
|
||||
// Reviewer axes that gate the challenger draw without touching approval.
|
||||
export const CONCEPT_BREADTHS = new Set(['general', 'niche']);
|
||||
// The registers of work a roll can be asked for. Kept here beside the review
|
||||
// validation that uses it; roll-selection.mjs filters on it and the seeder
|
||||
// validates the --mode flag against the same four.
|
||||
export const SEED_MODES = new Set(['persuade', 'operate', 'read', 'experience']);
|
||||
|
||||
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 = [
|
||||
'Palette/material:',
|
||||
@@ -289,6 +296,27 @@ export function validateConceptCatalog(catalog, reviewData, {
|
||||
errors.push(`review ${id} rating only applies to approved concepts`);
|
||||
}
|
||||
}
|
||||
// Breadth: a world too narrow to serve an arbitrary build keeps its approval
|
||||
// and leaves the challenger pool. Selection has honoured this for a while but
|
||||
// nothing validated it, so a typo would silently read as "general".
|
||||
if (review?.breadth !== undefined && !CONCEPT_BREADTHS.has(review.breadth)) {
|
||||
errors.push(`review ${id} breadth must be one of ${[...CONCEPT_BREADTHS].join(', ')}`);
|
||||
}
|
||||
// Mode eligibility: which registers of work this world can carry. Absent
|
||||
// means all of them, which is why it needs no backfill. Listing every mode
|
||||
// is the same as omitting it, and an empty list would deal nothing, so both
|
||||
// are rejected in favour of leaving the field out.
|
||||
if (review?.allowedModes !== undefined) {
|
||||
if (!Array.isArray(review.allowedModes) || review.allowedModes.length === 0) {
|
||||
errors.push(`review ${id} allowedModes must be a non-empty array, or omitted to allow every mode`);
|
||||
} else if (review.allowedModes.some(mode => !SEED_MODES.has(mode))) {
|
||||
errors.push(`review ${id} allowedModes may only contain ${[...SEED_MODES].join(', ')}`);
|
||||
} else if (new Set(review.allowedModes).size !== review.allowedModes.length) {
|
||||
errors.push(`review ${id} allowedModes must not repeat a mode`);
|
||||
} else if (review.allowedModes.length === SEED_MODES.size) {
|
||||
errors.push(`review ${id} allowedModes lists every mode; omit the field instead`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wellTierById = new Map((catalog?.wells || []).map(well => [well.id, well.tier]));
|
||||
|
||||
@@ -94,7 +94,15 @@ function compositionTickets(pool) {
|
||||
* @param {Array} options.concepts merged concepts with status, review, wellTier, familyId
|
||||
* @returns {Generator<string[], {approved: Array, picks: Array}, string[]>}
|
||||
*/
|
||||
export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = null, concepts }) {
|
||||
// A world with no allowedModes is eligible everywhere, which is what keeps this
|
||||
// additive: nothing has to be backfilled for the filter to be safe.
|
||||
function modeAllows(concept, mode) {
|
||||
const allowed = concept.review?.allowedModes;
|
||||
if (!Array.isArray(allowed) || allowed.length === 0) return true;
|
||||
return allowed.includes(mode);
|
||||
}
|
||||
|
||||
export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating = null, mode = 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 compositions. Duals serve
|
||||
@@ -122,6 +130,18 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating =
|
||||
if (rated.length > 0) approvedByTier.set(tier, rated);
|
||||
}
|
||||
}
|
||||
// Mode eligibility, per tier and skipped where it would empty a tier. Worlds
|
||||
// used to be drawn with no mode awareness at all, so a build asking for an app
|
||||
// UI could get six worlds that only make sense on a landing page. A world is an
|
||||
// identity and identities transfer further than compositions do, so this is a
|
||||
// ceiling the reviewer sets rather than a category assignment: eligible
|
||||
// everywhere until someone says otherwise.
|
||||
if (mode) {
|
||||
for (const [tier, pool] of approvedByTier) {
|
||||
const eligible = pool.filter(concept => modeAllows(concept, mode));
|
||||
if (eligible.length > 0) approvedByTier.set(tier, eligible);
|
||||
}
|
||||
}
|
||||
for (const [tier, pool] of approvedByTier) {
|
||||
const matching = pool.filter(concept => wanted.has(concept.strength));
|
||||
if (matching.length > 0) approvedByTier.set(tier, matching);
|
||||
@@ -192,11 +212,12 @@ export function* selectApprovedChallengers({ scope, key, reroll = 0, minRating =
|
||||
* @param {string} options.key
|
||||
* @param {number} [options.reroll]
|
||||
* @param {string|null} [options.mode] surface register to stay inside
|
||||
* @param {string|null} [options.area] area of concern to prefer within that surface
|
||||
* @param {Array} options.compositions merged compositions with status, review, surface, familyId
|
||||
* @param {number} [options.count]
|
||||
* @returns {Generator<string[], Array, string[]>}
|
||||
*/
|
||||
export function* selectApprovedCompositions({ scope, key, reroll = 0, mode = null, compositions, count = 3 }) {
|
||||
export function* selectApprovedCompositions({ scope, key, reroll = 0, mode = null, area = 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
|
||||
@@ -233,16 +254,26 @@ export function* selectApprovedCompositions({ scope, key, reroll = 0, mode = nul
|
||||
entry => `${entry.composition.id}#${entry.ticket}`
|
||||
)).map(entry => entry.composition);
|
||||
|
||||
// Area is a preference, not a filter. Requesting an onboarding flow should
|
||||
// deal onboarding compositions first and top up from the rest of the surface
|
||||
// rather than deal fewer than three, because the per-area pools are small and
|
||||
// an unset area is eligible everywhere. A stable partition of an already
|
||||
// deterministic ranking is still deterministic.
|
||||
const ordered = area
|
||||
? [...ranked.filter(composition => composition.area === area),
|
||||
...ranked.filter(composition => composition.area !== area)]
|
||||
: ranked;
|
||||
|
||||
const families = new Set();
|
||||
picks = [];
|
||||
for (const composition of ranked) {
|
||||
for (const composition of ordered) {
|
||||
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) {
|
||||
for (const composition of ordered) {
|
||||
if (picks.length >= count) break;
|
||||
if (!picks.some(pick => pick.id === composition.id)) picks.push(composition);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user