pbakaus/impeccable · error · Error

concept-seed: --grain must be one of ${COMPOSITION_GRAINS.jo

Error message

concept-seed: --grain must be one of ${COMPOSITION_GRAINS.join(', ')}

What it means

Thrown when `grain` is set to a value not in COMPOSITION_GRAINS = [product, flow, view, region]. Grain is optional (default null) and describes how much of the product is in play — it is intentionally independent of mode. The dynamic message interpolates the full allowed list.

Source

Thrown at skill/scripts/concept-seed.mjs:327

  if (!Number.isInteger(reroll) || reroll < 0) {
    throw new Error('concept-seed: --reroll must be a non-negative integer');
  }
  if (register !== null && register !== 'safer' && register !== 'bolder') {
    throw new Error('concept-seed: --register must be safer or bolder');
  }
  if (register !== null && reroll < 1) {
    throw new Error('concept-seed: --register steers a re-roll round; pass --reroll <n> with it');
  }
  if (register !== null && scope !== 'direction') {
    throw new Error('concept-seed: --register applies to direction rounds only');
  }
  if (mode !== null && !SEED_MODES.has(mode)) {
    throw new Error('concept-seed: --mode must be persuade, operate, read, or experience');
  }
  // Grain needs no mode: how much of the product is in play is independent of
  // which register of work it is.
  if (grain !== null && !COMPOSITION_GRAINS.includes(grain)) {
    throw new Error(`concept-seed: --grain must be one of ${COMPOSITION_GRAINS.join(', ')}`);
  }
  if (platform !== null && !COMPOSITION_PLATFORMS.includes(platform)) {
    throw new Error(`concept-seed: --platform must be one of ${COMPOSITION_PLATFORMS.join(', ')}`);
  }
  if (!Number.isInteger(candidateCount) || candidateCount < 5 || candidateCount > 7) {
    throw new Error('concept-seed: --candidate-count must be an integer from 5 to 7');
  }
  const unit = (salt) => {
    const h = crypto.createHash('sha256').update(`${scope}:${salt}:${key}`).digest();
    return h.readUInt32BE(0) / 0xffffffff;
  };
  const indexSalt = reroll === 0 ? 'index' : `index:reroll-${reroll}`;
  const buildIndex = 3 + Math.floor(unit(indexSalt) * (candidateCount - 2)); // 3..candidateCount
  // Surface scope deals a hand of three grounded structures: one card is not
  // a choice, and the full ranked list would hand selection back to the
  // model's taste. The dice pick all three; the primary index leads. The
  // no-lineup rule stays direction-only, where it was written for worlds.
  const dealtIndices = [buildIndex];

View on GitHub (pinned to d14711ae3d)

Solutions

  1. Use one of product, flow, view, region, or omit grain / pass null.
  2. Double-check you are not passing a platform value (web/ios/android) into grain.

Example fix

// before
await seedConcepts({ scope: 'surface', grain: 'page' });

// after
await seedConcepts({ scope: 'surface', grain: 'view' }); // or product|flow|region
Defensive patterns

Strategy: type-guard

Type guard

import { COMPOSITION_GRAINS, isGrain } from './lib/roll-selection.mjs';
function isValidGrain(v) {
  return v == null || isGrain(v);
}

Prevention

When it happens

Trigger: Passing 'page' or 'screen' (confusing grain with view); passing 'component', 'section', 'module'; confusing grain with platform (web/ios/android).

Common situations: Carrying over vocabulary from another design system; a caller mapping grain from a UI dropdown with different labels.

Related errors


AI-assisted analysis of pbakaus/impeccable@d14711ae3d (2026-08-13). Data as JSON: /api/errors/1895ca456ebbf1f0. Report an issue: GitHub.