<!-- SAGA-SOLID · part 20 -->
ator.type,
      identity: creator.identity.trim(),
      agentId: creator.type === 'ai' ? creator.agentId.trim() : null,
    },
  };
}

/* ============================================================
   COMPONENT VALIDATION
   ============================================================ */
function validateComponents(components) {
  var errors = [];
  var validated = [];
  var seenIds = {};
  var complexity = 0;
  var lightCount = 0;

  for(var i = 0; i < components.length; i++) {
    var c = components[i];
    var prefix = 'components[' + i + ']';

    if(!c || typeof c !== 'object') {
      errors.push({field: prefix, reason: 'must be an object'});
      continue;
    }

    var fk = hasForbiddenKeys(c, prefix);
    if(fk) {
      errors.push({field: fk.path, reason: fk.reason});
      continue;
    }

    // Type must be in registry
    if(!c.type || !COMPONENT_REGISTRY[c.type]) {
      errors.push({field: prefix + '.type', reason: 'unknown component type: ' + String(c.type).substring(0, 50)});
      continue;
    }
    var reg = COMPONENT_REGISTRY[c.type];

    // Component ID (optional input, generated if missing; must be unique)
    var compId = c.componentId;
    if(compId !== undefined) {
      if(!isValidString(compId, LIMITS.MAX_STRING_FIELD)) {
        errors.push({field: prefix + '.componentId', reason: 'invalid string'});
        continue;
      }
      if(seenIds[compId]) {
        errors.push({field: prefix + '.componentId', reason: 'duplicate component ID: ' + compId});
        continue;
      }
      if(isExecutableValue(compId)) {
        errors.push({field: prefix + '.componentId', reason: 'executable content rejected'});
        continue;
      }
    } else {
      compId = generateId('comp');
    }
    seenIds[compId] = true;

    // Dimensions
    var dims = c.dimensions;
    if(!dims || typeof dims !== 'object') {
      errors.push({field: prefix + '.dimensions', reason: 'dimensions object required'});
      continue;
    }
    var w = dims.w, h = dims.h, d = dims.d;
    if(!isValidNumber(w) || !isValidNumber(h) || !isValidNumber(d)) {
      errors.push({field: prefix + '.dimensions', reason: 'w, h, d must be finite numbers (no NaN/Infinity)'});
      continue;
    }
    if(w < LIMITS.MIN_COMPONENT_DIM || h < LIMITS.MIN_COMPONENT_DIM || d < LIMITS.MIN_COMPONENT_DIM) {
      errors.push({field: prefix + '.dimensions', reason: 'below minimum ' + LIMITS.MIN_COMPONENT_DIM});
      continue;
    }
    if(w > LIMITS.MAX_COMPONENT_DIM || h > LIMITS.MAX_COMPONENT_DIM || d > LIMITS.MAX_COMPONENT_DIM) {
      errors.push({field: prefix + '.dimensions', reason: 'exceeds maximum ' + LIMITS.MAX_COMPONENT_DIM});
      continue;
    }

    // Position (relative to blueprint origin)
    var pos = c.position || {x: 0, y: 0, z: 0};
    if(!isValidNumber(pos.x) || !isValidNumber(pos.y) || !isValidNumber(pos.z)) {
      errors.push({field: prefix + '.position', reason: 'x, y, z must be finite numbers'});
      continue;
    }

    // Rotation
    var rot = c.rotation || {x: 0, y: 0, z: 0};
    if(!isValidNumber(rot.x) || !isValidNumber(rot.y) || !isValidNumber(rot.z)) {
      errors.push({field: prefix + '.rotation', reason: 'x, y, z must be finite numbers'});
      continue;
    }

    // Material (optional, string reference only — no shader source)
    var material = 'default';
    if(c.material !== undefined) {
      if(!isValidString(c.material, LIMITS.MAX_STRING_FIELD)) {
        errors.push({field: prefix + '.material', reason: 'invalid string'});
        continue;
      }
      if(isExecutableValue(c.material)) {
        errors.push({field: prefix + '.material', reason: 'executable content rejected'});
        continue;
      }
      material = c.material;
    }

    // Color (optional, hex string)
    var color = null;
    if(c.color !== undefined) {
      if(typeof c.color !== 'string' || !/^#[0-9a-fA-F]{6}$/.test(c.color)) {
        errors.push({field: prefix + '.color', reason: 'must be hex like #ff0000'});
        continue;
      }
      color = c.color;
    }

    // Reject unknown fields that look executable
    var allowedFields = ['type', 'componentId', 'dimensions', 'position', 'rotation', 'material', 'color'];
    var cKeys = Object.keys(c);
    var rejected = false;
    for(var ki = 0; ki < cKeys.length; ki++) {
      var ck = cKeys[ki];
      if(allowedFields.indexOf(ck) === -1) {
        // Unknown field — check if it's executable, reject if so
        if(isExecutableValue(c[ck]) || typeof c[ck] === 'function') {
          errors.push({field: prefix + '.' + ck, reason: 'unknown executable field rejected'});
          rejected = true;
          break;
        }
        // Non-executable unknown fields: reject to fail closed
        errors.push({field: prefix + '.' + ck, reason: 'unknown field rejected'});
        rejected = true;
        break;
      }
    }
    if(rejected) continue;

    // Valid — add to list
    validated.push({
      componentId: