/* Абстракции
Образы неокортекса (_10_actions_image), которые представляют собой иерархию усложняющихся образов воприятия (конечные образы в третичной коре, где сходятся все виды сенсорики) жестко определены в соотвествующих периодах развития и не могут изменяться. Потеря распознавателя - утрата на всю жизнь и необходимость компенсировать прямое восприятие косвенным распознаванием.
При осознании конечные образы, которые ориентировоным рефлексов выбраны среди конкурентов как наиболее аткуальные, удерживаются и образуется канал осознанного внимания к префронтальной коре. За время осмысления такого образа на стороне лобной доли формируется собственный образ-клон, который впоследствии активируется при активации образа теменной коры (зеркально, зеркальные нейроны), но обратной возможности активации от лобных образов к теменным нет. Жесткие образы восприятия и образы действия получают предствителство клонов в префронтальной коре. В отличие от первых, вторыя оказываются свободными для манипуляции с ними, изменеения и дополнения любыми фантастическими дополненниям. Такие гибкие образы, с которым в фокусе осознанного внимания произвольно возможно делать что угодно, назовем абстракциями.
В Правилах эпизодов памяти прописываются жесткие образы восприятия и действия т.к. активный ревербирирующий контур в гиппокампе связывает новый нейрон (нейрогенез) с активностью в теменной коре и такой нейрон становится указателями на весь активной в то время образ, способен впоминать его - заново активировать.
Но при осмыслении о перациях с абстракциями в эпизоде памяти начинают записываться не только первичнеы жесткие образы, а указатели на абстракции и возникают боблее гибкий вид Правил - ментальные правила, в том числе сохраняющие последовательность удачный и неудачных мыслительных действий для использования впоследствии. Паралельно формируются все более привычные ментальные автоматизмы.
Эффект — появляется инструмент для гипотез: "что если яблоко полетит?", что недоступно жесткому образу.
В гиппокампе новый нейрон связывается не только с теменной активностью, но и с префронтальной (абстракцией). Правило эпизода: "стимул (круг) → абстракция (разделить на сектора мысленно) → эффект (диаграмма)". Последовательность сохраняет неудачи: "вращение кубика против часовой не дало видимость — пробовать по часовой". Это гибче моторных цепочек, так как оперирует образами произвольно.
Пример А: Арифметика (От костяшек к числам)
Жесткий образ (Теменная): Ребенок видит 5 костяшек на счетах. Это сенсорный факт.
Клон в ПФК: Образуется абстракция «Пятерка» (визуальный гештальт или символ 5).
Ментальное действие: Учитель говорит «прибавь еще две».
На этапе обучения: Ребенок пытается активировать моторную программу (двинуть руку).
На этапе автоматизма: В ПФК происходит операция над клоном. Образ «5» трансформируется в образ «7» без участия теменной коры (реальные счета не нужны).
Поскольку клоны свободны от физики, возможны сбои и гениальные озарения:
Случайная связь: Вы смотрите на клон «Облако». Случайно (из-за шума в нейросетях) к нему применяется операция «Причесать» (клон расчески).
Результат: Абстракция «Пушистое облако» соединяется с абстракцией «Ровные линии».
Итог: Рождается фантастический образ «Облако, подстриженное под газонокосилку». Это нелепо, но это пример чистого творчества, когда правила грамматики образов нарушаются, но записываются в эпизод как курьез.
Модель объясняет разницу между навыком (моторное/сенсорное, жесткое, теменное) и компетенцией/мышлением (абстрактное, гибкое, лобное).
Реализация.
Поддержка отзеркаливания (клонирование) жестких образов при осознании стимула, при записи в эпизод памяти - как стимула, так и образ действий с передачей значимости абстракции.
Первоначально массив образов не имеет членов, но с каждым осознанием стимула и сопровождающего его действия формируются абстрактные образы восприятия и действия. Это становится условным: восприятие и действия уже не имеют жестких рамок ("яблоко" (восприятие) и "ударить молотком" (действие) превращаются в абстракции — "яблоко с крыльями" или "молоток как рычаг для любого объекта".).
Каждый абстрактный образ имеет изначально семантическую структуру, он привязан к условиям и имеет значимость, а также коунтер числа опытов для усреднения значимости. Абстракция "яблоко как объект" изначально кодирует условия ("визуально красное, съедобное в кухне") и значимость ("полезно для голода, +5 по шкале полезности"). Это позволяет при активации сразу вызывать не только вид, но и связанные правила: "в контексте еды — съесть; в игре — бросить".
При каждой записи в эпизод памяти нужно сделать клонирование образов восприятия и действия в абстракции. Эти абстракции будут использоваться при осмыслении, особенно в пассивном режиме, для сотворения других абстракций.
*/

(function (global) {
  'use strict';

  /**
   * Заготовка системы абстрактных образов (клонирование жёстких образов восприятия/действия).
   *
   * Точка входа с ЭП: только AbstractImages_cloneFromEpisodeFrame, вызывается из episodic_memory.closeLastFrame
   * после расчёта effect (не при открытии нового кадра).
   *
   * Правила ID:
   * - perceptionNodeId и stimulusImageId должны быть ненулевыми; иначе clonePerception возвращает 0 (запись не трогаем).
   * - actionImageId: при истине — cloneAction; 0 из «нуляции» неполного кадра — пропуск клона действия, но клон
   *   восприятия и счётчик meanImportance по effect всё равно обновляются.
   * - effect: контракт ЭП — число [-10,+10]; защитно подставляется 0, если значение не число (старые данные).
   *
   * Ментальные правила (mental_automatism.js): берут abstractActionId || abstractPerceptionId, поэтому при отсутствии ОД
   * усредняется правило, привязанное к абстракции восприятия.
   *
   * semanticStructure — аналог семантического кадра по условиям (не по FinalImage со значимостью):
   * { frames: [ { perceptionNodeId, situationId, ruleKind, stimulusImageId, actionImageId, effect?, responseStimulusImageId? } ] }.
   * Ключ условия: пара (перцептивная ветка, ветка ситуации). Слот: episodic / teacher / fantasy (fantasy — из цепочки фантазма, не перекрывает teacher).
   */

  /** Маркер: закрытый кадр ЭП, при конфликте выбирается больший effect. */
  var RULE_KIND_EPISODIC = 'episodic';
  /** Маркер: учительское правило (ответ оператора); перебивает episodic для той же пары условий. */
  var RULE_KIND_TEACHER = 'teacher';
  /** Маркер: правило из цепочки пассивного фантазирования (сон / info_new_abstract_semantic); не перекрывает teacher. */
  var RULE_KIND_FANTASY = 'fantasy';

  // --- Данные ---
  if (!Array.isArray(global.AbstractPerceptionImages)) global.AbstractPerceptionImages = [];
  if (!Array.isArray(global.AbstractActionImages)) global.AbstractActionImages = [];

  var nextAbstractPerceptionId = 1;
  var nextAbstractActionId = 1;

  // Индексы для быстрого update без дублей.
  var byPerceptionKey = Object.create(null); // key = perceptionNodeId|stimulusImageId -> obj
  var byActionKey = Object.create(null);     // key = perceptionNodeId|actionImageId -> obj

  var absDirty = false;
  var lastSavedSig = '';

  function normalizeSemanticStructureForRecord(rec) {
    if (!rec) return;
    if (!rec.semanticStructure || typeof rec.semanticStructure !== 'object') {
      rec.semanticStructure = { frames: [] };
      return;
    }
    if (!Array.isArray(rec.semanticStructure.frames)) {
      rec.semanticStructure.frames = [];
    }
  }

  function classifyRuleKindFromEpisodeFrame(epFrame) {
    if (!epFrame) return null;
    var resp = epFrame.responseStimulusImageId != null ? parseInt(epFrame.responseStimulusImageId, 10) : 0;
    if (resp > 0) return RULE_KIND_TEACHER;
    return RULE_KIND_EPISODIC;
  }

  function buildSemanticRuleSnapshot(epFrame) {
    var kind = classifyRuleKindFromEpisodeFrame(epFrame);
    if (!kind || !epFrame) return null;
    var stim = epFrame.stimulusImageId != null ? parseInt(epFrame.stimulusImageId, 10) || 0 : 0;
    var act = epFrame.actionImageId != null ? parseInt(epFrame.actionImageId, 10) : 0;
    if (!act) act = null;
    var eff = epFrame.effect;
    if (typeof eff !== 'number' || isNaN(eff)) eff = null;
    var respOut = epFrame.responseStimulusImageId != null ? parseInt(epFrame.responseStimulusImageId, 10) : 0;
    return {
      perceptionNodeId: parseInt(epFrame.perceptionNodeId, 10) || 0,
      situationId: epFrame.situationId != null ? parseInt(epFrame.situationId, 10) || 0 : 0,
      ruleKind: kind,
      stimulusImageId: stim,
      actionImageId: act,
      effect: kind === RULE_KIND_TEACHER ? null : eff,
      responseStimulusImageId: respOut > 0 ? respOut : null,
      updatedAt: Date.now()
    };
  }

  function shouldReplaceSemanticRule(existing, incoming) {
    if (!incoming) return false;
    if (!existing) return true;
    if (incoming.ruleKind === RULE_KIND_TEACHER) return true;
    if (existing.ruleKind === RULE_KIND_TEACHER) return false;
    var ee = typeof existing.effect === 'number' && !isNaN(existing.effect) ? existing.effect : -Infinity;
    var ie = typeof incoming.effect === 'number' && !isNaN(incoming.effect) ? incoming.effect : -Infinity;
    return ie > ee;
  }

  /**
   * Добавить/обновить кадр условия в semanticStructure абстракции по закрытому кадру ЭП.
   * @param {Object} obj — запись AbstractPerceptionImages или AbstractActionImages
   * @param {Object} epFrame — кадр ЭП после closeLastFrame
   */
  function AbstractImages_upsertSemanticRuleFromEpisodeFrame(obj, epFrame) {
    if (!obj || !epFrame) return;
    var p = parseInt(epFrame.perceptionNodeId, 10) || 0;
    if (!p) return;
    var snap = buildSemanticRuleSnapshot(epFrame);
    if (!snap) return;
    normalizeSemanticStructureForRecord(obj);
    var frames = obj.semanticStructure.frames;
    var condP = snap.perceptionNodeId;
    var condS = snap.situationId;
    var idx = -1;
    for (var i = 0; i < frames.length; i++) {
      var fr = frames[i];
      if (!fr) continue;
      if ((fr.perceptionNodeId || 0) === condP && (fr.situationId || 0) === condS) {
        idx = i;
        break;
      }
    }
    if (idx === -1) {
      frames.push(snap);
      absDirty = true;
    } else if (shouldReplaceSemanticRule(frames[idx], snap)) {
      frames[idx] = snap;
      absDirty = true;
    }
  }

  function buildSemanticRuleSnapshotFantasy(epFrame) {
    if (!epFrame) return null;
    var p = parseInt(epFrame.perceptionNodeId, 10) || 0;
    if (!p) return null;
    var stim = epFrame.stimulusImageId != null ? parseInt(epFrame.stimulusImageId, 10) || 0 : 0;
    var act = epFrame.actionImageId != null ? parseInt(epFrame.actionImageId, 10) : 0;
    if (!act) act = null;
    var eff = epFrame.effect;
    if (typeof eff !== 'number' || isNaN(eff)) eff = 0;
    var respOut = epFrame.responseStimulusImageId != null ? parseInt(epFrame.responseStimulusImageId, 10) : 0;
    return {
      perceptionNodeId: p,
      situationId: epFrame.situationId != null ? parseInt(epFrame.situationId, 10) || 0 : 0,
      ruleKind: RULE_KIND_FANTASY,
      stimulusImageId: stim,
      actionImageId: act,
      effect: eff,
      responseStimulusImageId: respOut > 0 ? respOut : null,
      updatedAt: Date.now()
    };
  }

  /**
   * Кадр сюжета фантазии → semanticStructure клона; не перезаписывает учительский слот.
   * С episodic / fantasy существующего кадра усредняет effect и помечает как fantasy.
   *
   * @param {Object} obj — AbstractPerceptionImages или AbstractActionImages
   * @param {Object} epFrame — как кадр ЭП (perceptionNodeId, situationId, stimulusImageId, actionImageId, effect, responseStimulusImageId)
   * @returns {boolean} true если слот изменён или добавлен
   */
  function AbstractImages_upsertSemanticRuleFromFantasyFrame(obj, epFrame) {
    if (!obj || !epFrame) return false;
    var snap = buildSemanticRuleSnapshotFantasy(epFrame);
    if (!snap) return false;
    normalizeSemanticStructureForRecord(obj);
    var frames = obj.semanticStructure.frames;
    var condP = snap.perceptionNodeId;
    var condS = snap.situationId;
    var idx = -1;
    for (var i = 0; i < frames.length; i++) {
      var fr = frames[i];
      if (!fr) continue;
      if ((fr.perceptionNodeId || 0) === condP && (fr.situationId || 0) === condS) {
        idx = i;
        break;
      }
    }
    if (idx === -1) {
      frames.push(snap);
      absDirty = true;
      return true;
    }
    var existing = frames[idx];
    if (existing.ruleKind === RULE_KIND_TEACHER) {
      return false;
    }
    var merged = Object.assign({}, existing, snap);
    var e1 = typeof existing.effect === 'number' && !isNaN(existing.effect) ? existing.effect : null;
    var e2 = typeof snap.effect === 'number' && !isNaN(snap.effect) ? snap.effect : null;
    if (e1 !== null && e2 !== null) {
      merged.effect = (e1 + e2) / 2;
    } else if (e2 !== null) {
      merged.effect = e2;
    } else if (e1 !== null) {
      merged.effect = e1;
    }
    merged.ruleKind = RULE_KIND_FANTASY;
    merged.stimulusImageId = snap.stimulusImageId;
    merged.actionImageId = snap.actionImageId;
    merged.responseStimulusImageId = snap.responseStimulusImageId;
    merged.updatedAt = Date.now();
    frames[idx] = merged;
    absDirty = true;
    return true;
  }

  function rebuildIndex() {
    byPerceptionKey = Object.create(null);
    byActionKey = Object.create(null);
    var arrP = global.AbstractPerceptionImages;
    for (var i = 0; i < arrP.length; i++) {
      var o = arrP[i];
      if (!o) continue;
      normalizeSemanticStructureForRecord(o);
      byPerceptionKey[String(o.perceptionNodeId) + '|' + String(o.stimulusImageId)] = o;
      if (typeof o.id === 'number' && o.id >= nextAbstractPerceptionId) nextAbstractPerceptionId = o.id + 1;
    }
    var arrA = global.AbstractActionImages;
    for (var j = 0; j < arrA.length; j++) {
      var a = arrA[j];
      if (!a) continue;
      normalizeSemanticStructureForRecord(a);
      byActionKey[String(a.perceptionNodeId) + '|' + String(a.actionImageId)] = a;
      if (typeof a.id === 'number' && a.id >= nextAbstractActionId) nextAbstractActionId = a.id + 1;
    }
  }

  rebuildIndex();

  function addUsefulnessSample(obj, effect) {
    if (!obj) return;
    if (typeof effect !== 'number' || isNaN(effect)) return;
    var c = typeof obj.count === 'number' ? obj.count : 0;
    var cur = typeof obj.meanImportance === 'number' ? obj.meanImportance : 0;
    obj.meanImportance = (cur * c + effect) / (c + 1);
    obj.count = c + 1;
    obj.lastUsedAt = Date.now();
  }

  function clonePerception(perceptionNodeId, stimulusImageId, effect) {
    // Ноль в id недопустим: в кадре ЭП такие кадры считаются некорректными; не создаём запись.
    if (!perceptionNodeId || !stimulusImageId) return 0;
    var key = String(perceptionNodeId) + '|' + String(stimulusImageId);
    var obj = byPerceptionKey[key];
    if (!obj) {
      obj = {
        id: nextAbstractPerceptionId++,
        perceptionNodeId: perceptionNodeId,
        stimulusImageId: stimulusImageId, // жёсткий FinalImageId, который клонируем
        meanImportance: 0,
        count: 0,
        createdAt: Date.now(),
        lastUsedAt: Date.now(),
        semanticStructure: { frames: [] }
      };
      global.AbstractPerceptionImages.push(obj);
      byPerceptionKey[key] = obj;
    }
    addUsefulnessSample(obj, effect);
    absDirty = true;
    return obj.id;
  }

  function cloneAction(perceptionNodeId, actionImageId, effect) {
    if (!perceptionNodeId || !actionImageId) return 0;
    var key = String(perceptionNodeId) + '|' + String(actionImageId);
    var obj = byActionKey[key];
    if (!obj) {
      obj = {
        id: nextAbstractActionId++,
        perceptionNodeId: perceptionNodeId,
        actionImageId: actionImageId, // жёсткий ActionImageId, который клонируем
        meanImportance: 0,
        count: 0,
        createdAt: Date.now(),
        lastUsedAt: Date.now(),
        semanticStructure: { frames: [] }
      };
      global.AbstractActionImages.push(obj);
      byActionKey[key] = obj;
    }
    addUsefulnessSample(obj, effect);
    absDirty = true;
    return obj.id;
  }

  /**
   * Клон восприятия без усреднения по effect (ур.3, учитель): создать при отсутствии или обновить lastUsedAt.
   */
  function ensurePerceptionAbstraction(perceptionNodeId, stimulusImageId) {
    if (!perceptionNodeId || !stimulusImageId) return null;
    var key = String(perceptionNodeId) + '|' + String(stimulusImageId);
    var obj = byPerceptionKey[key];
    if (obj) {
      obj.lastUsedAt = Date.now();
      absDirty = true;
      return obj;
    }
    obj = {
      id: nextAbstractPerceptionId++,
      perceptionNodeId: perceptionNodeId,
      stimulusImageId: stimulusImageId,
      meanImportance: 0,
      count: 0,
      createdAt: Date.now(),
      lastUsedAt: Date.now(),
      semanticStructure: { frames: [] }
    };
    global.AbstractPerceptionImages.push(obj);
    byPerceptionKey[key] = obj;
    absDirty = true;
    return obj;
  }

  /**
   * Клон действия без усреднения по effect (ур.3, учитель).
   */
  function ensureActionAbstraction(perceptionNodeId, actionImageId) {
    if (!perceptionNodeId || !actionImageId) return null;
    var key = String(perceptionNodeId) + '|' + String(actionImageId);
    var obj = byActionKey[key];
    if (obj) {
      obj.lastUsedAt = Date.now();
      absDirty = true;
      return obj;
    }
    obj = {
      id: nextAbstractActionId++,
      perceptionNodeId: perceptionNodeId,
      actionImageId: actionImageId,
      meanImportance: 0,
      count: 0,
      createdAt: Date.now(),
      lastUsedAt: Date.now(),
      semanticStructure: { frames: [] }
    };
    global.AbstractActionImages.push(obj);
    byActionKey[key] = obj;
    absDirty = true;
    return obj;
  }

  /**
   * Ур.3 / учитель: активировать клоны под актуальный FinalImage и ОД из правила; сохранить учительский слот
   * в semanticStructure обоих (условие ключа — текущие ветка и situationId ФО).
   *
   * @param {Object} params
   * @param {number} params.perceptionNodeId — узел ветки
   * @param {number} params.situationId
   * @param {number} params.actualStimulusImageId — актуальный стимул (FinalImageId)
   * @param {Object} params.teacherEpisodicFrame — кадр из info_teacherEpisodicRuleForAction
   * @returns {{ abstractPerceptionId: number, abstractActionId: number }}
   */
  function AbstractImages_activateTeacherRuleOnAbstractions(params) {
    params = params || {};
    var branchId = parseInt(params.perceptionNodeId, 10) || 0;
    var situationId = parseInt(params.situationId, 10) || 0;
    var actualStim = parseInt(params.actualStimulusImageId, 10) || 0;
    var tf = params.teacherEpisodicFrame;
    if (!branchId || !actualStim || !tf) {
      return { abstractPerceptionId: 0, abstractActionId: 0 };
    }
    var odId = tf.actionImageId != null ? parseInt(tf.actionImageId, 10) : 0;
    if (!odId) {
      return { abstractPerceptionId: 0, abstractActionId: 0 };
    }

    var apObj = ensurePerceptionAbstraction(branchId, actualStim);
    var aaObj = ensureActionAbstraction(branchId, odId);

    var respRaw = tf.responseStimulusImageId != null ? parseInt(tf.responseStimulusImageId, 10) : 0;
    var eff = tf.effect;
    if (typeof eff !== 'number' || isNaN(eff)) eff = 0;

    var synth = {
      perceptionNodeId: branchId,
      situationId: situationId,
      stimulusImageId: actualStim,
      actionImageId: odId,
      effect: eff,
      responseStimulusImageId: respRaw > 0 ? respRaw : null
    };

    if (apObj) {
      AbstractImages_upsertSemanticRuleFromEpisodeFrame(apObj, synth);
    }
    if (aaObj) {
      AbstractImages_upsertSemanticRuleFromEpisodeFrame(aaObj, synth);
    }

    return {
      abstractPerceptionId: apObj && apObj.id ? apObj.id : 0,
      abstractActionId: aaObj && aaObj.id ? aaObj.id : 0
    };
  }

  /**
   * Элемент semanticStructure клона восприятия, строго совпадающий с контекстом (узел, ситуация, актуальный FinalImage).
   *
   * @param {Object} params — perceptionNodeId, situationId, actualStimulusImageId
   * @returns {Object|null} кадр из frames с ненулевым actionImageId или null
   */
  function AbstractImages_findRuleFrameMatchingContext(params) {
    params = params || {};
    var branchId = parseInt(params.perceptionNodeId, 10) || 0;
    var situationId = parseInt(params.situationId, 10) || 0;
    var actualStim = parseInt(params.actualStimulusImageId, 10) || 0;
    if (!branchId || !actualStim) return null;

    var arr = global.AbstractPerceptionImages;
    if (!Array.isArray(arr)) return null;

    var rec = null;
    for (var fi = 0; fi < arr.length; fi++) {
      var o = arr[fi];
      if (!o) continue;
      if ((o.perceptionNodeId || 0) !== branchId) continue;
      if ((o.stimulusImageId || 0) !== actualStim) continue;
      rec = o;
      break;
    }
    if (!rec) return null;
    normalizeSemanticStructureForRecord(rec);
    var frames = rec.semanticStructure.frames;
    for (var fj = 0; fj < frames.length; fj++) {
      var fr = frames[fj];
      if (!fr) continue;
      if ((fr.perceptionNodeId || 0) !== branchId) continue;
      if ((fr.situationId || 0) !== situationId) continue;
      if ((fr.stimulusImageId || 0) !== actualStim) continue;
      var aid = fr.actionImageId != null ? parseInt(fr.actionImageId, 10) : 0;
      if (!aid) continue;
      return fr;
    }
    return null;
  }

  /**
   * После ответа оператора на автоматизм: если для (ветка, ситуация, стимул) есть кадр в semanticStructure,
   * усреднить meanImportance клонов восприятия и действия тем же effect кадра ЭП (положительный — усиление, отрицательный — ослабление).
   *
   * @param {Object} params — perceptionNodeId, situationId, actualStimulusImageId, effect (число, обычно [-10,+10])
   */
  function AbstractImages_applyEpisodeEffectToSemanticContextAbstractions(params) {
    params = params || {};
    var branchId = parseInt(params.perceptionNodeId, 10) || 0;
    var situationId = parseInt(params.situationId, 10) || 0;
    var actualStim = parseInt(params.actualStimulusImageId, 10) || 0;
    var effect = params.effect;
    if (typeof effect !== 'number' || isNaN(effect)) return;
    if (!branchId || !actualStim) return;

    var fr = AbstractImages_findRuleFrameMatchingContext({
      perceptionNodeId: branchId,
      situationId: situationId,
      actualStimulusImageId: actualStim
    });
    if (!fr) return;

    var pKey = String(branchId) + '|' + String(actualStim);
    var apObj = byPerceptionKey[pKey];
    if (apObj) {
      addUsefulnessSample(apObj, effect);
      absDirty = true;
    }
    var odId = fr.actionImageId != null ? parseInt(fr.actionImageId, 10) : 0;
    if (odId) {
      var aKey = String(branchId) + '|' + String(odId);
      var aaObj = byActionKey[aKey];
      if (aaObj) {
        addUsefulnessSample(aaObj, effect);
        absDirty = true;
      }
    }
  }

  /**
   * Основная точка клонирования при записи в ЭП: вызывается после закрытия кадра.
   * effect у кадра к этому моменту — число в [-10, +10] (см. episodic_memory.closeLastFrame).
   * @param {Object} frame - кадр ЭП (perceptionNodeId, stimulusImageId, actionImageId, effect, ...)
   * @returns {{abstractPerceptionId:number, abstractActionId:number}}
   */
  function AbstractImages_cloneFromEpisodeFrame(frame) {
    if (!frame) return { abstractPerceptionId: 0, abstractActionId: 0 };
    var perceptionNodeId = frame.perceptionNodeId;
    var stimulusImageId = frame.stimulusImageId;
    var actionImageId = frame.actionImageId;
    var effect = frame.effect;
    if (typeof effect !== 'number' || isNaN(effect)) {
      effect = 0;
    }
    var ap = clonePerception(perceptionNodeId, stimulusImageId, effect);
    // actionImageId === 0 означает «ОД не было» (закрытие после таймаута без реакции) — только восприятие.
    var aa = actionImageId ? cloneAction(perceptionNodeId, actionImageId, effect) : 0;

    var pKey = String(perceptionNodeId) + '|' + String(stimulusImageId);
    var apObj = byPerceptionKey[pKey];
    if (apObj) {
      AbstractImages_upsertSemanticRuleFromEpisodeFrame(apObj, frame);
    }
    if (aa) {
      var aKey = String(perceptionNodeId) + '|' + String(actionImageId);
      var aaObj = byActionKey[aKey];
      if (aaObj) {
        AbstractImages_upsertSemanticRuleFromEpisodeFrame(aaObj, frame);
      }
    }

    return { abstractPerceptionId: ap, abstractActionId: aa };
  }

  // -------------------- Сохранение/восстановление (файл + IndexedDB) --------------------

  function serializeState() {
    return {
      nextAbstractPerceptionId: nextAbstractPerceptionId,
      nextAbstractActionId: nextAbstractActionId,
      abstractPerceptionImages: global.AbstractPerceptionImages.slice(0),
      abstractActionImages: global.AbstractActionImages.slice(0)
    };
  }

  function applyState(state) {
    state = state || {};
    nextAbstractPerceptionId = parseInt(state.nextAbstractPerceptionId, 10);
    if (isNaN(nextAbstractPerceptionId) || nextAbstractPerceptionId < 1) nextAbstractPerceptionId = 1;
    nextAbstractActionId = parseInt(state.nextAbstractActionId, 10);
    if (isNaN(nextAbstractActionId) || nextAbstractActionId < 1) nextAbstractActionId = 1;

    global.AbstractPerceptionImages = Array.isArray(state.abstractPerceptionImages) ? state.abstractPerceptionImages.slice(0) : [];
    global.AbstractActionImages = Array.isArray(state.abstractActionImages) ? state.abstractActionImages.slice(0) : [];
    absDirty = false;
    try { lastSavedSig = JSON.stringify(serializeState()); } catch (e) { lastSavedSig = ''; }
    rebuildIndex();
  }

  var ABS_DB_NAME = 'BEAST_AbstractImages_DB';
  /** Не уменьшать: IndexedDB VersionError, если в браузере уже выше (например после прошлого деплоя). */
  var ABS_DB_VERSION = 2;
  var STORE = 'abstract_images_state';
  var KEY = 'abstract_images_singleton';
  /** Одно открытое соединение — как в actions_image / episodic_memory (openDB на каждый put ненадёжен). */
  var absDb = null;

  function openAbsDb() {
    if (!global.indexedDB || !global.IndexedDBModule || typeof global.IndexedDBModule.openDatabase !== 'function') {
      return Promise.reject(new Error('IndexedDB недоступен или модуль не загружен.'));
    }
    if (absDb) return Promise.resolve(absDb);
    return global.IndexedDBModule.openDatabase({
      dbName: ABS_DB_NAME,
      version: ABS_DB_VERSION,
      stores: [{ name: STORE, options: { keyPath: 'id' } }]
    }).then(function (db) {
      absDb = db;
      return db;
    });
  }

  function saveAbsStateToIndexedDB(state) {
    if (!global.IndexedDBModule) return Promise.reject(new Error('IndexedDBModule'));
    state = state || serializeState();
    function doPut(db) {
      return global.IndexedDBModule.put({
        db: db,
        storeName: STORE,
        value: { id: KEY, state: state, ts: Date.now() }
      });
    }
    if (absDb) {
      return doPut(absDb);
    }
    return openAbsDb().then(doPut);
  }

  /**
   * Запись в IndexedDB при absDirty; lastSavedSig и absDirty — только после успешного put.
   */
  function tryPersistAbstractImagesToIndexedDB() {
    if (!absDirty) return;
    if (!global.indexedDB || !global.IndexedDBModule) return;
    var state = serializeState();
    var sig = '';
    try {
      sig = JSON.stringify(state);
    } catch (e) {
      sig = '';
    }
    if (sig && sig === lastSavedSig) {
      absDirty = false;
      return;
    }
    saveAbsStateToIndexedDB(state)
      .then(function () {
        lastSavedSig = sig;
        absDirty = false;
        saveAbsStateToLocalStorage(state);
      })
      .catch(function (err) {
        if (typeof global.console !== 'undefined' && global.console.warn) {
          global.console.warn('abstract_images IndexedDB put', err);
        }
        saveAbsStateToLocalStorage(state);
      });
  }

  var LS_ABS_KEY = 'BEAST_AbstractImages_LS';

  function saveAbsStateToLocalStorage(state) {
    try {
      state = state || serializeState();
      global.localStorage.setItem(LS_ABS_KEY, JSON.stringify({ state: state, ts: Date.now() }));
    } catch (e) {}
  }

  function tryLoadFromLocalStorage() {
    try {
      var raw = global.localStorage.getItem(LS_ABS_KEY);
      if (!raw) return;
      var parsed = JSON.parse(raw);
      if (parsed && parsed.state) {
        var pLen = Array.isArray(global.AbstractPerceptionImages) ? global.AbstractPerceptionImages.length : 0;
        var aLen = Array.isArray(global.AbstractActionImages) ? global.AbstractActionImages.length : 0;
        if (pLen > 0 || aLen > 0) return;
        applyState(parsed.state);
      }
    } catch (e) {}
  }

  function loadFromIndexedDB() {
    openAbsDb()
      .then(function (db) {
        return global.IndexedDBModule.get({ db: db, storeName: STORE, key: KEY });
      })
      .then(function (record) {
        if (record && record.state) {
          var pLen = Array.isArray(global.AbstractPerceptionImages) ? global.AbstractPerceptionImages.length : 0;
          var aLen = Array.isArray(global.AbstractActionImages) ? global.AbstractActionImages.length : 0;
          if (pLen > 0 || aLen > 0) {
            return;
          }
          applyState(record.state);
          saveAbsStateToLocalStorage();
        } else {
          tryLoadFromLocalStorage();
        }
      })
      .catch(function (err) {
        if (typeof global.console !== 'undefined' && global.console.warn) {
          global.console.warn('abstract_images IndexedDB load', err);
        }
        tryLoadFromLocalStorage();
      });
  }

  loadFromIndexedDB();

  if (typeof global.registerIndexedDBSaver === 'function') {
    global.registerIndexedDBSaver(tryPersistAbstractImagesToIndexedDB);
  }

  // Немедленный сброс после закрытия кадра ЭП — иначе при F5 до следующего пульса данные не попадают в IndexedDB.
  if (global.window && global.window.addEventListener) {
    global.window.addEventListener('beforeunload', function () {
      if (absDirty) tryPersistAbstractImagesToIndexedDB();
    });
  }

  /**
   * Ур.2, цель «значение»: усреднить meanImportance абстракций, в semanticStructure которых есть правило с данным ОД.
   */
  function boostSemanticRulesForActionInArray(arr, actionImageId, deltaEffect) {
    var aid = parseInt(actionImageId, 10) || 0;
    if (!aid || typeof deltaEffect !== 'number' || isNaN(deltaEffect)) return;
    if (!Array.isArray(arr)) return;
    for (var i = 0; i < arr.length; i++) {
      var obj = arr[i];
      if (!obj) continue;
      normalizeSemanticStructureForRecord(obj);
      var frames = obj.semanticStructure.frames;
      var hit = false;
      for (var j = 0; j < frames.length; j++) {
        var fr = frames[j];
        if (!fr) continue;
        var a = fr.actionImageId != null ? parseInt(fr.actionImageId, 10) : 0;
        if (a === aid) {
          hit = true;
          break;
        }
      }
      if (hit) addUsefulnessSample(obj, deltaEffect);
    }
  }

  function AbstractImages_boostSemanticRulesForAction(actionImageId, deltaEffect) {
    boostSemanticRulesForActionInArray(global.AbstractPerceptionImages, actionImageId, deltaEffect);
    boostSemanticRulesForActionInArray(global.AbstractActionImages, actionImageId, deltaEffect);
    absDirty = true;
  }

  // --- Экспорт ---
  global.AbstractImages_RULE_KIND_EPISODIC = RULE_KIND_EPISODIC;
  global.AbstractImages_RULE_KIND_TEACHER = RULE_KIND_TEACHER;
  global.AbstractImages_RULE_KIND_FANTASY = RULE_KIND_FANTASY;
  global.AbstractImages_ensurePerceptionAbstraction = ensurePerceptionAbstraction;
  global.AbstractImages_ensureActionAbstraction = ensureActionAbstraction;
  global.AbstractImages_cloneFromEpisodeFrame = AbstractImages_cloneFromEpisodeFrame;
  global.AbstractImages_upsertSemanticRuleFromEpisodeFrame = AbstractImages_upsertSemanticRuleFromEpisodeFrame;
  global.AbstractImages_upsertSemanticRuleFromFantasyFrame = AbstractImages_upsertSemanticRuleFromFantasyFrame;
  global.AbstractImages_activateTeacherRuleOnAbstractions = AbstractImages_activateTeacherRuleOnAbstractions;
  global.AbstractImages_findRuleFrameMatchingContext = AbstractImages_findRuleFrameMatchingContext;
  global.AbstractImages_applyEpisodeEffectToSemanticContextAbstractions = AbstractImages_applyEpisodeEffectToSemanticContextAbstractions;
  global.AbstractImages_boostSemanticRulesForAction = AbstractImages_boostSemanticRulesForAction;
  global.AbstractImages_serializeState = serializeState;
  global.AbstractImages_applyState = applyState;
  global.AbstractImages_flushIndexedDB = tryPersistAbstractImagesToIndexedDB;
})(window);

