﻿/BEAST/PROJECT_STATE_FOR_NEW_CHAT.md
════════════════════════════════════════════════════════════
# Состояние проекта BEAST — снимок для нового чата

**Дата снимка:** 2026-03-21

## Зачем этот документ

Передайте файл в начале нового чата (или приложите к задаче), чтобы быстро восстановить архитектуру, точки входа в код и договорённости по логике — без разбора всей истории переписки.

## Что это за проект

- **BEAST** — браузерное приложение («Существо Beast, пульт связи»). Точка входа: **`index.html`** в корне репозитория.
- Основной код: **`beast/`** — модули в подпапках `_1_` … `_11_` (виталы, базовые контексты, стимулы, действия, рефлексы, семантика, дерево восприятия, гиппокамп/ОР, сознание/ФО, эпизодическая память, образы действия, автоматизмы).
- **Пульс:** `beast/puls.js` — тики, `global.cpuls`, диспетчер сознания, рефлексы, ЭП.
- **Персистенция:** IndexedDB, сохранение в файл (`saving_all` / UI); часть — `localStorage`.
- Рабочая копия **может быть не в git** — ориентир по файлам на диске.

## Документы в корне

| Файл | Назначение |
|------|------------|
| **`PROJECT_STATE_FOR_NEW_CHAT.md`** | Этот снимок состояния для ИИ и разработчиков |
| **`Mad_programmer_manual.md`** | Развёрнутый мануал по принципам Beast, слоям, отладке и развитию кода |

## Порядок подключения скриптов (`index.html`)

Критичен **порядок в конце `body`**. Фрагмент цепочки сознания:

`consciousness_dispatcher.js` → `infofunctions.js` → `conscious_attention_channel.js` → … → **`mental_automatism.js`** → **`conscience_level_2.js`** → **`conscience_level_3.js`** → **`conscience_level_4.js`** → **`gestalt.js`** → … → **`abstract_images.js`** → **`episodic_memory.js`** → …

`gestalt.js` должен быть подключён **до** `episodic_memory.js` (хук `Gestalt_onEpisodeFeedbackClosed` в `closeLastFrame`).

На момент **вызова** ур.3 модули выше по списку уже загружены; `abstract_images.js` подключается позже файла ур.3, но к моменту исполнения кода в рантайме глобалы `AbstractImages_*` доступны.

## Карта документации

| Документ | Содержание |
|----------|------------|
| `beast/_10_Episodic_memory/about_episodic_memory_rules.md` | Правила ЭП, ОР, ожидание, второй проход, цепочки |
| `beast/_10_Episodic_memory/about_episodic_memory.md` | ЭП, связь с абстракциями, участие ур.3 |
| `beast/_9_Conscience/about_conscience.md` | ФО, диспетчер, ур.3–4, ссылки на гештальт |
| `beast/_9_Conscience/about_gestalt.md` | Гештальт, буфер резонанса, интеграция с ур.3–4 и ЭП |
| `beast/_8_Hippocampus/about_hippocampus.md` | Гиппокамп и ОР |
| `beast/_6_semantic_memory/about_semantic_memory.md` | Семантическая память |
| `beast/about_global_variables.md` | Справочник ключевых глобалов |
| `beast/docs/design_conscious_automatisms.md` | Концептуальный дизайн |
| `beast/docs/analysis_adaptivity_system.md` | Разбор адаптивности (если используется) |

Расхождения **код ↔ комментарии ↔ about_*** править у источника в `beast/`.

## Ключевая логика (файлы)

| Тема | Файлы |
|------|--------|
| ОР, конкуренция стимулов | `beast/_8_Hippocampus/orientation_reflex.js` |
| Вход ФО, «тишина мыслей», `Consciousness_runOneStep` | `beast/_9_Conscience/conscious_attention_channel.js` |
| Реестр циклов, **`Consciousness_cycleAppendLog`**, `expired` / `force` в логе | `beast/_9_Conscience/consciousness_dispatcher.js` |
| ЭП, `closeLastFrame`, effect ∈ [-10,+10] | `beast/_10_Episodic_memory/episodic_memory.js` |
| Абстракции, `semanticStructure`, поиск кадра контекста | `beast/_9_Conscience/abstract_images.js` |
| `info_automatismWithEpisodeFeedbackAndExpireCycle`, `info_bestEpisodicRule` | `beast/_9_Conscience/infofunctions.js` |
| **Ур.3:** пайплайн, гейты ожидания ЭП и значимости | `beast/_9_Conscience/conscience_level_3.js` |
| **Ур.4 / гештальт:** резонанс, контекст для ур.3, фидбек из ЭП | `beast/_9_Conscience/conscience_level_4.js`, `beast/_9_Conscience/gestalt.js`, `beast/_9_Conscience/about_gestalt.md` |
| Ур.2, ответ оператора, ментальный автоматизм из semantic rule | `beast/_9_Conscience/conscience_level_2.js` |
| Ментальные автоматизмы, МАИ | `beast/_9_Conscience/mental_automatism.js` |
| Инфокартина, `mode: passive \| target` | `beast/_9_Conscience/informing.js` |
| Цели жизненного опыта, умолчательная цель «Плачь», ОД | `beast/_9_Conscience/goals.js`, `beast/_10_actions_image/actions_image.js` |
| Генетические рефлексы, `processGeneticReflexes` | `beast/_5_Genetic__reflexes/genetic_reflexes_engine.js`, `genetic_reflexes.js` |
| Стимулы, UI | `beast/_3_perception/stimuls.js`, `stimuls_ui.js` |

## Третий уровень сознания (актуальное поведение)

Файл: **`beast/_9_Conscience/conscience_level_3.js`**. Вызов за тик: **`conscience_level_3_runFromChannel`** из `Consciousness_runOneStep`.

**Порядок попыток:** ментальный автоматизм (дефолт + согласованный кадр `semanticStructure`) → shortcut по `AbstractImages_findRuleFrameMatchingContext` → семантика стимула + лучшее правило ЭП (`info_bestEpisodicRule`, стимул, положительный effect) → учительский fallback.

**Устойчивость:** пока `awaitingAutomatismEpisodeFeedback && !automatismOperatorAnswerReceived`, полный пайплайн не выполняется (нет ложного «правило не найдено» при втором/третьем триггере в том же тике). Вложенный `runLevel3` проверяет то же до вызова `conscience_level_3`.

**Значимость перед мотором:**

- `conscience_level_3_semanticStructureFrameAllowsMotor(fr)` — для shortcut и ментального пути: teacher или episodic с **числовым effect > 0** (или маркеры учителя на кадре).
- Эпизодический путь: если у лучшего семантического кадра стимула **meanImportance < 0**, мотор не запускается.
- Учитель: если у кадра **числовой effect ≤ 0**, мотор не запускается.

**Лог цикла:** важные сообщения идут в **`Consciousness_cycleAppendLog(cycle, text, puls, opts)`**; при **`cycle.expired`** без **`opts.force`** dispatcher **не** дописывает строку — для ур.3 после автоматизма часто используется **`{ force: true }`**. Обёртка в файле: `appendCycleLog` (учёт главного цикла и `force`).

Подробнее: **`beast/_9_Conscience/about_conscience.md`**, **`beast/about_global_variables.md`**, **`beast/_10_Episodic_memory/about_episodic_memory.md`**.

## Четвёртый уровень и гештальт

- **Файлы:** `conscience_level_4.js` (тик канала), `gestalt.js` (данные и логика), `about_gestalt.md`.
- **Ур.3:** в начале `conscience_level_3_runFromChannel` вызывается **`Gestalt_prepareContextForCycle`** — в **`GestaltContextForLevel3`** попадает снимок открытого гештальта по образу цели (если есть).
- **Ур.4:** в `conscience_level_4_runFromChannel` — **`Gestalt_onConsciousnessTick`** только для **главного** цикла.
- **ЭП:** после расчёта `effect` в **`closeLastFrame`** вызывается **`Gestalt_onEpisodeFeedbackClosed`**.
- **UI:** в строке «Информационная картина» индикатор **гешт: N** (`informing.js`).

## Важные согласования поведения

- **`orientationReflexWinner`:** победитель ОР уже выбран — порог прерывания главного цикла не должен отменять старт нового ФО из‑за «недостаточной значимости».
- **`mainBlocksThoughtSilence`:** понижение значимости конкурента — только пока главный цикл существует и **не** `expired`.
- **Ожидание ЭП:** ответ оператора закрывает кадр; связка с абстракциями и **`MentalAutomatizms_tryCreateFromSemanticStructureRule`** — см. `about_episodic_memory.md`.
- **Умолчательная цель «Плачь» / ОД:** при загрузке образов действий из IndexedDB вызывается синхронизация `actionImageId` умолчательной цели с каноническим ОД (`getOrCreateActionImageId(0, [1], '')`) — см. `goals.js`, `actions_image.js`.
- **Еда / Вода (мотивация 1–2):** эффекты включают удержание «Хорошо» (`SetWellForHolding`); для ОР действует обход порога `OR_well_known` при активных стимулах 1001/1002; при активации сбрасывается `automatismRanForBranchIds`; инфокартина может подавлять индикатор критического витала при «Хорошо» + еда/вода — см. `genetic_reflexes.js`, `orientation_reflex.js`, `stimuls_ui.js`, `info_detectors.js`.

## Что считается стабильно работающим

1. Пульс, виталы, контексты, UI стимулов, дерево восприятия, рефлексы — в общем цикле.
2. **ОР → ФО → ЭП** при штатных порогах; кадр ЭП и прогресс ожидания при полном ОР.
3. Главный цикл осмысления помечается **`expired`** при простое; лог цикла учитывает `force` после автоматизма.
4. Ур.3 не «дёргается» повторно вслепую во время ожидания ЭП по уже запущенному автоматизму.

## Известные ограничения

1. Ранние пульсы после загрузки: контексты могут быть не готовы — индикатор готовности к ОР может быть временно неточным.
2. Индикатор на кнопке стимула не моделирует полную многокандидатную конкуренцию.
3. Ур.2 и пассивные сценарии — зона развития; ур.4 по гештальту реализован базово; расширения — см. `about_gestalt.md` и TODO в коде.

## Шаблон сообщения для нового чата

```
Контекст: проект BEAST, обязательно прочитай PROJECT_STATE_FOR_NEW_CHAT.md в корне.

Цель: <одним абзацем — что нужно сделать или проверить>

Ограничения: <окружение, что нельзя ломать, ссылки на файлы при необходимости>
```

После правок по архитектуре имеет смысл **обновить этот файл** и соответствующие `about_*.md` в `beast/`.

════════════════════════════════════════════════════════════

/BEAST/beast/_10_Episodic_memory/about_episodic_memory.md
════════════════════════════════════════════════════════════
# Эпизодическая память (_10_Episodic_memory)

## Назначение

Модуль реализует **эпизодическую память (ЭП)** — массив кадров в строгой исторической последовательности. Каждый кадр описывает правило: стимул (образ FinalImage) → ответное действие (образ действия, ОД) → эффект (разность getDiffImportance до и после) → ответный стимул оператора (при наличии в период ожидания). Кадры создаются только через ориентировочный рефлекс при выполнении условий по семантическому опыту и периоду ожидания.

## Основные понятия

- **Кадр эпизода:** perceptionNodeId, stimulusImageId, actionImageId, effect, responseStimulusImageId. Ответное действие + ответный стимул образуют «учительское» правило.
- **Период ожидания ответа** — response_waiting_period пульсов (по умолчанию 15). Закрытие по таймауту выполняется в EpisodicMemory_onPulse (вызов из puls.js), без setTimeout; при зависании вкладки логика не расходится с пульсом.
- **Режим тестирования** (EpisodicMemory_testMode) — пошаговые сообщения о проверке ОР, начале формирования кадра, ответном стимуле и отказе; на наличие прогресс-бара не влияет.

## Особенности поведения

- **Создание кадра и прогресс-бар:** при каждом **полном** срабатывании ОР (после прохождения порога `OR_well_known`, либо на втором проходе после ответа в ожидании, см. правила) вызывается `save_episodic_memory` — кадр создаётся и прогресс-бар периода ожидания запускается, не только в тестовом режиме.
- **Прогресс-бар виден только при активном ожидании в этой сессии:** контейнер прогресс-бара показывается только если `EpisodicMemory_waitingStartedAt != null` или `EpisodicMemory_barShowCompletePulsesLeft > 0`. После обновления страницы эти переменные не восстанавливаются из IndexedDB, поэтому прогресс-бар не отображается до первой активации стимула, запустившей ОР.
- **Период ожидания только в текущей сессии:** `isInWaitingPeriod()` возвращает true только при `EpisodicMemory_waitingStartedAt != null`. Незакрытый кадр, загруженный из IndexedDB (effect === null), не считается «активным ожиданием» — первый клик после обновления создаёт новый кадр с прогресс-баром от 0%, а не закрывает старый с показом 100%.
- **Деактивация стимулов:** при снятии всех стимулов (пустой список активных) ОР выходит без сообщения. Уже созданный кадр ЭП и период ожидания не отменяются; стимул уже записан в кадре.
- **Ответный стимул оператора:** при активации в периоде ожидания ОР закрывает кадр ответом (`closeLastFrame` / `EpisodicMemory_closeLastFrameWithResponse`), открывает следующий кадр и выполняет **повторный полный** ОР: новый цикл ФО и ещё один вызов `save_episodic_memory`. Порог `OR_well_known` для шага «ответ» не проверяется.

## Преемственность оценки эффекта (ЭП ↔ автоматизмы)

**Единая оценка эффекта:** один и тот же эффект вычисляется в ЭП и передаётся в автоматизм.

- В **closeLastFrame** эффект считается один раз: `effect = getDiffImportance() (после) − diffBefore` и записывается в кадр ЭП (`last.effect`).
- Тот же `last.effect` передаётся в штатный автоматизм ветки вызовом **Automatizms_addUsefulnessSample(staff.id, last.effect)**.
- Оценка не дублируется и не расходится: одно значение эффекта — одна запись в кадре ЭП и одно обновление Usefulness автоматизма (с усреднением по count). Алгоритм обеспечивает преемственность оценки эффекта между ЭП и автоматизмами.

- **Гештальт (ур.4):** после записи `effect` в кадр, до сброса глобалов ожидания по автоматизму, вызывается **`Gestalt_onEpisodeFeedbackClosed`** (`gestalt.js`): в **буфер резонанса** добавляется сигнатура закрытия; при наличии **`goalImageId`** у цикла ожидания обновляется соответствующая запись гештальта (статусы, инсайты). Не дублирует оценку эффекта — использует уже вычисленный `effect`. Подробнее — **`beast/_9_Conscience/about_gestalt.md`**.

## Эффект закрытого кадра и неполное правило (нули)

- После `closeLastFrame` поле `effect` кадра **всегда число** в диапазоне **[-10, +10]** (`clampEpisodeFrameEffect` в `episodic_memory.js`): либо по разности `getDiffImportance()` и сохранённого `diffBefore`, либо **0**, если база для разности недоступна (нейтральная оценка).
- Если к моменту закрытия в кадре ещё нет **образа действия (ОД)** и/или **ответного стимула**, перед закрытием вызывается `episodicFrameFillRuleZerosForIncompleteClose`: в соответствующие поля записывается **0** (в открытом кадре до этого хранится `null`). Так кадр остаётся в истории `Episodes`, а не удаляется.
- **Истечение ожидания без реакции** (`EpisodicMemory_onPulse`): кадр **не** снимается с массива — выставляются нули и `closeLastFrame(null)` (как и при закрытии с реакцией, но без ответа оператора).
- При **новом стимуле ОР** предыдущий открытый кадр закрывается с `episodicFrameFillRuleZerosForIncompleteClose(prev)` и `closeLastFrame(stimulusImageId)` нового победителя — ответ в «старом» кадре подставляется из нового стимула.
- В **компактной сериализации** в файл/IndexedDB явная **«0»** в полях ОД и ответного стимула при разборе сохраняется как число **0** (`parseIntOrNull`), чтобы не смешивать с «пусто» (`null`).

## Персистенция клонов (IndexedDB + резерв)

Состояние `abstract_images.js` хранится в IndexedDB (`BEAST_AbstractImages_DB`, открытие через `openDatabase`, одно закэшированное соединение — как у образов действия). Запись: по пульсу, **сразу после** `closeLastFrame` (`AbstractImages_flushIndexedDB`) и при `beforeunload`, если ещё `absDirty`. При ошибке `put` дублируется снимок в **localStorage** (`BEAST_AbstractImages_LS`); при пустой записи в IndexedDB загрузка страницы подхватывает этот резерв. Полный снимок в файле — ключ **`AbstractImagesState`**.

## Индикатор «абстр» в инфокартине

Счётчик показывает **число клонов** из `AbstractPerceptionImages` + `AbstractActionImages` (появляются при **закрытии** кадра ЭП). Доп. суффикс `+N` — элементы массива `Abstractions` (`mental_automatism.js`). Щелчок по индикатору открывает сводку по всем трём наборам.

## Связь с абстрактными образами (`abstract_images.js`)

- При **каждом** вызове `closeLastFrame` вызывается `AbstractImages_cloneFromEpisodeFrame(last)`: создаётся или усиливается (усреднение `meanImportance`, `count`) абстракция **восприятия** по паре `(perceptionNodeId, stimulusImageId)`.
- **Абстракция действия** обновляется только если `actionImageId` ненулевой; значение **0** после «нуляции» означает отсутствие ОД — клон действия не строится.
- **`semanticStructure`** у каждой абстракции — объект `{ frames: [...] }`: для каждой пары условий **(узел ветки восприятия, `situationId`)** хранится снимок лучшего эпизодического правила (`ruleKind: episodic`, выбор по большему `effect`) либо учительского (`ruleKind: teacher`, есть `responseStimulusImageId` на ОД; вес «доверия» не через `effect`). Обновление — в `AbstractImages_cloneFromEpisodeFrame` через `AbstractImages_upsertSemanticRuleFromEpisodeFrame`; при учительском fallback ур.3 — ещё `AbstractImages_activateTeacherRuleOnAbstractions` (клоны по актуальному FinalImage и по ОД из правила). **Ур.3:** `AbstractImages_findRuleFrameMatchingContext` для shortcut / ментального автоматизма; мотор по этому кадру только если значимость кадра «положительна» в смысле `conscience_level_3_semanticStructureFrameAllowsMotor` (teacher или episodic с `effect > 0`). Иначе выполняется обход семантики+ЭП и учительский путь. Пока цикл ждёт кадр ЭП после автоматизма, повторный полный ур.3 не гоняется (см. `about_conscience.md`).
- **Ответ оператора после автоматизма (`conscience_level_2_operatorAnswerPipeline`):** при числовом `effect` и совпадающем кадре `semanticStructure` (ветка, ситуация, стимул) **`AbstractImages_applyEpisodeEffectToSemanticContextAbstractions`** усредняет `meanImportance` клонов восприятия и действия (положительный эффект усиливает, отрицательный ослабляет). При **положительном** `effect` дополнительно **`MentalAutomatizms_tryCreateFromSemanticStructureRule`** — ментальный автоматизм; оба шага в т.ч. при **`goalImageId === 0`** (ур.3).
- `MentalRules_registerFromEpisodeFrame` (`mental_automatism.js`): использует `abstractActionId || abstractPerceptionId`, поэтому при отсутствии ОД ментальное правило всё равно может усилиться по абстракции восприятия.

## Приоритет стимулов «цепочки» ЭП (ОР и ФО)

Подряд идущие **с конца** массива `Episodes` кадры, у **каждого** из которых зафиксирована **реакция** (`actionImageId`), образуют **связную цепочку**. Если в суффиксе не меньше двух таких кадров (параметр `EpisodicMemory_chainPriorityMinFrames`), все их `stimulusImageId` и `responseStimulusImageId` получают **приоритет**:

- в **ориентировочном рефлексе** для них **не** вычитается гистерезис конкуренции, актуальность **усиливается** (`findMostActualStimulus` в `orientation_reflex.js`);
- при запуске **главного цикла ФО** (`stimuls_consciousness`) на них **не** распространяются понижающие множители (несовпадение темы/ситуации, ImportantProblem) и порог прерывания текущего мышления берётся как у «равной значимости» (ratio 1), чтобы цепочка не обрывалась чужими стимулами без реакции/ответа. Для остальных стимулов та же «тишина мыслей» действует **только пока главный цикл не `expired`**; победитель ОР помечается в контексте и не отсекается этим порогом (подробности — в **about_episodic_memory_rules.md** и **about_conscience.md**).

Подробнее — **about_episodic_memory_rules.md** (раздел про цепочки и прерывание).

## Связь с другими модулями

- Создание кадра запускается из **ОР** (_8_Hippocampus) при вызове save_episodic_memory.
- Заполнение actionImageId — из **genetic_reflexes_engine** (EpisodicMemory_setLastFrameActionFromActionId) при выполнении реакции.
- Отображение последних кадров, прогресс-бар и кнопки — в **_7_perception_tree** (updateActiveBranchView); видимость прогресс-бара по условию активного ожидания в этой сессии.
- **Результат ЭП — автоматизмы:** (1) При срабатывании рефлекса (в т.ч. задержанного) в setLastFrameActionFromActionId сразу создаётся автоматизм по образу действия, привязанный к ветке, с нулевым Usefulness; если к ветке ещё не привязан автоматизм — он назначается штатным. (2) При закрытии кадра (ответный стимул в период ожидания или таймаут) выявленный эффект записывается в штатный автоматизм ветки через addUsefulnessSample (усреднение Usefulness); см. выше **преемственность оценки эффекта**. Если сработал штатный автоматизм и затем пришёл ответный стимул — тот же штатный автоматизм дополняется новым значением значимости (усреднение).

## Подробные правила

Полное описание правил и условий формирования кадров, работы тестового режима и связи с ориентировочным рефлексом и условными рефлексами см. в **about_episodic_memory_rules.md** (там же **§9** — таблица: эффект, нули в полях правила, абстракции).

## Файлы

- **episodic_memory.js** — массив Episodes, save_episodic_memory, closeLastFrame, isInWaitingPeriod, setLastFrameActionFromActionId, персистенция (IndexedDB, компактный формат в файл), кнопки сброса и тестового режима.

════════════════════════════════════════════════════════════

/BEAST/beast/_10_Episodic_memory/about_episodic_memory_rules.html
════════════════════════════════════════════════════════════
<!DOCTYPE html>
                <html lang="en">
                <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>Markdown Export</title>
                <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.3.0/github-markdown.min.css">
                <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
                <style>
                    body {
                        background-color: #ffffff;
                        color: #24292e;
                    }
                    .markdown-body {
                        box-sizing: border-box;
                        min-width: 200px;
                        max-width: 980px;
                        margin: 0 auto;
                        padding: 45px;
                        background-color: #ffffff;
                        color: #24292e;
                    }
                    @media (max-width: 767px) {
                        .markdown-body {
                            padding: 15px;
                        }
                    }
                </style>
                </head>
                <body>
                <article class="markdown-body">
                    <h1>Правила и условия формирования кадров эпизодической памяти (ЭП)</h1>
<h2>1. Общая схема эпизодической памяти</h2>
<p>ЭП — массив кадров в <strong>строгой исторической последовательности</strong>. Порядок не перемешивается; допускаются только изменение и удаление без нарушения порядка.</p>
<p><strong>Правило (кадр эпизода):</strong><br>Стимул (образ FinalImage) → ответное действие (образ действия, ОД) → эффект (Effect) → ответный стимул оператора (при наличии).</p>
<p><strong>Содержимое кадра:</strong></p>
<ul>
<li><code>perceptionNodeId</code> — ID конечной ветки дерева восприятия (условия);</li>
<li><code>stimulusImageId</code> — ID образа стимулов (FinalImage), вызвавшего реакцию;</li>
<li><code>actionImageId</code> — ID образа ответного действия (ОД);</li>
<li><code>effect</code> — разность <code>getDiffImportance()</code> «после» минус «до» (до активации стимула);</li>
<li><code>responseStimulusImageId</code> — ID образа ответного стимула оператора (если появился в период ожидания).</li>
</ul>
<p>Ответное действие + ответный стимул образуют «учительское» правило: как оператор отвечает на действие Beast.</p>
<p><strong>Время ожидания ответа</strong> — <code>response_waiting_period</code> в <strong>пульсах</strong> (по умолчанию 15). Закрытие по таймауту в <code>EpisodicMemory_onPulse</code>. Кадры с незаполненным <code>effect</code> остаются на оптимизацию.</p>
<hr>
<h2>2. Условия запуска формирования кадра ЭП</h2>
<p>Формирование кадра ЭП <strong>запускается только через ориентировочный рефлекс (ОР)</strong>. Кадр создаётся когда: ОР сработал, выполнены условия ОР (порог OR_well_known и др.), не идёт период ожидания (иначе стимул трактуется как ответный). Вызов <code>save_episodic_memory</code> выполняется из ОР; <strong>кадр и прогресс-бар создаются при каждом таком срабатывании ОР</strong> (не только в тестовом режиме).</p>
<hr>
<h2>3. Жизненный цикл кадра</h2>
<ol>
<li><p><strong>Создание (при срабатывании ОР):</strong></p>
<ul>
<li>Закрывается предыдущий незакрытый кадр (если есть и не истёк период ожидания — текущий стимул записывается как ответный; иначе кадр закрывается без ответного стимула).</li>
<li>В массив ЭП добавляется «пустой» кадр: заданы <code>perceptionNodeId</code>, <code>stimulusImageId</code>, <code>diffBefore</code> (значение <code>getDiffImportance()</code> до активации стимула, из <code>PrevDiffForEpisode</code>), <code>actionImageId = null</code>, <code>effect = null</code>, <code>responseStimulusImageId = null</code>.</li>
<li>Устанавливаются счётчик пульсов и <code>EpisodicMemory_waitingStartedAt</code>; закрытие по пульсам в <code>EpisodicMemory_onPulse</code>. Прогресс-бар виден только при активном ожидании в этой сессии.</li>
</ul>
</li>
<li><p><strong>Заполнение ответным действием:</strong><br>Когда Beast выполняет реакцию (генетическую или условную), в последний кадр записывается <code>actionImageId</code> (образ действия) через <code>EpisodicMemory_setLastFrameActionFromActionId(actionId)</code>. Используется <code>stimulusImageId</code> уже записанный в кадре (чтобы кадр заполнялся и при отложенной реакции или снятых стимулах).</p>
</li>
<li><p><strong>Завершение кадра:</strong></p>
<ul>
<li><strong>Ответный стимул в период ожидания:</strong> ОР <strong>сначала</strong> проверяет <code>isInWaitingPeriod()</code>. Если истинно — стимул считается ответом оператора, вызывается <code>closeLastFrameWithResponse(winnerId)</code>. Порог OR_well_known для ответного стимула <strong>не проверяется</strong>.</li>
<li><strong>Истечение периода без ответного стимула:</strong> в <code>EpisodicMemory_onPulse</code> кадр закрывается. Если <code>actionImageId === null</code>, кадр удаляется; иначе остаётся с вычисленным <code>effect</code>.</li>
</ul>
</li>
</ol>
<p>Во время периода ожидания любой стимул трактуется только как ответ оператора (без проверки samplesCount).</p>
<hr>
<h2>4. Ориентировочный рефлекс (ОР) и условия для ЭП</h2>
<p><strong>Порядок проверок:</strong> (1) Нет активных стимулов — выход без сообщения (деактивация). (2) Нет ветки / нет победителя — выход. (3) Регистрация в СЗ. (4) <strong>Сначала</strong> <code>isInWaitingPeriod()</code> — если true, закрытие кадра с ответом, порог OR_well_known не применяется. (5) <strong>Затем</strong> проверка <code>samplesCount &gt;= OR_well_known</code> для начала нового процесса. (6) Иначе — <code>save_episodic_memory</code>, кадр и прогресс-бар создаются всегда. Период ожидания только при <code>waitingStartedAt != null</code> (загруженный незакрытый кадр не даёт «в ожидании»).</p>
<hr>
<h2>5. Режим тестирования формирования ЭП</h2>
<p>Включается кнопкой «т» в блоке «Активная ветка дерева восприятия». Флаг: <code>global.EpisodicMemory_testMode</code>.</p>
<p><strong>В тестовом режиме:</strong></p>
<ul>
<li>При <strong>деактивации</strong> (нет активных стимулов) сообщение не показывается.</li>
<li>При <strong>отказе</strong> в создании нового кадра (когда период ожидания не активен) выводятся причины; ответный стимул в период ожидания не проверяется на OR_well_known.<ul>
<li>нет функций дерева или образов; не найдена ветка; не выявлен победитель; слабый опыт (samplesCount &lt; OR_well_known) — только для начала нового процесса.</li>
</ul>
</li>
<li>При <strong>начале формирования кадра</strong> — «Начат процесс формирования кадра ЭП. Стимул: (детализация). Ожидание ответа или реакции.»</li>
<li>При <strong>фиксации ответного действия</strong> (реакция Beast) — «Активирован образ стимула: (детализация). Вызвавшая реакция: (детализация). Период ожидания ответа начат.»</li>
<li>При <strong>появлении ответного стимула в период ожидания</strong> — «Стимул в периоде ожидания: (детализация). Кадр эпизода завершен и записан.»</li>
<li>При <strong>истечении периода без реакции</strong> — кадр удаляется и выводится: «На стимул (детализация) реакция не возникла. Эпизод памяти не формируется.»</li>
</ul>
<p>В тестовом режиме кадр ЭП создаётся и при наличии семантической значимости (<code>runEpisodic = !hasSignificance || EpisodicMemory_testMode</code>), чтобы можно было пошагово наблюдать процесс.</p>
<hr>
<h2>6. Условные рефлексы в контексте ориентировочного рефлекса</h2>
<p><strong>Правило:</strong> если стимул <strong>привлёк ориентировочный рефлекс</strong>, по нему <strong>не запускается процесс образования условного рефлекса</strong>.</p>
<p><strong>Механизм:</strong></p>
<ol>
<li>ОР при срабатывании <strong>блокирует</strong> выполнение генетического (или условного) рефлекса на 2 пульса (<code>OR_BLOCK_PULSES</code>). Рефлекс не выполняется сразу, а помещается в очередь отложенных рефлексов (<code>delayedReflexes</code>).</li>
<li>При постановке в очередь и при последующем выполнении отложенного рефлекса в событие записывается флаг <strong><code>noConditionalReflex: true</code></strong>.</li>
<li>Когда отложенный рефлекс выполняется, в <code>startAction</code> передаётся опция <code>noConditionalReflex: true</code>. При этой опции <strong>не вызывается</strong> <code>synonyms_onGeneticActionFired</code> — подсистема условных (синонимических) рефлексов не получает сигнал для формирования или усиления условного рефлекса по данному стимулу.</li>
</ol>
<p>Таким образом, стимул, который привлёк ОР (т.е. по которому рефлекс был задержан), не участвует в образовании условного рефлекса; условные рефлексы формируются только по стимулам, по которым рефлекс сработал без блокировки ОР.</p>
<hr>
<h2>7. Сводка ключевых констант и флагов</h2>
<table>
<thead>
<tr>
<th>Параметр</th>
<th>Описание</th>
<th>Значение по умолчанию</th>
</tr>
</thead>
<tbody><tr>
<td><code>response_waiting_period</code></td>
<td>Длительность периода ожидания ответа (в пульсах)</td>
<td>15</td>
</tr>
<tr>
<td><code>OR_well_known</code></td>
<td>Порог для начала нового процесса ОР/ЭП (не для ответного стимула)</td>
<td>10</td>
</tr>
<tr>
<td><code>EpisodicMemory_testMode</code></td>
<td>Режим пошаговых сообщений по формированию ЭП</td>
<td>false</td>
</tr>
<tr>
<td><code>OR_BLOCK_PULSES</code></td>
<td>Число пульсов блокировки рефлекса после ОР</td>
<td>2</td>
</tr>
</tbody></table>
<p>Файлы реализации: <code>_10_Episodic_memory/episodic_memory.js</code>, <code>_8_Hippocampus/orientation_reflex.js</code>, <code>_5_Genetic__reflexes/genetic_reflexes_engine.js</code>.</p>

                </article>
                </body>
                </html>
════════════════════════════════════════════════════════════

/BEAST/beast/_10_Episodic_memory/about_episodic_memory_rules.md
════════════════════════════════════════════════════════════
# Правила и условия формирования кадров эпизодической памяти (ЭП)

## 1. Общая схема эпизодической памяти

ЭП — массив кадров в **строгой исторической последовательности**. Порядок не перемешивается; допускаются только изменение и удаление без нарушения порядка.

**Правило (кадр эпизода):**  
Стимул (образ FinalImage) → ответное действие (образ действия, ОД) → эффект (Effect) → ответный стимул оператора (при наличии).

**Содержимое кадра:**
- `perceptionNodeId` — ID конечной ветки дерева восприятия (условия);
- `stimulusImageId` — ID образа стимулов (FinalImage), вызвавшего реакцию;
- `actionImageId` — ID образа ответного действия (ОД);
- `effect` — разность `getDiffImportance()` «после» минус «до» (до активации стимула);
- `responseStimulusImageId` — ID образа ответного стимула оператора (если появился в период ожидания).

Ответное действие + ответный стимул образуют «учительское» правило: как оператор отвечает на действие Beast.

**Время ожидания ответа** — `response_waiting_period` в **пульсах** (по умолчанию 15). Закрытие по таймауту выполняется в `EpisodicMemory_onPulse` (вызов из puls.js). В течение периода ожидается либо ответный стимул оператора, либо фиксация эффекта. Кадры с незаполненным `effect` остаются на оптимизацию (например, во время сна).

---

## 2. Условия запуска формирования кадра ЭП

Формирование кадра ЭП **запускается только через ориентировочный рефлекс (ОР)**. Кадр создаётся когда:

1. **ОР сработал** — выбран «победитель» среди активных стимулов (наиболее актуальный образ).
2. **Выполнены условия ОР** (есть активные стимулы, ветка дерева, победитель, у победителя `samplesCount >= OR_well_known`; см. раздел 4).
3. Либо **идёт период ожидания** — тогда активация сначала обрабатывается как **ответный стимул** (без проверки `OR_well_known`), затем выполняется **второй полный проход ОР** с новым кадром ЭП и ФО (см. раздел 4). Либо периода ожидания нет — действуют обычные условия порога известности.

Вызов `save_episodic_memory(perceptionNodeId, stimulusImageId)` выполняется из ОР после блокировки рефлекса и вызова гиппокампа. **Кадр и прогресс-бар периода ожидания создаются при каждом таком срабатывании ОР** (не только при отсутствии значимости или в тестовом режиме).

---

## 3. Жизненный цикл кадра

1. **Создание (при срабатывании ОР):**
   - Закрывается предыдущий незакрытый кадр (если есть): перед `closeLastFrame` вызывается `episodicFrameFillRuleZerosForIncompleteClose(prev)` — в пустые поля ОД и ответного стимула пишется **0**; затем в `closeLastFrame` ответным стимулом предыдущего кадра становится **текущий** `stimulusImageId` (новый победитель ОР), вычисляется `effect` в [-10, +10].
   - В массив ЭП добавляется «пустой» кадр: заданы `perceptionNodeId`, `stimulusImageId`, `diffBefore` (из `PrevDiffForEpisode` / `getDiffImportance`), `actionImageId = null`, `effect = null`, `responseStimulusImageId = null`.
   - Устанавливаются `EpisodicMemory_waitingStartedAt` и счётчик пульсов ожидания; закрытие по таймауту выполняется в `EpisodicMemory_onPulse` (пульсы, не таймер). Прогресс-бар отображается только при активном ожидании в этой сессии (`waitingStartedAt != null` или показ 100% после закрытия с ответом).

2. **Заполнение ответным действием:**  
   Когда Beast выполняет реакцию (генетическую или условную), в последний кадр записывается `actionImageId` (образ действия) через `EpisodicMemory_setLastFrameActionFromActionId(actionId)`. Используется `stimulusImageId` уже записанный в кадре (чтобы кадр заполнялся и при отложенной реакции или снятых стимулах).

3. **Завершение кадра:**
   - **Ответный стимул в период ожидания:** при активации стимула ОР **сначала** проверяет `EpisodicMemory_isInWaitingPeriod()`. Если истинно, текущий стимул считается ответом оператора — вызывается `closeLastFrame` с ответным образом (экспорт `EpisodicMemory_closeLastFrameWithResponse`). Порог `OR_well_known` для этого шага **не проверяется**. Затем открывается **новый** открытый кадр (`save_episodic_memory`) по образу ответа, выставляется флаг `OrientationReflex_resumeFullOrAfterWaitingResponse`, и ОР вызывается **повторно** в той же активации: второй проход — полный (гиппокамп, `stimuls_consciousness` с контекстом победителя ОР, снова `save_episodic_memory`), чтобы начался новый цикл ФО и корректный кадр ЭП без «тишины мысли», блокирующей победителя уже отобранного конкуренцией.
   - **Истечение периода без ответного стимула:** в `EpisodicMemory_onPulse` при достижении `response_waiting_period` пульсов кадр **закрывается** (`closeLastFrame`), а не удаляется. Если реакции не было (`actionImageId` всё ещё `null`), перед закрытием в поля правила проставляются **нули** (ОД и ответный стимул → 0), выставляются индикаторы «игнорирование / нет ответа», затем полный пайплайн закрытия включая **абстракции** (`AbstractImages_cloneFromEpisodeFrame`). Если реакция была, закрытие — с уже записанным `actionImageId`, без подстановки нуля в ОД.
   - У **закрытого** кадра `effect` **всегда число** в **[-10, +10]** (ограничение дельты `getDiffImportance` или 0 при отсутствии `diffBefore`).

Пока открыт кадр без `effect`, «чужой» стимул в основном сценарии трактуется как ответ оператора; после закрытия и открытия кадра по ответу сразу следует **полный** ОР (см. выше), а не только одно закрытие без ФО.

**Преемственность оценки эффекта (ЭП ↔ автоматизмы):** При закрытии кадра один и тот же вычисленный `effect` (после `clamp` в [-10,+10]) записывается в кадр ЭП и передаётся в штатный автоматизм ветки через `Automatizms_addUsefulnessSample(staff.id, last.effect)`. Оценка эффекта единая: не дублируется, не расходится между ЭП и автоматизмами.

**Абстракции:** см. раздел 9 ниже и `about_episodic_memory.md` («Связь с абстрактными образами»).

---

## 4. Ориентировочный рефлекс (ОР) и условия для ЭП

ОР вызывается при активации стимулов (например, по клику). **Порядок проверок в ОР:**

1. **Нет активных стимулов** — массив пуст (деактивация). ОР выходит **без сообщения**; уже созданный кадр ЭП и период ожидания не отменяются.
2. Нет ветки дерева восприятия / не выявлен победитель — выход с сообщением в тестовом режиме.
3. Регистрация экспозиции в семантической памяти (один раз за активацию).
4. **Проверка «второго полного прохода» после ответа:** если установлен `OrientationReflex_resumeFullOrAfterWaitingResponse`, флаг сбрасывается; порог `OR_well_known` **не проверяется**; выполняется полный путь как в п. 7 (контекст ОР передаётся в `stimuls_consciousness` с признаком победителя ОР — см. `about_conscience.md`).
5. **Иначе — проверка периода ожидания:** если `EpisodicMemory_isInWaitingPeriod()` истинно, действует ветка ответного стимула (закрытие кадра, открытие следующего, планирование п. 4, рекурсивный повтор `runOROnStimulusActivation`). Порог `OR_well_known` для ответного стимула **не применяется**.
6. **Затем** проверка порога известности: для образа-победителя `samplesCount < OR_well_known` (по умолчанию 10) — отказ в создании нового кадра, сообщение в тестовом режиме (в состоянии «Плохо» порог не действует; см. код `orientation_reflex.js`).
7. Иначе — блокировка рефлекса, гиппокамп, `stimuls_consciousness(orContext)`, вызов `save_episodic_memory(terminalNodeId, winnerId)`. Кадр и прогресс-бар создаются **всегда** при этом срабатывании ОР.

**Период ожидания:** `isInWaitingPeriod()` возвращает true только если `EpisodicMemory_waitingStartedAt != null` и последний кадр без effect и счётчик пульсов не достиг лимита. Незакрытый кадр, загруженный из IndexedDB после обновления страницы, не считается активным ожиданием (waitingStartedAt не восстанавливается), чтобы первый клик создавал новый кадр с прогресс-баром от 0%.

---

## 5. Режим тестирования формирования ЭП

Включается кнопкой «т» в блоке «Активная ветка дерева восприятия». Флаг: `global.EpisodicMemory_testMode`.

**В тестовом режиме:**

- При **деактивации** (нет активных стимулов) сообщение **не показывается** — это нормальное действие.
- При **отказе** в создании нового кадра (когда период ожидания не активен) выводятся причины:
  - нет функций дерева восприятия или образов;
  - не найдена или не создана ветка дерева восприятия;
  - не выявлен наиболее актуальный стимул среди кандидатов;
  - опыт семантической значимости слабый (samplesCount < OR_well_known) — **не применяется к ответному стимулу в период ожидания**.
- При **начале формирования кадра** — «Начат процесс формирования кадра ЭП. Стимул: (детализация). Ожидание ответа или реакции. Прогресс-бар периода ожидания запущен.»
- При **фиксации ответного действия** (реакция Beast) — «Активирован образ стимула: (детализация). Вызвавшая реакция: (детализация). Период ожидания ответа начат.»
- При **появлении ответного стимула в период ожидания** — «Стимул в периоде ожидания: (детализация). Кадр эпизода завершен и записан.»
- При **истечении периода без реакции** — кадр **закрывается с нулями** в правиле; в тестовом режиме сообщение поясняет, что эпизод записан и абстракция восприятия усиливается по эффекту (формулировка — в коде `episodic_memory.js`).

Прогресс-бар отображается при активном ожидании в этой сессии независимо от тестового режима. В тестовом режиме добавляются только пошаговые сообщения.

---

## 6. Условные рефлексы в контексте ориентировочного рефлекса

**Правило:** если стимул **привлёк ориентировочный рефлекс**, по нему **не запускается процесс образования условного рефлекса**.

**Механизм:**

1. ОР при срабатывании **блокирует** выполнение генетического (или условного) рефлекса на 2 пульса (`OR_BLOCK_PULSES`). Рефлекс не выполняется сразу, а помещается в очередь отложенных рефлексов (`delayedReflexes`).
2. При постановке в очередь и при последующем выполнении отложенного рефлекса в событие записывается флаг **`noConditionalReflex: true`**.
3. Когда отложенный рефлекс выполняется, в `startAction` передаётся опция `noConditionalReflex: true`. При этой опции **не вызывается** `synonyms_onGeneticActionFired` — подсистема условных (синонимических) рефлексов не получает сигнал для формирования или усиления условного рефлекса по данному стимулу.

Таким образом, стимул, который привлёк ОР (т.е. по которому рефлекс был задержан), не участвует в образовании условного рефлекса; условные рефлексы формируются только по стимулам, по которым рефлекс сработал без блокировки ОР.

---

## 7. Связные цепочки кадров: приоритет перед «обрывающими» стимулами

**Мотивация.** Несколько кадров ЭП подряд могут образовать смысловую цепочку (например: действие → ответ оператора → следующий стимул). Стимулы, входящие в такую цепочку, не должны проигрывать конкуренцию посторонним активным стимулам, у которых нет зафиксированной реакции или которые не продолжают сценарий ожидания ответа — иначе цепочка постоянно обрывается.

**Определение (суффикс цепочки).** От конца массива `Episodes` идём назад и считаем подряд кадры, у **каждого** из которых задан ненулевой `actionImageId` (реакция уже привязана к кадру). Если длина такого суффикса **≥** `EpisodicMemory_chainPriorityMinFrames` (по умолчанию **2**), все `stimulusImageId` и ненулевые `responseStimulusImageId` этих кадров помечаются как стимулы **приоритета цепочки**.

**Реализация.**

- `EpisodicMemory_getChainPriorityStimulusSet()` — словарь id стимулов приоритета или `null`, если цепочки нет.
- `EpisodicMemory_isStimulusEpisodicChainPriority(finalImageId)` — проверка одного стимула.

**Ориентировочный рефлекс** (`findMostActualStimulus`): для кандидата с приоритетом цепочки **не** вычитается гистерезис относительно прошлого победителя; актуальность **повышается** (линейное масштабирование + смещение), чтобы при прочих равных выигрывал стимул, продолжающий сценарий ЭП.

**ФО, главный цикл** (`stimuls_consciousness`): понижение конкурирующего стимула («тишина мыслей») и сравнение с порогом `INTERRUPT_THRESHOLD_ATTENTION_RATIO` действуют **только пока есть главный цикл и он ещё не помечен `expired`**. Если главного нет или он уже завершён, эти ограничения не применяются.

Дополнительно:

- стимул **приоритета цепочки** ЭП — как и раньше: не применяются множитель несовпадения темы/ситуации и штраф при `ImportantProblem`, порог прерывания берётся с коэффициентом **1.0**;
- стимул, переданный из ОР как **победитель конкуренции** (`orientationReflexWinner` в контексте), **не отсекается** порогом «тишины мыслей» относительно текущего главного цикла: иначе ОР выбрал бы актуальный стимул, а ФО не создал бы новый цикл.

Один изолированный кадр с реакцией приоритета не даёт: нужны **минимум два** подряд кадра в суффиксе.

---

## 8. Сводка ключевых констант и флагов

| Параметр | Описание | Значение по умолчанию |
|----------|----------|----------------------|
| `response_waiting_period` | Длительность периода ожидания ответа (**в пульсах**) | 15 |
| `OR_well_known` | Порог числа семантических образцов для **начала** нового процесса ОР/ЭП (не для ответного стимула) | 10 |
| `EpisodicMemory_testMode` | Режим пошаговых сообщений по формированию ЭП | false |
| `OR_BLOCK_PULSES` | Число пульсов блокировки рефлекса после ОР | 2 |
| `EpisodicMemory_chainPriorityMinFrames` | Мин. длина суффикса кадров с `actionImageId` для приоритета цепочки в ОР/ФО | 2 |

**Прогресс-бар:** виден только при `EpisodicMemory_waitingStartedAt != null` или `EpisodicMemory_barShowCompletePulsesLeft > 0` (активное ожидание в этой сессии или показ 100% после закрытия с ответом). После обновления страницы не отображается до первой активации стимула, запустившей ОР.

**isInWaitingPeriod():** возвращает true только при `EpisodicMemory_waitingStartedAt != null` (ожидание начато в этой сессии). Загруженный из IndexedDB незакрытый кадр не даёт true, чтобы первый клик после обновления создавал новый кадр с прогресс-баром от 0%.

Файлы реализации: `_10_Episodic_memory/episodic_memory.js`, `_8_Hippocampus/orientation_reflex.js`, `_7_perception_tree/perception_tree.js`, `_5_Genetic__reflexes/genetic_reflexes_engine.js`.

---

## 9. Эффект, нули в полях правила, абстракции (краткая сводка)

| Момент | Поведение |
|--------|-----------|
| Закрытие кадра | `effect` ∈ [-10, +10], всегда число; см. `clampEpisodeFrameEffect` |
| Нет ОД или нет ответа к моменту закрытия | `episodicFrameFillRuleZerosForIncompleteClose` → в пустые поля **0** (в истории не `null` в закрытом кадре для этих слотов) |
| Таймаут, реакции не было | Не `ep.pop`: нули + `closeLastFrame(null)` + абстракции / ментальные правила |
| Новый ОР при открытом предыдущем кадре | Нули на предыдущем при необходимости + `closeLastFrame(новый стимул)` |
| Сохранение compact | `"0"` в action/response читается как **0**, не как `null` (`parseIntOrNull`) |
| `abstract_images.js` | Всегда вызов из `closeLastFrame`; клон ОД только если `actionImageId` ненулевой; **0** = не клонировать действие |

Реализация: `episodic_memory.js` (`closeLastFrame`, `EpisodicMemory_onPulse`, `episodicFrameFillRuleZerosForIncompleteClose`), `abstract_images.js`, `mental_automatism.js` (`MentalRules_registerFromEpisodeFrame`).

════════════════════════════════════════════════════════════

/BEAST/beast/_10_Episodic_memory/episodic_memory.js
════════════════════════════════════════════════════════════
/**
 * Эпизодическая память (ЭП).
 *
 * ЭП — массив со строгой исторической последовательностью кадров эпизодов.
 * Порядок не перемешивается; допускаются операции изменения и удаления без изменения порядка.
 *
 * Правило (кадр эпизода): стимул (FinalImage) → ответное действие (actions_image) → эффект (Effect).
 * Effect = getDiffImportance() до активации стимула минус getDiffImportance() при первом стимуле
 * после совершения действий. Период ожидания задаётся в пульсах (RESPONSE_WAITING_PULSES), не таймером.
 *
 * В кадре: perceptionNodeId, stimulusImageId, situationId, actionImageId, effect, responseStimulusImageId.
 * AnswerID + responseStimulusImageId — «учительское» правило: как отвечать на действие.
 * Открытый кадр: effect === null. После закрытия (closeLastFrame) effect всегда число в диапазоне [-10, +10].
 * Истечение ожидания без реакции: кадр не удаляется — в правило проставляются нули (ОД, ответный стимул), кадр закрывается, абстракции/ментальные правила обрабатываются как при любом закрытии.
 * Кадры с незаполненным effect в сохранённом снимке остаются на оптимизацию во время сна.
 *
 * ПРЕЕМСТВЕННОСТЬ ОЦЕНКИ ЭФФЕКТА (ЭП ↔ автоматизмы):
 * Один и тот же эффект вычисляется в ЭП (closeLastFrame: effect = clamp(diffAfter - diffBefore) в [-10,+10]) и передаётся
 * в штатный автоматизм ветки через addUsefulnessSample(automatizmId, last.effect). Оценка эффекта
 * единая: не дублируется, не расходится. Usefulness автоматизма усредняется по тем же значениям,
 * что и кадры ЭП (рефлекс или автоматизм сработал → кадр закрыт → один effect → одна запись в автоматизм).
 */
(function (global) {
  'use strict';

  /** Длительность периода ожидания в пульсах (вызов EpisodicMemory_onPulse из puls.js). */
  var response_waiting_period = 15;
  var pulsesWaiting = 0;
  /** Показать прогресс-бар 100% ещё N пульсов после закрытия кадра с ответом. */
  var barShowCompletePulsesLeft = 0;

  global.EpisodicMemory_waitingStartedAt = null;
  global.EpisodicMemory_testMode = false;
  global.EpisodicMemory_pulsesWaiting = 0;
  /** ID автоматизма ФО, чей effect будет усреднён при закрытии текущего открытого кадра (обнуляется после записи). */
  global.EpisodicMemory_pendingFeedbackAutomatizmId = 0;
  /** ID цикла осмысления, в котором был запущен автоматизм (период ожидания кадра ЭП). */
  global.EpisodicMemory_waitingAutomatismCycleId = 0;
  /** Effect только что закрытого кадра (для ур.2 при ответе оператора). */
  global.EpisodicMemory_lastClosedFrameEffect = null;

  if (!Array.isArray(global.Episodes)) {
    global.Episodes = [];
  }

  /**
   * Шкала эффекта закрытого кадра ЭП: всегда конечное число в [-10, +10].
   * Совпадает по смыслу с шкалой семантической значимости; выбросы от дельты diff отрезаются.
   */
  var EPISODE_EFFECT_MIN = -10;
  var EPISODE_EFFECT_MAX = 10;

  function clampEpisodeFrameEffect(n) {
    if (typeof n !== 'number' || isNaN(n)) return 0;
    if (n < EPISODE_EFFECT_MIN) return EPISODE_EFFECT_MIN;
    if (n > EPISODE_EFFECT_MAX) return EPISODE_EFFECT_MAX;
    return n;
  }

  /**
   * Проверить, идёт ли период ожидания ответа (незакрытый кадр, начатый в этой сессии, счётчик пульсов не достиг лимита).
   * Если последний кадр загружен из IndexedDB с effect === null, waitingStartedAt остаётся null — не считаем это активным ожиданием,
   * чтобы первый клик после обновления страницы создал новый кадр с прогресс-баром от 0%, а не закрыл старый с показом 100%.
   */
  function isInWaitingPeriod() {
    if (global.EpisodicMemory_waitingStartedAt == null) return false;
    var ep = global.Episodes;
    if (!ep.length) return false;
    var last = ep[ep.length - 1];
    if (last.effect !== null && last.effect !== undefined) return false;
    return pulsesWaiting < response_waiting_period;
  }

  /**
   * Закрыть последний кадр: вычислить effect и опционально записать ответный стимул.
   * ПРЕЕМСТВЕННОСТЬ: тот же effect записывается в кадр ЭП и передаётся в штатный автоматизм ветки
   * (addUsefulnessSample) — единая оценка эффекта для ЭП и автоматизма.
   *
   * Абстракции (`abstract_images.js`): в конце вызывается AbstractImages_cloneFromEpisodeFrame(last);
   * к этому моменту effect уже число в [-10,+10], а «дыры» в правиле при необходимости
   * заранее заполнены нулями — см. episodicFrameFillRuleZerosForIncompleteClose.
   *
   * @param {number|null} responseStimulusImageId — ID образа стимула в ответ (или null)
   */
  function closeLastFrame(responseStimulusImageId) {
    var ep = global.Episodes;
    if (!ep.length) return;
    var last = ep[ep.length - 1];
    if (last.effect !== null && last.effect !== undefined) return;
    var waitCycleId = typeof global.EpisodicMemory_waitingAutomatismCycleId === 'number'
      ? global.EpisodicMemory_waitingAutomatismCycleId : 0;
    // Effect закрытого кадра никогда не остаётся нечисловым: при нормальном diffBefore — clamp(Δdiff), иначе 0 (нейтрально).
    if (typeof global.getDiffImportance === 'function') {
      var diffAfter = global.getDiffImportance();
      var before = last.diffBefore;
      if (typeof before === 'number' && !isNaN(before)) {
        last.effect = clampEpisodeFrameEffect(diffAfter - before);
      } else {
        last.effect = 0;
      }
    } else {
      last.effect = 0;
    }
    global.EpisodicMemory_lastClosedFrameEffect = last.effect;
    if (responseStimulusImageId != null && waitCycleId && typeof global.Consciousness_getCycle === 'function') {
      var oc = global.Consciousness_getCycle(waitCycleId);
      if (oc && oc.awaitingAutomatismEpisodeFeedback) {
        oc.automatismOperatorAnswerReceived = true;
        oc.lastAutomatismEpisodeEffect = last.effect;
        if (typeof global.Info_setField === 'function') {
          global.Info_setField('isOperatorAnswerReceived', 1);
        } else if (global.InfoPicture) {
          global.InfoPicture.isOperatorAnswerReceived = 1;
        }
        if (typeof global.Consciousness_cycleAppendLog === 'function') {
          var pcEp = typeof global.cpuls === 'number' ? global.cpuls : 0;
          var effStr = String(last.effect);
          global.Consciousness_cycleAppendLog(oc, 'ЭП: закрыт кадр ответом оператора, automatismOperatorAnswerReceived=1, effect=' + effStr + '.', pcEp, { force: true });
        }
        // Цикл может быть уже expired / не главный (info_automatismWithEpisodeFeedbackAndExpireCycle) — runOneStep не вызовет ур.2.
        if (typeof global.conscience_level_2 === 'function') {
          var cxOr = oc.context || {};
          global.conscience_level_2({
            sourceCycle: oc,
            actualId: cxOr.actual_stimul_ID,
            branchId: cxOr.branchId,
            well: cxOr.well,
            emotionId: cxOr.emotionId,
            activityID: cxOr.activityID,
            _operatorAnswerCycleOnly: true
          });
        }
      }
    }
    if (responseStimulusImageId != null) {
      last.responseStimulusImageId = responseStimulusImageId;
      if (global.EpisodicMemory_testMode && typeof global.show_alert === 'function') {
        var respDetail = typeof global.PerceptionTree_getStimulusNamesForFinalImageId === 'function'
          ? global.PerceptionTree_getStimulusNamesForFinalImageId(responseStimulusImageId)
          : ('ID ' + responseStimulusImageId);
        global.show_alert('Стимул в периоде ожидания: ' + respDetail + '<br>Кадр эпизода завершен и записан.', 0);
      }
    }
    // Преемственность оценки эффекта: last.effect → автоматизм, запускавшийся в ФО (feedbackAutomatizmId на кадре), иначе штатный ветки.
    if (last.perceptionNodeId != null && typeof global.Automatizms_addUsefulnessSample === 'function') {
      var feedbackAutoId = (typeof last.feedbackAutomatizmId === 'number' && last.feedbackAutomatizmId > 0)
        ? last.feedbackAutomatizmId : 0;
      if (!feedbackAutoId && typeof global.EpisodicMemory_pendingFeedbackAutomatizmId === 'number' &&
          global.EpisodicMemory_pendingFeedbackAutomatizmId > 0) {
        feedbackAutoId = global.EpisodicMemory_pendingFeedbackAutomatizmId;
      }
      if (feedbackAutoId) {
        global.Automatizms_addUsefulnessSample(feedbackAutoId, last.effect);
      } else if (typeof global.Automatizms_getDefaultAutomatizmForBranch === 'function') {
        var staff = global.Automatizms_getDefaultAutomatizmForBranch(last.perceptionNodeId);
        if (staff && staff.id) {
          global.Automatizms_addUsefulnessSample(staff.id, last.effect);
        }
      }
      if (typeof global.PerceptionTree_updateActiveBranchView === 'function') {
        global.PerceptionTree_updateActiveBranchView();
      }
    }
    /**
     * Гештальт (ур.4): буфер резонанса и обновление записей по закрытому кадру.
     * Вызывается до сброса EpisodicMemory_pendingFeedbackAutomatizmId / waitingAutomatismCycleId,
     * чтобы передать ID автоматизма из ожидания.
     *
     * Сценарии:
     *   — Ответ оператора в периоде ожидания: waitCycleId > 0, effect число, в lastFrame — ветка/ситуация/ОД.
     *   — Закрытие без ответного стимула (responseStimulusImageId == null): эффект всё равно число; гештальт
     *     получает сигнал для возможной переинтерпретации (слабый/отрицательный effect).
     *   — Закрытие по новому стимулу ОР без ожидания автоматизма: waitCycleId === 0 — только push в резонанс
     *     при положительном effect (кросс-цикловая аналогия для других целей).
     */
    if (typeof global.Gestalt_onEpisodeFeedbackClosed === 'function') {
      try {
        var feedbackAutoIdForGestalt = 0;
        if (typeof last.feedbackAutomatizmId === 'number' && last.feedbackAutomatizmId > 0) {
          feedbackAutoIdForGestalt = last.feedbackAutomatizmId;
        } else if (typeof global.EpisodicMemory_pendingFeedbackAutomatizmId === 'number' &&
            global.EpisodicMemory_pendingFeedbackAutomatizmId > 0) {
          feedbackAutoIdForGestalt = global.EpisodicMemory_pendingFeedbackAutomatizmId;
        }
        global.Gestalt_onEpisodeFeedbackClosed({
          waitCycleId: waitCycleId,
          lastFrame: last,
          automatismId: feedbackAutoIdForGestalt,
          effect: last.effect
        });
      } catch (eGestalt) {}
    }
    global.EpisodicMemory_pendingFeedbackAutomatizmId = 0;
    global.EpisodicMemory_waitingAutomatismCycleId = 0;
    if (last.feedbackAutomatizmId !== undefined) delete last.feedbackAutomatizmId;
    if (waitCycleId && typeof global.Consciousness_getCycle === 'function') {
      var cWF = global.Consciousness_getCycle(waitCycleId);
      if (cWF) cWF.waitingFeedbackAutomatizmId = 0;
    }
    if (typeof global.InfoDet_updateActiveWaiting === 'function') {
      global.InfoDet_updateActiveWaiting(false);
    }

    if (waitCycleId && typeof global.Consciousness_getCycle === 'function') {
      var cEnd = global.Consciousness_getCycle(waitCycleId);
      // Без ответного стимула usefulness образа цели (cycle.goalImageId) не меняется — пайплайн ур.2 не вызывается.
      if (cEnd && cEnd.awaitingAutomatismEpisodeFeedback && !cEnd.automatismOperatorAnswerReceived) {
        cEnd.awaitingAutomatismEpisodeFeedback = false;
        cEnd.expired = true;
        cEnd.isMainCycle = false;
        if (typeof global.Consciousness_cycleAppendLog === 'function') {
          var pc = typeof global.cpuls === 'number' ? global.cpuls : 0;
          global.Consciousness_cycleAppendLog(cEnd, 'Период ожидания закрыт без ответного стимула — цикл expired.', pc, { force: true });
        }
        if (typeof global.Info_updateView === 'function') global.Info_updateView();
      }
    }

    // Абстракции (beast/_9_Conscience/abstract_images.js): при каждом закрытии кадра —
    // усиление/создание записи восприятия по (perceptionNodeId, stimulusImageId);
    // клон действия — только если actionImageId ненулевой (0 после «нуляции» дыры = нет клона ОД).
    var abstractClone = null;
    if (typeof global.AbstractImages_cloneFromEpisodeFrame === 'function') {
      try {
        abstractClone = global.AbstractImages_cloneFromEpisodeFrame(last);
      } catch (e) {
        if (typeof global.console !== 'undefined' && global.console.warn) {
          global.console.warn('AbstractImages_cloneFromEpisodeFrame', e);
        }
      }
    }
    // mental_automatism.js: усреднение ментального правила по abstractPerceptionId | abstractActionId и effect.
    if (abstractClone && typeof global.MentalRules_registerFromEpisodeFrame === 'function') {
      try {
        global.MentalRules_registerFromEpisodeFrame(last, abstractClone);
      } catch (e) {}
    }
    if (typeof global.AbstractImages_flushIndexedDB === 'function') {
      try {
        global.AbstractImages_flushIndexedDB();
      } catch (e) {}
    }
    last.diffBefore = undefined;
    pulsesWaiting = 0;
    global.EpisodicMemory_pulsesWaiting = 0;
    global.EpisodicMemory_waitingStartedAt = null;
    barShowCompletePulsesLeft = 2;
    global.EpisodicMemory_barShowCompletePulsesLeft = 2;
    epDirty = true;
  }

  /**
   * Сохранить эпизод (вызывается из ОР при новом наиболее актуальном стимуле).
   * Сначала при необходимости закрывается предыдущий кадр (effect + ответный стимул).
   * Затем добавляется «пустой» кадр; закрытие по таймауту выполняется по пульсам (EpisodicMemory_onPulse).
   *
   * @param {number} perceptionNodeId — ID конечной ветки дерева восприятия
   * @param {number} stimulusImageId — ID образа стимулов (FinalImage)
   */
  function save_episodic_memory(perceptionNodeId, stimulusImageId) {
    var ep = global.Episodes;
    var now = Date.now();
    barShowCompletePulsesLeft = 0;
    global.EpisodicMemory_barShowCompletePulsesLeft = 0;

    if (ep.length > 0) {
      var prev = ep[ep.length - 1];
      if (prev.effect === null || prev.effect === undefined) {
        // Новый победитель ОР закрывает предыдущий открытый кадр: ответный стимул = stimulusImageId.
        // Если в предыдущем кадре ещё нет ОД или ответа — в полях правила проставляются 0, затем closeLastFrame.
        episodicFrameFillRuleZerosForIncompleteClose(prev);
        closeLastFrame(stimulusImageId);
      }
    }

    var diffBefore = (typeof global.PrevDiffForEpisode === 'number' && !isNaN(global.PrevDiffForEpisode))
      ? global.PrevDiffForEpisode
      : (typeof global.getDiffImportance === 'function' ? global.getDiffImportance() : null);
    var situationId = 0;
    if (typeof global.Info_initIfNeeded === 'function') {
      try {
        var info = global.Info_initIfNeeded();
        if (info && typeof info.situationId === 'number') {
          situationId = info.situationId || 0;
        }
      } catch (e) {}
    }
    ep.push({
      perceptionNodeId: perceptionNodeId || 0,
      stimulusImageId: stimulusImageId || 0,
      situationId: situationId,
      actionImageId: null,
      diffBefore: diffBefore,
      effect: null,
      responseStimulusImageId: null,
      createdAt: now
    });
    epDirty = true;
    pulsesWaiting = 0;
    global.EpisodicMemory_pulsesWaiting = 0;
    global.EpisodicMemory_waitingStartedAt = now;
    barShowCompletePulsesLeft = 0;
    global.EpisodicMemory_barShowCompletePulsesLeft = 0;
  }

  /**
   * Вызывается из puls.js каждый пульс. Считает пульсы ожидания и при достижении лимита закрывает кадр без ответа.
   * Таймеры не используются — логика привязана к пульсу, при зависании вкладки не догоняет.
   *
   * Два исхода по истечении response_waiting_period:
   * — не было реакции (actionImageId пустой): нули в ОД и ответном стимуле, closeLastFrame (кадр в истории, абстракция восприятия);
   * — реакция была, ответа оператора нет: closeLastFrame(null) без удаления кадра.
   */
  function EpisodicMemory_onPulse() {
    if (barShowCompletePulsesLeft > 0) {
      barShowCompletePulsesLeft--;
      global.EpisodicMemory_barShowCompletePulsesLeft = barShowCompletePulsesLeft;
      if (barShowCompletePulsesLeft === 0) {
        global.EpisodicMemory_waitingStartedAt = null;
      }
      return;
    }
    var ep = global.Episodes;
    if (!ep.length) return;
    var last = ep[ep.length - 1];
    if (last.effect !== null && last.effect !== undefined) return;
    pulsesWaiting++;
    global.EpisodicMemory_pulsesWaiting = pulsesWaiting;
    if (pulsesWaiting < response_waiting_period) return;
    pulsesWaiting = 0;
    global.EpisodicMemory_pulsesWaiting = 0;
    global.EpisodicMemory_waitingStartedAt = null;
    // Таймаут без моторной реакции: раньше кадр удаляли (ep.pop); теперь — закрытие с нулевым ОД/ответом и полным пайплайном ЭП+абстракции.
    if (last.actionImageId === null || last.actionImageId === undefined) {
      if (global.EpisodicMemory_testMode && typeof global.show_alert === 'function' && last.stimulusImageId) {
        var stimDetail2 = typeof global.PerceptionTree_getStimulusNamesForFinalImageId === 'function'
          ? global.PerceptionTree_getStimulusNamesForFinalImageId(last.stimulusImageId)
          : ('ID ' + last.stimulusImageId);
        global.show_alert('На стимул (' + stimDetail2 + ') реакция не возникла.<br>Кадр ЭП закрывается: в правиле — нули за отсутствующие ОД и ответный стимул; абстракция восприятия усиливается по эффекту.', 0);
      }
      if (typeof global.InfoDet_updateOperatorIgnoring === 'function') {
        global.InfoDet_updateOperatorIgnoring(true);
      }
      if (typeof global.InfoDet_updateOperatorAnswer === 'function') {
        global.InfoDet_updateOperatorAnswer(false);
      }
      episodicFrameFillRuleZerosForIncompleteClose(last);
      closeLastFrame(null);
      if (typeof global.Consciousness_getMainCycle === 'function') {
        var mcNoAct = global.Consciousness_getMainCycle();
        if (mcNoAct) mcNoAct.waitingFeedbackAutomatizmId = 0;
      }
    } else {
      // Реакция была зафиксирована; ответный стимул не поступил — кадр закрывается с уже известным actionImageId.
      // Период ожидания закончился после реакции без ответного стимула —
      // оператор проигнорировал действие.
      if (typeof global.InfoDet_updateOperatorIgnoring === 'function') {
        global.InfoDet_updateOperatorIgnoring(true);
      }
      // Ответ оператора не получен в отведённый период.
      if (typeof global.InfoDet_updateOperatorAnswer === 'function') {
        global.InfoDet_updateOperatorAnswer(false);
      }
      closeLastFrame(null);
    }
  }

  /**
   * Дополнить последний кадр ID образа действия (AnswerID).
   * @param {number} actionImageId — ID образа действия (ОД)
   */
  function setLastFrameActionImageId(actionImageId) {
    var ep = global.Episodes;
    if (!ep.length || actionImageId == null) return;
    var last = ep[ep.length - 1];
    if (last.actionImageId !== null) return;
    last.actionImageId = actionImageId;
    epDirty = true;
  }

  /**
   * Зафиксировать ответное действие в последнем кадре по actionId.
   * Использует stimulusImageId уже записанный в кадре (не текущие стимулы),
   * чтобы кадр заполнялся и при отложенном рефлексе или когда стимулы уже сняты.
   * Вызывается из startAction после срабатывания реакции.
   *
   * @param {number} actionId — ID действия (BasicActions)
   */
  function setLastFrameActionFromActionId(actionId) {
    if (!actionId) return;
    var ep = global.Episodes;
    if (!ep.length) return;
    var last = ep[ep.length - 1];
    if (last.actionImageId !== null) return;
    var stimulusImageId = last.stimulusImageId;
    if (!stimulusImageId || typeof global.ActionImages_registerReaction !== 'function') return;
    var odId = 0;
    try {
      odId = global.ActionImages_registerReaction(stimulusImageId, actionId);
    } catch (e) {}
    if (odId) {
      last.actionImageId = odId;
      if (typeof global.EpisodicMemory_pendingFeedbackAutomatizmId === 'number' &&
          global.EpisodicMemory_pendingFeedbackAutomatizmId > 0) {
        last.feedbackAutomatizmId = global.EpisodicMemory_pendingFeedbackAutomatizmId;
        global.EpisodicMemory_pendingFeedbackAutomatizmId = 0;
      }
      epDirty = true;
      // Создание автоматизма по образу действия; effect будет записан при закрытии кадра (преемственность оценки).
      var pnId = last.perceptionNodeId;
      if (pnId && typeof global.Automatizms_createAutomatizm === 'function') {
        var aId = global.Automatizms_createAutomatizm(pnId, odId);
        if (aId) {
          var list = (typeof global.Automatizms_getAutomatizmsByBranchId === 'function')
            ? global.Automatizms_getAutomatizmsByBranchId(pnId) : [];
          if (list && list.length === 1 && typeof global.Automatizms_setDefaultAutomatizm === 'function') {
            global.Automatizms_setDefaultAutomatizm(aId);
          }
          if (typeof global.PerceptionTree_updateActiveBranchView === 'function') {
            global.PerceptionTree_updateActiveBranchView();
          }
        }
      }
      if (global.EpisodicMemory_testMode && typeof global.show_alert === 'function') {
        var stimDetail = typeof global.PerceptionTree_getStimulusNamesForFinalImageId === 'function'
          ? global.PerceptionTree_getStimulusNamesForFinalImageId(stimulusImageId)
          : ('ID ' + stimulusImageId);
        var actDetail = typeof global.PerceptionTree_getActionNamesForActionImage === 'function'
          ? global.PerceptionTree_getActionNamesForActionImage(odId)
          : ('ID ' + odId);
        global.show_alert('Активирован образ стимула: ' + stimDetail + '<br>Вызвавшая реакция: ' + actDetail + '<br>Период ожидания ответа начат.', 0);
      }
    }
  }

  var epDirty = false;
  var lastEpSavedSig = '';

  /**
   * Компактная сериализация кадра: perceptionNode|stimulus|situation|action|effect|response.
   * Пустой сегмент при разборе → null. Явная строка "0" для action/response сохраняется как число 0
   * (отсутствие ОД или ответа в правиле); см. parseIntOrNull — не использовать (x||null), иначе 0 теряется.
   */
  var EP_SEP = '|';

  /**
   * Перед closeLastFrame: заменить отсутствующие части «учительского» правила на 0, чтобы кадр оставался валидной записью.
   * Открытый кадр хранит null в незаполненных полях; 0 после закрытия значит «компонент не был» (таймаут, обрыв сценария).
   */
  function episodicFrameFillRuleZerosForIncompleteClose(frame) {
    if (!frame || frame.effect !== null && frame.effect !== undefined) return;
    if (frame.actionImageId === null || frame.actionImageId === undefined) {
      frame.actionImageId = 0;
    }
    if (frame.responseStimulusImageId === null || frame.responseStimulusImageId === undefined) {
      frame.responseStimulusImageId = 0;
    }
  }

  function serializeState() {
    var lines = [];
    if (Array.isArray(global.Episodes)) {
      for (var i = 0; i < global.Episodes.length; i++) {
        var f = global.Episodes[i];
        if (!f) continue;
        var a = f.perceptionNodeId != null ? String(f.perceptionNodeId) : '';
        var b = f.stimulusImageId != null ? String(f.stimulusImageId) : '';
        var c = f.situationId != null ? String(f.situationId) : '';
        var d = f.actionImageId != null ? String(f.actionImageId) : '';
        var e = (f.effect !== null && f.effect !== undefined) ? String(f.effect) : '';
        var r = f.responseStimulusImageId != null ? String(f.responseStimulusImageId) : '';
        lines.push(a + EP_SEP + b + EP_SEP + c + EP_SEP + d + EP_SEP + e + EP_SEP + r);
      }
    }
    return {
      compact: lines.join('\n'),
      response_waiting_period: response_waiting_period
    };
  }

  function parseCompactLine(line) {
    var parts = line.split(EP_SEP);
    // Старые строки могли быть в формате без situationId (5 полей).
    // Новый формат: perceptionNodeId|stimulusImageId|situationId|actionImageId|effect|responseStimulusImageId
    var p = parts[0];
    var s = parts[1];
    var sit = parts.length > 5 ? parts[2] : ''; // если старый формат, situationId считается 0
    var a = parts.length > 5 ? parts[3] : (parts[2] || '');
    var ef = parts.length > 5 ? parts[4] : (parts[3] || '');
    var r = parts.length > 5 ? parts[5] : (parts[4] || '');
    var eff = ef === '' ? null : parseFloat(ef);
    if (eff !== null && isNaN(eff)) eff = null;
    /** Пустая строка → null; иначе целое (в т.ч. 0). Нужно для кадров с нулевым ОД/ответом после таймаута. */
    function parseIntOrNull(str) {
      if (str === '') return null;
      var x = parseInt(str, 10);
      return isNaN(x) ? null : x;
    }
    return {
      perceptionNodeId: p === '' ? 0 : parseInt(p, 10) || 0,
      stimulusImageId: s === '' ? 0 : parseInt(s, 10) || 0,
      situationId: sit === '' ? 0 : (parseInt(sit, 10) || 0),
      actionImageId: parseIntOrNull(a),
      effect: eff,
      responseStimulusImageId: parseIntOrNull(r),
      createdAt: 0
    };
  }

  function applyState(state) {
    if (!state) return;
    var list = [];
    if (typeof state.compact === 'string' && state.compact.length >= 0) {
      var lineArr = state.compact.split('\n');
      for (var i = 0; i < lineArr.length; i++) {
        var line = lineArr[i].trim();
        if (!line) continue;
        list.push(parseCompactLine(line));
      }
    } else if (Array.isArray(state.episodes)) {
      for (var j = 0; j < state.episodes.length; j++) {
        var o = state.episodes[j];
        if (!o) continue;
        list.push({
          perceptionNodeId: o.perceptionNodeId != null ? o.perceptionNodeId : 0,
          stimulusImageId: o.stimulusImageId != null ? o.stimulusImageId : 0,
          situationId: o.situationId != null ? o.situationId : 0,
          actionImageId: o.actionImageId != null ? o.actionImageId : null,
          effect: o.effect != null ? o.effect : null,
          responseStimulusImageId: o.responseStimulusImageId != null ? o.responseStimulusImageId : null,
          createdAt: 0
        });
      }
    }
    global.Episodes = list;
    if (typeof state.response_waiting_period === 'number' && state.response_waiting_period > 0) {
      response_waiting_period = state.response_waiting_period;
    }
  }

  var epDb = null;
  var EP_DB_NAME = 'BEAST_Episodes_DB';
  var EP_DB_VERSION = 1;
  var EP_STORE = 'episodes_state';
  var EP_KEY = 'episodes_singleton';

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

  function saveEpStateToIndexedDB(state) {
    if (!global.IndexedDBModule) return;
    state = state || serializeState();
    function doPut(db) {
      return global.IndexedDBModule.put({
        db: db,
        storeName: EP_STORE,
        value: { id: EP_KEY, state: state, ts: Date.now() }
      }).catch(function () {});
    }
    if (epDb) {
      doPut(epDb);
      return;
    }
    openEpDb().then(doPut).catch(function () {});
  }

  function loadEpStateFromIndexedDB() {
    if (!global.IndexedDBModule) return;
    openEpDb()
      .then(function (db) {
        return global.IndexedDBModule.get({
          db: db,
          storeName: EP_STORE,
          key: EP_KEY
        });
      })
      .then(function (record) {
        if (record && record.state) {
          applyState(record.state);
          lastEpSavedSig = JSON.stringify(serializeState());
        }
      })
      .catch(function () {});
  }

  if (typeof global.registerIndexedDBSaver === 'function') {
    global.registerIndexedDBSaver(function () {
      if (!epDirty) return;
      epDirty = false;
      var state = serializeState();
      var sig = JSON.stringify(state);
      if (sig === lastEpSavedSig) return;
      lastEpSavedSig = sig;
      saveEpStateToIndexedDB(state);
    });
  }

  function clearAllEpisodicMemory() {
    global.Episodes = [];
    global.EpisodicMemory_pendingFeedbackAutomatizmId = 0;
    global.EpisodicMemory_waitingAutomatismCycleId = 0;
    global.EpisodicMemory_lastClosedFrameEffect = null;
    global.EpisodicMemory_waitingStartedAt = null;
    pulsesWaiting = 0;
    global.EpisodicMemory_pulsesWaiting = 0;
    barShowCompletePulsesLeft = 0;
    global.EpisodicMemory_barShowCompletePulsesLeft = 0;
    epDirty = true;
    lastEpSavedSig = '';
    saveEpStateToIndexedDB(serializeState());
  }

  loadEpStateFromIndexedDB();

  if (global.document) {
    function bindClearButton() {
      var btn = global.document.getElementById('btnClearEpisodicMemory');
      if (!btn || btn._epBound) return;
      btn._epBound = true;
      btn.addEventListener('click', function () {
        if (typeof global.show_confirm !== 'function') {
          if (global.confirm('Точно удалить всю эпизодическую память?')) clearAllEpisodicMemory();
          return;
        }
        global.show_confirm('Точно удалить всю эпизодическую память?', function (result) {
          if (result === 'OK') clearAllEpisodicMemory();
        });
      });
    }
    function bindTestModeButton() {
      var btn = global.document.getElementById('btnEpTestMode');
      if (!btn || btn._epTestBound) return;
      btn._epTestBound = true;
      btn.addEventListener('click', function () {
        global.EpisodicMemory_testMode = !global.EpisodicMemory_testMode;
        if (global.EpisodicMemory_testMode) {
          btn.classList.add('active');
        } else {
          btn.classList.remove('active');
        }
      });
    }
    function bindButtons() {
      bindClearButton();
      bindTestModeButton();
    }
    if (global.document.readyState === 'loading') {
      global.document.addEventListener('DOMContentLoaded', bindButtons);
    } else {
      bindButtons();
    }
  }

  global.save_episodic_memory = save_episodic_memory;
  /**
   * Выборка всех кадров эпизодической памяти (Правил) по ветке восприятия и ветке ситуации.
   * @param {number} perceptionNodeId - ID ветки дерева восприятия (обязателен, >0)
   * @param {number} situationId - ID образа ситуации (0 = не учитывать ситуацию)
   * @returns {Object[]} новый массив кадров (shallow copy)
   */
  function EpisodicMemory_getFramesByPerceptionAndSituation(perceptionNodeId, situationId) {
    perceptionNodeId = parseInt(perceptionNodeId, 10) || 0;
    situationId = parseInt(situationId, 10) || 0;
    if (!perceptionNodeId || !Array.isArray(global.Episodes)) return [];
    var out = [];
    for (var i = 0; i < global.Episodes.length; i++) {
      var f = global.Episodes[i];
      if (!f) continue;
      if ((f.perceptionNodeId || 0) !== perceptionNodeId) continue;
      if (situationId && (f.situationId || 0) !== situationId) continue;
      // shallow copy, чтобы не ломать оригинал
      out.push({
        perceptionNodeId: f.perceptionNodeId || 0,
        stimulusImageId: f.stimulusImageId || 0,
        situationId: f.situationId || 0,
        actionImageId: f.actionImageId != null ? f.actionImageId : null,
        effect: f.effect != null ? f.effect : null,
        responseStimulusImageId: f.responseStimulusImageId != null ? f.responseStimulusImageId : null,
        createdAt: f.createdAt || 0
      });
    }
    return out;
  }

  /**
   * Выборка всех кадров эпизодической памяти (Правил) по ID стимула (FinalImageId).
   * @param {number} stimulusImageId - ID образа стимула (FinalImage)
   * @returns {Object[]} новый массив кадров (shallow copy)
   */
  function EpisodicMemory_getFramesByStimulus(stimulusImageId) {
    stimulusImageId = parseInt(stimulusImageId, 10) || 0;
    if (!stimulusImageId || !Array.isArray(global.Episodes)) return [];
    var out = [];
    for (var i = 0; i < global.Episodes.length; i++) {
      var f = global.Episodes[i];
      if (!f) continue;
      if ((f.stimulusImageId || 0) !== stimulusImageId) continue;
      out.push({
        perceptionNodeId: f.perceptionNodeId || 0,
        stimulusImageId: f.stimulusImageId || 0,
        situationId: f.situationId || 0,
        actionImageId: f.actionImageId != null ? f.actionImageId : null,
        effect: f.effect != null ? f.effect : null,
        responseStimulusImageId: f.responseStimulusImageId != null ? f.responseStimulusImageId : null,
        createdAt: f.createdAt || 0
      });
    }
    return out;
  }

  /**
   * Все кадры ЭП, где ответный стимул оператора совпадает с заданным FinalImageId.
   *
   * Семантика поля responseStimulusImageId в кадре: «после реакции Beast оператор ответил образом с этим id».
   * Пассивное фантазирование (info_fantasizming) подбирает кадры, где этот id совпадает с текущим фокусом
   * цепочки, чтобы строить ассоциативное продолжение по уже пережитым учительским эпизодам.
   *
   * @param {number} responseStimulusImageId — обычно FinalImageId стимула-ответа; в цепочке фантазма на шаге N+1
   *   часто передаётся actionImageId с шага N (как следующий «якорь» поиска — см. fantasizming.js).
   * @returns {Object[]} копии полей + _episodeIndex (индекс в global.Episodes)
   */
  function EpisodicMemory_getFramesByResponseStimulus(responseStimulusImageId) {
    responseStimulusImageId = parseInt(responseStimulusImageId, 10) || 0;
    if (!responseStimulusImageId || !Array.isArray(global.Episodes)) return [];
    var out = [];
    for (var i = 0; i < global.Episodes.length; i++) {
      var f = global.Episodes[i];
      if (!f) continue;
      var rs = f.responseStimulusImageId != null ? parseInt(f.responseStimulusImageId, 10) : 0;
      if (rs !== responseStimulusImageId) continue;
      out.push({
        _episodeIndex: i,
        perceptionNodeId: f.perceptionNodeId || 0,
        stimulusImageId: f.stimulusImageId || 0,
        situationId: f.situationId || 0,
        actionImageId: f.actionImageId != null ? f.actionImageId : null,
        effect: f.effect != null ? f.effect : null,
        responseStimulusImageId: f.responseStimulusImageId != null ? f.responseStimulusImageId : null
      });
    }
    return out;
  }

  /**
   * Кадр с максимальным |effect| среди переданных (учительское/эпизодическое правило с ненулевым ОД).
   *
   * Игнорирует кадры без числового effect или без actionImageId. Используется там, где важна сильная
   * эмоционально-оценочная окраска эпизода независимо от знака эффекта (в т.ч. пассивное фантазирование).
   *
   * @param {Object[]} frames — обычно результат EpisodicMemory_getFramesByResponseStimulus
   * @returns {Object|null}
   */
  function EpisodicMemory_pickBestFrameByAbsEffect(frames) {
    if (!Array.isArray(frames) || !frames.length) return null;
    var best = null;
    var bestAbs = -1;
    for (var i = 0; i < frames.length; i++) {
      var f = frames[i];
      if (!f || typeof f.effect !== 'number' || isNaN(f.effect)) continue;
      var aid = f.actionImageId != null ? parseInt(f.actionImageId, 10) : 0;
      if (!aid) continue;
      var ab = Math.abs(f.effect);
      if (best === null || ab > bestAbs) {
        bestAbs = ab;
        best = f;
      }
    }
    return best;
  }

  /**
   * Из массива кадров выбрать одно с максимальным положительным effect и ненулевым actionImageId.
   */
  function EpisodicMemory_pickBestPositiveFrame(frames) {
    if (!Array.isArray(frames) || !frames.length) return null;
    var best = null;
    var bestEff = 0;
    for (var i = 0; i < frames.length; i++) {
      var f = frames[i];
      if (!f || typeof f.effect !== 'number' || f.effect <= 0 || f.actionImageId == null || !f.actionImageId) continue;
      if (best === null || f.effect > bestEff) {
        bestEff = f.effect;
        best = f;
      }
    }
    return best;
  }

  /**
   * Как pickBestPositiveFrame, но только кадры с заданным стимулом (актуальный FinalImageId осмысления).
   */
  function EpisodicMemory_pickBestPositiveFrameForStimulus(frames, stimulusImageId) {
    stimulusImageId = parseInt(stimulusImageId, 10) || 0;
    if (!stimulusImageId || !Array.isArray(frames) || !frames.length) return null;
    var best = null;
    var bestEff = 0;
    for (var i = 0; i < frames.length; i++) {
      var f = frames[i];
      if (!f || (f.stimulusImageId || 0) !== stimulusImageId) continue;
      if (typeof f.effect !== 'number' || f.effect <= 0 || f.actionImageId == null || !f.actionImageId) continue;
      if (best === null || f.effect > bestEff) {
        bestEff = f.effect;
        best = f;
      }
    }
    return best;
  }

  /**
   * Найти в ленте Episodes цепочку кадров, соответствующую стеку образов цели (порядок в массиве Episodes).
   * goalIdsBottomToTop — элементы Target_rule_stack: [дно … вершина], как последовательность ходов/правил
   * (аналог партии: ход → ответ партнёра → следующий ход зависит от цепочки предыдущих кадров ЭП).
   * Кадры между шагами могут пропускаться: ищется первое подходящее правило, затем после него — следующее и т.д.
   * @param {number[]} goalIdsBottomToTop
   * @returns {Object[]|null} кадры-оригиналы из Episodes (те же объекты, что в массиве памяти)
   */
  function EpisodicMemory_findChainForGoalStack(goalIdsBottomToTop) {
    if (!Array.isArray(goalIdsBottomToTop) || !goalIdsBottomToTop.length) return null;
    if (!Array.isArray(global.Episodes) || !global.Episodes.length) return null;

    function signatureForGoalId(gid) {
      var id = parseInt(gid, 10) || 0;
      if (!id || typeof global.Goals_getGoalById !== 'function') return null;
      var g = global.Goals_getGoalById(id);
      if (!g) return null;
      var r = g.episodicRule;
      if (r && typeof r === 'object') {
        return {
          perceptionNodeId: r.perceptionNodeId || g.perceptionNodeId || 0,
          situationId: r.situationId != null ? r.situationId : (g.situationId || 0),
          stimulusImageId: r.stimulusImageId || g.stimulusImageId || 0,
          actionImageId: r.actionImageId != null ? r.actionImageId : g.actionImageId
        };
      }
      return {
        perceptionNodeId: g.perceptionNodeId || 0,
        situationId: g.situationId != null ? g.situationId : 0,
        stimulusImageId: g.stimulusImageId || 0,
        actionImageId: g.actionImageId != null ? g.actionImageId : 0
      };
    }

    function matches(f, sig) {
      if (!f || !sig) return false;
      if ((f.perceptionNodeId || 0) !== (sig.perceptionNodeId || 0)) return false;
      if ((sig.situationId || 0) !== 0 && (f.situationId || 0) !== (sig.situationId || 0)) return false;
      if ((sig.stimulusImageId || 0) !== 0 && (f.stimulusImageId || 0) !== (sig.stimulusImageId || 0)) return false;
      if ((sig.actionImageId || 0) !== 0 && (f.actionImageId || 0) !== (sig.actionImageId || 0)) return false;
      if (typeof f.effect !== 'number' || f.effect <= 0) return false;
      return true;
    }

    var sigs = [];
    for (var s = 0; s < goalIdsBottomToTop.length; s++) {
      var sg = signatureForGoalId(goalIdsBottomToTop[s]);
      if (!sg) return null;
      sigs.push(sg);
    }

    var ep = global.Episodes;
    var bestChain = null;
    var bestSum = -Infinity;

    for (var start = 0; start < ep.length; start++) {
      var idx = start;
      var matched = [];
      for (var j = 0; j < sigs.length; j++) {
        var found = false;
        while (idx < ep.length) {
          var fr = ep[idx];
          idx += 1;
          if (matches(fr, sigs[j])) {
            matched.push(fr);
            found = true;
            break;
          }
        }
        if (!found) {
          matched = null;
          break;
        }
      }
      if (matched && matched.length === sigs.length) {
        var sum = 0;
        for (var m = 0; m < matched.length; m++) sum += matched[m].effect || 0;
        if (sum > bestSum) {
          bestSum = sum;
          bestChain = matched.slice();
        }
      }
    }

    return bestChain;
  }

  /** Минимальное число подряд кадров с reaction (actionImageId) в суффиксе Episodes для приоритета цепочки. */
  var EPISODIC_CHAIN_PRIORITY_MIN_FRAMES = 2;
  global.EpisodicMemory_chainPriorityMinFrames = EPISODIC_CHAIN_PRIORITY_MIN_FRAMES;

  /**
   * Множество FinalImageId стимулов, входящих в «хвостовую» цепочку ЭП: подряд с конца массива кадры,
   * у каждого записана реакция (actionImageId). Учитываются stimulusImageId и responseStimulusImageId кадров.
   * Цепочка из одного кадра не считается — нужно ≥ EpisodicMemory_chainPriorityMinFrames (по умолчанию 2).
   * @returns {Object|null} объект-словарь { "id": true } или null, если цепочки нет
   */
  function EpisodicMemory_getChainPriorityStimulusSet() {
    var ep = global.Episodes;
    var minN = typeof global.EpisodicMemory_chainPriorityMinFrames === 'number' && global.EpisodicMemory_chainPriorityMinFrames >= 2
      ? global.EpisodicMemory_chainPriorityMinFrames
      : EPISODIC_CHAIN_PRIORITY_MIN_FRAMES;
    if (!Array.isArray(ep) || ep.length < minN) return null;
    var run = 0;
    var i = ep.length - 1;
    while (i >= 0) {
      var f = ep[i];
      if (!f || f.actionImageId == null || !f.actionImageId) break;
      run++;
      i--;
    }
    if (run < minN) return null;
    var set = Object.create(null);
    for (var j = ep.length - run; j < ep.length; j++) {
      var fr = ep[j];
      if (!fr) continue;
      if (fr.stimulusImageId) set[String(fr.stimulusImageId)] = true;
      if (fr.responseStimulusImageId) set[String(fr.responseStimulusImageId)] = true;
    }
    return set;
  }

  /**
   * Стимул (FinalImageId) принадлежит активной цепочке кадров ЭП с реакциями — для приоритета в ОР/ФО.
   */
  function EpisodicMemory_isStimulusEpisodicChainPriority(stimulusFinalImageId) {
    var id = parseInt(stimulusFinalImageId, 10) || 0;
    if (!id) return false;
    var s = EpisodicMemory_getChainPriorityStimulusSet();
    return !!(s && s[String(id)]);
  }

  global.EpisodicMemory_setLastFrameActionImageId = setLastFrameActionImageId;
  global.EpisodicMemory_setLastFrameActionFromActionId = setLastFrameActionFromActionId;
  global.EpisodicMemory_clearAll = clearAllEpisodicMemory;
  global.EpisodicMemory_serializeState = serializeState;
  global.EpisodicMemory_applyState = applyState;
  global.EpisodicMemory_response_waiting_period = response_waiting_period;
  global.EpisodicMemory_isInWaitingPeriod = isInWaitingPeriod;
  global.EpisodicMemory_closeLastFrameWithResponse = closeLastFrame;
  global.EpisodicMemory_onPulse = EpisodicMemory_onPulse;
  global.EpisodicMemory_barShowCompletePulsesLeft = 0;
  global.EpisodicMemory_getFramesByPerceptionAndSituation = EpisodicMemory_getFramesByPerceptionAndSituation;
  global.EpisodicMemory_getFramesByStimulus = EpisodicMemory_getFramesByStimulus;
  global.EpisodicMemory_getFramesByResponseStimulus = EpisodicMemory_getFramesByResponseStimulus;
  global.EpisodicMemory_pickBestFrameByAbsEffect = EpisodicMemory_pickBestFrameByAbsEffect;
  global.EpisodicMemory_pickBestPositiveFrame = EpisodicMemory_pickBestPositiveFrame;
  global.EpisodicMemory_pickBestPositiveFrameForStimulus = EpisodicMemory_pickBestPositiveFrameForStimulus;
  global.EpisodicMemory_findChainForGoalStack = EpisodicMemory_findChainForGoalStack;
  global.EpisodicMemory_getChainPriorityStimulusSet = EpisodicMemory_getChainPriorityStimulusSet;
  global.EpisodicMemory_isStimulusEpisodicChainPriority = EpisodicMemory_isStimulusEpisodicChainPriority;

})(window);

════════════════════════════════════════════════════════════

/BEAST/beast/_10_actions_image/about_actions_image.md
════════════════════════════════════════════════════════════
# Образы действия (_10_actions_image)

## Назначение

Модуль реализует **образы действия (ОД)** — по аналогии с образами восприятия (FinalImage). Каждый ОД связывает образ восприятия с одним или несколькими действиями (параллельно или последовательно). ОД используются эпизодической памятью: в кадре хранится actionImageId — ID образа действия, а не сырой Id из BasicActions.

## Структура ОД

- **id** — уникальный идентификатор.
- **finalImageId** — образ восприятия (FinalImage), к которому привязан ОД (0 если не задан).
- **actionIds** — массив ID действий (BasicActions): одно действие или несколько параллельных.
- **odSequence** — строка ID других ОД через запятую для последовательного выполнения (опционально).

## Формирование

Каждый конечный образ восприятия (FinalImage), по которому произошла хотя бы одна реакция, порождает или дополняет ОД. При нескольких реакциях от разных стимулов в одном образе в ОД записывается массив этих действий. Регистрация выполняется из genetic_reflexes_engine (ActionsImage_registerReaction) при startAction; для эпизодической памяти последний кадр заполняется через EpisodicMemory_setLastFrameActionFromActionId с использованием stimulusImageId уже записанного в кадре.

## Персистенция

Массив ActionImages сохраняется в IndexedDB (по пульсу при изменении) и в файл при «Сохранить» / загружается при «Загрузить» (saving_ui.js).

## Файлы

- **actions_image.js** — массив ActionImages, getOrCreateActionImageId, registerReaction, сериализация/восстановление, регистрация в системе сохранения.

════════════════════════════════════════════════════════════

/BEAST/beast/_10_actions_image/actions_image.js
════════════════════════════════════════════════════════════
/**
 * Образы действия (ОД). По аналогии с _3_perception и FinalImage.
 *
 * Массив известных образов действий (ActionImages), сохраняется в IndexedDB и на диске.
 *
 * Структура одного ОД:
 *   - id: number
 *   - finalImageId: number — образ восприятия (0 если не привязан)
 *   - actionIds: number[] — массив ID действий (одно или несколько параллельно)
 *   - odSequence: string — строка ID других ОД через запятую для последовательности (опционально)
 *
 * Формирование: каждый конечный образ восприятия порождает ОД, если хотя бы один
 * из составляющих образ восприятия имеет реакцию. При нескольких реакциях от разных
 * стимулов в ОД прописывается массив этих действий.
 */
(function (global) {
  'use strict';

  if (!Array.isArray(global.ActionImages)) {
    global.ActionImages = [];
  }

  var nextOdId = 1;

  function normalizeActionIds(ids) {
    if (!Array.isArray(ids)) return [];
    var arr = [];
    for (var i = 0; i < ids.length; i++) {
      var v = parseInt(ids[i], 10);
      if (!isNaN(v) && v > 0 && arr.indexOf(v) === -1) {
        arr.push(v);
      }
    }
    arr.sort(function (a, b) { return a - b; });
    return arr;
  }

  function arraysEqual(a, b) {
    if (a.length !== b.length) return false;
    for (var i = 0; i < a.length; i++) {
      if (a[i] !== b[i]) return false;
    }
    return true;
  }

  function findActionImageByFinalImageId(finalImageId) {
    if (!finalImageId) return null;
    for (var i = 0; i < global.ActionImages.length; i++) {
      if (global.ActionImages[i].finalImageId === finalImageId) {
        return global.ActionImages[i];
      }
    }
    return null;
  }

  function getNextActionImageId() {
    var maxId = 0;
    for (var i = 0; i < global.ActionImages.length; i++) {
      if (global.ActionImages[i].id > maxId) {
        maxId = global.ActionImages[i].id;
      }
    }
    return maxId + 1;
  }

  /**
   * Найти ОД по finalImageId и набору actionIds (и опционально odSequence).
   */
  function findActionImageId(finalImageId, actionIds, odSequence) {
    var normIds = normalizeActionIds(actionIds);
    var seq = (typeof odSequence === 'string') ? odSequence.trim() : '';
    for (var i = 0; i < global.ActionImages.length; i++) {
      var od = global.ActionImages[i];
      if (od.finalImageId !== finalImageId) continue;
      if (!arraysEqual(od.actionIds || [], normIds)) continue;
      if ((od.odSequence || '') !== seq) continue;
      return od.id;
    }
    return 0;
  }

  /**
   * Получить или создать ОД по параметрам.
   * @param {number} finalImageId
   * @param {number[]} actionIds
   * @param {string} [odSequence] — строка ID других ОД через запятую
   * @returns {number} id образа действия
   */
  function getOrCreateActionImageId(finalImageId, actionIds, odSequence) {
    var normIds = normalizeActionIds(actionIds || []);
    var seq = (typeof odSequence === 'string') ? odSequence.trim() : '';
    var existing = findActionImageId(finalImageId, normIds, seq);
    if (existing) return existing;
    var newId = getNextActionImageId();
    global.ActionImages.push({
      id: newId,
      finalImageId: finalImageId || 0,
      actionIds: normIds.slice(0),
      odSequence: seq
    });
    return newId;
  }

  /**
   * Получить или создать ОД для образа восприятия (finalImageId) и добавить в него действие.
   * Вызывается при срабатывании реакции: образ восприятия порождает/дополняет ОД.
   */
  function registerReaction(finalImageId, actionId) {
    if (!finalImageId || !actionId) return 0;
    var od = findActionImageByFinalImageId(finalImageId);
    if (od) {
      var ids = od.actionIds || [];
      if (ids.indexOf(actionId) === -1) {
        ids.push(actionId);
        od.actionIds = normalizeActionIds(ids);
        odDirty = true;
      }
      return od.id;
    }
    var newId = getOrCreateActionImageId(finalImageId, [actionId], '');
    odDirty = true;
    return newId;
  }

  function getActionImage(odId) {
    for (var i = 0; i < global.ActionImages.length; i++) {
      if (global.ActionImages[i].id === odId) return global.ActionImages[i];
    }
    return null;
  }

  // ---------- IndexedDB и сохранение на диск ----------

  var odDb = null;
  var OD_DB_NAME = 'BEAST_ActionImages_DB';
  var OD_DB_VERSION = 1;
  var OD_STORE = 'action_images_state';
  var OD_KEY = 'action_images_singleton';
  var odDirty = false;
  var lastOdSavedSig = '';

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

  function serializeState() {
    var list = [];
    if (Array.isArray(global.ActionImages)) {
      for (var i = 0; i < global.ActionImages.length; i++) {
        var od = global.ActionImages[i];
        if (!od) continue;
        list.push({
          id: od.id,
          finalImageId: od.finalImageId || 0,
          actionIds: Array.isArray(od.actionIds) ? od.actionIds.slice(0) : [],
          odSequence: (typeof od.odSequence === 'string') ? od.odSequence : ''
        });
      }
    }
    return { actionImages: list };
  }

  function applyState(state) {
    if (!state || !Array.isArray(state.actionImages)) return;
    global.ActionImages = [];
    for (var i = 0; i < state.actionImages.length; i++) {
      var o = state.actionImages[i];
      if (!o) continue;
      global.ActionImages.push({
        id: o.id,
        finalImageId: o.finalImageId || 0,
        actionIds: Array.isArray(o.actionIds) ? o.actionIds.slice(0) : [],
        odSequence: (typeof o.odSequence === 'string') ? o.odSequence : ''
      });
    }
    if (typeof global.Goals_ensureDefaultCryGoal === 'function') {
      global.Goals_ensureDefaultCryGoal();
    }
  }

  function saveOdStateToIndexedDB(state) {
    if (!global.IndexedDBModule) return;
    state = state || serializeState();
    function doPut(db) {
      return global.IndexedDBModule.put({
        db: db,
        storeName: OD_STORE,
        value: { id: OD_KEY, state: state, ts: Date.now() }
      }).catch(function () {});
    }
    if (odDb) {
      doPut(odDb);
      return;
    }
    openOdDb().then(doPut).catch(function () {});
  }

  function loadOdStateFromIndexedDB() {
    if (!global.IndexedDBModule) return;
    openOdDb()
      .then(function (db) {
        return global.IndexedDBModule.get({
          db: db,
          storeName: OD_STORE,
          key: OD_KEY
        });
      })
      .then(function (record) {
        if (record && record.state) {
          applyState(record.state);
          lastOdSavedSig = JSON.stringify(serializeState());
        }
      })
      .catch(function () {});
  }

  if (typeof global.registerIndexedDBSaver === 'function') {
    global.registerIndexedDBSaver(function () {
      if (!odDirty) return;
      odDirty = false;
      var state = serializeState();
      var sig = JSON.stringify(state);
      if (sig === lastOdSavedSig) return;
      lastOdSavedSig = sig;
      saveOdStateToIndexedDB(state);
    });
  }

  loadOdStateFromIndexedDB();

  // ---------- Экспорт ----------

  global.ActionImages_findActionImageId = findActionImageId;
  global.ActionImages_getOrCreateActionImageId = getOrCreateActionImageId;
  global.ActionImages_registerReaction = registerReaction;
  global.ActionImages_getActionImage = getActionImage;
  global.ActionImages_serializeState = serializeState;
  global.ActionImages_applyState = applyState;

  // Умолчательная цель «Плачь» в goals.js зависит от ОД (BasicActions Id=1); goals.js грузится раньше этого файла.
  if (typeof global.Goals_ensureDefaultCryGoal === 'function') {
    global.Goals_ensureDefaultCryGoal();
  }

})(window);

════════════════════════════════════════════════════════════

/BEAST/beast/_11_Automatizms/about_automatizms.md
════════════════════════════════════════════════════════════
# Автоматизмы (_11_Automatizms)

## Назначение

Модуль реализует **автоматизмы** — правила, привязанные к узлу ветки дерева восприятия (BranchID) и к образу действия (ОД). Активация или блокировка автоматизма выполняется в процессе осознания (в `stimuls_consciousness(actual_stimul_ID)`) при активации соответствующего узла дерева.

## Структура автоматизма

| Поле | Тип | Описание |
|------|-----|----------|
| **id** | number | Уникальный ID, автовозрастает при создании. |
| **branchId** | number | ID узла ветки дерева восприятия. |
| **usefulness** | number | Польза/вред: &lt; 0 вред, &gt; 0 польза; уточняется в период ожидания после активности. |
| **actionsImageId** | number | ID образа действия (из _10_actions_image). Может содержать массив действий и/или строку последовательности ОД. |
| **count** | number | Надёжность — счётчик запусков для усреднения usefulness. |
| **isDefault** | boolean | Если true — штатный автоматизм для данной ветки (главный среди привязанных к ветке). |

К одному узлу дерева может быть привязано неограниченное число автоматизмов, но только один из них имеет **isDefault=true**.

**Независимость от первичного рефлекса:** автоматизм, вызываемый в функции осознания, может быть произвольно модифицирован (изменён его образ действия, назначен другой штатным и т.д.). Связи между первичным рефлексом и вторичным автоматизмом нет: автоматизм — самостоятельное правило по ветке дерева; его действие может не совпадать с тем, что изначально зафиксировал рефлекс.

## API

- **Automatizms_getAutomatizmsByBranchId(branchId)** — все автоматизмы ветки (быстрый поиск по индексу).
- **Automatizms_getDefaultAutomatizmForBranch(branchId)** — штатный автоматизм для ветки (isDefault=true).
- **Automatizms_createAutomatizm(branchId, actionsImageId)** — создать автоматизм. Не создаёт дубль по (branchId, actionsImageId); при дубликате возвращает id существующего. Требуется существующий actionsImageId в ActionImages. Возвращает 0 при недопустимых аргументах.
- **Automatizms_setDefaultAutomatizm(automatizmId)** — назначить автоматизм штатным; у остальных автоматизмов этой ветки isDefault сбрасывается в false.
- **Automatizms_addUsefulnessSample(automatizmId, effect)** — обновить usefulness и count (усреднение). Значение **effect** приходит из ЭП (см. «Преемственность оценки эффекта» ниже).
- **Automatizms_serializeState** / **Automatizms_applyState** — сериализация и восстановление для файла и IndexedDB.

## Персистенция

- **IndexedDB:** сохранение по пульсу только при изменении данных (флаг automatismsDirty + подпись lastAutomatizmsSavedSig). База `BEAST_Automatizms_DB`, хранилище `automatizms_state`.
- **Файл:** ключ `AutomatizmsState` в реестре `BEAST.persistentModules` (saving_ui.js).

## Преемственность оценки эффекта (ЭП → автоматизм)

Оценка эффекта для автоматизма **совпадает с оценкой в ЭП**: одно и то же значение эффекта вычисляется в ЭП при закрытии кадра (`effect = diffAfter − diffBefore`) и передаётся в штатный автоматизм ветки через **addUsefulnessSample(automatizmId, effect)**. Не дублируется, не расходится — единый источник оценки; Usefulness автоматизма усредняется по тем же величинам, что и кадры эпизодов. Подробнее см. **about_episodic_memory.md** (раздел «Преемственность оценки эффекта»).

## Связь с другими модулями

- **Дерево восприятия** (_7_perception_tree) — branchId есть id узла дерева.
- **Образы действия** (_10_actions_image) — actionsImageId ссылается на ОД; при создании автоматизма проверяется существование ОД.
- **Процесс осознания** (_9_Conscience) — в `stimuls_consciousness` по контексту ОР (branchId, novelty, significance и др.) проверяется наличие штатного автоматизма для узла; при наличии — запускается образ действия (runActionImage). **При запуске автоматизма** вызывается removeDelayedReflexesForBranch(branchId): из очереди отложенных рефлексов удаляются все записи по этой ветке (без сравнения действий). Если автоматизм отклонён — удаление не вызывается, отложенный рефлекс выполнится по истечении задержки. Место для будущей оценки приемлемости запуска по новизне условий.
- **Эпизодическая память** (_10_Episodic_memory) — при срабатывании рефлекса создаётся автоматизм (createAutomatizm, setDefault при первом); при закрытии кадра тот же эффект из ЭП передаётся в штатный автоматизм (addUsefulnessSample). Преемственность оценки эффекта — см. выше.

## Файлы

- **automatizms.js** — массив Automatizms, индекс byBranchId, создание/штатность/усреднение usefulness, IndexedDB и экспорт API.

════════════════════════════════════════════════════════════

/BEAST/beast/_11_Automatizms/automatizms.js
════════════════════════════════════════════════════════════
/**
 * Автоматизмы (_11_Automatizms).
 *
 * Автоматизм привязан к узлу ветки дерева восприятия (BranchID) и к образу действия (ActionsImageID).
 * Активируется или блокируется в процессе осознания (stimuls_consciousness) при активации узла.
 *
 * Структура: id, branchId, usefulness, actionsImageId, count, isDefault.
 * - Usefulness: < 0 вред, > 0 польза (уточняется в период ожидания после активности).
 * - Count: число запусков для усреднения Usefulness.
 * - isDefault: штатный автоматизм для ветки (только один на ветку).
 *
 * Уникальность по паре (branchId, actionsImageId). Сохранение в IndexedDB и в файл.
 *
 * ПРЕЕМСТВЕННОСТЬ ОЦЕНКИ ЭФФЕКТА (ЭП → автоматизм):
 * Значение effect для addUsefulnessSample приходит из ЭП (closeLastFrame: effect = diffAfter - diffBefore).
 * Одна и та же оценка эффекта записывается в кадр ЭП и передаётся в штатный автоматизм ветки —
 * без дублирования и расхождений; Usefulness усредняется по тем же величинам, что и кадры эпизодов.
 */
(function (global) {
  'use strict';

  if (!Array.isArray(global.Automatizms)) {
    global.Automatizms = [];
  }

  var nextId = 1;
  /** Индекс по branchId → массив автоматизмов этой ветки (для быстрого поиска и штатного). */
  var byBranchId = Object.create(null);
  var automatismsDirty = false;
  var lastAutomatizmsSavedSig = '';

  function rebuildIndex() {
    byBranchId = Object.create(null);
    var arr = global.Automatizms;
    if (!Array.isArray(arr)) return;
    for (var i = 0; i < arr.length; i++) {
      var a = arr[i];
      if (!a || a.branchId == null) continue;
      var key = String(a.branchId);
      if (!byBranchId[key]) byBranchId[key] = [];
      byBranchId[key].push(a);
    }
  }

  function getNextId() {
    var max = 0;
    var arr = global.Automatizms;
    for (var i = 0; i < (arr && arr.length) || 0; i++) {
      if (arr[i] && arr[i].id > max) max = arr[i].id;
    }
    return max + 1;
  }

  /**
   * Все автоматизмы, привязанные к ветке branchId.
   * @param {number} branchId — ID узла дерева восприятия
   * @returns {Object[]}
   */
  function getAutomatizmsByBranchId(branchId) {
    if (branchId == null || branchId === 0) return [];
    var list = byBranchId[String(branchId)];
    return Array.isArray(list) ? list.slice(0) : [];
  }

  /**
   * Штатный автоматизм для ветки (isDefault === true). Не более одного на ветку.
   * @param {number} branchId
   * @returns {Object|null}
   */
  function getDefaultAutomatizmForBranch(branchId) {
    var list = getAutomatizmsByBranchId(branchId);
    for (var i = 0; i < list.length; i++) {
      if (list[i].isDefault) return list[i];
    }
    return null;
  }

  /**
   * Найти автоматизм по паре (branchId, actionsImageId).
   * @returns {Object|null}
   */
  function findAutomatizmByBranchAndAction(branchId, actionsImageId) {
    var list = byBranchId[String(branchId)];
    if (!Array.isArray(list)) return null;
    for (var i = 0; i < list.length; i++) {
      if (list[i].actionsImageId === actionsImageId) return list[i];
    }
    return null;
  }

  /**
   * Создать автоматизм. Не создаёт дубль по (branchId, actionsImageId) — возвращает id существующего.
   * Требуется реальный actionsImageId (существующий в ActionImages).
   * @param {number} branchId — ID узла дерева восприятия
   * @param {number} actionsImageId — ID образа действия (ОД)
   * @returns {number} id созданного или существующего автоматизма, 0 если actionsImageId недопустим
   */
  function createAutomatizm(branchId, actionsImageId) {
    if (branchId == null || branchId === 0) return 0;
    var odId = parseInt(actionsImageId, 10);
    if (isNaN(odId) || odId <= 0) return 0;
    if (typeof global.ActionImages_getActionImage === 'function' && !global.ActionImages_getActionImage(odId)) {
      return 0;
    }
    var existing = findAutomatizmByBranchAndAction(branchId, odId);
    if (existing) return existing.id;

    var id = getNextId();
    var obj = {
      id: id,
      branchId: branchId,
      usefulness: 0,
      actionsImageId: odId,
      count: 0,
      isDefault: false
    };
    global.Automatizms.push(obj);
    var key = String(branchId);
    if (!byBranchId[key]) byBranchId[key] = [];
    byBranchId[key].push(obj);
    automatismsDirty = true;
    return id;
  }

  /**
   * Назначить автоматизм штатным для своей ветки. Остальные автоматизмы этой ветки получают isDefault=false.
   * @param {number} automatizmId
   */
  function setDefaultAutomatizm(automatizmId) {
    var id = parseInt(automatizmId, 10);
    if (isNaN(id) || id <= 0) return;
    var arr = global.Automatizms;
    var target = null;
    for (var i = 0; i < (arr && arr.length) || 0; i++) {
      if (arr[i] && arr[i].id === id) {
        target = arr[i];
        break;
      }
    }
    if (!target) return;
    var branchId = target.branchId;
    var list = byBranchId[String(branchId)];
    if (Array.isArray(list)) {
      for (var j = 0; j < list.length; j++) {
        list[j].isDefault = false;
      }
    }
    target.isDefault = true;
    automatismsDirty = true;
  }

  /**
   * Обновить usefulness и count после периода ожидания (по эффекту из ЭП).
   * ПРЕЕМСТВЕННОСТЬ: effect — тот же, что записан в кадре ЭП (closeLastFrame), единая оценка.
   * @param {number} automatizmId
   * @param {number} effect — значение эффекта из ЭП (diffAfter - diffBefore) для усреднения usefulness
   */
  function addUsefulnessSample(automatizmId, effect) {
    var id = parseInt(automatizmId, 10);
    if (isNaN(id) || id <= 0) return;
    var arr = global.Automatizms;
    for (var i = 0; i < (arr && arr.length) || 0; i++) {
      if (arr[i] && arr[i].id === id) {
        var a = arr[i];
        var n = (a.count || 0) + 1;
        var u = a.usefulness != null ? a.usefulness : 0;
        a.usefulness = (u * (n - 1) + (typeof effect === 'number' ? effect : 0)) / n;
        a.count = n;
        automatismsDirty = true;
        return;
      }
    }
  }

  function serializeState() {
    var arr = global.Automatizms;
    if (!Array.isArray(arr)) return { automatizms: [], nextId: 1 };
    var list = [];
    for (var i = 0; i < arr.length; i++) {
      var a = arr[i];
      if (!a) continue;
      list.push({
        id: a.id,
        branchId: a.branchId,
        usefulness: a.usefulness != null ? a.usefulness : 0,
        actionsImageId: a.actionsImageId,
        count: a.count != null ? a.count : 0,
        isDefault: !!a.isDefault
      });
    }
    return { automatizms: list, nextId: getNextId() };
  }

  function applyState(state) {
    if (!state || !Array.isArray(state.automatizms)) {
      global.Automatizms = [];
      rebuildIndex();
      return;
    }
    global.Automatizms = [];
    var list = state.automatizms;
    for (var i = 0; i < list.length; i++) {
      var s = list[i];
      if (!s || s.id == null) continue;
      global.Automatizms.push({
        id: s.id,
        branchId: s.branchId != null ? s.branchId : 0,
        usefulness: typeof s.usefulness === 'number' ? s.usefulness : 0,
        actionsImageId: s.actionsImageId != null ? s.actionsImageId : 0,
        count: (s.count != null && s.count >= 0) ? s.count : 0,
        isDefault: !!s.isDefault
      });
    }
    if (typeof state.nextId === 'number' && state.nextId > 0) {
      nextId = state.nextId;
    } else {
      nextId = getNextId();
    }
    rebuildIndex();
    lastAutomatizmsSavedSig = JSON.stringify(serializeState());
    automatismsDirty = false;
  }

  var db = null;
  var DB_NAME = 'BEAST_Automatizms_DB';
  var DB_VERSION = 1;
  var STORE = 'automatizms_state';
  var KEY = 'automatizms_singleton';

  function openDb() {
    if (!global.indexedDB || !global.IndexedDBModule) return Promise.reject(new Error('IndexedDB не доступен'));
    if (db) return Promise.resolve(db);
    return global.IndexedDBModule.openDatabase({
      dbName: DB_NAME,
      version: DB_VERSION,
      stores: [{ name: STORE, options: { keyPath: 'id' } }]
    }).then(function (d) {
      db = d;
      return d;
    });
  }

  function saveToIndexedDB(state) {
    if (!global.IndexedDBModule) return;
    state = state || serializeState();
    function put(db) {
      return global.IndexedDBModule.put({
        db: db,
        storeName: STORE,
        value: { id: KEY, state: state, ts: Date.now() }
      }).catch(function () {});
    }
    if (db) {
      put(db);
      return;
    }
    openDb().then(put).catch(function () {});
  }

  function loadFromIndexedDB() {
    if (!global.IndexedDBModule) return;
    openDb()
      .then(function (d) {
        return global.IndexedDBModule.get({ db: d, storeName: STORE, key: KEY });
      })
      .then(function (record) {
        if (record && record.state) {
          applyState(record.state);
          lastAutomatizmsSavedSig = JSON.stringify(serializeState());
          if (typeof global.Info_updateView === 'function') global.Info_updateView();
        }
      })
      .catch(function () {});
  }

  if (typeof global.registerIndexedDBSaver === 'function') {
    global.registerIndexedDBSaver(function () {
      if (!automatismsDirty) return;
      automatismsDirty = false;
      var state = serializeState();
      var sig = JSON.stringify(state);
      if (sig === lastAutomatizmsSavedSig) return;
      lastAutomatizmsSavedSig = sig;
      saveToIndexedDB(state);
    });
  }

  function clearAllAutomatizms() {
    global.Automatizms = [];
    rebuildIndex();
    nextId = 1;
    automatismsDirty = true;
    if (typeof global.Info_updateView === 'function') global.Info_updateView();
  }

  rebuildIndex();
  loadFromIndexedDB();

  global.Automatizms_getAutomatizmsByBranchId = getAutomatizmsByBranchId;
  global.Automatizms_getDefaultAutomatizmForBranch = getDefaultAutomatizmForBranch;
  global.Automatizms_createAutomatizm = createAutomatizm;
  global.Automatizms_setDefaultAutomatizm = setDefaultAutomatizm;
  global.Automatizms_addUsefulnessSample = addUsefulnessSample;
  global.Automatizms_serializeState = serializeState;
  global.Automatizms_applyState = applyState;
  global.Automatizms_clearAll = clearAllAutomatizms;
})(window);

════════════════════════════════════════════════════════════

/BEAST/beast/_1_Genetic_vitals/about_genetic_vitals.md
════════════════════════════════════════════════════════════
# Генетические виталы (_1_Genetic_vitals)

## Назначение

Модуль задаёт и обслуживает **жизненные параметры (виталы)** — генетически предопределённые показатели состояния организма. Значения виталов и их динамика лежат в основе гомеостатической регуляции и влияют на базовое состояние (Плохо/Норма/Хорошо), выбор контекстов и реакций.

## Основные данные

- **VitalsArr** — массив виталов. У каждого элемента:
  - `Id` — идентификатор;
  - `value` — текущее значение (0..100), именно его нужно сохранять между сессиями;
  - `importance` — вес значимости параметра;
  - `shift` — изменение в % за пульс (ухудшение или улучшение);
  - `name`, `title` — название и подсказка.

- **BadVitalsValue** — степень выхода виталов из нормы (для учёта в регуляции).

- **IntergalConditionImportance** / **IntergalConditionImportanceOld** — интегральная значимость состояния организма.

## Файлы

- **vitals.js** — объявление массивов виталов и связанных глобальных переменных.
- **vitals_prepare.js** — загрузка/сохранение, пересчёт виталов по пульсу, связь с базовым состоянием и контекстами.
- **vitals_ui.js** — отображение виталов в интерфейсе.

Вся логика пересчёта и персистенции сосредоточена в `vitals_prepare.js`; в `vitals.js` задаются только исходные данные.

════════════════════════════════════════════════════════════

/BEAST/beast/_1_Genetic_vitals/vitals.js
════════════════════════════════════════════════════════════
/* жизненные параметры (виталы) — только задатчик массива.
 *
 * Глобальное объявление (в браузере — становится свойством window)
 * value      - текущее значение параметра (0..100), ИМЕННО value нужно сохранять
 * importance - вес значимости
 * shift      - на сколько % ухудшаются или улучшаются параметры с каждым пульсом
 *
 * Вся логика загрузки/сохранения и пересчёта — в vitals_prepare.js
 */
window.VitalsArr = [
  { Id: 1, value: 0, importance: 100, shift: -0.05, name: "Раны",      title: "Повреждения" },
  { Id: 2, value: 0, importance: 90,  shift: -0.05, name: "Удушье",    title: "Гипоксия / кислородный дефицит" },
  { Id: 3, value: 0, importance: 80,  shift: 0.003, name: "Жажда",     title: "Обезвоживание / водный дефицит" },
  { Id: 4, value: 0, importance: 70,  shift: 0,     name: "Жара",      title: "Перегрев" },
  { Id: 5, value: 0, importance: 60,  shift: 0,     name: "Холод",     title: "Переохлаждение" },
  { Id: 6, value: 0, importance: 50,  shift: 0.002, name: "Голод",     title: "Голод (дефицит энергии)" },
  { Id: 7, value: 0, importance: 40,  shift: -0.1,  name: "Стресс",    title: "Стресс" },
  { Id: 8, value: 0, importance: 30,  shift: 0.002, name: "Гон",       title: "Гон (половая активность)" },
  { Id: 9, value: 0, importance: 20,  shift: 0.004, name: "Общение",   title: "Потребность в общении" },
  { Id: 10, value: 0, importance: 10,  shift: 0.004, name: "Интерес",   title: "Потребность в изучении" }
];

// массив степени выхода виталов из нормы
window.BadVitalsValue = {
  1: 0,
  2: 0,
  3: 0,
  4: 0,
  5: 0,
  6: 0,
  7: 0,
  8: 0,
  9: 0,
  10: 0
};
  
// индикатор интергальной значимости состояния организма 
window.IntergalConditionImportance = 0;
// индикатор старого значения интергальной значимости состояния организма 
window.IntergalConditionImportanceOld = 0;
  
// Индикатор базового состояния организма Плохо (1) Норма (2) Хорошо (3)
window.BadNormWell = 0;  

// Индикатор изменения базового состояния организма в сравнении с предыдущим значением
window.DiffZnacher = 0; // дифференциатор значимости

// Шоковое состояние (true при витале 1 >= 90%)
window.isShockState = false;

// Игровой режим Beast
window.isGameMode = false;

// Режим авторитарного обучения
window.isAuthoritarianEducation = false;

// Умолчательные value и shift для кнопки «Сбросить» (снимок на момент загрузки)
window.VitalsArrDefaults = window.VitalsArr.map(function (v) {
  return { value: v.value, shift: v.shift };
});

// состояние сна
window.IsSpeeping = false;

════════════════════════════════════════════════════════════

/BEAST/beast/_1_Genetic_vitals/vitals_prepare.js
════════════════════════════════════════════════════════════
/**
 * Подготовка виталов: загрузка/сохранение value и пересчёт по shift.
 *
 * Назначение:
 * - Восстановление значений value виталов из localStorage при старте.
 * - Сохранение value в localStorage (периодически и по кнопке «Сохранить память»).
 * - performSensorOperations() — пересчёт value по коэффициенту shift, вызывается из пульса.
 *
 * Зависимость: перед этим скриптом должен быть подключён vitals.js (window.VitalsArr).
 */

(function (global) {
  'use strict';

  var STORAGE_KEY = 'VitalsValues';

  /**
   * Восстановить значения value виталов из localStorage (если есть сохранение).
   * Сохраняются только значения value, остальные поля берутся из исходного массива.
   */
  function loadVitalsFromStorage() {
    var data;
    try {
      var raw = global.localStorage.getItem(STORAGE_KEY);
      if (!raw) {
        return;
      }
      data = JSON.parse(raw);
    } catch (e) {
      return;
    }

    if (!Array.isArray(data) || !global.VitalsArr) {
      return;
    }

    for (var i = 0; i < global.VitalsArr.length && i < data.length; i++) {
      var v = parseFloat(data[i]);
      if (isNaN(v)) {
        continue;
      }
      if (v < 0) v = 0;
      if (v > 100) v = 100;
      global.VitalsArr[i].value = v;
    }
  }

  /**
   * Сохранить текущие значения value виталов в localStorage.
   * Хранится только массив чисел.
   */
  function saveVitalsToStorage() {
    if (!global.VitalsArr) {
      return;
    }
    var values = [];
    for (var i = 0; i < global.VitalsArr.length; i++) {
      values.push(global.VitalsArr[i].value);
    }
    try {
      global.localStorage.setItem(STORAGE_KEY, JSON.stringify(values));
    } catch (e) {}
  }

  /**
   * Пересчёт жизненных параметров по коэффициенту shift.
   * Вызывается из центрального пульса раз в пульс.
   */
  function performSensorOperations() {
    if (!global.VitalsArr) {
      return;
    }

    for (var i = 0; i < global.VitalsArr.length; i++) {
      var vital = global.VitalsArr[i];
      var newVal = vital.value + vital.shift;
      if (newVal < 0) newVal = 0;
      if (newVal > 100) newVal = 100;
      vital.value = newVal;
    }

    // Дополнительное правило: при высоких значениях виталов 1–6 растёт витал 1.
    // Если виталы с Id 1,2,3,4,5,6 достигают 90% и выше, витал 1 увеличивается
    // на сумму (importance / 1000) этих виталов. Например: при Id 2 и 5 >= 90
    // добавка = 0.09 (90/1000) + 0.06 (60/1000).
    var vital1 = null;
    var extraGrowth = 0;
    for (var j = 0; j < global.VitalsArr.length; j++) {
      var v = global.VitalsArr[j];
      if (v.Id === 1) {
        vital1 = v;
      }
      if (v.Id >= 1 && v.Id <= 6 && v.value >= 90) {
        extraGrowth += (v.importance || 0) / 1000;
      }
    }
    if (vital1) {
      if (extraGrowth > 0) {
        var v1 = vital1.value + extraGrowth;
        if (v1 > 100) v1 = 100;
        if (v1 < 0) v1 = 0;
        vital1.value = v1;
      }
      // Шоковое состояние и смерть по виталу 1
      if (vital1.value >= 90) {
        global.isShockState = true;
      } else {
        global.isShockState = false;
      }
      if (vital1.value >= 100 && !global.DeathSequenceStarted && typeof global.startDeathSequence === 'function') {
        global.DeathSequenceStarted = true;
        global.startDeathSequence();
      }
    }

    // Применяем сдвиг от активных стимулов (shift на каждом пульсе)
    if (global.StimulMotivarion && global.ActiveMotivationStimuls && global.ActiveMotivationStimuls.length) {
      for (var si = 0; si < global.ActiveMotivationStimuls.length; si++) {
        var stimId = global.ActiveMotivationStimuls[si];
        for (var sj = 0; sj < global.StimulMotivarion.length; sj++) {
          var stim = global.StimulMotivarion[sj];
          if (stim.Id === stimId && stim.vitalID > 0 && stim.vitalID <= 10 && stim.shift) {
            for (var vk = 0; vk < global.VitalsArr.length; vk++) {
              var vv = global.VitalsArr[vk];
              if (vv.Id === stim.vitalID) {
                var nv = vv.value + stim.shift;
                if (nv < 0) nv = 0;
                if (nv > 100) nv = 100;
                vv.value = nv;
                break;
              }
            }
            break;
          }
        }
      }
    }

    saveVitalsToStorage();

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

  loadVitalsFromStorage();

  global.saveVitalsToStorage = saveVitalsToStorage;
  global.performSensorOperations = performSensorOperations;
})(window);

════════════════════════════════════════════════════════════

/BEAST/beast/_1_Genetic_vitals/vitals_ui.js
════════════════════════════════════════════════════════════
/**
 * Визуализация виталов на главной странице.
 *
 * Назначение:
 * - Строит ряд индикаторов по массиву window.VitalsArr.
 * - Цвет индикатора зависит от value в диапазоне 0..100
 *   (0 — светло-зелёный, 100 — красный, плавный переход).
 * - В title индикатора выводится title витала, под ним мелким шрифтом — name.
 */

(function (global) {
  'use strict';

  var ROW_ID = 'vitalsRow';
  var TITLE_ID = 'vitalsTitle';

  function valueToColor(value) {
    var v = value;
    if (v < 0) v = 0;
    if (v > 100) v = 100;
    var t = v / 100;
    var gStart = { r: 182, g: 247, b: 182 }; // светло-зелёный
    var rEnd = { r: 255, g: 0, b: 0 };       // красный
    var r = Math.round(gStart.r + (rEnd.r - gStart.r) * t);
    var g = Math.round(gStart.g + (rEnd.g - gStart.g) * t);
    var b = Math.round(gStart.b + (rEnd.b - gStart.b) * t);
    return 'rgb(' + r + ',' + g + ',' + b + ')';
  }

  function resetVitalsToDefaults() {
    if (global.VitalsArr && global.VitalsArrDefaults) {
      for (var i = 0; i < global.VitalsArr.length && i < global.VitalsArrDefaults.length; i++) {
        global.VitalsArr[i].value = global.VitalsArrDefaults[i].value;
        global.VitalsArr[i].shift = global.VitalsArrDefaults[i].shift;
      }
      global.isShockState = false;
      global.DeathSequenceStarted = false;
      if (typeof global.saveVitalsToStorage === 'function') {
        global.saveVitalsToStorage();
      }
      if (typeof global.update_vitals_view === 'function') {
        global.update_vitals_view();
      }
    }
  }

  function buildRowIfNeeded() {
    var row = global.document.getElementById(ROW_ID);
    if (row) {
      return row;
    }
    var content = global.document.querySelector('.beast-content');
    if (!content) {
      return null;
    }
    var titleWrap = global.document.getElementById(TITLE_ID);
    if (!titleWrap) {
      titleWrap = global.document.createElement('div');
      titleWrap.id = TITLE_ID;
      titleWrap.style.display = 'flex';
      titleWrap.style.alignItems = 'center';
      titleWrap.style.gap = '8px';
      titleWrap.style.marginBottom = '4px';

      var titleText = global.document.createElement('span');
      titleText.textContent = 'Жизненные параметры организма (виталы), в % отклонения от нормы';
      titleText.style.fontSize = '10pt';
      titleWrap.appendChild(titleText);

      var resetBtn = global.document.createElement('button');
      resetBtn.type = 'button';
      resetBtn.title = 'Сбросить установки виталов к умолчательным значениям';
      resetBtn.textContent = '×';
      resetBtn.style.cssText = 'width:25px;height:25px;padding:0;border-radius:50%;border:1px solid #0a0;background:#fff;color:#0a0;font-size:18px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;';
      resetBtn.addEventListener('click', function () {
        if (!global.confirm('Точно сбросить?')) {
          return;
        }
        resetVitalsToDefaults();
      });
      titleWrap.appendChild(resetBtn);

      content.insertBefore(titleWrap, content.firstChild || null);
    }
    row = global.document.createElement('div');
    row.id = ROW_ID;
    row.style.display = 'flex';
    row.style.flexWrap = 'wrap';
    row.style.gap = '6px';
    row.style.marginBottom = '10px';
    if (titleWrap.nextSibling) {
      content.insertBefore(row, titleWrap.nextSibling);
    } else {
      content.appendChild(row);
    }
    return row;
  }

  function ensureIndicators() {
    var row = buildRowIfNeeded();
    if (!row || !global.VitalsArr) {
      return;
    }
    if (row.children.length === global.VitalsArr.length) {
      return;
    }
    row.innerHTML = '';
    for (var i = 0; i < global.VitalsArr.length; i++) {
      var vital = global.VitalsArr[i];
      var item = global.document.createElement('div');
      item.style.display = 'flex';
      item.style.flexDirection = 'column';
      item.style.alignItems = 'stretch';
      item.style.flex = '1 1 80px';
      item.style.minWidth = '80px';
      item.style.maxWidth = '200px';

      var indicator = global.document.createElement('div');
      indicator.className = 'vital-indicator';
      indicator.setAttribute('data-vital-index', String(i));
      indicator.title = vital.title || '';
      indicator.style.width = '100%';
      indicator.style.height = '18px';
      indicator.style.borderRadius = '10%';
      indicator.style.border = '1px solid #888';
      indicator.style.boxSizing = 'border-box';
      indicator.style.backgroundColor = valueToColor(vital.value);
      indicator.style.display = 'flex';
      indicator.style.alignItems = 'center';
      indicator.style.justifyContent = 'center';
      indicator.style.fontSize = '10pt';
      indicator.style.color = '#000';
      indicator.style.cursor = 'pointer';

      var valueSpan = global.document.createElement('span');
      valueSpan.className = 'vital-value';
      valueSpan.textContent = vital.value.toFixed(1);
      indicator.appendChild(valueSpan);

      var nameEl = global.document.createElement('div');
      nameEl.className = 'vital-name';
      nameEl.textContent = vital.name || '';
      nameEl.style.fontSize = '9pt';
      nameEl.style.marginTop = '2px';
      nameEl.style.textAlign = 'center';

      indicator.addEventListener('click', (function (idx) {
        return function () {
          showVitalSetDialog(idx);
        };
      })(i));

      item.appendChild(indicator);
      item.appendChild(nameEl);
      row.appendChild(item);
    }
  }

  /**
   * Показать диалог ввода значения витала и shift; по «Установить» записать оба.
   * value округляется до одного знака после запятой.
   */
  function showVitalSetDialog(vitalIndex) {
    if (!global.VitalsArr || vitalIndex < 0 || vitalIndex >= global.VitalsArr.length) {
      return;
    }
    var vital = global.VitalsArr[vitalIndex];
    var overlay = global.document.createElement('div');
    overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:9998;display:flex;align-items:center;justify-content:center;';
    var box = global.document.createElement('div');
    box.style.cssText = 'background:#fff;padding:16px;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.2);min-width:220px;';
    var label = global.document.createElement('div');
    label.textContent = vital.title || vital.name || 'Витал #' + vitalIndex;
    label.style.marginBottom = '8px';
    label.style.fontWeight = 'bold';

    var valueLabel = global.document.createElement('div');
    valueLabel.textContent = 'Значение (0–100), %:';
    valueLabel.style.marginBottom = '4px';
    valueLabel.style.fontSize = '10pt';
    var input = global.document.createElement('input');
    input.type = 'text';
    input.inputMode = 'decimal';
    input.placeholder = '0–100';
    input.value = Number(vital.value).toFixed(1);
    input.style.cssText = 'width:100%;padding:6px;margin-bottom:10px;box-sizing:border-box;font-size:12pt;';

    var shiftLabel = global.document.createElement('div');
    shiftLabel.textContent = 'Shift (скорость изменения за пульс):';
    shiftLabel.style.marginBottom = '4px';
    shiftLabel.style.fontSize = '10pt';
    var shiftInput = global.document.createElement('input');
    shiftInput.type = 'text';
    shiftInput.inputMode = 'decimal';
    shiftInput.value = String(vital.shift);
    shiftInput.style.cssText = 'width:100%;padding:6px;margin-bottom:10px;box-sizing:border-box;font-size:12pt;';

    var buttonsRow = global.document.createElement('div');
    buttonsRow.style.cssText = 'display:flex;justify-content:flex-end;gap:8px;margin-top:4px;';

    var cancelBtn = global.document.createElement('button');
    cancelBtn.textContent = 'Отмена';
    cancelBtn.style.cssText = 'padding:6px 12px;cursor:pointer;background:#f0f0f0;color:#333;border:1px solid #bbb;border-radius:4px;font-size:10pt;';
    cancelBtn.addEventListener('click', function () {
      if (overlay.parentNode) {
        overlay.parentNode.removeChild(overlay);
      }
    });

    var btn = global.document.createElement('button');
    btn.textContent = 'Установить';
    btn.style.cssText = 'padding:8px 16px;cursor:pointer;background:#2196F3;color:#fff;border:none;border-radius:4px;font-size:12pt;';
    btn.addEventListener('click', function () {
      var v = parseFloat(input.value, 10);
      if (!isNaN(v)) {
        if (v < 0) v = 0;
        if (v > 100) v = 100;
        vital.value = Math.round(v * 10) / 10;
      }
      var s = parseFloat(shiftInput.value, 10);
      if (!isNaN(s)) {
        vital.shift = Math.round(s * 1000) / 1000;
      }
      if (typeof global.saveVitalsToStorage === 'function') {
        global.saveVitalsToStorage();
      }
      if (typeof global.update_vitals_view === 'function') {
        global.update_vitals_view();
      }
      if (overlay.parentNode) {
        overlay.parentNode.removeChild(overlay);
      }
    });
    buttonsRow.appendChild(cancelBtn);
    buttonsRow.appendChild(btn);

    box.appendChild(label);
    box.appendChild(valueLabel);
    box.appendChild(input);
    box.appendChild(shiftLabel);
    box.appendChild(shiftInput);
    box.appendChild(buttonsRow);
    overlay.appendChild(box);
    global.document.body.appendChild(overlay);
    input.focus();
  }

  function update_vitals_view() {
    ensureIndicators();
    var row = global.document.getElementById(ROW_ID);
    if (!row || !global.VitalsArr) {
      return;
    }
    var indicators = row.querySelectorAll('.vital-indicator');
    var values = row.querySelectorAll('.vital-value');
    for (var i = 0; i < indicators.length && i < global.VitalsArr.length; i++) {
      var vital = global.VitalsArr[i];
      var el = indicators[i];
      el.style.backgroundColor = valueToColor(vital.value);
      el.title = vital.title || '';
      if (values[i]) {
        values[i].textContent = vital.value.toFixed(1);
      }
    }

    // Фон страницы в разных режимах
    var doc = global.document;
    var root = doc.querySelector('.beast-root') || doc.body;
    if (!root.dataset) {
      root.dataset = {};
    }
    if (!root.dataset.originalBgColor) {
      // Запоминаем исходный фон; если не задан инлайном, считаем белым для .beast-root
      var initial = root.style.backgroundColor || '';
      if (!initial) {
        initial = '#ffffff';
      }
      root.dataset.originalBgColor = initial;
    }
    if (!global.DeathSequenceStarted) {
      var bg = root.dataset.originalBgColor || '#ffffff';
      if (global.isShockState) {
        bg = '#f0f0f0'; // светло-серый при шоке
      } else if (global.isAuthoritarianEducation) {
        bg = '#e0f2ff'; // светло-голубой при авторитарном обучении
      } else if (global.isGameMode) {
        bg = '#ffe6f0'; // светло-розовый в игровом режиме
      }
      root.style.backgroundColor = bg;
    }
  }

  global.update_vitals_view = update_vitals_view;

  function startDeathSequence() {
    var doc = global.document;
    var root = doc.querySelector('.beast-root') || doc.body;
    var originalBg = root.style.backgroundColor || '';

    var flashes = 0;
    function flashOnce() {
      root.style.transition = 'background-color 0.6s ease';
      root.style.backgroundColor = '#800000';
      setTimeout(function () {
        root.style.backgroundColor = '#000000';
        flashes++;
        if (flashes < 3) {
          setTimeout(flashOnce, 650);
        } else {
          showDeathOverlay();
        }
      }, 600);
    }

    function showDeathOverlay() {
      var overlay = doc.createElement('div');
      overlay.id = 'deathOverlay';
      overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:#000;z-index:10000;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#fff;font-family:Arial,sans-serif;text-align:center;';

      var title = doc.createElement('div');
      title.textContent = 'СМЕРТЬ СИСТЕМЫ ГОМЕОСТАЗА';
      title.style.fontSize = '28pt';
      title.style.marginBottom = '20px';
      overlay.appendChild(title);

      var btn = doc.createElement('button');
      btn.textContent = 'Сброс';
      btn.style.cssText = 'padding:10px 24px;font-size:12pt;border-radius:6px;border:1px solid #fff;background:#222;color:#fff;cursor:pointer;';
      btn.addEventListener('click', function () {
        resetVitalsToDefaults();
        var ov = doc.getElementById('deathOverlay');
        if (ov && ov.parentNode) {
          ov.parentNode.removeChild(ov);
        }
        root.style.backgroundColor = originalBg;
      });
      overlay.appendChild(btn);

      doc.body.appendChild(overlay);
    }

    flashOnce();
  }

  global.startDeathSequence = startDeathSequence;
  global.resetVitalsToDefaults = resetVitalsToDefaults;

  if (global.document.readyState === 'loading') {
    global.document.addEventListener('DOMContentLoaded', update_vitals_view);
  } else {
    update_vitals_view();
  }
})(window);


════════════════════════════════════════════════════════════

/BEAST/beast/_2_Genetic_basic_styles/BasicContexts.js
════════════════════════════════════════════════════════════
/* Базовые контексты
 * Назначение и название контекстов - по функции восстановления параметров.
 */

window.BasicContexts = [
  { id: 1, importance: 90, name: "Вентиляция", title: "Необходимость вентиляции, восстановление дыхания" },
  { id: 2, importance: 80, name: "Водопой", title: "Необходимость утоления жажды" },
  { id: 3, importance: 60, name: "Охлаждение", title: "Необходимость охлаждения, тяга к холоду, поиск тени" },
  { id: 4, importance: 70, name: "Обогрев", title: "Необходимость согреться, тяга к теплу" },
  { id: 5, importance: 70, name: "Питание", title: "Необходимость утоления голода" },
  { id: 6, importance: 40, name: "Страх", title: "Испуг, замирание, Замирание защиты" },
  { id: 7, importance: 30, name: "Агрессия", title: "Агрессивное поведение, ярость, безжалостность" },
  { id: 8, importance: 30, name: "Гон", title: "Репродуктивное поведение" },
  { id: 9, importance: 50, name: "Избегание", title: "Осторожность, осмотрительность, хитрость" },
  { id: 10, importance: 100, name: "Лечение", title: "Иммобилизация, бережливость к себе, необходимость лечения" },
  { id: 11, importance: 30, name: "Игра", title: "Игровое поведение, радостный настрой" },
  { id: 12, importance: 30, name: "Любопытство", title: "неудовлетворенность существующим, исследования, поиск, экспериментивароние" },
  { id: 13, importance: 30, name: "Общительность", title: "социальность, доброта, эмпатия, альтруизм" },
  { id: 14, importance: 10, name: "Покой", title: "минимально активный исследовательский/обслуживающий режим" },
  { id: 15, importance: 20, name: "Сон", title: "Засыпание, базовый контекст сна и сновидений" }
];

// массив соотвествия выхода из нормы ID витала - активации ID базовых контекстов
//Доступ: VitalsContextsActivingArr[2] → [1,9]
window.VitalsContextsActivingArr = {
  1: [10, 6, 9],
  2: [1,9],
  3: [2],
  4: [3,9],
  5: [4,9],
  6: [5,7],
  7: [7],
  8: [8],
  9: [13,11],
  10: [12, 11]
};

// Массив ID активированных базовых контекстов (0–3 значения). Доступен глобально. Обновляется каждым пульсом в prepare.js.
window.BasicContextsActived = undefined;

// Глобальные значения, обновляемые в prepare.js при каждом пульсе (старое значение интегральной важности сохраняется глобально)
window.IntergalConditionImportanceOld = undefined;
window.IntergalConditionImportance = undefined;
window.DiffZnacher = undefined;
// Индикатор базового состояния организма: Плохо (1), Норма (2), Хорошо (3). Изначально 0.
window.BadNormWell = 0;


════════════════════════════════════════════════════════════

/BEAST/beast/_2_Genetic_basic_styles/about_genetic_basic_styles.md
════════════════════════════════════════════════════════════
# Генетические базовые стили (_2_Genetic_basic_styles)

## Назначение

Модуль задаёт **базовые контексты** (эмоционально-поведенческие режимы) и их связь с виталами. Контексты определяют «образ ситуации» и участвуют в выборе реакций, условных рефлексах, дереве восприятия и семантической памяти.

## Основные данные

- **BasicContexts** — массив базовых контекстов (Вентиляция, Водопой, Охлаждение, Обогрев, Питание, Страх, Агрессия, Гон, Избегание, Лечение, Игра, Любопытство, Общительность, Покой, Сон). У каждого: `id`, `importance`, `name`, `title`.

- **VitalsContextsActivingArr** — соответствие «выход витала из нормы» → активируемые контексты (например, жажда активирует Водопой).

- **BasicContextsActived** — массив ID активных в данный момент базовых контекстов (заполняется по виталам и внешним воздействиям).

- **BadNormWell** — текущее базовое состояние: 1 = Плохо, 2 = Норма, 3 = Хорошо. Используется в гомеостатике (в т.ч. в `prepare.js` с учётом `WellBadEffect`), дереве восприятия и семантике.

## Файлы

- **BasicContexts.js** — объявление массивов контекстов и связей витал–контексты.
- **prepare.js** — пересчёт виталов, базового состояния (BadNormWell), интегральной значимости; учёт WellBadEffect при нормализации.
- **basic_contexts_ui.js** — отображение и переключение активных контекстов в интерфейсе.

Контексты используются при расчёте эмоции (EmotionId), формировании условных рефлексов и построении ветки дерева восприятия.

════════════════════════════════════════════════════════════

/BEAST/beast/_2_Genetic_basic_styles/basic_contexts_ui.js
════════════════════════════════════════════════════════════
/**
 * Визуализация индикаторов базовых контекстов под строкой виталов.
 *
 * Назначение:
 * - Строит ряд индикаторов по window.BasicContexts.
 * - По умолчанию: светло-серый фон. При активации (ID контекста есть в BasicContextsActived):
 *   светло-зелёный фон и круговая тень светло-зелёного свечения.
 * - Под каждым индикатором выводится name контекста мелким шрифтом.
 *
 * Зависимости: BasicContexts.js (window.BasicContexts, window.BasicContextsActived).
 * Ряд вставляется после элемента #vitalsRow (создаётся в vitals_ui.js).
 *
 * Обновление при активации: вызвать window.update_basic_contexts_view() после
 * изменения BasicContextsActived (например, из getActiveBasicContexts).
 */

(function (global) {
  'use strict';

  var ROW_ID = 'basicContextsRow';
  var TITLE_ID = 'basicContextsTitle';
  var TITLE_TEXT = 'Состояние базовых контекстов поведения';
  function ensureRowStyles(row) {
    row.style.display = 'flex';
    row.style.flexWrap = 'wrap';
    row.style.gap = '6px';
    row.style.marginBottom = '10px';
  }

  function fillRowWithIndicators(row) {
    if (!global.BasicContexts || !global.BasicContexts.length) {
      return;
    }
    if (row.children.length === global.BasicContexts.length) {
      return;
    }
    row.innerHTML = '';
    for (var i = 0; i < global.BasicContexts.length; i++) {
      var ctx = global.BasicContexts[i];
      var item = global.document.createElement('div');
      item.style.display = 'flex';
      item.style.flexDirection = 'column';
      item.style.alignItems = 'stretch';
      item.style.flex = '1 1 80px';
      item.style.minWidth = '80px';
      item.style.maxWidth = '200px';

      var indicator = global.document.createElement('div');
      indicator.className = 'basic-context-indicator';
      indicator.setAttribute('data-context-index', String(i));
      indicator.title = ctx.title || ctx.name || '';
      indicator.style.width = '100%';
      indicator.style.minHeight = '28px';
      indicator.style.borderRadius = '10%';
      indicator.style.border = '1px solid #999';
      indicator.style.boxSizing = 'border-box';
      indicator.style.transition = 'background-color 0.2s, box-shadow 0.2s';
      indicator.style.display = 'flex';
      indicator.style.alignItems = 'center';
      indicator.style.justifyContent = 'center';
      indicator.style.padding = '4px 6px';
      indicator.style.textAlign = 'center';

      var nameEl = global.document.createElement('span');
      nameEl.className = 'basic-context-name';
      nameEl.textContent = ctx.name || '';
      nameEl.style.fontSize = '9pt';

      indicator.appendChild(nameEl);
      item.appendChild(indicator);
      row.appendChild(item);
    }
  }

  var BADNORMWELL_ID = 'basicContextsBadNormWell';
  var DIFF_LABEL_ID = 'basicContextsDiffLabel';

  function getBadNormWellLabel() {
    var v = global.BadNormWell;
    if (v === 1) return 'Плохо';
    if (v === 2) return 'Норма';
    if (v === 3) return 'Хорошо';
    return '';
  }

  function getDiffLabelText() {
    var d = global.DiffZnacher;
    if (typeof d !== 'number') return '';
    var rounded = Math.round(Math.abs(d) * 10) / 10;
    if (rounded === 0) return '';
    var absVal = Math.abs(d).toFixed(1);
    return d < 0 ? 'стало хуже на ' + absVal : 'стало лучше на ' + absVal;
  }

  function ensureTitle(row) {
    var title = global.document.getElementById(TITLE_ID);
    if (title) {
      return;
    }
    title = global.document.createElement('div');
    title.id = TITLE_ID;
    title.style.fontSize = '10pt';
    title.style.marginBottom = '4px';
    title.style.display = 'flex';
    title.style.justifyContent = 'space-between';
    title.style.alignItems = 'center';

    var leftPart = global.document.createElement('span');
    leftPart.appendChild(global.document.createTextNode(TITLE_TEXT + ' '));
    var stateSpan = global.document.createElement('span');
    stateSpan.id = BADNORMWELL_ID;
    stateSpan.style.fontWeight = 'bold';
    stateSpan.style.cursor = 'pointer';
    stateSpan.title = 'Щелкните для вывода значений';
    stateSpan.textContent = getBadNormWellLabel();
    stateSpan.addEventListener('click', function () {
      var d = global.DiffZnacher;
      var imp = global.IntergalConditionImportance;
      var impOld = global.IntergalConditionImportanceOld;
      var fmt = function (v) { return (typeof v === 'number' ? v.toFixed(1) : '—'); };
      var msg = '<b>Отслеживающая с каждым пульсом разница состояний</b><br>' + 
	  'DiffZnacher (приведенная разница): ' + fmt(d) + '<br>' +
        'IntergalConditionImportance: ' + fmt(imp) + '<br>' +
        'IntergalConditionImportanceOld: ' + fmt(impOld);
      global.show_alert(msg);
    });
    leftPart.appendChild(stateSpan);
    leftPart.appendChild(global.document.createTextNode(' '));
    var diffSpan = global.document.createElement('span');
    diffSpan.id = DIFF_LABEL_ID;
    diffSpan.style.padding = '2px 6px';
    diffSpan.style.borderRadius = '4px';
    diffSpan.textContent = getDiffLabelText();
    leftPart.appendChild(diffSpan);
    title.appendChild(leftPart);

    var buttonsRow = global.document.createElement('div');
    buttonsRow.style.display = 'flex';
    buttonsRow.style.alignItems = 'center';
    buttonsRow.style.gap = '4px';
    buttonsRow.style.flexShrink = '0';
    buttonsRow.style.position = 'relative';

    var countdownLabel = global.document.createElement('span');
    countdownLabel.id = 'holdCountdownLabel';
    countdownLabel.style.position = 'absolute';
    countdownLabel.style.top = '-12px';
    countdownLabel.style.left = '0';
    countdownLabel.style.fontSize = '8pt';
    countdownLabel.style.color = '#666';
    countdownLabel.style.pointerEvents = 'none';
    countdownLabel.textContent = '';
    buttonsRow.appendChild(countdownLabel);

    // Кнопка удержания Плохо (B)
    var badHoldBtn = global.document.createElement('button');
    badHoldBtn.type = 'button';
    badHoldBtn.textContent = 'B';
    badHoldBtn.title = 'Запустить период удержания Плохо';
    badHoldBtn.style.width = '22px';
    badHoldBtn.style.height = '22px';
    badHoldBtn.style.borderRadius = '50%';
    badHoldBtn.style.border = '1px solid #888';
    badHoldBtn.style.fontSize = '11pt';
    badHoldBtn.style.fontWeight = 'bold';
    badHoldBtn.style.color = '#c00';
    badHoldBtn.style.backgroundColor = '#f5f5f5';
    badHoldBtn.style.cursor = 'pointer';
    badHoldBtn.style.padding = '0';
    badHoldBtn.style.flexShrink = '0';
    badHoldBtn.addEventListener('click', function () {
      if (typeof global.SetBadForHolding === 'function') {
        global.SetBadForHolding();
      }
    });
    buttonsRow.appendChild(badHoldBtn);

    var wellHoldBtn = global.document.createElement('button');
    wellHoldBtn.type = 'button';
    wellHoldBtn.textContent = 'W';
    wellHoldBtn.title = 'Запустить период удержания Хорошо';
    wellHoldBtn.style.width = '22px';
    wellHoldBtn.style.height = '22px';
    wellHoldBtn.style.borderRadius = '50%';
    wellHoldBtn.style.border = '1px solid #888';
    wellHoldBtn.style.fontSize = '11pt';
    wellHoldBtn.style.fontWeight = 'bold';
    wellHoldBtn.style.color = '#0a0';
    wellHoldBtn.style.backgroundColor = '#f5f5f5';
    wellHoldBtn.style.cursor = 'pointer';
    wellHoldBtn.style.padding = '0';
    wellHoldBtn.style.flexShrink = '0';
    wellHoldBtn.addEventListener('click', function () {
      if (typeof global.SetWellForHolding === 'function') {
        global.SetWellForHolding();
      }
    });
    buttonsRow.appendChild(wellHoldBtn);

    var diffBtn = global.document.createElement('button');
    diffBtn.type = 'button';
    diffBtn.textContent = 'diff';
    diffBtn.title = 'Измерить разницу текущего и прошлого состояния';
    diffBtn.style.height = '18px';
    diffBtn.style.fontSize = '9pt';
    diffBtn.style.padding = '0 6px';
    diffBtn.style.cursor = 'pointer';
    diffBtn.addEventListener('click', function () {
      var prev = global.PrevDiffZnacherForGetDiff;
      var curr = typeof global.IntergalConditionImportance === 'number' ? global.IntergalConditionImportance : 0;
      var diff = typeof global.getDiffImportance === 'function' ? global.getDiffImportance() : 0;
      var fmt = function (v) { return (typeof v === 'number' ? v.toFixed(1) : '—'); };
      var msg = 'старое значение изменения состояния: ' + fmt(prev) + '<br>' +
        'новое значение изменения состояния: ' + fmt(curr) + '<br>' +
		'разница: ' + fmt(prev - curr) + '<br>' +  
        'приведенная разница: ' + fmt(diff);
      global.show_alert(msg);
    });
    buttonsRow.appendChild(diffBtn);

    var btnGroup = global.document.createElement('div');
    btnGroup.style.display = 'flex';
    btnGroup.style.alignItems = 'center';
    btnGroup.style.flexShrink = '0';
    btnGroup.appendChild(buttonsRow);
    title.appendChild(btnGroup);

    if (row.parentNode) {
      row.parentNode.insertBefore(title, row);
    }
  }

  function buildRowIfNeeded() {
    var row = global.document.getElementById(ROW_ID);
    if (!row) {
      return null;
    }
    if (!global.BasicContexts || !global.BasicContexts.length) {
      return row;
    }
    ensureTitle(row);
    ensureRowStyles(row);
    fillRowWithIndicators(row);
    return row;
  }

  function update_basic_contexts_view() {
    buildRowIfNeeded();
    var row = global.document.getElementById(ROW_ID);
    if (!row || !global.BasicContexts) {
      return;
    }
    var actived = Array.isArray(global.BasicContextsActived) ? global.BasicContextsActived : [];
    var indicators = row.querySelectorAll('.basic-context-indicator');
    for (var i = 0; i < indicators.length && i < global.BasicContexts.length; i++) {
      var el = indicators[i];
      var ctxId = global.BasicContexts[i].id;
      var active = actived.indexOf(ctxId) !== -1;
      if (active) {
        el.classList.add('active');
      } else {
        el.classList.remove('active');
      }
    }

    // Бордер вокруг строки контекстов в зависимости от Плохо / Хорошо
    var bnw = global.BadNormWell;
    var activityBlock = global.document.getElementById('beastActivityBlock');
    if (bnw === 1) {
      // Плохо — розовый бордер
      row.style.border = '3px solid #ff99c2';
      row.style.borderRadius = '8px';
      row.style.padding = '4px';
      if (activityBlock) {
        activityBlock.style.border = '2px solid #ff99c2';
      }
    } else if (bnw === 3) {
      // Хорошо — светло-зелёный бордер
      row.style.border = '3px solid #9fe6b8';
      row.style.borderRadius = '8px';
      row.style.padding = '4px';
      if (activityBlock) {
        activityBlock.style.border = '2px solid #9fe6b8';
      }
    } else {
      // Норма или неопределено — без дополнительного бордера
      row.style.border = 'none';
      row.style.padding = '0';
      if (activityBlock) {
        activityBlock.style.border = '2px solid transparent';
      }
    }
    var stateEl = global.document.getElementById(BADNORMWELL_ID);
    if (stateEl) {
      stateEl.textContent = getBadNormWellLabel();
    }
    var diffEl = global.document.getElementById(DIFF_LABEL_ID);
    if (diffEl) {
      var d = global.DiffZnacher;
      var txt = getDiffLabelText();
      diffEl.textContent = txt;
      if (typeof d === 'number' && d !== 0) {
        diffEl.style.backgroundColor = d < 0 ? '#fffacd' : '#d8f0d8';
      } else {
        diffEl.style.backgroundColor = 'transparent';
      }
    }
    var countdownLabel = global.document.getElementById('holdCountdownLabel');
    if (countdownLabel) {
      var badRem = global.BadHoldRemainingPulses;
      var wellRem = global.WellHoldRemainingPulses;
      if (typeof badRem === 'number' && badRem > 0) {
        countdownLabel.textContent = String(badRem);
        countdownLabel.style.left = '3px'; // над кнопкой B
      } else if (typeof wellRem === 'number' && wellRem > 0) {
        countdownLabel.textContent = String(wellRem);
        countdownLabel.style.left = '29px'; // над кнопкой W (22px + 4px отступ + чуть смещения)
      } else {
        countdownLabel.textContent = '';
      }
    }

    // Период удержания базовых контекстов: подсветка блока и центральный обратный отсчёт
    var ctxRem = typeof global.ContextHoldRemainingPulses === 'number' ? global.ContextHoldRemainingPulses : 0;
    var titleEl = global.document.getElementById(TITLE_ID);
    var overlayId = 'basicContextsHoldOverlay';
    var overlay = global.document.getElementById(overlayId);

    if (ctxRem > 0) {
      // Жёлтый фон для блока (пояснение + индикаторы)
      if (titleEl) {
        titleEl.style.backgroundColor = '#fffacd';
        titleEl.style.padding = '4px 8px';
        titleEl.style.borderRadius = '8px 8px 0 0';
      }
      row.style.backgroundColor = '#fffacd';
      row.style.borderRadius = '0 0 8px 8px';
      row.style.padding = '4px 8px';

      // Центральный фиксированный счётчик по высоте строки пояснения
      if (!overlay) {
        overlay = global.document.createElement('div');
        overlay.id = overlayId;
        overlay.style.position = 'fixed';
        overlay.style.left = '50%';
        overlay.style.transform = 'translateX(-50%)';
        overlay.style.zIndex = '9999';
        overlay.style.fontSize = '18pt';
        overlay.style.fontWeight = 'bold';
        overlay.style.color = '#c08000';
        overlay.style.textShadow = '0 0 4px #fff';
        global.document.body.appendChild(overlay);
      }
      overlay.textContent = String(ctxRem);
      var rect = titleEl ? titleEl.getBoundingClientRect() : row.getBoundingClientRect();
      // Поднимаем счётчик на уровень строки пояснения
      overlay.style.top = Math.round(rect.top) + 'px';
    } else {
      // Нет удержания: убираем жёлтый фон и оверлей
      if (titleEl) {
        titleEl.style.backgroundColor = '';
        titleEl.style.padding = '0 0 4px 0';
        titleEl.style.borderRadius = '';
      }
      row.style.backgroundColor = '';
      row.style.borderRadius = row.style.border ? row.style.borderRadius : '';
      row.style.padding = row.style.border ? row.style.padding : '0';
      if (overlay && overlay.parentNode) {
        overlay.parentNode.removeChild(overlay);
      }
    }
  }

  global.update_basic_contexts_view = update_basic_contexts_view;

  function init() {
    if (!global.BasicContexts || !global.BasicContexts.length) {
      return;
    }
    buildRowIfNeeded();
    update_basic_contexts_view();
  }

  function runInit() {
    if (global.document.readyState === 'loading') {
      global.document.addEventListener('DOMContentLoaded', function () {
        setTimeout(init, 0);
      });
    } else {
      setTimeout(init, 0);
    }
  }
  runInit();
})(window);

════════════════════════════════════════════════════════════

/BEAST/beast/_2_Genetic_basic_styles/prepare.js
════════════════════════════════════════════════════════════
/**
 * Подготовка и активация базовых контекстов по виталам.
 *
 * Использует упрощённый доступ:
 * - BadVitalsValue[vitalId] — степень выхода витала из нормы (Kbad).
 * - VitalsContextsActivingArr[vitalId] — массив id базовых контекстов для активации.
 * - BasicContextsActived — массив ID активных базовых контекстов (0–3 числа); до первого вызова не определён.
 * Глобальные BadVitalsValue и BasicContextsActived обновляются только по завершении расчётов,
 * без промежуточных значений. VitalsContextsActivingArr только читается, не изменяется.
 *
 * После активации контекстов: при отсутствии активных — активируется Покой (id=14);
 * вычисляются IntergalConditionImportance, DiffZnacher, вызывается GetBadNormWell().
 * getDiffImportance() — произвольно вызываемая функция, возвращает normalizeValue(текущий DiffZnacher − сохранённый).
 * Перед нормализацией сырой diff (до normalizeValue) кладётся в LastDiffImportanceRaw — для «лупы» семантики в registerSemanticSample.
 */

(function (global) {
  'use strict';

  /** Длительность удержания состояния Хорошо (3) в пульсах после выхода из Плохо или по вызову SetWellForHolding(). */
  var state_retention_period = 15;

  /** Осталось пульсов удержания Хорошо (3). */
  var hold_well_remaining = 0;

  /** Осталось пульсов удержания Плохо (1). */
  var hold_bad_remaining = 0;

  /** Осталось пульсов удержания Норма (2). */
  var hold_norm_remaining = 0;

  /** Осталось пульсов удержания заданного набора базовых контекстов. */
  var hold_contexts_remaining = 0;

  /** Насильственно удерживаемые базовые контексты (их id). */
  var forced_basic_contexts = null;

  /** Значение BadNormWell, установленное в предыдущем пульсе (для определения перехода из Плохо). */
  var lastSetBadNormWell = 0;

  /** Снимок value по Id витала с предыдущего пульса — для относительного порога ухудшения. */
  var prevVitalValuesSnapshot = null;

  /** Рост value витала за один пульс, при котором запускается новый цикл ФО: (curr−prev)/prev ≥ порога. */
  var VITAL_RELATIVE_WORSEN_THRESHOLD = 0.3;

  function snapshotVitalValues(VitalsArr) {
    var o = {};
    if (!VitalsArr || !VitalsArr.length) return o;
    for (var i = 0; i < VitalsArr.length; i++) {
      var v = VitalsArr[i];
      if (v && typeof v.Id === 'number') {
        o[v.Id] = typeof v.value === 'number' ? v.value : 0;
      }
    }
    return o;
  }

  /**
   * Рост value более чем на VITAL_RELATIVE_WORSEN_THRESHOLD (от предыдущего значения), либо переход в Плохо.
   * При prev≈0: порог «не ниже 30 баллов» (30% шкалы 0…100), чтобы с нуля срабатывало осмысление.
   */
  function detectSharpVitalValueWorsening(VitalsArr, prevSnap) {
    if (!prevSnap || !VitalsArr || !VitalsArr.length) return false;
    for (var i = 0; i < VitalsArr.length; i++) {
      var v = VitalsArr[i];
      var vid = v.Id;
      var prevVal = prevSnap[vid];
      if (typeof prevVal !== 'number') prevVal = 0;
      var currVal = typeof v.value === 'number' ? v.value : 0;
      if (currVal <= prevVal) continue;
      if (prevVal > 0.001) {
        if ((currVal - prevVal) / prevVal >= VITAL_RELATIVE_WORSEN_THRESHOLD) return true;
      } else if (currVal >= 30) {
        return true;
      }
    }
    return false;
  }

  /**
   * Переход в Плохо или резкий рост value витала (>30% к предыдущему) — новый главный цикл осмысления.
   * Пульс 3 не трогаем: там отдельный стартовый вызов ФО в puls.js.
   */
  function tryTriggerConsciousnessOnVitalWorsening(suddenBadTransition, sharpValueWorsening) {
    if (!suddenBadTransition && !sharpValueWorsening) return;
    if (typeof global.cpuls === 'number' && global.cpuls === 3) return;
    if (typeof global.stimuls_consciousness !== 'function') return;

    var branchId = typeof global.PerceptionTree_getActiveTerminalNodeId === 'function'
      ? global.PerceptionTree_getActiveTerminalNodeId()
      : 0;
    var emotionId = 0;
    if (typeof global.Emotions_getOrCreateEmotionId === 'function' && Array.isArray(global.BasicContextsActived)) {
      emotionId = global.Emotions_getOrCreateEmotionId(global.BasicContextsActived) || 0;
    }
    var igr = typeof global.IntergalConditionImportance === 'number' ? global.IntergalConditionImportance : 0;
    var significance = Math.max(12, igr / 50);

    global.stimuls_consciousness({
      actual_stimul_ID: 0,
      branchId: branchId,
      well: global.BadNormWell || 0,
      emotionId: emotionId,
      activityID: 0,
      significance: significance,
      vitalEmergency: true
    });
  }

  /** Глобально: оставшееся число пульсов удержания Хорошо (для отображения обратного отсчёта в UI). */
  global.WellHoldRemainingPulses = 0;
  /** Глобально: оставшееся число пульсов удержания Плохо (для отображения обратного отсчёта в UI). */
  global.BadHoldRemainingPulses = 0;
  /** Глобально: оставшееся число пульсов удержания Норма (для UI). */
  global.NormHoldRemainingPulses = 0;
  /** Глобально: оставшееся число пульсов удержания базовых контекстов (для UI). */
  global.ContextHoldRemainingPulses = 0;

  /** Базовая величина эффекта по состоянию (можно менять вручную для оптимизации). */
  global.WellBadEffectManual = 5;

  function normalizeValue(value, k) {
    if (k === undefined) k = 0.5;
    var max = 10;
    return max * (1 - Math.exp(-k * Math.abs(value))) * (value >= 0 ? 1 : -1);
  }

  /**
   * Вычисляет window.BadNormWell: Плохо (1), Норма (2), Хорошо (3).
   * 1. Сначала определяется Норма или Плохо (по сумме BadVitalsValue).
   * 2. Если DiffZnacher > 2 — запускается период удержания Хорошо.
   * 3. В конце периода: если снова DiffZnacher > 2 — новый период Хорошо; иначе — Норма или Плохо.
   */
  function GetBadNormWell() {
    // Приоритетно удерживаем состояние Плохо, если запущено SetBadForHolding()
    if (hold_bad_remaining > 0) {
      hold_bad_remaining--;
      global.BadNormWell = 1;
      global.BadHoldRemainingPulses = hold_bad_remaining;
      global.WellHoldRemainingPulses = hold_well_remaining;
      global.NormHoldRemainingPulses = hold_norm_remaining;
      return;
    }
    if (hold_norm_remaining > 0) {
      hold_norm_remaining--;
      global.BadNormWell = 2;
      global.NormHoldRemainingPulses = hold_norm_remaining;
      global.BadHoldRemainingPulses = hold_bad_remaining;
      global.WellHoldRemainingPulses = hold_well_remaining;
      return;
    }
    var VitalsArr = global.VitalsArr || [];
    var sum = 0;
    for (var i = 0; i < VitalsArr.length; i++) {
      sum += global.BadVitalsValue[VitalsArr[i].Id] || 0;
    }
    var base = sum >= 2 ? 1 : 2; // 1 — Плохо, 2 — Норма

    var d = typeof global.DiffZnacher === 'number' ? global.DiffZnacher : 0;

    if (hold_well_remaining > 0) {
      hold_well_remaining--;
      global.BadNormWell = 3;
      if (hold_well_remaining === 0 && d > 2) {
        hold_well_remaining = state_retention_period;
      } else if (hold_well_remaining === 0) {
        global.BadNormWell = base;
      }
    } else {
      if (d > 2) {
        hold_well_remaining = state_retention_period;
        global.BadNormWell = 3;
      } else {
        global.BadNormWell = base;
      }
    }
    global.WellHoldRemainingPulses = hold_well_remaining;
    global.BadHoldRemainingPulses = hold_bad_remaining;
    global.NormHoldRemainingPulses = hold_norm_remaining;
  }

  /**
   * Запускает период удержания состояния Хорошо (3) на state_retention_period пульсов.
   */
  function SetWellForHolding() {
    hold_bad_remaining = 0;
    global.BadHoldRemainingPulses = 0;
    hold_norm_remaining = 0;
    global.NormHoldRemainingPulses = 0;
    hold_well_remaining = state_retention_period;
    global.WellHoldRemainingPulses = hold_well_remaining;
  }

  /**
   * Запускает период удержания состояния Плохо (1) на state_retention_period пульсов.
   */
  function SetBadForHolding() {
    hold_norm_remaining = 0;
    global.NormHoldRemainingPulses = 0;
    hold_well_remaining = 0;
    global.WellHoldRemainingPulses = 0;
    hold_bad_remaining = state_retention_period;
    global.BadHoldRemainingPulses = hold_bad_remaining;
    // Запуск удержания Плохо трактуем как ориентировочный сигнал «стало плохо»:
    // переводим осмысление в целевой режим и зажигаем лампочку осмысления.
    if (typeof global.Info_setMode === 'function') {
      global.Info_setMode('target');
    }
    if (typeof global.Info_thinkingFlash === 'function') {
      global.Info_thinkingFlash();
    }
  }

  /**
   * Запускает период удержания состояния Норма (2) на state_retention_period пульсов.
   */
  function SetNormForHolding() {
    hold_bad_remaining = 0;
    global.BadHoldRemainingPulses = 0;
    hold_well_remaining = 0;
    global.WellHoldRemainingPulses = 0;
    hold_norm_remaining = state_retention_period;
    global.NormHoldRemainingPulses = hold_norm_remaining;
  }

  /**
   * Запускает период удержания заданного набора базовых контекстов
   * на state_retention_period пульсов. В этот период выбор контекстов по виталам блокируется.
   * @param {number[]} contextIds - массив id из window.BasicContexts.
   */
  function ActivateBasicContextsForHolding(contextIds) {
    if (!Array.isArray(contextIds) || !contextIds.length) {
      return;
    }
    forced_basic_contexts = contextIds.slice(0);
    hold_contexts_remaining = state_retention_period;
    global.ContextHoldRemainingPulses = hold_contexts_remaining;
  }

  /** Глобально: предыдущее значение совокупного выхода из нормы (IntergalConditionImportance) для getDiffImportance. */
  global.PrevDiffZnacherForGetDiff = null;

  /**
   * Сырой diff для семантики: (getWellBadEffect() + prev − curr) / 100 до normalizeValue в последнем вызове getDiffImportance.
   * Используется в semantic_memory.registerSemanticSample («лупа» при почти нулевой meanImportance и норме виталов).
   */
  global.LastDiffImportanceRaw = null;

  /**
   * Смещение для diff в зависимости от удерживаемого базового состояния.
   * Модуль берётся из WellBadEffectManual (по умолчанию 5):
   *  - при Хорошо (BadNormWell=3) возвращает +5,
   *  - при Плохо (BadNormWell=1) возвращает -5,
   *  - в остальных случаях (Норма и пр.) — 0.
   */
  global.WellBadEffect = 0;

  function getWellBadEffect() {
    var base = (typeof global.WellBadEffectManual === 'number' && !isNaN(global.WellBadEffectManual))
      ? Math.abs(global.WellBadEffectManual)
      : 5;
    if (global.BadNormWell === 3) return base;
    if (global.BadNormWell === 1) return -base;
    return 0;
  }

  /**
   * Произвольно вызываемая функция: сравнивает текущий IntergalConditionImportance с сохранённым при предыдущем вызове.
   * @returns {number} normalizeValue(разница). Увеличение важности = хуже (отрицательный), уменьшение = лучше (положительный).
   */
  function getDiffImportance() {
    var curr = typeof global.IntergalConditionImportance === 'number' ? global.IntergalConditionImportance : 0;
    var prev = global.PrevDiffZnacherForGetDiff !== null && global.PrevDiffZnacherForGetDiff !== undefined
      ? global.PrevDiffZnacherForGetDiff
      : curr;
    global.PrevDiffZnacherForGetDiff = curr;
    var diff = (getWellBadEffect() + prev - curr) / 100;
    global.LastDiffImportanceRaw = diff;
    return normalizeValue(diff);
  }

  /**
   * Вычисляет степень выхода виталов из нормы (Kbad по Id витала).
   * Kbad = 0 при value <= 40; при value > 40: Kbad = 2^((value-40)*0.1).
   * @returns {Object} BadVitalsValue — объект vitalId -> Kbad. Пустой объект, если нет виталов.
   */
  function calculateBadVitalsValues() {
    var VitalsArr = global.VitalsArr;
    var BadVitalsValue = {};
    if (!VitalsArr || !VitalsArr.length) return BadVitalsValue;
    for (var i = 0; i < VitalsArr.length; i++) {
      var vitalId = VitalsArr[i].Id;
      var value = VitalsArr[i].value;
      var Kbad = value > 40 ? Math.pow(2, 0.1 * (value - 40)) : 0;
      BadVitalsValue[vitalId] = Kbad;
    }
    return BadVitalsValue;
  }

  /**
   * Выбирает массив ID активных базовых контекстов по рассчитанным BadVitalsValue.
   * Виталы учитываются в порядке убывания importance; не более 3 контекстов.
   * Учитывает удержание (forced_basic_contexts). При отсутствии контекстов — Покой (14).
   * Побочный эффект: при использовании удержания уменьшает hold_contexts_remaining.
   * @param {Object} badVitals — объект из calculateBadVitalsValues().
   * @returns {number[]} массив ID контекстов (BasicContextsActived).
   */
  function selectBasicContexts(badVitals) {
    var VitalsArr = global.VitalsArr;
    var VitalsContextsActivingArr = global.VitalsContextsActivingArr;
    var BasicContextsActived = [];

    if (!VitalsArr || !VitalsArr.length || !global.BasicContexts || !global.BasicContexts.length) {
      if (hold_contexts_remaining > 0 && Array.isArray(forced_basic_contexts) && forced_basic_contexts.length) {
        hold_contexts_remaining--;
        return forced_basic_contexts.slice(0);
      }
      return BasicContextsActived.length ? BasicContextsActived : [14];
    }

    var list = [];
    for (var i = 0; i < VitalsArr.length; i++) {
      list.push({
        vitalId: VitalsArr[i].Id,
        importance: VitalsArr[i].importance
      });
    }
    list.sort(function (a, b) { return b.importance - a.importance; });

    for (var s = 0; s < list.length; s++) {
      var vitalId = list[s].vitalId;
      var Kbad = badVitals[vitalId] || 0;
      if (Kbad > 0 && BasicContextsActived.length < 3 && VitalsContextsActivingArr) {
        var contextIds = VitalsContextsActivingArr[vitalId] || VitalsContextsActivingArr[String(vitalId)] || [];
        for (var c = 0; c < contextIds.length && BasicContextsActived.length < 3; c++) {
          var contextId = contextIds[c];
          if (BasicContextsActived.indexOf(contextId) === -1) BasicContextsActived.push(contextId);
        }
      }
    }

    if (hold_contexts_remaining > 0 && Array.isArray(forced_basic_contexts) && forced_basic_contexts.length) {
      hold_contexts_remaining--;
      BasicContextsActived = forced_basic_contexts.slice(0);
    }
    if (BasicContextsActived.length === 0) BasicContextsActived.push(14);

    return BasicContextsActived;
  }

  /**
   * Обновляет глобальные метрики важности и диффа: IntergalConditionImportance, DiffZnacher,
   * а также ContextHoldRemainingPulses для UI.
   * @param {Object} badVitals — объект из calculateBadVitalsValues().
   */
  function updateImportanceMetrics(badVitals) {
    var VitalsArr = global.VitalsArr || [];
    var IntergalConditionImportanceNew = 0;
    for (var v = 0; v < VitalsArr.length; v++) {
      var vid = VitalsArr[v].Id;
      var imp = VitalsArr[v].importance;
      IntergalConditionImportanceNew += (badVitals[vid] || 0) * imp;
    }
    var currentImportance = (typeof global.IntergalConditionImportance === 'number')
      ? global.IntergalConditionImportance
      : 0;

    global.IntergalConditionImportanceOld = currentImportance;
    global.IntergalConditionImportance = IntergalConditionImportanceNew;
    var rawDiff = (getWellBadEffect() + currentImportance - IntergalConditionImportanceNew) / 100;
    global.DiffZnacher = normalizeValue(rawDiff);
    // Детекторы badState/suddenChange обновляются в getActiveBasicContexts (см. ниже).
    global.ContextHoldRemainingPulses = hold_contexts_remaining;
  }

  /**
   * Активация базовых контекстов по текущим значениям виталов.
   * Сводится к вызову трёх шагов: расчёт BadVitalsValue, выбор контекстов, обновление метрик и GetBadNormWell.
   *
   * @param {number} [cpuls] — счётчик пульсов; при cpuls === 2 вызывается getDiffImportance() для инициализации PrevDiffZnacherForGetDiff.
   * @returns {Object} — объект BadVitalsValue (Kbad по ID витала).
   */
  function getActiveBasicContexts(cpuls) {
    var VitalsArr = global.VitalsArr;
    if (!VitalsArr || !VitalsArr.length || !global.BasicContexts || !global.BasicContexts.length) {
      return global.BadVitalsValue || {};
    }

    // Нужно для детекторов «badState» и «suddenChange»: сравниваем предыдущее и текущее базовое состояние.
    var prevBadNormWell = (typeof global.BadNormWell === 'number') ? global.BadNormWell : 0;

    var badVitals = calculateBadVitalsValues();
    var contextIds = selectBasicContexts(badVitals);

    global.BadVitalsValue = badVitals;
    global.BasicContextsActived = contextIds;

    updateImportanceMetrics(badVitals);
    GetBadNormWell();

    // Инфо-детекторы по базовым контекстам и виталам:
    // badState/suddenChange, критический витал, игровой режим, режим обучения / поисковый интерес, страх, агрессия, защита.
    if (typeof global.InfoDet_updateBadState === 'function') {
      global.InfoDet_updateBadState(prevBadNormWell, global.BadNormWell || 0);
    }
    if (typeof global.InfoDet_updateCriticalVital === 'function') {
      global.InfoDet_updateCriticalVital(global.VitalsArr || []);
    }
    if (typeof global.InfoDet_updatePlayMode === 'function') {
      global.InfoDet_updatePlayMode(global.BasicContextsActived || []);
    }
    if (typeof global.InfoDet_updateLearningAndSearch === 'function') {
      global.InfoDet_updateLearningAndSearch(global.BasicContextsActived || [], global.VitalsArr || []);
    }
    // Страх (id контекста 6) и агрессия (id контекста 7) – см. BasicContexts.js.
    var act = global.BasicContextsActived || [];
    var fearActive = act.indexOf(6) !== -1;
    var aggrActive = act.indexOf(7) !== -1;
    if (typeof global.InfoDet_updateFear === 'function') {
      global.InfoDet_updateFear(fearActive);
    }
    if (typeof global.InfoDet_updateAggression === 'function') {
      global.InfoDet_updateAggression(aggrActive);
    }
    if (typeof global.InfoDet_updateDefense === 'function') {
      global.InfoDet_updateDefense(fearActive || aggrActive);
    }

    var suddenBadTransition = (prevBadNormWell !== 1 && (global.BadNormWell || 0) === 1);
    var sharpValueWorsening = detectSharpVitalValueWorsening(VitalsArr, prevVitalValuesSnapshot);
    tryTriggerConsciousnessOnVitalWorsening(suddenBadTransition, sharpValueWorsening);
    prevVitalValuesSnapshot = snapshotVitalValues(VitalsArr);

    if (cpuls === 2) {
      getDiffImportance();
    }

    return badVitals;
  }

  global.getActiveBasicContexts = getActiveBasicContexts;
  global.GetBadNormWell = GetBadNormWell;
  global.getDiffImportance = getDiffImportance;
  global.normalizeValue = normalizeValue;
  global.SetWellForHolding = SetWellForHolding;
  global.SetBadForHolding = SetBadForHolding;
  global.SetNormForHolding = SetNormForHolding;
  global.ActivateBasicContextsForHolding = ActivateBasicContextsForHolding;
})(window);

════════════════════════════════════════════════════════════

/BEAST/beast/_3_perception/about_perception.md
════════════════════════════════════════════════════════════
# Восприятие и стимулы (_3_perception)

## Назначение

Модуль задаёт **стимулы восприятия** и их отображение в интерфейсе. Стимулы — входы, на которые возможны генетические и условные рефлексы; из активных стимулов формируется конечный образ (FinalImage) и ветка дерева восприятия.

## Виды стимулов

- **StimulMotivarion** — мотивирующие стимулы (Еда, Вода, Тепло, Холод, Поощрение, Наказание, Приятно, Смех, Играть, Учить и др.). Связаны с виталами (`vitalID`, `shift`), при активации могут менять виталы и контексты.

- **StimulPositiv** — позитивные стимулы (Улыбка, Смех, Пение, Ласка и т.д.).

- **StimulNegative** — негативные стимулы (Злоба, Вопль, Стон, Огонь и т.д.).

- **StimulNeutral** — нейтральные стимулы (цвета, формы, объекты).

Активные в данный момент наборы хранятся в **ActiveMotivationStimuls**, **ActivePositivStimuls**, **ActiveNegativeStimuls**, **ActiveNeutralStimuls** (массивы ID).

## Файлы

- **stimuls.js** — объявление массивов стимулов и глобальных массивов активных стимулов.
- **stimuls_ui.js** — кнопки стимулов, переключение активности (toggle), вызов эффектов контекстов и **OrientationReflex_runOnStimulusActivation** после изменения набора стимулов.

При активации/деактивации стимулов вызывается ориентировочный рефлекс (с отложенным запуском на следующий тик), обновляются образы и дерево восприятия.

## Индикатор «готовности к ориентировочному рефлексу»

В `stimuls_ui.js` рядом с кнопками стимулов показывается зрелость семантических структур для запуска ОР.

- Бордер утолщается у кнопок, у которых для их (условного) `FinalImageId` выполняется `samplesCount >= OR_well_known` (поэтому `remainingActivations === 0`).
- Обновление бордера сделано **экономно**:
  - полный пересчёт вызывается из `puls.js` **только на 2-м пульсе после старта** (чтобы учесть `BadNormWell` и `BasicContextsActived`);
  - далее бордер пересчитывается **точечно** для конкретного `finalImageId` после изменения семантики в `semantic_memory.js`.
- `mouseenter` обновляет только `title` (подсказку), чтобы программисты не путали визуальный бордер с hover-логикой.

════════════════════════════════════════════════════════════

/BEAST/beast/_3_perception/stimuls.js
════════════════════════════════════════════════════════════
/*
Стимулы воприятия
Генетически предопределенные реакции на стимулы.
На любые такие стимулы может вырабатываться условный рефлекс.
*/

/* Мотивирующие стимулы: 
vitalID на какой витал воздейставуют, 
shift - воздействие с каждым пульсов активного стимулы
bg - фоновый цвет индикатора: 1 - розовый, 2 - белый, 3- светло-зеленый
Вода и еда как и Играть с Учить должны возвращаться норму действием Beast  
Когда Beast начинает есть или пить улучшение его состояния вызывает Хорошо и продолжается до насыщения (это уже сделано).
Для Гона тоже нужно такое сделать...
*/
window.StimulMotivarion = [
{ Id: 1, vitalID: 6, shift: 0, name: "Еда",   bg: 3,   title: "Дать еду" },// вода и еда должны поглощаться действием Beast и тогда возвращать норму
{ Id: 2, vitalID: 3, shift: 0, name: "Вода",   bg: 3,   title: "Дать воду" },
{ Id: 3, vitalID: 4, shift: -1, name: "Тепло",  bg: 2,    title: "Согреть" },
{ Id: 4, vitalID: 5, shift: -1, name: "Холод",   bg: 2,   title: "Охладить" },
{ Id: 5, vitalID: 2, shift: -2, name: "Воздух",  bg: 3,    title: "Свежий воздух" },
{ Id: 6, vitalID: 1, shift: -0.5, name: "Лечение",  bg: 3,    title: "Начать лечение" },
{ Id: 7, vitalID: 7, shift: 0, name: "Поощрение",  bg: 3,    title: "Наградить " },
{ Id: 8, vitalID: 7, shift: 0, name: "Наказание",  bg: 1,    title: "Наказать" },
{ Id: 9, vitalID: 7, shift: 0, name: "Приятно",   bg: 3,   title: "Сделать приятно" },
{ Id: 10, vitalID: 7, shift: 0, name: "Неприятно",  bg: 1,    title: "Сделать больно" },
{ Id: 11, vitalID: 7, shift: 0, name: "Успокоить",  bg: 3,    title: "Успокоить" },
{ Id: 12, vitalID: 7, shift: 0, name: "Понятно",   bg: 3,   title: "Понятно" },
{ Id: 13, vitalID: 7, shift: 0, name: "Непонятно",  bg: 1,    title: "Непонятно" },
{ Id: 14, vitalID: 7, shift: 0, name: "Простить",  bg: 3,    title: "Прощение за проступок" },
{ Id: 15, vitalID: 7, shift: 0, name: "Смех",   bg: 3,   title: "Засемяться" },
{ Id: 16, vitalID: 7, shift: 0, name: "Радость",  bg: 3,    title: "Показать радость" },
{ Id: 17, vitalID: 8, shift: 2, name: "Краш",    bg: 2,  title: "Объект полового влечения" },
{ Id: 18, vitalID: 7, shift: 0, name: "Игнорировать",  bg: 1,    title: "Демонстративно проигнорирвать сделанное" },
{ Id: 19, vitalID: 11, shift: 0, name: "Играть",  bg: 3,    title: "Предложить играть" },
{ Id: 20, vitalID: 12, shift: 0, name: "Учить",   bg: 2,   title: "Предложить учиться" },
];

// Активные мотивирующие стимулы (по Id) для применения shift на каждом пульсе
window.ActiveMotivationStimuls = [];

/* Позитивные стимулы - фон светло-зеленый
ОТМЕНЕНО: При клике кнопка подсвечивается и витаз ID 7 изменяет значение на -10
При повторном клике кнопка дезактивируется, подсветка убирается.
Так можно набирать картину активированных кнопок, на которые будет реагирвоать Beast.
*/
window.StimulPositiv = [
  { Id: 1, name: "Улыбка" },
  { Id: 2, name: "Ребенок" },
  { Id: 3, name: "Щенок" },
  { Id: 4, name: "Цветок" },
  { Id: 5, name: "Солнце" },
  { Id: 6, name: "Бабочка" },
  { Id: 7, name: "Ягода" },
  { Id: 8, name: "Колокольчик" },
  { Id: 9, name: "Смех" },
  { Id: 10, name: "Пение" },
  { Id: 11, name: "Мелодия" },
  { Id: 12, name: "Шелест" },
  { Id: 13, name: "Мурлыкание" },
  { Id: 14, name: "Сладость" },
 {  Id: 15, name: "Аромат" },
  { Id: 16, name: "Тепло" },
  { Id: 17, name: "Ветерок" },
  { Id: 18, name: "Прохлада" },
  { Id: 19, name: "Прикосновение" },
  { Id: 20, name: "Ласка" },
];
  
/* Негативные стимулы - фон розовый
ОТМЕНЕНО: При клике кнопка подсвечивается и витаз ID 7 увеличивает значение на +10
При повторном клике кнопка дезактивируется, подсветка убирается.
Так можно набирать картину активированных кнопок, на которые будет реагирвоать Beast.
*/
window.StimulNegative = [
  { Id: 1, name: "Злоба" },
  { Id: 2, name: "Кровь" },
  { Id: 3, name: "Разруха" },
  { Id: 4, name: "Паук" },
  { Id: 5, name: "Змея" },
  { Id: 6, name: "Гриб" },
  { Id: 7, name: "Гниль" },
  { Id: 8, name: "Вопль" },
  { Id: 9, name: "Стон" },
  { Id: 10, name: "Сирена" },
  { Id: 11, name: "Писк" },
  { Id: 12, name: "Скрип" },
  { Id: 13, name: "Рычание" },
  { Id: 14, name: "Осколки" },
  { Id: 15, name: "Молния" },
  { Id: 16, name: "Кинжал" },
  { Id: 17, name: "Горечь" },
  { Id: 18, name: "Кислятина" },
  { Id: 19, name: "Вонь" },
  { Id: 20, name: "Огонь" }
];
/* Нейтральные стимулы - фон светло-серый
При клике кнопка подсвечивается.
При повторном клике кнопка дезактивируется, подсветка убирается.
Так можно набирать картину активированных кнопок, на которые будет реагирвоать Beast.
На основе этих стимулов могут возникать условные рефлексы.
*/
window.StimulNeutral = [
  { Id: 1, name: "Коричневое" },
  { Id: 2, name: "Красное" },
  { Id: 3, name: "Желтое" },
  { Id: 4, name: "Зеленое" },
  { Id: 5, name: "Голубое" },
  { Id: 6, name: "Синее" },
  { Id: 7, name: "Фиолетовое" },
  { Id: 8, name: "Черное" },
  { Id: 9, name: "Белое" },
  { Id: 10, name: "Круг" },
  { Id: 11, name: "Квадрат" },
  { Id: 12, name: "Дуга" },
  { Id: 13, name: "Точка" },
  { Id: 14, name: "Зигзаг" },
  { Id: 15, name: "Линия" },
  { Id: 16, name: "Звезда" },
  { Id: 17, name: "Крест" },
  { Id: 18, name: "Треугольник" },
  { Id: 19, name: "Шум" },
  { Id: 20, name: "Свист" },
  { Id: 21, name: "Гул" },
  { Id: 22, name: "Звон" },
  { Id: 23, name: "Вибрация" },
  { Id: 24, name: "Книга" },
  { Id: 25, name: "Стол" },
  { Id: 26, name: "Стул" },
  { Id: 27, name: "Кровать" },
  { Id: 28, name: "Лампочка" },
  { Id: 29, name: "Растение" },
  { Id: 30, name: "Животное" },
  { Id: 31, name: "Человек" },
];


════════════════════════════════════════════════════════════

/BEAST/beast/_3_perception/stimuls_ui.js
════════════════════════════════════════════════════════════
(function (global) {
  'use strict';

  var ROW_ID = 'stimulsRow';

  /**
   * Есть ли среди кнопок стимулов хотя бы один с семантикой, достаточной для старта ОР
   * (тот же критерий, что «Участвует в ориентировочном рефлексe»: remainingActivations === 0).
   * Полный обход — на 2-м пульсе (puls.js); далее флаг поднимается точечно при обновлении семантики.
   * Используется диспетчером: авто-show_alert лога первого цикла после загрузки страницы.
   */
  if (typeof global.OrientationReflex_anyStimulusReadyForOr !== 'boolean') {
    global.OrientationReflex_anyStimulusReadyForOr = false;
  }

  /** Ключ контекста для «готовности к ОР»: well|emotionId|ветка дерева — при смене нужен полный пересчёт бордеров. */
  var lastStimulusReadinessContextKey = null;

  /** Игровой режим / обучение на инфокартине ↔ isGameMode, isAuthoritarianEducation. */
  function syncInfoPlayLearningDetectors() {
    if (typeof global.InfoDet_updatePlayMode === 'function') {
      global.InfoDet_updatePlayMode(global.BasicContextsActived || []);
    }
    if (typeof global.InfoDet_updateLearningAndSearch === 'function') {
      global.InfoDet_updateLearningAndSearch(global.BasicContextsActived || [], global.VitalsArr || []);
    }
    if (typeof global.InfoDet_updateOperatorIgnoringFromStimulus === 'function') {
      global.InfoDet_updateOperatorIgnoringFromStimulus();
    }
  }

  /**
   * Краткая подсветка кнопки на один пульс при запуске ОР/ФО.
   * Сияние добавляется сверху и снизу по 15px.
   */
  function flashStimulusButtonForOnePulse(button) {
    if (!button) return;
    var pulseNow = (typeof global.Cur_puls_val === 'number') ? global.Cur_puls_val : 0;
    var untilPulse = pulseNow + 1;
    var glow = '0 -15px 12px -4px rgba(80, 170, 255, 0.9), 0 15px 12px -4px rgba(80, 170, 255, 0.9)';
    var prevShadow = button.style.boxShadow || '';
    button.dataset.orGlowPrevShadow = prevShadow;
    button.dataset.orGlowUntilPulse = String(untilPulse);
    button.style.boxShadow = prevShadow ? (prevShadow + ', ' + glow) : glow;

    if (button._orGlowTimer) {
      global.clearInterval(button._orGlowTimer);
    }
    button._orGlowTimer = global.setInterval(function () {
      var curPulse = (typeof global.Cur_puls_val === 'number') ? global.Cur_puls_val : 0;
      if (curPulse < untilPulse) return;
      if (button._orGlowTimer) {
        global.clearInterval(button._orGlowTimer);
        button._orGlowTimer = null;
      }
      var restore = button.dataset.orGlowPrevShadow || '';
      button.style.boxShadow = restore;
      delete button.dataset.orGlowPrevShadow;
      delete button.dataset.orGlowUntilPulse;
    }, 50);
  }

  /**
   * Обновить видимость «готовых к ОР» кнопок без наведения мыши.
   * Готовая кнопка = `remainingActivations === 0` для её (условного) FinalImageId.
   *
   * Зависит от:
   * - SemanticMemoryIndex (для samplesCount)
   * - BadNormWell и BasicContextsActived (для well/emotionId)
   *
   * Полный обход — при смене базового состояния/эмоции/ID активной ветки дерева восприятия
   * (см. `scheduleStimulusReadinessRefreshIfNeeded` из `puls.js`); иначе бордеры залипают
   * после смены виталов или ветки. Точечно — при изменении семантики
   * (см. `updateStimulusButtonsReadinessViewForFinalImageId`).
   *
   * Важно: обработчики `mouseenter`/`mouseleave` обновляют только `title`, не бордер.
   */
  function updateStimulusButtonsReadinessView() {
    if (!global.document) return;
    if (typeof global.OrientationReflex_getOrReadinessForFinalImageId !== 'function') return;
    if (typeof global.StimulusGlobalId_toGlobal !== 'function') return;
    if (typeof global.StimulusGlobalId_getTypeFromRowId !== 'function') return;
    if (typeof global.FinalImages_findFinalImageId !== 'function') return;

    var anyOrReady = false;
    var sawNumericReadiness = false;
    var rowIds = [ROW_ID, 'stimulsRowPositiv', 'stimulsRowNegative', 'stimulsRowNeutral'];
    for (var r = 0; r < rowIds.length; r++) {
      var rowId = rowIds[r];
      var row = global.document.getElementById(rowId);
      if (!row) continue;
      var buttons = row.querySelectorAll('button[data-stim-id]');
      for (var i = 0; i < buttons.length; i++) {
        var btn = buttons[i];
        var stimId = parseInt(btn.dataset.stimId, 10);
        if (!stimId) continue;

        var baseTitle = btn.dataset.baseTitle || btn.title || '';
        if (!baseTitle) baseTitle = String(btn.textContent || '');

        var type = global.StimulusGlobalId_getTypeFromRowId(rowId);
        var globalStimId = global.StimulusGlobalId_toGlobal(type, stimId);
        if (!globalStimId) {
          btn.style.border = '1px solid #888';
          btn.title = baseTitle;
          continue;
        }

        var cachedFinalImageId = parseInt(btn.dataset.readinessFinalImageId, 10);
        var finalImageId = (cachedFinalImageId && cachedFinalImageId > 0)
          ? cachedFinalImageId
          : (global.FinalImages_findFinalImageId([globalStimId]) || 0);
        // Если finalImageId ещё не найден (0), будем продолжать пытаться на следующих пульсах.
        btn.dataset.readinessFinalImageId = String(finalImageId);
        var readiness = global.OrientationReflex_getOrReadinessForFinalImageId(finalImageId);

        if (readiness && typeof readiness.remainingActivations === 'number') {
          sawNumericReadiness = true;
          if (readiness.remainingActivations === 0) {
            anyOrReady = true;
            btn.style.border = '2px solid #888';
            btn.title = baseTitle + '\nУчаствует в ориентировочном рефлексе';
          } else {
            btn.style.border = '1px solid #888';
            btn.title = baseTitle + '\nУровень готовности для ОР: ' + String(readiness.remainingActivations) + ' активаций осталось';
          }
        } else {
          btn.style.border = '1px solid #888';
          btn.title = baseTitle + '\nУровень готовности для ОР: неизвестно (контексты ещё не определены)';
        }
      }
    }
    if (!anyOrReady) {
      if (Array.isArray(global.ActiveMotivationStimuls) &&
          (global.ActiveMotivationStimuls.indexOf(1) !== -1 || global.ActiveMotivationStimuls.indexOf(2) !== -1)) {
        anyOrReady = true;
      }
    }
    if (!anyOrReady) {
      var bnw = typeof global.BadNormWell === 'number' ? global.BadNormWell : 0;
      if (bnw === 1) {
        anyOrReady = true;
      } else if (!sawNumericReadiness) {
        // На раннем 2-м пульсе emotionId/well часто ещё не дают числовой готовности (null у всех
        // кнопок) — иначе блокировался бы стартовый лог первого цикла на 3-м пульсе.
        anyOrReady = true;
      }
    }
    global.OrientationReflex_anyStimulusReadyForOr = anyOrReady;
  }

  /**
   * Вызов из пульса: если изменились BadNormWell, эмоция (BasicContexts) или ID активной ветки
   * дерева восприятия — заново прогоняем `updateStimulusButtonsReadinessView`.
   */
  function scheduleStimulusReadinessRefreshIfNeeded() {
    var well = typeof global.BadNormWell === 'number' ? global.BadNormWell : 0;
    var emotionId = 0;
    if (typeof global.Emotions_getOrCreateEmotionId === 'function' && Array.isArray(global.BasicContextsActived)) {
      emotionId = global.Emotions_getOrCreateEmotionId(global.BasicContextsActived) || 0;
    }
    var branchId = 0;
    if (typeof global.PerceptionTree_getActiveTerminalNodeId === 'function') {
      branchId = global.PerceptionTree_getActiveTerminalNodeId() || 0;
    }
    var key = String(well) + '|' + String(emotionId) + '|' + String(branchId);
    if (key === lastStimulusReadinessContextKey) return;
    lastStimulusReadinessContextKey = key;
    updateStimulusButtonsReadinessView();
  }

  /**
   * Точечно обновить готовность для кнопок, относящихся к конкретному finalImageId.
   * Вызывается при изменении семантики (registerSemanticSample) после добавления нового образца.
   *
   * Так мы избегаем перерасчёта для всех кнопок на каждом пульсе.
   * @param {number} finalImageId
   */
  function updateStimulusButtonsReadinessViewForFinalImageId(finalImageId) {
    finalImageId = parseInt(finalImageId, 10);
    if (!finalImageId) return;
    if (!global.document) return;
    if (typeof global.OrientationReflex_getOrReadinessForFinalImageId !== 'function') return;
    if (typeof global.StimulusGlobalId_toGlobal !== 'function') return;
    if (typeof global.StimulusGlobalId_getTypeFromRowId !== 'function') return;
    if (typeof global.FinalImages_findFinalImageId !== 'function') return;

    var baseReadiness = global.OrientationReflex_getOrReadinessForFinalImageId(finalImageId);

    var rowIds = [ROW_ID, 'stimulsRowPositiv', 'stimulsRowNegative', 'stimulsRowNeutral'];
    for (var r = 0; r < rowIds.length; r++) {
      var rowId = rowIds[r];
      var row = global.document.getElementById(rowId);
      if (!row) continue;
      var buttons = row.querySelectorAll('button[data-stim-id]');
      for (var i = 0; i < buttons.length; i++) {
        var btn = buttons[i];
        var stimId = parseInt(btn.dataset.stimId, 10);
        if (!stimId) continue;

        var cachedFinalImageId = parseInt(btn.dataset.readinessFinalImageId, 10) || 0;
        if (cachedFinalImageId && cachedFinalImageId !== finalImageId) continue;

        var type = global.StimulusGlobalId_getTypeFromRowId(rowId);
        var globalStimId = global.StimulusGlobalId_toGlobal(type, stimId);
        if (!globalStimId) continue;
        var computedFinalImageId = global.FinalImages_findFinalImageId([globalStimId]) || 0;
        if (computedFinalImageId !== finalImageId) continue;

        btn.dataset.readinessFinalImageId = String(computedFinalImageId);
        var baseTitle = btn.dataset.baseTitle || btn.title || '';
        if (!baseTitle) baseTitle = String(btn.textContent || '');

        if (baseReadiness && typeof baseReadiness.remainingActivations === 'number') {
          if (baseReadiness.remainingActivations === 0) {
            btn.style.border = '2px solid #888';
            btn.title = baseTitle + '\nУчаствует в ориентировочном рефлексе';
          } else {
            btn.style.border = '1px solid #888';
            btn.title = baseTitle + '\nУровень готовности для ОР: ' +
              String(baseReadiness.remainingActivations) + ' активаций осталось';
          }
        } else {
          btn.style.border = '1px solid #888';
          btn.title = baseTitle + '\nУровень готовности для ОР: неизвестно (контексты ещё не определены)';
        }
      }
    }
    if (baseReadiness && baseReadiness.remainingActivations === 0) {
      global.OrientationReflex_anyStimulusReadyForOr = true;
    }
  }

  function clearStimRow(rowId) {
    var activeKey = null;
    if (rowId === ROW_ID) {
      activeKey = 'ActiveMotivationStimuls';
      // Сброс режимов Играть / Учить
      global.isGameMode = false;
      global.isAuthoritarianEducation = false;
    } else if (rowId === 'stimulsRowPositiv') {
      activeKey = 'ActivePositivStimuls';
    } else if (rowId === 'stimulsRowNegative') {
      activeKey = 'ActiveNegativeStimuls';
    } else if (rowId === 'stimulsRowNeutral') {
      activeKey = 'ActiveNeutralStimuls';
    }

    if (activeKey && Array.isArray(global[activeKey])) {
      global[activeKey] = [];
    }

    var row = global.document.getElementById(rowId);
    if (!row) return;

    var buttons = row.querySelectorAll('button');
    for (var i = 0; i < buttons.length; i++) {
      var btn = buttons[i];
      var baseBg = btn.dataset.baseBg || '#f8f8f8';
      btn.style.backgroundColor = baseBg;
      btn.style.boxShadow = 'none';
    }

    if (rowId === ROW_ID && typeof updateMotivationButtonsView === 'function') {
      updateMotivationButtonsView();
    }
    if (rowId === ROW_ID) {
      syncInfoPlayLearningDetectors();
    }
  }

  /** Дезактивировать все стимулы всех видов (мотивирующие, позитивные, негативные, нейтральные). */
  function clearAllStimuli() {
    var rowIds = [ROW_ID, 'stimulsRowPositiv', 'stimulsRowNegative', 'stimulsRowNeutral'];
    for (var r = 0; r < rowIds.length; r++) {
      clearStimRow(rowIds[r]);
    }
    if (typeof global.update_vitals_view === 'function') {
      global.update_vitals_view();
    }
    syncInfoPlayLearningDetectors();
  }

  function updateMotivationButtonsView() {
    var row = global.document.getElementById(ROW_ID);
    if (!row || !global.StimulMotivarion) return;
    var buttons = row.querySelectorAll('button[data-stim-id]');
    for (var i = 0; i < buttons.length; i++) {
      var btn = buttons[i];
      var id = parseInt(btn.dataset.stimId, 10);
      if (!id) continue;
      var stim = null;
      for (var j = 0; j < global.StimulMotivarion.length; j++) {
        if (global.StimulMotivarion[j].Id === id) {
          stim = global.StimulMotivarion[j];
          break;
        }
      }
      if (!stim) continue;

      // Цвета по полю bg: 1 - розовый, 2 - белый, 3 - светло-зелёный
      var bgType = stim.bg || 2;
      var baseBg = '#f8f8f8';
      var activeBg = '#ffe4b5';
      var shadowColor = 'rgba(200, 160, 80, 0.8)';
      if (bgType === 1) {
        baseBg = '#ffe6f0';
        activeBg = '#ffccd9';
        shadowColor = 'rgba(255, 140, 180, 0.8)';
      } else if (bgType === 3) {
        baseBg = '#e0f8e0';
        activeBg = '#c8f0c8';
        shadowColor = 'rgba(80, 200, 120, 0.8)';
      }
      btn.dataset.baseBg = baseBg;

      var active = false;
      if (stim.vitalID === 11) {
        active = !!global.isGameMode;
      } else if (stim.vitalID === 12) {
        active = !!global.isAuthoritarianEducation;
      } else {
        active = isStimActive(stim.Id);
      }

      if (active) {
        btn.style.backgroundColor = activeBg;
        btn.style.boxShadow = '0 0 8px 2px ' + shadowColor;
      } else {
        btn.style.backgroundColor = baseBg;
        btn.style.boxShadow = 'none';
      }
    }
  }

  function ensureStimulRow(rowId, labelText, insertAfterId) {
    var row = global.document.getElementById(rowId);
    if (row) {
      return row;
    }
    var content = global.document.querySelector('.beast-content');
    if (!content) {
      return null;
    }
    var label = global.document.createElement('div');
    label.style.display = 'flex';
    label.style.alignItems = 'center';
    label.style.justifyContent = 'space-between';
    label.style.fontSize = '10pt';
    label.style.marginTop = '4px';
    label.style.marginBottom = '2px';

    var labelTextSpan = global.document.createElement('span');
    labelTextSpan.textContent = labelText;
    label.appendChild(labelTextSpan);

    var clearBtn = global.document.createElement('button');
    clearBtn.type = 'button';
    clearBtn.textContent = '×';
    clearBtn.title = 'Сбросить все стимулы этой строки\nСброс всех видов стимулов - по двойному клику на любом стимуле';
    clearBtn.style.width = '18px';
    clearBtn.style.height = '18px';
    clearBtn.style.borderRadius = '50%';
    clearBtn.style.border = '1px solid #aaa';
    clearBtn.style.backgroundColor = '#f0f0f0';
    clearBtn.style.color = '#666';
    clearBtn.style.cursor = 'pointer';
    clearBtn.style.padding = '0';
    clearBtn.style.fontSize = '11px';
    clearBtn.style.lineHeight = '16px';
    clearBtn.addEventListener('click', function () {
      clearStimRow(rowId);
    });
    label.appendChild(clearBtn);

    row = global.document.createElement('div');
    row.id = rowId;
    row.style.display = 'flex';
    row.style.flexWrap = 'wrap';
    row.style.gap = '4px';
    row.style.marginBottom = '10px';
    row.style.marginTop = '6px';

    var ref = global.document.getElementById(insertAfterId);
    if (ref && ref.parentNode === content) {
      content.insertBefore(label, ref.nextSibling);
      content.insertBefore(row, label.nextSibling);
    } else {
      content.appendChild(label);
      content.appendChild(row);
    }
    return row;
  }

  function ensureRow() {
    return ensureStimulRow(ROW_ID, 'Мотивирующие стимулы:', 'basicContextsRow');
  }

  function isStimActive(id) {
    if (!Array.isArray(global.ActiveMotivationStimuls)) return false;
    return global.ActiveMotivationStimuls.indexOf(id) !== -1;
  }

  function toggleStimActive(id) {
    if (!Array.isArray(global.ActiveMotivationStimuls)) {
      global.ActiveMotivationStimuls = [];
    }
    var idx = global.ActiveMotivationStimuls.indexOf(id);
    if (idx === -1) {
      global.ActiveMotivationStimuls.push(id);
    } else {
      global.ActiveMotivationStimuls.splice(idx, 1);
    }
  }

  function applyVitalDelta(vitalID, delta) {
    if (!global.VitalsArr || delta === 0 || vitalID <= 0 || vitalID > 10) return;
    for (var i = 0; i < global.VitalsArr.length; i++) {
      if (global.VitalsArr[i].Id === vitalID) {
        var v = global.VitalsArr[i];
        var newVal = v.value + delta;
        if (newVal < 0) newVal = 0;
        if (newVal > 100) newVal = 100;
        v.value = newVal;
        if (typeof global.saveVitalsToStorage === 'function') global.saveVitalsToStorage();
        if (typeof global.update_vitals_view === 'function') global.update_vitals_view();
        break;
      }
    }
  }

  function applyVital7Delta(delta) {
    applyVitalDelta(7, delta);
  }

  function applyMotivationEffects(stimId) {
    if (!global.StimulMotivarion_EffectsToBContexts) return;
    var effects = global.StimulMotivarion_EffectsToBContexts[stimId];
    if (!effects || !effects.length) return;
    for (var i = 0; i < effects.length; i++) {
      var eff = effects[i];
      if (!eff) continue;
      var vitalID = eff.vitalID;
      var delta = eff.delta || 0;
      if (vitalID >= 1 && vitalID <= 10) {
        applyVitalDelta(vitalID, delta);
      } else if (vitalID === 101) {
        if (typeof global.SetWellForHolding === 'function') {
          global.SetWellForHolding();
        }
      } else if (vitalID === 102) {
        if (typeof global.SetBadForHolding === 'function') {
          global.SetBadForHolding();
        }
      } else if (vitalID === 103) {
        var makeActiveGame = true;
        global.isGameMode = makeActiveGame;
        if (makeActiveGame) {
          global.isAuthoritarianEducation = false;
          // сбросить подсветку кнопки «Учить» (Id 20)
          var teachBtn = global.document.querySelector('#' + ROW_ID + ' button[data-stim-id=\"20\"]');
          if (teachBtn) {
            teachBtn.style.backgroundColor = teachBtn.dataset.baseBg || '#f8f8f8';
            teachBtn.style.boxShadow = 'none';
          }
        }
        if (typeof global.update_vitals_view === 'function') {
          global.update_vitals_view();
        }
        syncInfoPlayLearningDetectors();
      } else if (vitalID === 104) {
        var makeActiveEdu = true;
        global.isAuthoritarianEducation = makeActiveEdu;
        if (makeActiveEdu) {
          global.isGameMode = false;
          // сбросить подсветку кнопки «Играть» (Id 19)
          var playBtn = global.document.querySelector('#' + ROW_ID + ' button[data-stim-id=\"19\"]');
          if (playBtn) {
            playBtn.style.backgroundColor = playBtn.dataset.baseBg || '#f8f8f8';
            playBtn.style.boxShadow = 'none';
          }
        }
        if (typeof global.update_vitals_view === 'function') {
          global.update_vitals_view();
        }
        syncInfoPlayLearningDetectors();
      }
    }
  }

  function applyPerceptionContextEffects(rowId, stimId) {
    var map = null;
    if (rowId === 'stimulsRowPositiv') {
      map = global.StimulPositiv_EffectsToBContexts;
    } else if (rowId === 'stimulsRowNegative') {
      map = global.StimulNegative_EffectsToBContexts;
    }
    if (!map) return;
    var codes = map[stimId];
    if (!codes || !codes.length) return;

    var contextIds = [];
    for (var i = 0; i < codes.length; i++) {
      var code = codes[i];
      if (typeof code !== 'number') continue;
      if (code >= 1 && code <= 20) {
        contextIds.push(code);
      } else if (code === 101) {
        if (typeof global.SetWellForHolding === 'function') {
          global.SetWellForHolding();
        }
      } else if (code === 102) {
        if (typeof global.SetBadForHolding === 'function') {
          global.SetBadForHolding();
        }
      } else if (code === 103) {
        global.isGameMode = true;
        global.isAuthoritarianEducation = false;
      } else if (code === 104) {
        global.isAuthoritarianEducation = true;
        global.isGameMode = false;
      }
    }
    syncInfoPlayLearningDetectors();
    if (contextIds.length && typeof global.ActivateBasicContextsForHolding === 'function') {
      global.ActivateBasicContextsForHolding(contextIds);
    }
  }

  function isSimpleStimActive(activeKey, id) {
    if (!Array.isArray(global[activeKey])) return false;
    return global[activeKey].indexOf(id) !== -1;
  }

  function toggleSimpleStim(activeKey, id) {
    if (!Array.isArray(global[activeKey])) {
      global[activeKey] = [];
    }
    var idx = global[activeKey].indexOf(id);
    if (idx === -1) {
      global[activeKey].push(id);
    } else {
      global[activeKey].splice(idx, 1);
    }
  }

  function showStimulusUnderstanding(stimId, sourceType, frameIndex) {
    if (typeof global.show_alert !== 'function') {
      return;
    }
    var name = '';
    var arr = null;
    if (sourceType === 'motivation') {
      arr = global.StimulMotivarion;
    } else if (sourceType === 'positive') {
      arr = global.StimulPositiv;
    } else if (sourceType === 'negative') {
      arr = global.StimulNegative;
    } else if (sourceType === 'neutral') {
      arr = global.StimulNeutral;
    }
    if (arr && Array.isArray(arr)) {
      for (var i = 0; i < arr.length; i++) {
        if (arr[i] && arr[i].Id === stimId) {
          name = arr[i].name || '';
          break;
        }
      }
    }

    if (typeof global.FinalImages_getOrCreateFinalImageId !== 'function' ||
        typeof global.getImageUnderstandingModel !== 'function') {
      global.show_alert('Модель понимания для стимула ' + name + ' пока недоступна.');
      return;
    }

    // Глобальный id стимула (type*1000+localId), чтобы образ восприятия однозначно соответствовал типу стимула.
    var globalStimId = stimId;
    if (typeof global.StimulusGlobalId_toGlobal === 'function' && typeof global.StimulusGlobalId_getTypeFromSourceType === 'function') {
      var type = global.StimulusGlobalId_getTypeFromSourceType(sourceType);
      globalStimId = global.StimulusGlobalId_toGlobal(type, stimId);
    }
    var finalImageId = global.FinalImages_getOrCreateFinalImageId([globalStimId]);
    if (!finalImageId) {
      global.show_alert('Для стимула ' + name + ' ещё нет семантических данных.');
      return;
    }

    // Пересборка модели из текущей SemanticMemory, чтобы число обучений и значимость всегда были актуальными.
    if (typeof global.buildImageUnderstandingModel === 'function') {
      global.buildImageUnderstandingModel(finalImageId);
    }
    var model = global.getImageUnderstandingModel(finalImageId) || [];
    if (!model.length) {
      global.show_alert('Для стимула ' + name + ' ещё нет семантических данных.');
      return;
    }

    function wellLabel(w) {
      if (w === 1) return 'Плохо';
      if (w === 2) return 'Норма';
      if (w === 3) return 'Хорошо';
      return String(w);
    }

    function emotionContexts(emotionId) {
      if (!Array.isArray(global.Emotions) || !Array.isArray(global.BasicContexts)) {
        return '';
      }
      var ctxIds = null;
      for (var i = 0; i < global.Emotions.length; i++) {
        if (global.Emotions[i] && global.Emotions[i].id === emotionId) {
          ctxIds = global.Emotions[i].contextIds || [];
          break;
        }
      }
      if (!ctxIds || !ctxIds.length) return '';
      var names = [];
      for (var j = 0; j < ctxIds.length; j++) {
        var cid = ctxIds[j];
        for (var k = 0; k < global.BasicContexts.length; k++) {
          if (global.BasicContexts[k] && global.BasicContexts[k].id === cid) {
            names.push(global.BasicContexts[k].name || '');
            break;
          }
        }
      }
      return names.filter(Boolean).join(', ');
    }

    // Вспомогательная функция для поиска названия стимула по Id
    function findStimulusNameById(id) {
      var groups = [global.StimulMotivarion, global.StimulPositiv, global.StimulNegative, global.StimulNeutral];
      for (var g = 0; g < groups.length; g++) {
        var arrG = groups[g];
        if (!arrG || !Array.isArray(arrG)) continue;
        for (var h = 0; h < arrG.length; h++) {
          if (arrG[h] && arrG[h].Id === id) {
            return arrG[h].name || '';
          }
        }
      }
      return '';
    }

    // Если запрошен конкретный кадр модели — показываем детализацию по нему
    if (typeof frameIndex === 'number' && frameIndex >= 0 && frameIndex < model.length) {
      var r = model[frameIndex];
      var htmlDetail = '<b>Модель понимания стимула ' + (name || ('Id ' + stimId)) + '</b><br>' +
        '<b>Кадр памяти #' + (frameIndex + 1) + '</b><br><br>' +
        '<b>Состояние:</b> ' + wellLabel(r.wellNormaBad) + '<br>' +
        '<b>EmotionId:</b> ' + r.emotionId + '<br>' +
        '<b>Эмоциональные контексты:</b> ' + (emotionContexts(r.emotionId) || '—') + '<br><br>' +
        '<b>Статистика значимости:</b><br>' +
        'mean z = ' + (r.meanImportance || 0).toFixed(3) + '<br>' +
        'last z = ' + (r.lastImportance || 0).toFixed(3) + '<br>' +
        'эпизодов = ' + (r.samplesCount || 0) + '<br><br>';

      // Компоненты конечного образа (FinalImage)
      var components = [];
      if (Array.isArray(global.FinalImages)) {
        for (var fi = 0; fi < global.FinalImages.length; fi++) {
          var fiItem = global.FinalImages[fi];
          if (fiItem && fiItem.id === finalImageId && Array.isArray(fiItem.stimulIds)) {
            for (var si = 0; si < fiItem.stimulIds.length; si++) {
              var sid = fiItem.stimulIds[si];
              var sName = findStimulusNameById(sid);
              components.push(sName || ('Id ' + sid));
            }
            break;
          }
        }
      }
      htmlDetail += '<b>Компоненты конечного образа (FinalImageId ' + finalImageId + '):</b><br>' +
        (components.length ? components.join(', ') : '—') + '<br>';

      global.show_alert(htmlDetail);
      return;
    }

    // Иначе показываем сводную модель по всем кадрам с кликабельными значениями last z
    var totalSamples = 0;
    var sumWeighted = 0;
    for (var m = 0; m < model.length; m++) {
      var it = model[m];
      var cnt = it.samplesCount || 0;
      totalSamples += cnt;
      sumWeighted += (it.meanImportance || 0) * cnt;
    }
    var overallMean = totalSamples > 0 ? (sumWeighted / totalSamples) : 0;

    var html = '<b>Модель понимания стимула ' + (name || ('Id ' + stimId)) + '</b><br>' +
      'Средняя семантическая значимость z ~ ' + overallMean.toFixed(2) + '<br>' +
      'Число обучений: ' + totalSamples + '<br>' +
      '<br><b>Кадры памяти:</b> ';

    for (var n = 0; n < model.length; n++) {
      var r2 = model[n];
      var zLast = (r2.lastImportance || 0).toFixed(1);
      html += '<span data-frame-idx="' + n + '" ' +
        'style="cursor:pointer;text-decoration:underline;color:#1565C0;margin-right:6px;">' +
        zLast + '</span>';
    }
    html += '<br>';

    global.show_alert(html);

    // После показа диалога навешиваем обработчики на числа z
    global.setTimeout(function () {
      var container = global.document.querySelector('.alert-dialog .alert-message');
      if (!container) return;
      var nodes = container.querySelectorAll('[data-frame-idx]');
      for (var i2 = 0; i2 < nodes.length; i2++) {
        (function (el) {
          el.addEventListener('click', function (ev) {
            ev.preventDefault();
            ev.stopPropagation();
            var idx = parseInt(el.getAttribute('data-frame-idx'), 10);
            if (!isNaN(idx)) {
              if (typeof global.close_alert === 'function') {
                global.close_alert();
              }
              // Перезапускаем диалог уже для одного кадра
              showStimulusUnderstanding(stimId, sourceType, idx);
            }
          });
        })(nodes[i2]);
      }
    }, 0);
  }

  function attachInfoIcon(button, stimId, sourceType) {
    button.style.position = 'relative';
    button.style.borderTopRightRadius = '0';
    button.style.paddingRight = '16px';

    var info = global.document.createElement('span');
    info.textContent = 'i';
    info.title = 'Показать модель понимания стимула';
    info.style.position = 'absolute';
    info.style.top = '-3px';
    info.style.right = '-3px';
    info.style.width = '10px';
    info.style.height = '10px';
    info.style.fontSize = '8pt';
    info.style.lineHeight = '10px';
    info.style.textAlign = 'center';
    info.style.borderRadius = '2px';
    info.style.border = '1px solid #555';
    info.style.backgroundColor = '#fff';
    info.style.cursor = 'pointer';
    info.style.padding = '0';

    info.addEventListener('click', function (ev) {
      ev.stopPropagation();
      ev.preventDefault();
      showStimulusUnderstanding(stimId, sourceType);
    });

    button.appendChild(info);
  }

  function buildRowForSimpleStimuls(rowId, labelText, stimulArray, activeKey, deltaVital7, insertAfterId) {
    var row = ensureStimulRow(rowId, labelText, insertAfterId);
    if (!row || !Array.isArray(stimulArray) || stimulArray.length === 0) return;
    row.innerHTML = '';
    var baseBg = deltaVital7 > 0 ? '#ffe6f0' : (deltaVital7 < 0 ? '#e0f8e0' : '#f0f0f0');
    var activeBg = deltaVital7 > 0 ? '#ffccd9' : (deltaVital7 < 0 ? '#c8f0c8' : '#e8e8e8');
    var shadowColor = deltaVital7 > 0 ? 'rgba(255, 140, 180, 0.8)' : (deltaVital7 < 0 ? 'rgba(80, 200, 120, 0.8)' : 'rgba(120, 120, 120, 0.5)');
    for (var i = 0; i < stimulArray.length; i++) {
      var stim = stimulArray[i];
      var btn = global.document.createElement('button');
      btn.type = 'button';
      btn.textContent = stim.name || ('S' + stim.Id);
      btn.title = stim.title || stim.name || '';
      btn.dataset.baseTitle = btn.title;
      btn.dataset.stimId = String(stim.Id);
      btn.style.padding = '2px 8px';
      btn.style.fontSize = '9pt';
      btn.style.borderRadius = '10px';
      btn.style.border = '1px solid #888';
      btn.style.cursor = 'pointer';
      btn.style.color = '#000';
      btn.style.backgroundColor = baseBg;
      btn.dataset.baseBg = baseBg;
      attachInfoIcon(
        btn,
        stim.Id,
        rowId === 'stimulsRowPositiv'
          ? 'positive'
          : rowId === 'stimulsRowNegative'
            ? 'negative'
            : 'neutral'
      );

      // Наведение мыши: в подсказке показываем «готовность для старта ОР».
      // Бордер управляется обновлением из `updateStimulusButtonsReadinessView*`,
      // а не hover-логикой (иначе визуальный индикатор "скачет").
      // (сколько активаций осталось до порога OR_well_known).
      (function (sLocal, buttonLocal, rowIdLocal) {
        var baseTitle = sLocal && (sLocal.title || sLocal.name) ? (sLocal.title || sLocal.name) : ('S' + (sLocal ? sLocal.Id : ''));
        buttonLocal.addEventListener('mouseenter', function () {
          if (typeof global.OrientationReflex_getOrReadinessForFinalImageId !== 'function' ||
              typeof global.StimulusGlobalId_toGlobal !== 'function' ||
              typeof global.StimulusGlobalId_getTypeFromRowId !== 'function' ||
              typeof global.FinalImages_findFinalImageId !== 'function') {
            buttonLocal.title = baseTitle;
            return;
          }

          var type = global.StimulusGlobalId_getTypeFromRowId(rowIdLocal);
          var globalStimId = global.StimulusGlobalId_toGlobal(type, sLocal.Id);
          if (!globalStimId) {
            buttonLocal.title = baseTitle;
            return;
          }
          var finalImageId = global.FinalImages_findFinalImageId([globalStimId]) || 0;
          var readiness = global.OrientationReflex_getOrReadinessForFinalImageId(finalImageId);
          var remaining = readiness && typeof readiness.remainingActivations === 'number'
            ? readiness.remainingActivations
            : '';
          if (typeof readiness.remainingActivations === 'number') {
            if (readiness.remainingActivations === 0) {
              buttonLocal.title = baseTitle + '\nУчаствует в ориентировочном рефлексе';
            } else {
              buttonLocal.title = baseTitle + '\nУровень готовности для ОР: ' + String(remaining) + ' активаций осталось';
            }
          } else {
            buttonLocal.title = baseTitle + '\nУровень готовности для ОР: неизвестно (контексты ещё не определены)';
          }
        });
        buttonLocal.addEventListener('mouseleave', function () {
          buttonLocal.title = baseTitle;
        });
      })(stim, btn, rowId);

      (function (s, button, rowKey) {
        button.addEventListener('dblclick', function () {
          clearAllStimuli();
        });
        button.addEventListener('click', function () {
          if (typeof global.getDiffImportance === 'function') {
            // Базовая фиксация значимости ДО действий Beast
            global.getDiffImportance();
          }
          toggleSimpleStim(activeKey, s.Id);
          var active = isSimpleStimActive(activeKey, s.Id);
          if (active) {
            button.style.backgroundColor = activeBg;
            button.style.boxShadow = '0 0 8px 2px ' + shadowColor;
            // Позитивные/негативные стимулы воздействуют на базовые контексты,
            // а не напрямую на витал 7
            if (rowKey === 'stimulsRowPositiv' || rowKey === 'stimulsRowNegative') {
              if (typeof applyPerceptionContextEffects === 'function') {
                applyPerceptionContextEffects(rowKey, s.Id);
              }
            }
          } else {
            button.style.backgroundColor = button.dataset.baseBg || baseBg;
            button.style.boxShadow = 'none';
          }
          if (typeof global.update_vitals_view === 'function') {
            global.update_vitals_view();
          }
          if (typeof global.OrientationReflex_runOnStimulusActivation === 'function') {
            var startedFoSimple = global.OrientationReflex_runOnStimulusActivation();
            if (startedFoSimple) {
              flashStimulusButtonForOnePulse(button);
            }
          }
        });
      })(stim, btn, rowId);
      row.appendChild(btn);
    }
  }

  function buildButtons() {
    var row = ensureRow();
    if (!row || !global.StimulMotivarion) {
      return;
    }
    row.innerHTML = '';
    for (var i = 0; i < global.StimulMotivarion.length; i++) {
      var stim = global.StimulMotivarion[i];
      var btn = global.document.createElement('button');
      btn.type = 'button';
      btn.textContent = stim.name || ('S' + stim.Id);
      btn.title = stim.title || stim.name || '';
      btn.dataset.baseTitle = btn.title;
      btn.dataset.stimId = String(stim.Id);
      btn.dataset.vitalId = String(stim.vitalID);
      btn.style.padding = '2px 8px';
      btn.style.fontSize = '9pt';
      btn.style.borderRadius = '10px';
      btn.style.border = '1px solid #888';
      btn.style.cursor = 'pointer';
      btn.style.color = '#000';

      // Цвета по полю bg: 1 - розовый, 2 - белый, 3 - светло-зелёный
      var bgType = stim.bg || 2;
      var baseBg = '#f8f8f8';
      if (bgType === 1) {
        baseBg = '#ffe6f0'; // светло-розовый
      } else if (bgType === 3) {
        baseBg = '#e0f8e0'; // светло-зелёный
      }
      btn.style.backgroundColor = baseBg;
      btn.dataset.baseBg = baseBg;
      attachInfoIcon(btn, stim.Id, 'motivation');

      (function (s, button, bgTypeLocal) {
        // Наведение мыши: подсказка с готовностью для старта ОР
        var baseTitle = (s && (s.title || s.name)) ? (s.title || s.name) : ('S' + (s ? s.Id : ''));
        button.addEventListener('mouseenter', function () {
          if (typeof global.OrientationReflex_getOrReadinessForFinalImageId !== 'function' ||
              typeof global.StimulusGlobalId_toGlobal !== 'function' ||
              typeof global.StimulusGlobalId_getTypeFromRowId !== 'function' ||
              typeof global.FinalImages_findFinalImageId !== 'function') {
            button.title = baseTitle;
            return;
          }

          var type = global.StimulusGlobalId_getTypeFromRowId(ROW_ID);
          var globalStimId = global.StimulusGlobalId_toGlobal(type, s.Id);
          if (!globalStimId) {
            button.title = baseTitle;
            return;
          }

          var finalImageId = global.FinalImages_findFinalImageId([globalStimId]) || 0;
          var readiness = global.OrientationReflex_getOrReadinessForFinalImageId(finalImageId);
          var remaining = readiness && typeof readiness.remainingActivations === 'number'
            ? readiness.remainingActivations
            : '';
          if (typeof readiness.remainingActivations === 'number') {
            if (readiness.remainingActivations === 0) {
              button.title = baseTitle + '\nУчаствует в ориентировочном рефлексе';
            } else {
              button.title = baseTitle + '\nУровень готовности для ОР: ' + String(remaining) + ' активаций осталось';
            }
          } else {
            button.title = baseTitle + '\nУровень готовности для ОР: неизвестно (контексты ещё не определены)';
          }
        });
        button.addEventListener('mouseleave', function () {
          button.title = baseTitle;
        });

        button.addEventListener('dblclick', function () {
          clearAllStimuli();
        });
        button.addEventListener('click', function () {
          // Особый случай: Играть / Учить — завязаны на глобальных режимах,
          // а не на массиве ActiveMotivationStimuls
          if (s.vitalID === 11 || s.vitalID === 12) {
            var makeActive;
            if (s.vitalID === 11) {
              makeActive = !global.isGameMode;
              global.isGameMode = makeActive;
              if (makeActive) {
                global.isAuthoritarianEducation = false;
                // сбросить подсветку кнопки «Учить» (Id 20)
                var teachBtn = global.document.querySelector('#' + ROW_ID + ' button[data-stim-id="20"]');
                if (teachBtn) {
                  teachBtn.style.backgroundColor = teachBtn.dataset.baseBg || '#f8f8f8';
                  teachBtn.style.boxShadow = 'none';
                }
              }
            } else {
              makeActive = !global.isAuthoritarianEducation;
              global.isAuthoritarianEducation = makeActive;
              if (makeActive) {
                global.isGameMode = false;
                // сбросить подсветку кнопки «Играть» (Id 19)
                var playBtn = global.document.querySelector('#' + ROW_ID + ' button[data-stim-id="19"]');
                if (playBtn) {
                  playBtn.style.backgroundColor = playBtn.dataset.baseBg || '#f8f8f8';
                  playBtn.style.boxShadow = 'none';
                }
              }
            }

            // визуальная подсветка для самой кнопки по bg
            if (makeActive) {
              if (bgTypeLocal === 1) {
                button.style.backgroundColor = '#ffccd9';
                button.style.boxShadow = '0 0 8px 2px rgba(255, 140, 180, 0.8)';
              } else if (bgTypeLocal === 3) {
                button.style.backgroundColor = '#c8f0c8';
                button.style.boxShadow = '0 0 8px 2px rgba(80, 200, 120, 0.8)';
              } else {
                button.style.backgroundColor = '#ffe4b5';
                button.style.boxShadow = '0 0 8px 2px rgba(200, 160, 80, 0.8)';
              }
            } else {
              button.style.backgroundColor = button.dataset.baseBg || '#f8f8f8';
              button.style.boxShadow = 'none';
            }

            // разовые эффекты мотивационного стимула — только при активации
            if (makeActive) {
              applyMotivationEffects(s.Id);
            }

            if (typeof global.update_vitals_view === 'function') {
              global.update_vitals_view();
            }
            if (typeof global.OrientationReflex_runOnStimulusActivation === 'function') {
              var startedFoMode = global.OrientationReflex_runOnStimulusActivation();
              if (startedFoMode) {
                flashStimulusButtonForOnePulse(button);
              }
            }
            syncInfoPlayLearningDetectors();
            return;
          }

          // Обычные мотивирующие стимулы: сначала фиксируем базовую значимость,
          // затем переключаем активность в массиве
          if (typeof global.getDiffImportance === 'function') {
            global.getDiffImportance();
          }
          toggleStimActive(s.Id);
          var active = isStimActive(s.Id);
          syncInfoPlayLearningDetectors();

          // визуальная подсветка
          if (active) {
            if (bgTypeLocal === 1) {
              button.style.backgroundColor = '#ffccd9'; // более насыщённый розовый
              button.style.boxShadow = '0 0 8px 2px rgba(255, 140, 180, 0.8)';
            } else if (bgTypeLocal === 3) {
              button.style.backgroundColor = '#c8f0c8'; // более насыщённый зелёный
              button.style.boxShadow = '0 0 8px 2px rgba(80, 200, 120, 0.8)';
            } else {
              button.style.backgroundColor = '#ffe4b5';
              button.style.boxShadow = '0 0 8px 2px rgba(200, 160, 80, 0.8)';
            }
          } else {
            button.style.backgroundColor = button.dataset.baseBg || '#f8f8f8';
            button.style.boxShadow = 'none';
          }

          // разовые эффекты стимула по новой схеме: только при активации
          if (active) {
            applyMotivationEffects(s.Id);
            if (s.Id === 1 || s.Id === 2) {
              if (global.automatismRanForBranchIds && typeof global.automatismRanForBranchIds === 'object') {
                global.automatismRanForBranchIds = {};
              }
            }
          }
          if (typeof global.OrientationReflex_runOnStimulusActivation === 'function') {
            var startedFoMotivation = global.OrientationReflex_runOnStimulusActivation();
            if (startedFoMotivation) {
              flashStimulusButtonForOnePulse(button);
            }
          }
        });
      })(stim, btn, bgType);
      row.appendChild(btn);
    }
    updateMotivationButtonsView();
  }

  function init() {
    buildButtons();
    if (!Array.isArray(global.ActivePositivStimuls)) global.ActivePositivStimuls = [];
    if (!Array.isArray(global.ActiveNegativeStimuls)) global.ActiveNegativeStimuls = [];
    if (!Array.isArray(global.ActiveNeutralStimuls)) global.ActiveNeutralStimuls = [];
    // Позитивные: зелёный фон, воздействие через StimulPositiv_EffectsToBContexts
    buildRowForSimpleStimuls('stimulsRowPositiv', 'Позитивные стимулы:', global.StimulPositiv, 'ActivePositivStimuls', -1, ROW_ID);
    // Негативные: розовый фон, воздействие через StimulNegative_EffectsToBContexts
    buildRowForSimpleStimuls('stimulsRowNegative', 'Негативные стимулы:', global.StimulNegative, 'ActiveNegativeStimuls', 1, 'stimulsRowPositiv');
    // Дополнительная красная кнопка «Тест» после негативных стимулов — жёсткий запуск прерывания осмысления.
    var negRow = global.document && global.document.getElementById('stimulsRowNegative');
    if (negRow && typeof global.runTestNegativeInterrupt === 'function') {
      var btn = global.document.createElement('button');
      btn.textContent = 'Тест';
      btn.title = 'Тестовый стимул с максимально негативной значимостью для вызова прерывания цикла осмысления';
      btn.style.backgroundColor = '#ff4444';
      btn.style.color = '#ffffff';
      btn.style.border = '1px solid #aa0000';
      btn.style.marginLeft = '8px';
      btn.style.cursor = 'pointer';
      btn.onclick = function () {
        global.runTestNegativeInterrupt();
      };
      negRow.appendChild(btn);
    }
    buildRowForSimpleStimuls('stimulsRowNeutral', 'Нейтральные стимулы:', global.StimulNeutral, 'ActiveNeutralStimuls', 0, 'stimulsRowNegative');
  }

  if (global.document.readyState === 'loading') {
    global.document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }

  /**
   * Включить стимул по записи window.BasicMirrorActions (Id): тот же смысл, что нажатие соответствующей кнопки оператора.
   * Если стимул уже активен — состояние и разовые эффекты не дублируются; вызывается OrientationReflex_runOnStimulusActivation.
   *
   * @param {number} basicMirrorActionId — поле Id из BasicMirrorActions
   * @returns {boolean}
   */
  function activateStimulusForBasicMirrorActionId(basicMirrorActionId) {
    var mid = parseInt(basicMirrorActionId, 10);
    if (!mid) return false;
    var list = global.BasicMirrorActions;
    if (!Array.isArray(list)) return false;
    var rec = null;
    for (var mi = 0; mi < list.length; mi++) {
      if (list[mi] && list[mi].Id === mid) {
        rec = list[mi];
        break;
      }
    }
    if (!rec || rec.mirrorParentId == null) return false;
    var parts = String(rec.mirrorParentId).split('_');
    var typeNum = parseInt(parts[0], 10);
    var stimId = parseInt(parts[1], 10);
    if (!typeNum || !stimId) return false;

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

    if (typeNum === 1) {
      var stimObj = null;
      if (global.StimulMotivarion) {
        for (var mj = 0; mj < global.StimulMotivarion.length; mj++) {
          if (global.StimulMotivarion[mj] && global.StimulMotivarion[mj].Id === stimId) {
            stimObj = global.StimulMotivarion[mj];
            break;
          }
        }
      }
      if (stimObj && (stimObj.vitalID === 11 || stimObj.vitalID === 12)) {
        var wantGame = stimObj.vitalID === 11;
        var already = wantGame ? !!global.isGameMode : !!global.isAuthoritarianEducation;
        if (!already) {
          if (wantGame) {
            global.isGameMode = true;
            global.isAuthoritarianEducation = false;
          } else {
            global.isAuthoritarianEducation = true;
            global.isGameMode = false;
          }
          applyMotivationEffects(stimId);
        }
        if (typeof global.update_vitals_view === 'function') {
          global.update_vitals_view();
        }
        updateMotivationButtonsView();
        if (typeof global.OrientationReflex_runOnStimulusActivation === 'function') {
          global.OrientationReflex_runOnStimulusActivation();
        }
        syncInfoPlayLearningDetectors();
        return true;
      }
      if (!isStimActive(stimId)) {
        toggleStimActive(stimId);
        applyMotivationEffects(stimId);
      }
      syncInfoPlayLearningDetectors();
      if (typeof global.update_vitals_view === 'function') {
        global.update_vitals_view();
      }
      updateMotivationButtonsView();
      if (typeof global.OrientationReflex_runOnStimulusActivation === 'function') {
        global.OrientationReflex_runOnStimulusActivation();
      }
      return true;
    }

    var activeKey = null;
    var rowId = null;
    if (typeNum === 2) {
      rowId = 'stimulsRowPositiv';
      activeKey = 'ActivePositivStimuls';
    } else if (typeNum === 3) {
      rowId = 'stimulsRowNegative';
      activeKey = 'ActiveNegativeStimuls';
    } else if (typeNum === 4) {
      rowId = 'stimulsRowNeutral';
      activeKey = 'ActiveNeutralStimuls';
    } else {
      return false;
    }

    if (!isSimpleStimActive(activeKey, stimId)) {
      toggleSimpleStim(activeKey, stimId);
      if (rowId === 'stimulsRowPositiv' || rowId === 'stimulsRowNegative') {
        applyPerceptionContextEffects(rowId, stimId);
      }
    }
    if (typeof global.update_vitals_view === 'function') {
      global.update_vitals_view();
    }
    if (typeof global.OrientationReflex_runOnStimulusActivation === 'function') {
      global.OrientationReflex_runOnStimulusActivation();
    }
    return true;
  }

  // Экспортируем обновление «готовности к ОР» для вызова из puls.js.
  global.updateStimulusButtonsReadinessView = updateStimulusButtonsReadinessView;
  global.scheduleStimulusReadinessRefreshIfNeeded = scheduleStimulusReadinessRefreshIfNeeded;
  global.updateStimulusButtonsReadinessViewForFinalImageId = updateStimulusButtonsReadinessViewForFinalImageId;
  global.activateStimulusForBasicMirrorActionId = activateStimulusForBasicMirrorActionId;
})(window);


════════════════════════════════════════════════════════════

/BEAST/beast/_3_perception/stimulus_global_id.js
════════════════════════════════════════════════════════════
/**
 * Глобально уникальные ID стимулов (type * MULTIPLIER + localId).
 *
 * ПРИЧИНА ВВОДА: В разных массивах стимулов (StimulMotivarion, StimulPositiv, StimulNegative, StimulNeutral)
 * используются одни и те же числовые Id (например 10 = "Неприятно" в мотивации и 10 = "Пение" в позитивных).
 * При сохранении образа восприятия (FinalImage) и расшифровке по Id возникают коллизии — показывается
 * неверное имя стимула. Глобальный Id однозначно задаёт тип и локальный Id, поэтому расшифровка
 * и сравнение образов работают корректно при любом количестве стимулов в массивах (до 999 на тип).
 */
(function (global) {
  'use strict';

  /** Типы стимулов (совпадают с порядком массивов в коде). */
  var TYPE_MOTIVATION = 1;
  var TYPE_POSITIV = 2;
  var TYPE_NEGATIVE = 3;
  var TYPE_NEUTRAL = 4;

  /**
   * Максимум локальных id на один тип (1..MULTIPLIER-1).
   * При изменении числа стимулов в массивах решение остаётся рабочим, пока в каждом типе < 1000.
   */
  var MULTIPLIER = 1000;

  /**
   * Глобальный id по типу и локальному id.
   * @param {number} type — 1=Motivation, 2=Positiv, 3=Negative, 4=Neutral
   * @param {number} localId — Id в соответствующем массиве (Stimul*)
   */
  function toGlobalStimulusId(type, localId) {
    var t = parseInt(type, 10);
    var id = parseInt(localId, 10);
    if (isNaN(t) || isNaN(id) || id <= 0) return 0;
    if (t < 1 || t > 4) return 0;
    return t * MULTIPLIER + id;
  }

  /**
   * Разбор глобального id в тип и локальный id.
   * @param {number} globalId
   * @returns {{ type: number, localId: number } | null}
   */
  function fromGlobalStimulusId(globalId) {
    var g = parseInt(globalId, 10);
    if (isNaN(g) || g < MULTIPLIER) return null;
    var type = Math.floor(g / MULTIPLIER);
    var localId = g % MULTIPLIER;
    if (type < 1 || type > 4 || localId <= 0) return null;
    return { type: type, localId: localId };
  }

  /**
   * Имя стимула по глобальному id (для алертов и отображения).
   * Поддерживает старый формат: если globalId < MULTIPLIER, считается локальным id без типа
   * (обратная совместимость со старыми сохранёнными образами).
   */
  function getStimulusNameByGlobalId(globalId) {
    var g = parseInt(globalId, 10);
    if (isNaN(g) || g <= 0) return '';
    var parsed = fromGlobalStimulusId(g);
    var arr = null;
    if (parsed) {
      if (parsed.type === TYPE_MOTIVATION) arr = global.StimulMotivarion;
      else if (parsed.type === TYPE_POSITIV) arr = global.StimulPositiv;
      else if (parsed.type === TYPE_NEGATIVE) arr = global.StimulNegative;
      else if (parsed.type === TYPE_NEUTRAL) arr = global.StimulNeutral;
      if (arr && Array.isArray(arr)) {
        for (var i = 0; i < arr.length; i++) {
          if (arr[i] && arr[i].Id === parsed.localId) return arr[i].name || ('Id ' + parsed.localId);
        }
      }
      return '';
    }
    // Старый формат: id < MULTIPLIER — ищем по всем массивам (первое совпадение).
    var groups = [global.StimulMotivarion, global.StimulPositiv, global.StimulNegative, global.StimulNeutral];
    for (var h = 0; h < groups.length; h++) {
      if (!groups[h] || !Array.isArray(groups[h])) continue;
      for (var k = 0; k < groups[h].length; k++) {
        if (groups[h][k] && groups[h][k].Id === g) return groups[h][k].name || ('Id ' + g);
      }
    }
    return '';
  }

  /**
   * Массив глобальных id по текущим активным стимулам (для getOrCreateFinalImageId).
   * Активные массивы (ActiveMotivationStimuls и т.д.) хранят локальные id — здесь переводим в глобальные.
   */
  function getActiveGlobalStimulusIds() {
    var out = [];
    function add(type, activeArr) {
      if (!Array.isArray(activeArr)) return;
      for (var i = 0; i < activeArr.length; i++) {
        var localId = activeArr[i];
        if (typeof localId !== 'number' || localId <= 0) continue;
        var g = toGlobalStimulusId(type, localId);
        if (g && out.indexOf(g) === -1) out.push(g);
      }
    }
    add(TYPE_MOTIVATION, global.ActiveMotivationStimuls);
    add(TYPE_POSITIV, global.ActivePositivStimuls);
    add(TYPE_NEGATIVE, global.ActiveNegativeStimuls);
    add(TYPE_NEUTRAL, global.ActiveNeutralStimuls);
    out.sort(function (a, b) { return a - b; });
    return out;
  }

  /**
   * Тип стимула по идентификатору строки UI (для вызова из stimuls_ui).
   * @param {string} rowId — 'stimulsRowPositiv' | 'stimulsRowNegative' | 'stimulsRowNeutral' или контекст мотивации
   */
  function getStimulusTypeFromRowId(rowId) {
    if (rowId === 'stimulsRowPositiv') return TYPE_POSITIV;
    if (rowId === 'stimulsRowNegative') return TYPE_NEGATIVE;
    if (rowId === 'stimulsRowNeutral') return TYPE_NEUTRAL;
    return TYPE_MOTIVATION;
  }

  /** Тип по строке sourceType: 'motivation' | 'positive' | 'negative' | 'neutral'. */
  function getStimulusTypeFromSourceType(sourceType) {
    if (sourceType === 'motivation') return TYPE_MOTIVATION;
    if (sourceType === 'positive') return TYPE_POSITIV;
    if (sourceType === 'negative') return TYPE_NEGATIVE;
    if (sourceType === 'neutral') return TYPE_NEUTRAL;
    return TYPE_MOTIVATION;
  }

  global.StimulusGlobalId_toGlobal = toGlobalStimulusId;
  global.StimulusGlobalId_fromGlobal = fromGlobalStimulusId;
  global.StimulusGlobalId_getName = getStimulusNameByGlobalId;
  global.StimulusGlobalId_getActiveGlobalIds = getActiveGlobalStimulusIds;
  global.StimulusGlobalId_getTypeFromRowId = getStimulusTypeFromRowId;
  global.StimulusGlobalId_getTypeFromSourceType = getStimulusTypeFromSourceType;
  global.StimulusGlobalId_MULTIPLIER = MULTIPLIER;
})(typeof window !== 'undefined' ? window : this);

════════════════════════════════════════════════════════════

/BEAST/beast/_4_actions/about_actions.md
════════════════════════════════════════════════════════════
# Действия и эффекторы (_4_actions)

## Назначение

Модуль задаёт **генетически предопределённые программы действий** Beast: моторные и «внутренние» (влияние на базовое состояние и контексты). Действия — выходы рефлексов; на их основе строятся образы действия (ОД) и эпизодическая память.

## Основные данные

- **BasicActions** — базовые действия (Плачет, Смеется, Ест, Пьет, Хватает, Убегает, Улыбается, Злится и т.д.). Каждое имеет `Id` и `name`. С ними связаны генетические и условные рефлексы.

- **BasicVerbal** — базовые «физиологические» звуки (а, о, у, слоги, лепет). Используются для действия с Id == 100 (вербальный рефлекс).

- **BasicMirrorActions** — зеркальные действия оператора по кадрам ЭП (авторитарное обучение). Каждый элемент: `Id` (для образа действий), `mirrorParentId` в формате `"n_m"` (n — вид стимула 1..4, m — Id стимула в stimuls.js), `name`. Исключены дубликаты базовых действий (Еда, Вода, Играть, Учить).

## Файлы

- **effectors.js** — объявление массивов BasicActions, BasicVerbal и статического массива BasicMirrorActions.

Действия выбираются по картам генетических рефлексов (в _5_Genetic__reflexes) и по условным рефлексам (в _5_2_synonyms_reflexes); выполнение идёт через `startAction` / `runGeneticActionFromSynonymReflex` в genetic_reflexes_engine.js, с регистрацией в образах действия и эпизодической памяти.

════════════════════════════════════════════════════════════

/BEAST/beast/_4_actions/effectors.js
════════════════════════════════════════════════════════════
/* Генетически предопределенные программы действий.
Делятся на моторные и "внутренние" - изменение базового сотсояни или базовых контекстов.

Первичные внешние действия:
плакать начать есть, пить, издавать звуки какие получится, 
Первичные внутренние действия есть генетические, а позже (эпиз.память) отзеркаливаются спобобы управления - не плакать а что-то делать 
Простейшие реакции взрослых, такие как мимика, жесты или базовые моторные действия, начинают отзеркаливаться детьми примерно с 3 лет.

Дети часто способны быстро менять настроение с плохого на нормальное или хорошее благодаря внешним стимулам или внутренним механизмам саморегуляции. Это нормальная черта детской психики, особенно у дошкольников и младших школьников.
Объятия, поцелуи или простое объятие с родителем часто мгновенно улучшают настроение капризного ребенка, вызывая ощущение безопасности и любви. Дети эмоционально откликаются на такие проявления, переходя от плача к улыбке за минуты.
У годовалого ребенка внутренние механизмы смены настроения в основном ограничены примитивными формами саморегуляции, такими как отвлечение внимания и естественное угасание эмоций. 
В период кризиса (9-18 месяцев) ребенок испытывает противоречие между желанием самостоятельности и реальными возможностями, что вызывает истерики, но эмоции угасают сами через 1-5 минут, когда фрустрация ослабевает. Например, неудача дотянуться до игрушки провоцирует плач, который прекращается при переключении на другой стимул в поле зрения.

Ребёнок начинает реагировать мышечным избеганием неприятного и стремлением к приятному с первых недель жизни, уже на этапе новорождённости. Это базовые рефлексы, такие как отворот головы от раздражающего звука или света и поворот к приятным стимулам (голосу матери, теплу).

Новорождённые (0–3 месяца)
В этот период доминируют врожденные рефлексы: ребёнок отстраняется от неприятных ощущений (резкий свет, громкий шум, дискомфорт кожи), проявляя плач, сжатие кулачков или отталкивание. Стремление к приятному выражается в хватательном рефлексе и ориентацией на источник тепла или знакомый запах.

Раннее детство (3–12 месяцев)
Мышечные реакции усиливаются: малыш ползёт прочь от раздражителя, закрывает глаза/лицо руками или уши от шума. К 6–7 месяцам появляется избирательность — реакция на "чужих" с избеганием физического контакта, в то время как приятные стимулы вызывают ползание навстречу.

После 1 года
С 1–2 лет поведение усложняется: ходьба прочь, отворачивание, отказ от еды/игр с неприятными свойствами. Формируются защитные механизмы, включая отрицание и имитацию для снижения тревоги, что опирается на двигательные реакции.

Младенцы совершают множество рефлекторных и осознанных движений, которые развиваются по мере роста. Перечень основан на типичных этапах моторного развития от рождения до года.

При резком звуке, ярком свете или неприятном запахе ребёнок отворачивает голову в сторону, закрывает глаза или сжимает кулачки. Например, при громком шуме новорождённый морщится, отстраняется и может заплакать, напрягая мышцы шеи и лица.
При болезненном уколе или грубом прикосновении активируется рефлекс отведения: конечность отдергивается, пальцы разжимаются или сжимаются в защитный кулак. Классический пример — рефлекс Моро: при внезапном звуке или падении голова откидывается назад, руки разводятся и затем сводятся к груди.
Неприятный холод или тесная одежда вызывает общее напряжение мышц — поджатие ножек к животу или выгибание спины. Ребёнок выгибается дугой (опистотонус) от дискомфорта в животике или при рвоте, стремясь минимизировать контакт с раздражителем.
Малыш может сам отвлечься на новый звук, свет или движение, переводя внимание с негатива на любопытство, что быстро улучшает настроение.

Годовалый ребенок переключает базовые стили поведения (эмоциональные и поведенческие паттерны) в основном за счет внутренних механизмов, таких как привыкание, отвлечение внимания и сенсорная обработка, без сознательного контроля. Это происходит в течение секунд-минут, когда стимул ослабевает или появляется новый интерес, особенно во время кризиса 1 года.
Испуг → любопытство
При первом столкновении с новым объектом (чужой человек, громкий звук) активируется защитная реакция — ребенок замирает, плачет или отстраняется, но через 10–30 секунд привыкание снижает страх, и внимание переключается на исследование: малыш тянется, трогает или улыбается. Это базовый механизм адаптации: низкоинтенсивный страх (опасение) переходит в интерес при отсутствии угрозы.
Пищевое → защитное
Во время еды голод провоцирует хватательный стиль (поиск пищи), но если еда неприятна (горькая, слишком горячая), ребенок переключается на защитное поведение — отталкивает ложку, отворачивается или выплевывает за 5–15 секунд, активируя инстинкт избегания. Обратное (защитное → пищевое) происходит, когда голод перевешивает осторожность, например, при знакомой еде после паузы.
юбопытство → избегание возникает при пересыщении стимулами (усталость): малыш отвлекается от игрушки на сон или укрытие за 1–2 минуты.
*/

/* Базовые генетические действия
С ними есть генетические рефлексы и могту возникать условные рефлексы.
*/
window.BasicActions = [
  { Id: 1, name: "Плачет" },
  { Id: 2, name: "Смеется" },
  { Id: 3, name: "Ест" },
  { Id: 4, name: "Пьет" },
  { Id: 5, name: "Хватает" },
  { Id: 6, name: "Прибегает" },
  { Id: 7, name: "Убегает" },
  { Id: 8, name: "Пугается" },
  { Id: 9, name: "Ударяет" },
  { Id: 10, name: "Кричит" },
  { Id: 11, name: "Царапается" },
  { Id: 12, name: "Играет" },
  { Id: 13, name: "Тошнит" },
  { Id: 14, name: "Задыхается" },
  { Id: 15, name: "Прячется" },
  { Id: 16, name: "Ломает" },
  { Id: 17, name: "Спит" },
  { Id: 18, name: "Бросает" },
  { Id: 19, name: "Падает" },
  { Id: 20, name: "Удивляется" },
  { Id: 21, name: "Болеет" },
  { Id: 22, name: "Мерзнет" },
  { Id: 23, name: "Потеет" },
  { Id: 24, name: "Кашляет" },
  { Id: 25, name: "Молчит" },
  { Id: 26, name: "Кусается" },
  { Id: 27, name: "Улыбается" },
  { Id: 28, name: "Злится" },
  { Id: 29, name: "Туалет" },
  { Id: 30, name: "Дрожит" },
  ];

/* Базовые «физиологические» звуки
Случаные выборки и сочетания выборок с которыми могут возникать условные рефлексы.
*/
window.BasicVerbal = [
  { Id: 1, name: "а" },
  { Id: 2, name: "о" },
  { Id: 3, name: "у" },
  { Id: 4, name: "ы" },
  { Id: 5, name: "ао" },
  { Id: 6, name: "аы" },
  { Id: 7, name: "гы" },
  { Id: 8, name: "гу" },
  { Id: 9, name: "аааа" },
  { Id: 10, name: "ээээ" },
  { Id: 11, name: "кх" },
  { Id: 12, name: "гыыы" },
  { Id: 13, name: "гуу" },
  { Id: 14, name: "буу" },
  { Id: 15, name: "мгу" },
  { Id: 16, name: "гыа" },
  { Id: 17, name: "ба-ба-ба" },
  { Id: 18, name: "ма-ма-ма" },
  { Id: 19, name: "па-па-па" },
  { Id: 20, name: "да-да-да" }
];

/* Зеркальные действия оператора по кадрам ЭП (авторитарное обучение).
 * Каждый элемент — действие «оператор применил стимул»; Id используется в образе действий (ОД).
 * mirrorParentId: "n_m" — n вид стимула (1=Motivation, 2=Positiv, 3=Negative, 4=Neutral), m — Id стимула в stimuls.js.
 * Исключены: Еда(1_1), Вода(1_2), Играть(1_19), Учить(1_20) — дублируют базовые действия.
 */
window.BasicMirrorActions = [
  { Id: 1, mirrorParentId: "1_3", name: "Тепло" },
  { Id: 2, mirrorParentId: "1_4", name: "Холод" },
  { Id: 3, mirrorParentId: "1_5", name: "Воздух" },
  { Id: 4, mirrorParentId: "1_6", name: "Лечение" },
  { Id: 5, mirrorParentId: "1_7", name: "Поощрение" },
  { Id: 6, mirrorParentId: "1_8", name: "Наказание" },
  { Id: 7, mirrorParentId: "1_9", name: "Приятно" },
  { Id: 8, mirrorParentId: "1_10", name: "Неприятно" },
  { Id: 9, mirrorParentId: "1_11", name: "Успокоить" },
  { Id: 10, mirrorParentId: "1_12", name: "Понятно" },
  { Id: 11, mirrorParentId: "1_13", name: "Непонятно" },
  { Id: 12, mirrorParentId: "1_14", name: "Простить" },
  { Id: 13, mirrorParentId: "1_15", name: "Смех" },
  { Id: 14, mirrorParentId: "1_16", name: "Радость" },
  { Id: 15, mirrorParentId: "1_17", name: "Краш" },
  { Id: 16, mirrorParentId: "1_18", name: "Игнорировать" },
  { Id: 17, mirrorParentId: "2_1", name: "Улыбка" },
  { Id: 18, mirrorParentId: "2_2", name: "Ребенок" },
  { Id: 19, mirrorParentId: "2_3", name: "Щенок" },
  { Id: 20, mirrorParentId: "2_4", name: "Цветок" },
  { Id: 21, mirrorParentId: "2_5", name: "Солнце" },
  { Id: 22, mirrorParentId: "2_6", name: "Бабочка" },
  { Id: 23, mirrorParentId: "2_7", name: "Ягода" },
  { Id: 24, mirrorParentId: "2_8", name: "Колокольчик" },
  { Id: 25, mirrorParentId: "2_9", name: "Смех" },
  { Id: 26, mirrorParentId: "2_10", name: "Пение" },
  { Id: 27, mirrorParentId: "2_11", name: "Мелодия" },
  { Id: 28, mirrorParentId: "2_12", name: "Шелест" },
  { Id: 29, mirrorParentId: "2_13", name: "Мурлыкание" },
  { Id: 30, mirrorParentId: "2_14", name: "Сладость" },
  { Id: 31, mirrorParentId: "2_15", name: "Аромат" },
  { Id: 32, mirrorParentId: "2_16", name: "Тепло" },
  { Id: 33, mirrorParentId: "2_17", name: "Ветерок" },
  { Id: 34, mirrorParentId: "2_18", name: "Прохлада" },
  { Id: 35, mirrorParentId: "2_19", name: "Прикосновение" },
  { Id: 36, mirrorParentId: "2_20", name: "Ласка" },
  { Id: 37, mirrorParentId: "3_1", name: "Злоба" },
  { Id: 38, mirrorParentId: "3_2", name: "Кровь" },
  { Id: 39, mirrorParentId: "3_3", name: "Разруха" },
  { Id: 40, mirrorParentId: "3_4", name: "Паук" },
  { Id: 41, mirrorParentId: "3_5", name: "Змея" },
  { Id: 42, mirrorParentId: "3_6", name: "Гриб" },
  { Id: 43, mirrorParentId: "3_7", name: "Гниль" },
  { Id: 44, mirrorParentId: "3_8", name: "Вопль" },
  { Id: 45, mirrorParentId: "3_9", name: "Стон" },
  { Id: 46, mirrorParentId: "3_10", name: "Сирена" },
  { Id: 47, mirrorParentId: "3_11", name: "Писк" },
  { Id: 48, mirrorParentId: "3_12", name: "Скрип" },
  { Id: 49, mirrorParentId: "3_13", name: "Рычание" },
  { Id: 50, mirrorParentId: "3_14", name: "Осколки" },
  { Id: 51, mirrorParentId: "3_15", name: "Молния" },
  { Id: 52, mirrorParentId: "3_16", name: "Кинжал" },
  { Id: 53, mirrorParentId: "3_17", name: "Горечь" },
  { Id: 54, mirrorParentId: "3_18", name: "Кислятина" },
  { Id: 55, mirrorParentId: "3_19", name: "Вонь" },
  { Id: 56, mirrorParentId: "3_20", name: "Огонь" },
  { Id: 57, mirrorParentId: "4_1", name: "Коричневое" },
  { Id: 58, mirrorParentId: "4_2", name: "Красное" },
  { Id: 59, mirrorParentId: "4_3", name: "Желтое" },
  { Id: 60, mirrorParentId: "4_4", name: "Зеленое" },
  { Id: 61, mirrorParentId: "4_5", name: "Голубое" },
  { Id: 62, mirrorParentId: "4_6", name: "Синее" },
  { Id: 63, mirrorParentId: "4_7", name: "Фиолетовое" },
  { Id: 64, mirrorParentId: "4_8", name: "Черное" },
  { Id: 65, mirrorParentId: "4_9", name: "Белое" },
  { Id: 66, mirrorParentId: "4_10", name: "Круг" },
  { Id: 67, mirrorParentId: "4_11", name: "Квадрат" },
  { Id: 68, mirrorParentId: "4_12", name: "Дуга" },
  { Id: 69, mirrorParentId: "4_13", name: "Точка" },
  { Id: 70, mirrorParentId: "4_14", name: "Зигзаг" },
  { Id: 71, mirrorParentId: "4_15", name: "Линия" },
  { Id: 72, mirrorParentId: "4_16", name: "Звезда" },
  { Id: 73, mirrorParentId: "4_17", name: "Крест" },
  { Id: 74, mirrorParentId: "4_18", name: "Треугольник" },
  { Id: 75, mirrorParentId: "4_19", name: "Шум" },
  { Id: 76, mirrorParentId: "4_20", name: "Свист" },
  { Id: 77, mirrorParentId: "4_21", name: "Гул" },
  { Id: 78, mirrorParentId: "4_22", name: "Звон" },
  { Id: 79, mirrorParentId: "4_23", name: "Вибрация" },
  { Id: 80, mirrorParentId: "4_24", name: "Книга" },
  { Id: 81, mirrorParentId: "4_25", name: "Стол" },
  { Id: 82, mirrorParentId: "4_26", name: "Стул" },
  { Id: 83, mirrorParentId: "4_27", name: "Кровать" },
  { Id: 84, mirrorParentId: "4_28", name: "Лампочка" },
  { Id: 85, mirrorParentId: "4_29", name: "Растение" },
  { Id: 86, mirrorParentId: "4_30", name: "Животное" },
  { Id: 87, mirrorParentId: "4_31", name: "Человек" },
  { Id: 88, mirrorParentId: "1_19", name: "Хочет играть" },
  { Id: 89, mirrorParentId: "1_20", name: "Хочет учить" },
];
════════════════════════════════════════════════════════════

/BEAST/beast/_5_2_synonyms_reflexes/about_synonyms_reflexes.md
════════════════════════════════════════════════════════════
# Условные рефлексы — синонимы стимулов (_5_2_synonyms_reflexes)

## Назначение

Модуль реализует **условные (синонимические) рефлексы**: после нескольких сочетаний «совокупный образ стимулов + безусловная реакция» в одном контексте тот же образ в том же контексте начинает запускать то же действие без безусловного раздражителя. Ключом служит FinalImageId (совокупность активных стимулов), а не отдельная кнопка.

## Условия формирования

- Условный рефлекс **не формируется** по стимулу, который сам запустил безусловную реакцию (предотвращение самоссылки).
- Совокупный образ для ключа строится по **набору активных стимулов**, кроме того, что непосредственно запустил рефлекс.
- Учитывается **контекст**: строка из базового состояния (BadNormWell) и активных базовых контекстов (EmotionId). Рефлекс срабатывает только в подходящем контексте.
- После **REFLEX_READY_THRESHOLD** (3) подтверждённых сочетаний рефлекс считается зрелым (isReady) и может срабатывать по одному условному стимулу.

## Структура рефлекса

- `id`, `context` (строка контекста), `finalImageId`, `actionId`, `birthTime`, `lastActivation`, `tryCount`, `isReady`. Сохраняются в IndexedDB и в файл при «Сохранить».

## Файлы

- **synonyms_reflexes.js** — массив SynonymReflexes, формирование и усиление (onGeneticActionFired), попытка запуска (tryRunSynonymReflexes), проверка блокировки ОР при запуске (через genetic_reflexes_engine), сохранение/загрузка, кнопка «Ух» (сброс всех условных рефлексов).

Запуск действия из условного рефлекса идёт через **runGeneticActionFromSynonymReflex** в genetic_reflexes_engine; при блокировке ОР рефлекс откладывается с флагом noConditionalReflex, чтобы не запускать повторное обучение по этому же стимулу.

════════════════════════════════════════════════════════════

/BEAST/beast/_5_2_synonyms_reflexes/synonyms_reflexes.js
════════════════════════════════════════════════════════════
/**
 * Условные рефлексы (синонимы стимулов) для Beast.
 *
 * Базовая идея взята из OLD_CODE/InsectAgent/sys/condition_reflex.js:
 * - сочетание стимулов (условных) несколько раз сочетается с раздражителем,
 *   запускающим врождённый (генетический) рефлекс;
 * - в конкретном сочетании базовых контекстов (контекст = образ ситуации);
 * - после нескольких подтверждений (tryCount > 3) одно и то же сочетание стимулов
 *   в том же контексте запускает то же действие.
 *
 * Дополнение по сравнению с прототипом:
 * - если одновременно активировано несколько стимулов (любых видов), из них
 *   строится совокупный конечный образ (FinalImageId в FinalImage.js);
 * - именно этот FinalImageId становится "ключом" условного рефлекса;
 * - условный рефлекс создаёт синоним реагирования не только для нейтральных,
 *   а для любых видов стимулов;
 * - при формировании и срабатывании условного рефлекса соответствующий
 *   совокупный стимул (FinalImageId) должен быть активен "здесь и сейчас",
 *   иначе условный рефлекс не формируется и не запускается.
 *
 * Действие — это Id из window.BasicActions, запускаемый через
 * runGeneticActionFromSynonymReflex(), чтобы сохранить общую механику Beast.
 */
(function (global) {
  'use strict';

  /**
   * Структура условного (синонимического) рефлекса:
   * {
   *   id: number,
   *   context: string,      // "0,3,9" — сочетание базовых контекстов
   *   finalImageId: number, // Id конечного образа (FinalImageId)
   *   actionId: number,     // Id действия из BasicActions
   *   birthTime: number,    // Cur_puls_val при создании
   *   lastActivation: number, // последний пуск (Cur_puls_val)
   *   tryCount: number      // число подтверждённых сочетаний (зрелость > 3)
   * }
   */

  if (!Array.isArray(global.SynonymReflexes)) {
    global.SynonymReflexes = [];
  }
  if (typeof global.NextSynonymReflexId !== 'number') {
    global.NextSynonymReflexId = 0;
  }

  var REFLEX_READY_THRESHOLD = 3; // порог зрелости условного рефлекса (tryCount > threshold)
  var DEBUG_SYNONYM_REFLEX = false; // тестовый alert отключён по умолчанию
  var PrevActiveStimulIdsKey = null; // ключ активных стимулов для подавления повторного запуска на том же наборе

  /** Сравнение двух массивов как множеств (одни и те же элементы, порядок не важен). */
  function setsEqual(a, b) {
    if (!Array.isArray(a) || !Array.isArray(b)) return false;
    var sa = a.slice(0).sort(function (x, y) { return x - y; });
    var sb = b.slice(0).sort(function (x, y) { return x - y; });
    if (sa.length !== sb.length) return false;
    for (var i = 0; i < sa.length; i++) {
      if (sa[i] !== sb[i]) return false;
    }
    return true;
  }

  /** Устойчивый строковый ключ набора id стимулов (для анти-спама). */
  function activeIdsKey(ids) {
    if (!Array.isArray(ids) || !ids.length) return '';
    var a = ids.slice(0).sort(function (x, y) { return x - y; });
    return a.join(',');
  }

  function getCurrentPulse() {
    var v = global.Cur_puls_val;
    if (typeof v === 'number' && !isNaN(v)) return v;
    return 0;
  }

  /**
   * Построить строковый ключ текущего контекста для условных рефлексов.
   *
   * КОНТЕКСТ = сочетание:
   * - базового состояния BadNormWell (1 — Плохо, 2 — Норма, 3 — Хорошо);
   * - набора активных базовых контекстов (их id из BasicContextsActived).
   *
   * Формат ключа: "BadNormWell:id1,id2,..." например "1:10,13" или "3:9".
   * Такой ключ устойчив между сессиями (не зависит от внутренних EmotionId).
   * Если BadNormWell ещё не определён — возвращается 'none'.
   */
  function getCurrentContextKey() {
    var well = global.BadNormWell || 0;
    if (!well) return 'none';

    var ctxIds = [];
    if (Array.isArray(global.BasicContextsActived)) {
      for (var i = 0; i < global.BasicContextsActived.length; i++) {
        var cid = global.BasicContextsActived[i];
        if (typeof cid === 'number') {
          ctxIds.push(cid);
        }
      }
    }
    ctxIds.sort(function (a, b) { return a - b; });
    var ctxPart = ctxIds.length ? ctxIds.join(',') : '0';

    return String(well) + ':' + ctxPart;
  }

  function findSynonymReflexIndex(contextKey, finalImageId, actionId) {
    for (var i = 0; i < global.SynonymReflexes.length; i++) {
      var cr = global.SynonymReflexes[i];
      if (!cr) continue;
      if (cr.context === contextKey &&
          cr.finalImageId === finalImageId &&
          cr.actionId === actionId) {
        return i;
      }
    }
    return -1;
  }

  function getActionName(actionId) {
    var name = '';
    if (Array.isArray(global.BasicActions)) {
      for (var i = 0; i < global.BasicActions.length; i++) {
        var a = global.BasicActions[i];
        if (a && a.Id === actionId) {
          name = a.name || '';
          break;
        }
      }
    }
    return name;
  }

  function showTransientAlert(message) {
    if (typeof global.show_alert !== 'function') return;
    try {
      global.show_alert(message);
      if (typeof global.close_alert === 'function') {
        global.setTimeout(function () {
          try {
            global.close_alert();
          } catch (e) {}
        }, 1500);
      }
    } catch (e2) {}
  }

  function debugAlert(message) {
    if (!DEBUG_SYNONYM_REFLEX) return;
  }

  // ---------- Сохранение условных рефлексов в IndexedDB ----------

  var urDb = null;
  var UR_DB_NAME = 'BEAST_UR_DB';
  var UR_DB_VERSION = 1;
  var UR_STORE = 'conditional_reflexes_state';
  var UR_KEY = 'ur_singleton';
  var urDirty = false;
  var lastURSavedSig = '';

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

  function serializeConditionalReflexesState() {
    var list = [];
    if (Array.isArray(global.SynonymReflexes)) {
      for (var i = 0; i < global.SynonymReflexes.length; i++) {
        var r = global.SynonymReflexes[i];
        if (!r) continue;
        list.push({
          id: r.id,
          context: r.context,
          finalImageId: r.finalImageId,
          stimulIds: Array.isArray(r.stimulIds) ? r.stimulIds.slice(0) : [],
          actionId: r.actionId,
          birthTime: r.birthTime,
          lastActivation: r.lastActivation,
          tryCount: r.tryCount,
          isReady: !!r.isReady
        });
      }
    }
    return {
      reflexes: list,
      nextId: (typeof global.NextSynonymReflexId === 'number' ? global.NextSynonymReflexId : 0)
    };
  }

  function applyConditionalReflexesState(state) {
    if (!state) return;
    var list = Array.isArray(state.reflexes) ? state.reflexes : [];
    global.SynonymReflexes = [];
    for (var i = 0; i < list.length; i++) {
      var r = list[i];
      if (!r) continue;
      var tryCount = typeof r.tryCount === 'number' ? r.tryCount : 0;
      // После загрузки все зрелые рефлексы считаем готовыми к запуску,
      // чтобы не требовался лишний «шаг обучения» после обновления страницы.
      var isReady = tryCount > REFLEX_READY_THRESHOLD ? true : !!r.isReady;
      global.SynonymReflexes.push({
        id: r.id,
        context: r.context,
        finalImageId: r.finalImageId,
        stimulIds: Array.isArray(r.stimulIds) ? r.stimulIds.slice(0) : [],
        actionId: r.actionId,
        birthTime: r.birthTime,
        lastActivation: r.lastActivation,
        tryCount: tryCount,
        isReady: isReady
      });
    }
    if (typeof state.nextId === 'number' && state.nextId >= 0) {
      global.NextSynonymReflexId = state.nextId;
    } else {
      var maxId = 0;
      for (var j = 0; j < global.SynonymReflexes.length; j++) {
        if (global.SynonymReflexes[j].id > maxId) {
          maxId = global.SynonymReflexes[j].id;
        }
      }
      global.NextSynonymReflexId = maxId;
    }
  }

  function saveURStateToIndexedDB(state) {
    if (!global.IndexedDBModule) return;
    state = state || serializeConditionalReflexesState();
    function doPut(db) {
      return global.IndexedDBModule.put({
        db: db,
        storeName: UR_STORE,
        value: {
          id: UR_KEY,
          state: state,
          ts: Date.now()
        }
      }).catch(function () {});
    }
    if (urDb) {
      doPut(urDb);
      return;
    }
    openURDb().then(doPut).catch(function () {});
  }

  function scheduleURStateSave() {
    urDirty = true;
  }

  function loadURStateFromIndexedDB() {
    if (!global.IndexedDBModule) return;
    openURDb()
      .then(function (db) {
        return global.IndexedDBModule.get({
          db: db,
          storeName: UR_STORE,
          key: UR_KEY
        });
      })
      .then(function (record) {
        if (record && record.state) {
          applyConditionalReflexesState(record.state);
          lastURSavedSig = JSON.stringify(serializeConditionalReflexesState());
        }
      })
      .catch(function () {});
  }

  /**
   * Сбросить все условные рефлексы в памяти и удалить их из IndexedDB.
   * Вызывается по кнопке «Ух» в UI.
   */
  function clearAllConditionalReflexes() {
    global.SynonymReflexes = [];
    global.NextSynonymReflexId = 0;
    PrevActiveStimulIdsKey = null;
    var emptyState = { reflexes: [], nextId: 0 };
    lastURSavedSig = JSON.stringify(emptyState);
    if (global.IndexedDBModule) {
      function doPut(db) {
        global.IndexedDBModule.put({
          db: db,
          storeName: UR_STORE,
          value: {
            id: UR_KEY,
            state: emptyState,
            ts: Date.now()
          }
        }).catch(function () {});
      }
      if (urDb) {
        doPut(urDb);
      } else {
        openURDb().then(doPut).catch(function () {});
      }
    }
  }

  // Заглушка для совместимости с UI нейтральных стимулов.
  // В текущей версии условный рефлекс опирается на совокупный FinalImageId,
  // поэтому отдельное запоминание "предыдущего нейтрального" не требуется.
  function onNeutralStimulusActivated(stimId) {
    void stimId;
  }

  /**
   * Попытка формирования/усиления условного рефлекса.
   * Вызывается из genetic_reflexes_engine, когда сработал генетический рефлекс
   * (из StimulMotivarion / StimulPositiv / StimulNegative).
   *
   * stimId     — Id стимула-«безусловного раздражителя»
   * sourceType — 'motivation' | 'positive' | 'negative'
   * actionId   — Id действия из BasicActions, которое реально выполнено.
   */
  function onGeneticActionFired(stimId, sourceType, actionId) {
    if (typeof actionId !== 'number' || actionId < 0) return;
    if (typeof global.FinalImages_getOrCreateFinalImageId !== 'function') {
      return;
    }

    // Глобальные id стимулов (type*1000+localId) — устраняют коллизии между массивами (см. stimulus_global_id.js).
    var activeIds = typeof global.StimulusGlobalId_getActiveGlobalIds === 'function'
      ? global.StimulusGlobalId_getActiveGlobalIds()
      : [];

    // Если нет ни одного активного стимула — условный рефлекс не формируется.
    if (!activeIds.length) {
      return;
    }

    // Глобальный id стимула, вызвавшего реакцию (для исключения его из образа условного рефлекса).
    var triggerGlobalId = 0;
    if (typeof global.StimulusGlobalId_toGlobal === 'function' && typeof global.StimulusGlobalId_getTypeFromSourceType === 'function') {
      var type = global.StimulusGlobalId_getTypeFromSourceType(sourceType);
      triggerGlobalId = global.StimulusGlobalId_toGlobal(type, stimId);
    }

    // Важно: не создавать условный рефлекс по самому стимулу, который уже запустил реакцию.
    if (activeIds.length === 1 && activeIds[0] === triggerGlobalId) {
      return;
    }

    // Совокупный образ для условного рефлекса — все активные стимулы КРОМЕ триггера (глобальные id).
    var conditionalIds = [];
    for (var ci = 0; ci < activeIds.length; ci++) {
      if (activeIds[ci] !== triggerGlobalId) {
        conditionalIds.push(activeIds[ci]);
      }
    }
    if (!conditionalIds.length) {
      return;
    }

    var contextNow = getCurrentContextKey();
    if (!contextNow || contextNow === 'none') {
      return;
    }

    // Текущий совокупный образ предшествующих стимулов (FinalImageId) — ключ условного рефлекса.
    var finalImageId = global.FinalImages_getOrCreateFinalImageId(conditionalIds);
    if (!finalImageId && finalImageId !== 0) {
      return;
    }

    var now = getCurrentPulse();

    var idx = findSynonymReflexIndex(contextNow, finalImageId, actionId);
    if (idx === -1) {
      // Создаём прототип условного рефлекса
      global.NextSynonymReflexId += 1;
      var newReflex = {
        id: global.NextSynonymReflexId,
        context: contextNow,
        finalImageId: finalImageId,
        stimulIds: conditionalIds.slice(0), // предшествующие стимулы
        actionId: actionId,
        birthTime: now,
        lastActivation: now,
        tryCount: 1,
        isReady: false
      };
      global.SynonymReflexes.push(newReflex);

      // 1-е сочетание стимула и реакции
      var aName1 = getActionName(actionId) || ('действие ' + actionId);
      showTransientAlert('Зафиксировано 1-е сочетание стимула и реакции («' + aName1 + '»).');

      if (typeof global.setBeastActivityText === 'function') {
        try {
          global.setBeastActivityText('Создан прототип условного рефлекса (FinalImageId ' +
            finalImageId + ' → действие ' + actionId + ')');
        } catch (e) {}
      }
      scheduleURStateSave();
    } else {
      // Усиливаем уже существующий прототип
      var cr = global.SynonymReflexes[idx];
      var oldCount = cr.tryCount;

      // После достижения порога зрелости больше не спамим сообщениями
      // и не наращиваем tryCount, только обновляем время последней активации.
      if (oldCount > REFLEX_READY_THRESHOLD) {
        cr.lastActivation = now;
      } else {
        cr.tryCount += 1;
        cr.lastActivation = now;

        // Сообщение об N-м сочетании (пока рефлекс ещё укрепляется)
        var aName2 = getActionName(actionId) || ('действие ' + actionId);
        showTransientAlert('Зафиксировано ' + cr.tryCount + '-е сочетание стимула и реакции («' + aName2 + '»).');

        // Переход через порог зрелости: образован новый условный рефлекс.
        // Но он станет "готовым" только после гашения участвовавших стимулов.
        if (oldCount <= REFLEX_READY_THRESHOLD && cr.tryCount > REFLEX_READY_THRESHOLD) {
          showTransientAlert('Образован новый условный рефлекс с реакцией «' + aName2 + '».');
          cr.isReady = false; // станет true после полного гашения стимула(ов)
        }
      }

      if (typeof global.setBeastActivityText === 'function') {
        try {
          global.setBeastActivityText('Усилен (' + cr.tryCount +
            ') прототип условного рефлекса (FinalImageId ' +
            finalImageId + ' → действие ' + actionId + ')');
        } catch (e2) {}
      }
      scheduleURStateSave();
    }
  }

  /**
   * Попытка запустить уже сформированные условные рефлексы.
   * Вызывается из processGeneticReflexes() ДО обработки генетических карт.
   *
   * Возвращает true, если какой-то условный рефлекс был запущен.
   */
  function tryRunSynonymReflexes() {
    if (typeof global.FinalImages_getOrCreateFinalImageId !== 'function') {
      return false;
    }

    // Контекст учитывается: условный рефлекс должен запускаться
    // только в том же сочетании (BadNormWell, EmotionId), в котором обучался.
    var contextNow = getCurrentContextKey();
    if (!contextNow || contextNow === 'none') {
      return false;
    }

    // Совокупный активный образ — глобальные id (type*1000+localId), см. stimulus_global_id.js.
    var activeIds = typeof global.StimulusGlobalId_getActiveGlobalIds === 'function'
      ? global.StimulusGlobalId_getActiveGlobalIds()
      : [];

    if (!activeIds.length) {
      // Все стимулы погашены — сбрасываем ключ, чтобы при следующей активации UR мог сработать
      PrevActiveStimulIdsKey = null;
      // Также обновим флаг готовности для всех рефлексов: если их образ сейчас не активен, они могут стать ready
      // (гашение образа завершено).
      var iReset;
      for (iReset = 0; iReset < global.SynonymReflexes.length; iReset++) {
        var r = global.SynonymReflexes[iReset];
        if (!r) continue;
        if (r.tryCount > REFLEX_READY_THRESHOLD) {
          r.isReady = true;
        }
      }
      // Сохраним обновлённое состояние готовности в IndexedDB
      scheduleURStateSave();
      return false;
    }

    // Сравниваем по набору стимулов, а не по FinalImageId — после перезагрузки
    // тот же набор может получить другой id, если FinalImages загрузился позже или иначе.
    var currentKey = activeIdsKey(activeIds);
    if (!currentKey) return false;

    if (PrevActiveStimulIdsKey !== null && PrevActiveStimulIdsKey === currentKey) {
      return false;
    }

    var now = getCurrentPulse();

    // Ищем зрелый и готовый рефлекс по совпадению набора активных стимулов с stimulIds рефлекса.
    // Сначала — строго в текущем контексте, затем (если не нашли) допускаем запуск
    // в «близком» контексте: по тем же стимулам, но с другим contextKey.
    var candidateAnyCtx = null;
    for (var j = 0; j < global.SynonymReflexes.length; j++) {
      var cr = global.SynonymReflexes[j];
      if (!cr) continue;
      if (!setsEqual(activeIds, cr.stimulIds)) continue;
      if (cr.context !== contextNow) continue;
      if (cr.tryCount <= REFLEX_READY_THRESHOLD) continue;
      if (!cr.isReady) continue;
      candidateAnyCtx = cr;
      break;
    }

    // Если строго в том же контексте не нашли — ищем любой зрелый UR
    // по тем же стимулам, но игнорируя context. Это позволяет рефлексу
    // срабатывать после перезагрузки даже если строковый ключ контекста
    // немного изменился (например, порядок/состав базовых контекстов).
    if (!candidateAnyCtx) {
      for (var k = 0; k < global.SynonymReflexes.length; k++) {
        var cr2 = global.SynonymReflexes[k];
        if (!cr2) continue;
        if (!setsEqual(activeIds, cr2.stimulIds)) continue;
        if (cr2.tryCount <= REFLEX_READY_THRESHOLD) continue;
        if (!cr2.isReady) continue;
        candidateAnyCtx = cr2;
        break;
      }
    }

    var chosen = candidateAnyCtx;
    if (chosen) {
      var finalImageId = global.FinalImages_getOrCreateFinalImageId
        ? global.FinalImages_getOrCreateFinalImageId(activeIds) : 0;
      debugAlert('RUN: сработал UR, actionId=' + chosen.actionId +
        ' FI=' + finalImageId);
      if (typeof global.runGeneticActionFromSynonymReflex === 'function') {
        global.runGeneticActionFromSynonymReflex(chosen.actionId, 'synonym', finalImageId);
      }
      chosen.lastActivation = now;
      chosen.tryCount += 1;
      PrevActiveStimulIdsKey = currentKey;
      // фиксируем изменения UR (счётчик, lastActivation) в IndexedDB
      scheduleURStateSave();
      return true;
    }

    return false;
  }

  // Экспорт API для связи с UI и генетическими рефлексами
  global.synonyms_onNeutralStimulusActivated = onNeutralStimulusActivated;
  global.synonyms_onGeneticActionFired = onGeneticActionFired;
  global.synonyms_tryRunReflexes = tryRunSynonymReflexes;

  // Вспомогательная функция может быть полезна при отладке
  global.get_cur_context_for_synonyms = getCurrentContextKey;
  global.serializeConditionalReflexesState = serializeConditionalReflexesState;
  global.applyConditionalReflexesState = applyConditionalReflexesState;
  global.clearAllConditionalReflexes = clearAllConditionalReflexes;

  // Загрузка ранее сохранённых условных рефлексов при инициализации модуля
  loadURStateFromIndexedDB();

  // Кнопка «Ух» — сброс всех условных рефлексов
  (function bindResetButton() {
    try {
      var btn = global.document && global.document.getElementById('btnResetConditionalReflexes');
      if (btn && typeof btn.addEventListener === 'function') {
        btn.addEventListener('click', function () {
          if (!global.confirm('Уверены, что нужно удалить все условные рефлексы?')) return;
          clearAllConditionalReflexes();
        });
      }
    } catch (e) {}
  })();

  if (typeof global.registerIndexedDBSaver === 'function') {
    global.registerIndexedDBSaver(function () {
      if (!urDirty) return;
      urDirty = false;
      var state = serializeConditionalReflexesState();
      var sig = JSON.stringify(state);
      if (sig === lastURSavedSig) return;
      lastURSavedSig = sig;
      saveURStateToIndexedDB(state);
    });
  }

})(window);


════════════════════════════════════════════════════════════

/BEAST/beast/_5_Genetic__reflexes/about_genetic_reflexes.md
════════════════════════════════════════════════════════════
# Генетические рефлексы (_5_Genetic__reflexes)

## Назначение

Модуль реализует **генетически заданные моторные рефлексы**: по активации стимулов (мотивирующих, позитивных, негативных) по картам *_GeneticReflexes выбираются базовые действия (BasicActions), выполняются с учётом ориентировочного рефлекса и блокировок, с регистрацией в семантике, образах действия и эпизодической памяти.

## Логика

- На каждый пульс вызывается **processGeneticReflexes()** (из puls.js).
- Сначала обрабатываются **отложенные рефлексы** (delayedReflexes). Заблокированный ОР генетический рефлекс ставится в очередь из ОР (addBlockedReflexToQueue) с branchId активной ветки дерева. **Если в stimuls_consciousness автоматизм запущен**, из очереди удаляются все рефлексы по этой ветке (removeDelayedReflexesForBranch) — без сравнения действий, только по branchId. Если автоматизм отклонён, удаление не вызывается, отложенный рефлекс выполнится по истечении задержки.
- Затем проверяются **условные рефлексы** (synonyms_tryRunReflexes); если сработал условный — выполняется его действие через runGeneticActionFromSynonymReflex.
- Иначе по **новым** активированным стимулам (мотивация, позитив, негатив) собираются события из StimulMotivarion_GeneticReflexes, StimulPositiv_GeneticReflexes, StimulNegative_GeneticReflexes. Если рефлекс заблокирован ОР (OrientationReflex_isReflexBlockedByOR), событие помещается в delayedReflexes с noConditionalReflex: true; иначе вызывается startAction.

**startAction** регистрирует опыт в семантической памяти, обновляет блок «Активность Beast», пролонгированные действия (еда, питьё и т.д.), образы действия (ActionsImage_registerReaction) и последний кадр ЭП (EpisodicMemory_setLastFrameActionFromActionId). Вызов synonyms_onGeneticActionFired (формирование условного рефлекса) не выполняется при sourceType === 'synonym' или при noConditionalReflex.

## Файлы

- **genetic_reflexes.js** — карты стимул → действие (StimulMotivarion_GeneticReflexes, StimulPositiv_GeneticReflexes, StimulNegative_GeneticReflexes), плюс вербальный рефлекс (Id 100) и пролонгаторы действий.
- **genetic_reflexes_engine.js** — процесс рефлексов, очередь отложенных рефлексов, startAction, runGeneticActionFromSynonymReflex, отображение активности.

════════════════════════════════════════════════════════════

/BEAST/beast/_5_Genetic__reflexes/genetic_reflexes.js
════════════════════════════════════════════════════════════
/* Генетически предрасположенные рефлексы
 *
 * Табличные данные воздействий стимулов на виталы, базовые контексты
 * и базовые генетические действия.
 */

/* ВНУТРЕННИЕ РЕФЛЕКСЫ - соматические рефлексы */

/* Список воздействий ID стимулов StimulMotivarion на ВИТАЛЫ (window.VitalsArr)
Для каждого Id стимула задаётся массив эффектов:
  vitalID 1–10  - разовое изменение соответствующего витала на delta при активации стимула
  vitalID 101  - запуск SetWellForHolding(), delta игнорируется
  vitalID 102  - запуск SetBadForHolding(), delta игнорируется
  vitalID 103  - включение игрового режима Beast isGameMode=true, delta игнорируется
  vitalID 104  - включение режима авторитарного обчения isAuthoritarianEducation=true, delta игнорируется
  
Это "внутренние" (т.е. не мышесные) генетические рефлексы, на основе которых могут возникать условные соматические рефлексы.
*/
window.StimulMotivarion_EffectsToBContexts = {
  // Еда / Вода: снятие стресса + удержание «Хорошо» (как в комментарии к stimuls.js — нормализация через действие Beast)
  1:  [{ vitalID: 7,  delta: -20 }, { vitalID: 101, delta: 0 }],
  2:  [{ vitalID: 7,  delta: -20 }, { vitalID: 101, delta: 0 }],
  3:  [],
  4:  [],
  5:  [{ vitalID: 7,  delta: -20 }],
  6:  [{ vitalID: 7,  delta: -20 } ],
  7:  [{ vitalID: 7,  delta: -50 }, { vitalID: 101, delta: 0 }],
  8:  [{ vitalID: 7,  delta: 50 }, { vitalID: 102, delta: 0 }],
  9:  [{ vitalID: 7,  delta: -30 }, { vitalID: 17, delta: 20 }],
  10: [{ vitalID: 7,  delta: 30 }],
  11: [{ vitalID: 7,  delta: -20 }],
  12: [{ vitalID: 7,  delta: -20 } ],
  13: [{ vitalID: 7,  delta: 20 }],
  14: [{ vitalID: 7,  delta: -30 }],
  15: [{ vitalID: 7,  delta: -20 } ],
  16: [{ vitalID: 7,  delta: -40 } ],
  17: [{ vitalID: 7,  delta: 20 } ],
  18: [{ vitalID: 7,  delta: 20 }],
  19: [{ vitalID: 103, delta: 0 }],
  20: [{ vitalID: 104, delta: 0 }, ]
};

/* Список воздействий ID стимулов StimulPositiv на КОНТЕКСТЫ (id window.StimulMotivarion)
Для каждого Id стимула задаётся массив эффектов:
  1–20  - активация соответствующего контекста при активации стимула на период времени state_retention_period
на время state_retention_period активация контекстов пульсом блокируется.
  101  - запуск SetWellForHolding(), 
  102  - запуск SetBadForHolding(), 
  103  - включение игрового режима Beast isGameMode=true, 
  104  - включение режима авторитарного обчения isAuthoritarianEducation=true.
Это "внутренние" (т.е. не мышесные) генетические рефлексы, на основе которых могут возникать условные соматические рефлексы.
*/
window.StimulPositiv_EffectsToBContexts = {
  1:  [13,11],
  2:  [11],
  3:  [13],
  4:  [12],
  5:  [12],
  6:  [12],
  7:  [5],
  8:  [12],
  9:  [13,11],
  10: [12],
  11: [12],
  12: [12],
  13: [13],
  14: [5],
  15: [5],
  16: [12],
  17: [12],
  18: [12],
  19: [13],
  20: [13,8],
};

/* Список воздействий ID стимулов StimulNegative на КОНТЕКСТЫ (id window.StimulMotivarion)
Для каждого Id стимула задаётся массив эффектов:
  1–20  - активация соответствующего контекста при активации стимула на период времени state_retention_period
на время state_retention_period активация контекстов пульсом блокируется.
  101  - запуск SetWellForHolding(), 
  102  - запуск SetBadForHolding(), 
  103  - включение игрового режима Beast isGameMode=true, 
  104  - включение режима авторитарного обчения isAuthoritarianEducation=true.
Это "внутренние" (т.е. не мышесные) генетические рефлексы, на основе которых могут возникать условные соматические рефлексы.
*/
window.StimulNegative_EffectsToBContexts = {
  1:  [6, 9],
  2:  [6, 9],
  3:  [9],
  4:  [6, 9],
  5:  [6, 9, 7],
  6:  [9],
  7:  [9],
  8:  [6, 9],
  9:  [6],
  10: [6, 9, 102],
  11: [12],
  12: [12],
  13: [6, 9],
  14: [9],
  15: [6, 9],
  16: [6, 9],
  17: [9],
  18: [9],
  19: [9, 102],
  20: [6, 9, 102],
};
  
  /* ВНЕШНИЕ, моторные РЕФЛЕКСЫ 
Попытки "самостоятельных" действий у ребенка в период кризиса 9-18 месяцев 
в основном обусловлены внутренним механизмом развития автономии, 
а не только внешними стимулами восприятия.  
Спонтанные действия связаны с естественной энергией психики, 
где ребенок "пробует" мир, не требуя внешнего подкрепления сразу; это основа для будущей произвольности.
Один вид рефлексов - по активному стимулу,
другой вид рефлексов - по возникшему позыву что-то попробовать - начинает собираться СЕМАНТИЧЕСКАЯ ПАМЯТЬ.
  */
  
/* Список воздействий ID StimulMotivarion на ID Базовых генетических действий (window.BasicActions)
При ID действия == 100 - раз в 5 пульсов выдавать одно (случайно) window.BasicVerbal из массива - набирать модель понимания.
(Ранее объект задавался дважды — второе определение перезаписывало первое; оставлен один набор.)
*/
window.StimulMotivarion_GeneticReflexes = {
  1:  [3], //ест
  2:  [4], //пьет
  3:  [23],
  4:  [30],
  5:  [],
  6:  [],
  7:  [2],
  8:  [1],
  9:  [27],
  10: [7],
  11: [25],
  12: [],
  13: [],
  14: [],
  15: [2],
  16: [27],
  17: [],
  18: [28],
  19: [12],
  20: [20,100],
}

/* Список воздействий ID стимулов StimulPositiv на ID Базовых генетических действий (window.BasicActions)
При ID действия == 100 - раз в 5 пульсов выдавать одно (случайно) window.BasicVerbal из массива - набирать модель понимания.
*/
window.StimulPositiv_GeneticReflexes = {
  1:  [27],
  2:  [6,100],
  3:  [100],
  4:  [20],
  5:  [20],
  6:  [20],
  7:  [3],
  8:  [20],
  9:  [27],
  10: [20,100],
  11: [20],
  12: [20],
  13: [27],
  14: [3],
  15: [5],
  16: [23],
  17: [20, 27],
  18: [],
  19: [5],
  20: [27],
}

/* Список воздействий ID стимулов StimulNegative на ID Базовых генетических действий (window.BasicActions)

*/
window.StimulNegative_GeneticReflexes = {
  1:  [8],
  2:  [20],
  3:  [20],
  4:  [20],
  5:  [8],
  6:  [20],
  7:  [13],
  8:  [15],
  9:  [20],
  10: [8,15],
  11: [20],
  12: [20],
  13: [19],
  14: [20],
  15: [8],
  16: [20],
  17: [13],
  18: [13],
  19: [14],
  20: [8,20],
}

/* Удержание совершения действий до полного восстановления виталов.
actionID совершаемого действия, vitalID - воздействие на витал, shift - на сколько меньшается значение витала с каждым пульсом.
Если действие началось, оно удерживается, пока vitalID не уменьшит значение до 0 
или пока активен вызвавшее это действие window.StimulMotivarion.
*/
window.ActionProlongator = [
  { actionID: 3, vitalID: 6, shift: 5 },
  { actionID: 4, vitalID: 3, shift: 10 }
];


════════════════════════════════════════════════════════════

/BEAST/beast/_5_Genetic__reflexes/genetic_reflexes_engine.js
════════════════════════════════════════════════════════════
/**
 * Обработка генетически заданных моторных рефлексов и вывод активности Beast.
 *
 * Логика:
 * - При активации стимулов (мотивирующих, позитивных, негативных) по картам
 *   *_GeneticReflexes выбираются базовые действия (window.BasicActions).
 * - Для выбранного действия в блоке "Активность Beast" показывается его название
 *   (например, "Плачет", "Хватает" и т.п.).
 * - Для специальных действий с Id == 100 раз в 5 пульсов выводится случайный
 *   "физиологический звук" из window.BasicVerbal: "Произносит: ма-ма-ма".
 * - Для действий из window.ActionProlongator дополнительно по каждому пульсу
 *   уменьшается указанный витал на shift, пока он не достигнет 0.
 *
 * Ожидается вызов processGeneticReflexes() из центрального пульса (puls.js)
 * один раз на каждый пульс.
 */
(function (global) {
  'use strict';

  var prevMotivation = [];
  var prevPositiv = [];
  var prevNegative = [];

  var prolongedActions = {}; // actionId -> { vitalID, shift }
  var verbalReflexActive = false;
  var verbalPulseCounter = 0;

  /**
   * Отложенность выполнения рефлекса: ОР не даёт выполнить рефлекс сразу; он ставится в очередь
   * delayedReflexes из ОР (addBlockedReflexToQueue) с branchId активной ветки. Если в stimuls_consciousness
   * автоматизм запущен, по этой ветке рефлексы удаляются из очереди (removeDelayedReflexesForBranch).
   */
  var delayedReflexes = [];
  var OR_BLOCK_PULSES = 2;

  function getCurrentFullFinalImageId() {
    if (typeof global.FinalImages_getOrCreateFinalImageId !== 'function') return 0;
    // Глобальные id стимулов (type*1000+localId), чтобы один числовой id в разных типах не коллидировал.
    var ids = typeof global.StimulusGlobalId_getActiveGlobalIds === 'function'
      ? global.StimulusGlobalId_getActiveGlobalIds()
      : [];
    return ids.length ? global.FinalImages_getOrCreateFinalImageId(ids) : 0;
  }

  /**
   * Обработка отложенных рефлексов (задержанных ОР на 2 пульса).
   * Если автоматизм был запущен в stimuls_consciousness, рефлекс по этой ветке уже удалён из очереди
   * (removeDelayedReflexesForBranch), сравнение по действию не нужно — только по branchId.
   */
  function runDelayedReflexes() {
    var pulse = typeof global.Cur_puls_val === 'number' ? global.Cur_puls_val : 0;
    for (var i = delayedReflexes.length - 1; i >= 0; i--) {
      var d = delayedReflexes[i];
      if (d.untilPulse > pulse) continue;
      delayedReflexes.splice(i, 1);
      startAction(d.actionId, d.sourceType, d.stimId, d.noConditionalReflex ? { noConditionalReflex: true } : undefined);
    }
  }

  /**
   * Удалить из очереди отложенных рефлексов все записи по данной ветке дерева.
   * Вызывается из stimuls_consciousness при запуске автоматизма — рефлекс для этой ветки аннулируется без сравнения действий.
   */
  function removeDelayedReflexesForBranch(branchId) {
    if (branchId == null) return;
    for (var i = delayedReflexes.length - 1; i >= 0; i--) {
      if (delayedReflexes[i].branchId === branchId) {
        delayedReflexes.splice(i, 1);
      }
    }
  }

  /**
   * Первый «новый» генетический рефлекс по текущим активным стимулам (для постановки в очередь из ОР).
   */
  function getBlockedGeneticReflexEvent() {
    var activeMotiv = Array.isArray(global.ActiveMotivationStimuls) ? global.ActiveMotivationStimuls.slice() : [];
    var activePositiv = Array.isArray(global.ActivePositivStimuls) ? global.ActivePositivStimuls.slice() : [];
    var activeNegative = Array.isArray(global.ActiveNegativeStimuls) ? global.ActiveNegativeStimuls.slice() : [];
    var events = [];
    events = events.concat(
      collectNewActionEvents(activeMotiv, prevMotivation, global.StimulMotivarion_GeneticReflexes, 'motivation'),
      collectNewActionEvents(activePositiv, prevPositiv, global.StimulPositiv_GeneticReflexes, 'positive'),
      collectNewActionEvents(activeNegative, prevNegative, global.StimulNegative_GeneticReflexes, 'negative')
    );
    return events.length ? events[0] : null;
  }

  /**
   * Поставить заблокированный ОР генетический рефлекс в очередь (вызов из ОР при setBlockedReflex).
   * Рефлекс привязан к ветке branchId; при запуске автоматизма по этой ветке очередь чистится через removeDelayedReflexesForBranch.
   */
  function addBlockedReflexToQueue(branchId) {
    if (branchId == null) return;
    var ev = getBlockedGeneticReflexEvent();
    if (!ev) return;
    var pulse = typeof global.Cur_puls_val === 'number' ? global.Cur_puls_val : 0;
    delayedReflexes.push({
      actionId: ev.actionId,
      sourceType: ev.sourceType,
      stimId: ev.stimId,
      untilPulse: pulse + OR_BLOCK_PULSES,
      noConditionalReflex: true,
      branchId: branchId
    });
  }

  function getBasicActionName(actionId) {
    if (!global.BasicActions || !Array.isArray(global.BasicActions)) {
      return '';
    }
    for (var i = 0; i < global.BasicActions.length; i++) {
      if (global.BasicActions[i].Id === actionId) {
        return global.BasicActions[i].name || '';
      }
    }
    return '';
  }

  var activityHighlightTimeout = null;

  function setBeastActivityText(text) {
    if (!global.document) return;
    var block = global.document.getElementById('beastActivityBlock');
    if (!block) return;

    // Сбрасываем предыдущий таймер и возвращаем базовый стиль
    if (activityHighlightTimeout !== null) {
      global.clearTimeout(activityHighlightTimeout);
      activityHighlightTimeout = null;
    }

    block.textContent = text || '';

    // Кратковременное визуальное выделение новой реакции (1 секунда)
    if (text) {
      block.style.color = '#1565C0';       // синий цвет
      block.style.fontWeight = 'bold';
      block.style.fontSize = '18pt';

      activityHighlightTimeout = global.setTimeout(function () {
        // Возврат к стандартному виду (наследуемому из CSS)
        block.style.color = '';
        block.style.fontWeight = '';
        block.style.fontSize = '';
        activityHighlightTimeout = null;
      }, 1000);
    }
  }

  function registerProlongatedAction(actionId) {
    if (!global.ActionProlongator || !Array.isArray(global.ActionProlongator)) {
      return;
    }
    for (var i = 0; i < global.ActionProlongator.length; i++) {
      var cfg = global.ActionProlongator[i];
      if (cfg && cfg.actionID === actionId) {
        prolongedActions[String(actionId)] = {
          vitalID: cfg.vitalID,
          shift: cfg.shift
        };
        break;
      }
    }
  }

  function getStimulusName(sourceType, stimId) {
    var arr = null;
    if (sourceType === 'motivation') {
      arr = global.StimulMotivarion;
    } else if (sourceType === 'positive') {
      arr = global.StimulPositiv;
    } else if (sourceType === 'negative') {
      arr = global.StimulNegative;
    }
    if (!arr || !Array.isArray(arr)) {
      return '';
    }
    for (var i = 0; i < arr.length; i++) {
      if (arr[i] && arr[i].Id === stimId) {
        return arr[i].name || '';
      }
    }
    return '';
  }

  function getSemanticImportanceValue() {
    if (typeof global.getDiffImportance === 'function') {
      var z = global.getDiffImportance();
      if (typeof z === 'number' && !isNaN(z)) {
        return z;
      }
    }
    return null;
  }

  /**
   * Не вызывать из startAction и нигде больше.
   * Учёт семантики выполняется один раз за активацию в orientation_reflex;
   * повторный вызов привёл бы к двойному увеличению счётчика обучений.
   * Заглушка оставлена, чтобы случайный вызов не давал двойного учёта.
   */
  function registerSemanticExperience(sourceType, stimId, z) {
    return;
  }

  function startAction(actionId, sourceType, stimId, options) {
    if (actionId === 100) {
      verbalReflexActive = true;
      return;
    }
    var name = getBasicActionName(actionId);
    var stimulusName = getStimulusName(sourceType, stimId);

    // Семантическая значимость учитывается один раз за активацию в ОР (orientation_reflex),
    // чтобы счётчик обучений увеличивался на 1 и усреднение значимости было верным.
    // Здесь только берём z для отображения в «Активность Beast», без повторной регистрации.
    var z = null;
    if (sourceType !== 'synonym') {
      z = getSemanticImportanceValue();
    }
    var typeLabel = '';
    if (sourceType === 'automatism') {
      typeLabel = 'автоматизм';
    } else if (sourceType === 'synonym') {
      typeLabel = 'условный рефлекс';
    } else if (sourceType === 'motivation' || sourceType === 'positive' || sourceType === 'negative') {
      typeLabel = 'генетический рефлекс';
    }
    var line1 = name ? (name + (typeLabel ? ' (' + typeLabel + ')' : '')) : typeLabel;
    var line2 = '';
    if (stimulusName) {
      line2 = 'Семантическая значимость ' + stimulusName;
      if (z !== null) {
        var zStr = String(Math.round(z * 100) / 100);
        line2 += ' - ' + zStr;
      }
    }
    var line = line2 ? (line1 + '\n' + line2) : line1;
    setBeastActivityText(line || name || '');
    // Любая выполненная реакция (генетическая, условная, автоматизм) переводит осмысление в целевой режим.
    if (typeof global.Info_setMode === 'function') {
      global.Info_setMode('target');
    }
    registerProlongatedAction(actionId);

    var fi = getCurrentFullFinalImageId();
    if (fi && typeof global.ActionsImage_registerReaction === 'function') {
      try {
        global.ActionsImage_registerReaction(fi, actionId);
      } catch (e) {}
    }
    if (typeof global.EpisodicMemory_setLastFrameActionFromActionId === 'function') {
      try {
        global.EpisodicMemory_setLastFrameActionFromActionId(actionId);
      } catch (e) {}
    }

    // Информируем подсистему условных (синонимических) рефлексов
    // ТОЛЬКО для "врождённых" путей (motivation / positive / negative).
    // Для sourceType === 'synonym' (запуск по условному рефлексу) — повторное обучение не выполняем.
    // Если стимул привлёк ОР (рефлекс был отложен ОР) — процесс образования условного рефлекса не запускаем.
    // Если идёт сборка кадра ЭП (период ожидания ответа) — условный рефлекс не создаём.
    // Если кадр только что закрыт ответным стимулом — рефлекс по нему не должен образовывать УР (флаг сбрасывается после пульса).
    // Условный рефлекс — это «сочетание» стимулов: при одном активном стимуле шаг сочетания не показываем и не создаём УР.
    var noConditionalReflex = options && options.noConditionalReflex === true;
    var inEpWaiting = typeof global.EpisodicMemory_isInWaitingPeriod === 'function' && global.EpisodicMemory_isInWaitingPeriod();
    var skipBecauseResponseStimulus = global.EpisodicMemory_skipConditionalReflexFormation === true;
    var activeIdsForUR = typeof global.StimulusGlobalId_getActiveGlobalIds === 'function'
      ? global.StimulusGlobalId_getActiveGlobalIds() : [];
    var singleStimulusNoPairing = !activeIdsForUR || activeIdsForUR.length < 2;
    if (sourceType !== 'synonym' && !noConditionalReflex && !inEpWaiting && !skipBecauseResponseStimulus && !singleStimulusNoPairing &&
        typeof global.synonyms_onGeneticActionFired === 'function') {
      try {
        global.synonyms_onGeneticActionFired(stimId, sourceType, actionId);
      } catch (e) {}
    }
  }

  /**
   * Выполнить образ действия (ОД): массив actionIds и/или последовательность других ОД (odSequence).
   * Используется при срабатывании автоматизма в stimuls_consciousness.
   * @param {number} actionsImageId — ID образа действия
   */
  function runActionImage(actionsImageId) {
    if (!actionsImageId || typeof global.ActionImages_getActionImage !== 'function') return;
    var od = global.ActionImages_getActionImage(actionsImageId);
    if (!od) return;
    var ids = od.actionIds || [];
    for (var i = 0; i < ids.length; i++) {
      startAction(ids[i], 'automatism', 0);
    }
    var seq = (od.odSequence || '').split(',');
    for (var j = 0; j < seq.length; j++) {
      var nextId = parseInt(seq[j].trim(), 10);
      if (nextId) runActionImage(nextId);
    }
  }

  /**
   * Внешний хук для запуска действия из условного (синонимического) рефлекса.
   * Оборачивает внутренний startAction, чтобы сохранялась единая логика
   * семантического учёта и пролонгированных действий.
   */
  function runGeneticActionFromSynonymReflex(actionId, sourceType, stimId) {
    var pulse = typeof global.Cur_puls_val === 'number' ? global.Cur_puls_val : 0;
    if (typeof global.OrientationReflex_isReflexBlockedByOR === 'function' &&
        global.OrientationReflex_isReflexBlockedByOR(stimId, actionId)) {
      var branchId = (typeof global.PerceptionTree_getActiveTerminalNodeId === 'function')
        ? global.PerceptionTree_getActiveTerminalNodeId() : 0;
      delayedReflexes.push({
        actionId: actionId,
        sourceType: sourceType || 'synonym',
        stimId: stimId,
        untilPulse: pulse + OR_BLOCK_PULSES,
        noConditionalReflex: true,
        branchId: branchId
      });
      return;
    }
    startAction(actionId, sourceType, stimId);
  }

  function updateProlongatedActions() {
    var hasChanges = false;
    if (!global.VitalsArr || !Array.isArray(global.VitalsArr)) {
      return;
    }
    for (var key in prolongedActions) {
      if (!Object.prototype.hasOwnProperty.call(prolongedActions, key)) continue;
      var cfg = prolongedActions[key];
      var vital = null;
      for (var i = 0; i < global.VitalsArr.length; i++) {
        if (global.VitalsArr[i].Id === cfg.vitalID) {
          vital = global.VitalsArr[i];
          break;
        }
      }
      if (!vital) {
        delete prolongedActions[key];
        continue;
      }
      if (vital.value <= 0) {
        delete prolongedActions[key];
        continue;
      }
      var newVal = vital.value - cfg.shift;
      if (newVal < 0) newVal = 0;
      if (newVal !== vital.value) {
        vital.value = newVal;
        hasChanges = true;
      }
      if (vital.value <= 0) {
        delete prolongedActions[key];
      }
    }
    if (hasChanges) {
      if (typeof global.update_vitals_view === 'function') {
        global.update_vitals_view();
      }
      if (typeof global.saveVitalsToStorage === 'function') {
        global.saveVitalsToStorage();
      }
    }
  }

  function collectNewActionEvents(activeIds, prevIds, mapping, sourceType) {
    var events = [];
    if (!activeIds || !activeIds.length || !mapping) {
      return events;
    }
    for (var i = 0; i < activeIds.length; i++) {
      var id = activeIds[i];
      if (prevIds.indexOf(id) !== -1) continue;
      var acts = mapping[id];
      if (!acts || !acts.length) continue;
      for (var j = 0; j < acts.length; j++) {
        var actId = acts[j];
        events.push({
          actionId: actId,
          sourceType: sourceType,
          stimId: id
        });
      }
    }
    return events;
  }

  function updateVerbalReflexFlag() {
    verbalReflexActive = false;
    function checkMapping(activeIds, mapping) {
      if (!activeIds || !activeIds.length || !mapping) return;
      for (var i = 0; i < activeIds.length; i++) {
        var id = activeIds[i];
        var acts = mapping[id];
        if (!acts || !acts.length) continue;
        for (var j = 0; j < acts.length; j++) {
          if (acts[j] === 100) {
            verbalReflexActive = true;
            return;
          }
        }
      }
    }
    checkMapping(global.ActiveMotivationStimuls, global.StimulMotivarion_GeneticReflexes);
    checkMapping(global.ActivePositivStimuls, global.StimulPositiv_GeneticReflexes);
    checkMapping(global.ActiveNegativeStimuls, global.StimulNegative_GeneticReflexes);
  }

  function handleVerbalReflex() {
    if (!verbalReflexActive) {
      verbalPulseCounter = 0;
      return;
    }
    verbalPulseCounter++;
    if (verbalPulseCounter % 5 !== 0) {
      return;
    }
    if (!global.BasicVerbal || !Array.isArray(global.BasicVerbal) || !global.BasicVerbal.length) {
      return;
    }
    var idx = Math.floor(Math.random() * global.BasicVerbal.length);
    if (idx < 0 || idx >= global.BasicVerbal.length) {
      return;
    }
    var phrase = global.BasicVerbal[idx];
    if (!phrase) return;
    var name = phrase.name || '';
    if (!name) return;
    setBeastActivityText('Произносит: ' + name);
  }

  function processGeneticReflexes() {
    var pulse = typeof global.Cur_puls_val === 'number' ? global.Cur_puls_val : 0;

    runDelayedReflexes();

    if (typeof global.synonyms_tryRunReflexes === 'function') {
      try {
        if (global.synonyms_tryRunReflexes()) {
          updateProlongatedActions();
          updateVerbalReflexFlag();
          handleVerbalReflex();
          return;
        }
      } catch (e) {}
    }

    var activeMotiv = Array.isArray(global.ActiveMotivationStimuls) ? global.ActiveMotivationStimuls.slice() : [];
    var activePositiv = Array.isArray(global.ActivePositivStimuls) ? global.ActivePositivStimuls.slice() : [];
    var activeNegative = Array.isArray(global.ActiveNegativeStimuls) ? global.ActiveNegativeStimuls.slice() : [];

    var events = [];
    events = events.concat(
      collectNewActionEvents(activeMotiv, prevMotivation, global.StimulMotivarion_GeneticReflexes, 'motivation'),
      collectNewActionEvents(activePositiv, prevPositiv, global.StimulPositiv_GeneticReflexes, 'positive'),
      collectNewActionEvents(activeNegative, prevNegative, global.StimulNegative_GeneticReflexes, 'negative')
    );

    if (events.length) {
      var ev = events[0];
      var currentBranchId = (typeof global.PerceptionTree_getActiveTerminalNodeId === 'function')
        ? global.PerceptionTree_getActiveTerminalNodeId() : 0;
      // Сбрасываем флаг «автоматизм сработал» только для других веток — при смене стимула/контекста генрефлекс снова разрешён.
      if (global.automatismRanForBranchIds && currentBranchId != null) {
        for (var bid in global.automatismRanForBranchIds) {
          if (Number(bid) !== Number(currentBranchId)) {
            delete global.automatismRanForBranchIds[bid];
          }
        }
      }
      var fullFinalImageId = getCurrentFullFinalImageId();
      var blocked = typeof global.OrientationReflex_isReflexBlockedByOR === 'function' &&
        global.OrientationReflex_isReflexBlockedByOR(fullFinalImageId, ev.actionId);
      var automatismRanForThisBranch = global.automatismRanForBranchIds && global.automatismRanForBranchIds[currentBranchId];
      if (blocked) {
        // Рефлекс уже поставлен в очередь из ОР (addBlockedReflexToQueue); здесь не дублируем.
      } else if (automatismRanForThisBranch) {
        // По этой ветке уже выполнен автоматизм в stimuls_consciousness — генрефлекс не запускаем (кардинально исключаем повтор).
      } else {
        startAction(ev.actionId, ev.sourceType, ev.stimId);
      }
    }

    prevMotivation = activeMotiv;
    prevPositiv = activePositiv;
    prevNegative = activeNegative;

    updateProlongatedActions();
    updateVerbalReflexFlag();
    handleVerbalReflex();
  }

  global.processGeneticReflexes = processGeneticReflexes;
  global.runGeneticActionFromSynonymReflex = runGeneticActionFromSynonymReflex;
  global.runActionImage = runActionImage;
  global.addBlockedReflexToQueue = addBlockedReflexToQueue;
  global.removeDelayedReflexesForBranch = removeDelayedReflexesForBranch;

  function copyTextToClipboard(text) {
    var t = (text || '').trim();
    if (!t) return;
    if (global.navigator && global.navigator.clipboard && typeof global.navigator.clipboard.writeText === 'function') {
      global.navigator.clipboard.writeText(t).catch(function () {
        fallbackCopy(t);
      });
      return;
    }
    fallbackCopy(t);
  }

  function fallbackCopy(text) {
    var doc = global.document;
    if (!doc || !doc.body) return;
    var ta = doc.createElement('textarea');
    ta.value = text;
    ta.style.position = 'fixed';
    ta.style.left = '-9999px';
    ta.style.top = '0';
    doc.body.appendChild(ta);
    ta.focus();
    ta.select();
    try {
      doc.execCommand('copy');
    } catch (e) {}
    doc.body.removeChild(ta);
  }

  function showCopySuccessAlert() {
    if (typeof global.show_alert === 'function') {
      global.show_alert('Скопировано в буфер обмена');
      if (typeof global.close_alert === 'function') {
        global.setTimeout(function () {
          try {
            global.close_alert();
          } catch (e) {}
        }, 1500);
      }
    }
  }

  function bindCopyButtons() {
    var doc = global.document;
    if (!doc) return;
    var btnBranch = doc.getElementById('btnCopyActiveBranch');
    if (btnBranch && typeof btnBranch.addEventListener === 'function') {
      btnBranch.addEventListener('click', function () {
        var el = doc.getElementById('perceptionTreeActiveBranchText');
        if (el) {
          copyTextToClipboard(el.textContent);
          showCopySuccessAlert();
        }
      });
    }
    var btnActivity = doc.getElementById('btnCopyActivityBlock');
    if (btnActivity && typeof btnActivity.addEventListener === 'function') {
      btnActivity.addEventListener('click', function () {
        var block = doc.getElementById('beastActivityBlock');
        if (block) {
          copyTextToClipboard(block.textContent);
          showCopySuccessAlert();
        }
      });
    }
  }

  if (global.document.readyState === 'loading') {
    global.document.addEventListener('DOMContentLoaded', bindCopyButtons);
  } else {
    bindCopyButtons();
  }
})(window);


════════════════════════════════════════════════════════════

/BEAST/beast/_6_semantic_memory/Emotion.js
════════════════════════════════════════════════════════════
(function (global) {
  'use strict';

  // Массив известных Emotion: сочетаний базовых контекстов
  // Формат элемента: { id: number, contextIds: number[] }
  if (!Array.isArray(global.Emotions)) {
    global.Emotions = [];
  }

  function normalizeIds(ids) {
    if (!Array.isArray(ids)) return [];
    var arr = [];
    for (var i = 0; i < ids.length; i++) {
      var v = parseInt(ids[i], 10);
      if (!isNaN(v) && v > 0) {
        if (arr.indexOf(v) === -1) {
          arr.push(v);
        }
      }
    }
    arr.sort(function (a, b) { return a - b; });
    return arr;
  }

  function arraysEqual(a, b) {
    if (a.length !== b.length) return false;
    for (var i = 0; i < a.length; i++) {
      if (a[i] !== b[i]) return false;
    }
    return true;
  }

  function findEmotionIdByContexts(contextIds) {
    var norm = normalizeIds(contextIds);
    if (!norm.length) return 0;
    for (var i = 0; i < global.Emotions.length; i++) {
      if (arraysEqual(global.Emotions[i].contextIds, norm)) {
        return global.Emotions[i].id;
      }
    }
    return 0;
  }

  function getNextEmotionId() {
    var maxId = 0;
    for (var i = 0; i < global.Emotions.length; i++) {
      if (global.Emotions[i].id > maxId) {
        maxId = global.Emotions[i].id;
      }
    }
    return maxId + 1;
  }

  function getOrCreateEmotionId(contextIds) {
    var norm = normalizeIds(contextIds);
    if (!norm.length) return 0;
    var existingId = findEmotionIdByContexts(norm);
    if (existingId) return existingId;
    var newId = getNextEmotionId();
    global.Emotions.push({ id: newId, contextIds: norm });
    if (Array.isArray(global.KnownEmotionIds) && global.KnownEmotionIds.indexOf(newId) === -1) {
      global.KnownEmotionIds.push(newId);
    }
    return newId;
  }

  global.Emotions_findEmotionId = findEmotionIdByContexts;
  global.Emotions_getOrCreateEmotionId = getOrCreateEmotionId;

})(window);


════════════════════════════════════════════════════════════

/BEAST/beast/_6_semantic_memory/FinalImage.js
════════════════════════════════════════════════════════════
(function (global) {
  'use strict';

  // Массив известных FinalImage: сочетаний активных стимулов.
  // Формат элемента: { id: number, stimulIds: number[] }.
  // stimulIds с версии с глобальными id хранят значения type*1000+localId (см. stimulus_global_id.js).
  if (!Array.isArray(global.FinalImages)) {
    global.FinalImages = [];
  }

  function normalizeIds(ids) {
    if (!Array.isArray(ids)) return [];
    var arr = [];
    for (var i = 0; i < ids.length; i++) {
      var v = parseInt(ids[i], 10);
      if (!isNaN(v) && v > 0) {
        if (arr.indexOf(v) === -1) {
          arr.push(v);
        }
      }
    }
    arr.sort(function (a, b) { return a - b; });
    return arr;
  }

  function arraysEqual(a, b) {
    if (a.length !== b.length) return false;
    for (var i = 0; i < a.length; i++) {
      if (a[i] !== b[i]) return false;
    }
    return true;
  }

  function findFinalImageIdByStimuls(stimulIds) {
    var norm = normalizeIds(stimulIds);
    if (!norm.length) return 0;
    for (var i = 0; i < global.FinalImages.length; i++) {
      if (arraysEqual(global.FinalImages[i].stimulIds, norm)) {
        return global.FinalImages[i].id;
      }
    }
    return 0;
  }

  function getNextFinalImageId() {
    var maxId = 0;
    for (var i = 0; i < global.FinalImages.length; i++) {
      if (global.FinalImages[i].id > maxId) {
        maxId = global.FinalImages[i].id;
      }
    }
    return maxId + 1;
  }

  function getOrCreateFinalImageId(stimulIds) {
    var norm = normalizeIds(stimulIds);
    if (!norm.length) return 0;
    var existingId = findFinalImageIdByStimuls(norm);
    if (existingId) return existingId;
    var newId = getNextFinalImageId();
    global.FinalImages.push({ id: newId, stimulIds: norm });
    if (Array.isArray(global.KnownFinalImageIds) && global.KnownFinalImageIds.indexOf(newId) === -1) {
      global.KnownFinalImageIds.push(newId);
    }
    return newId;
  }

  global.FinalImages_findFinalImageId = findFinalImageIdByStimuls;
  global.FinalImages_getOrCreateFinalImageId = getOrCreateFinalImageId;

})(window);


════════════════════════════════════════════════════════════

/BEAST/beast/_6_semantic_memory/Understanding_model.js
════════════════════════════════════════════════════════════
/* Модели понимания образов (FinalImage).
 *
 * Для заданного finalImageId необходимо уметь получить временный массив
 * всех семантических "кадров" памяти (SemanticObjectImportance) во всех
 * условиях (WellNormaBad, EmotionId), в которых этот образ встречался.
 *
 * На основе этой выборки строится модель понимания образа:
 * ImageUnderstandingModelArr[finalImageId] — массив записей, каждая из которых
 * содержит сводную информацию по одному сочетанию условий.
 */

(function (global) {
  'use strict';

  // Глобальный массив моделей понимания образов.
  // Индекс: ImageUnderstandingModelArr[finalImageId] = модель или undefined.
  if (!global.ImageUnderstandingModelArr) {
    global.ImageUnderstandingModelArr = {};
  }

  var MULTIPLIER = (typeof global.StimulusGlobalId_MULTIPLIER === 'number') ? global.StimulusGlobalId_MULTIPLIER : 1000;

  /**
   * Для образа с глобальными stimulIds (например [2010]) возвращает id других FinalImage,
   * у которых stimulIds в старом формате (например [10]). Нужно для слияния кадров после перехода на глобальные id.
   */
  function getLegacyEquivalentFinalImageIds(finalImageId) {
    var ids = [];
    if (!Array.isArray(global.FinalImages)) return ids;
    var current = null;
    for (var i = 0; i < global.FinalImages.length; i++) {
      if (global.FinalImages[i] && global.FinalImages[i].id === finalImageId) {
        current = global.FinalImages[i];
        break;
      }
    }
    if (!current || !Array.isArray(current.stimulIds) || !current.stimulIds.length) return ids;
    var legacy = [];
    for (var j = 0; j < current.stimulIds.length; j++) {
      var s = current.stimulIds[j];
      var v = parseInt(s, 10);
      if (isNaN(v) || v <= 0) continue;
      legacy.push(v >= MULTIPLIER ? v % MULTIPLIER : v);
    }
    legacy.sort(function (a, b) { return a - b; });
    for (var k = 0; k < global.FinalImages.length; k++) {
      var im = global.FinalImages[k];
      if (!im || im.id === finalImageId || !Array.isArray(im.stimulIds)) continue;
      var other = im.stimulIds.slice(0).sort(function (a, b) { return a - b; });
      if (other.length !== legacy.length) continue;
      var eq = true;
      for (var n = 0; n < legacy.length; n++) {
        if (other[n] !== legacy[n]) { eq = false; break; }
      }
      if (eq) ids.push(im.id);
    }
    return ids;
  }

  /**
   * Получить выборку всех семантических кадров для данного finalImageId
   * во всех условиях (по всему SemanticMemory).
   * Учитываются и кадры, записанные под «legacy»-образами (со старыми локальными stimulIds),
   * чтобы не терять данные после перехода на глобальные id стимулов.
   *
   * @param {number} finalImageId
   * @returns {Array<Object>} массив объектов SemanticObjectImportance,
   *          отфильтрованных по finalImageId и эквивалентным legacy-образам.
   */
  function getSemanticFramesForFinalImage(finalImageId) {
    var result = [];
    var getFrames = global.getSemanticFramesForFinalImageId;
    if (typeof getFrames !== 'function') return result;
    var idsToInclude = [finalImageId].concat(getLegacyEquivalentFinalImageIds(finalImageId));
    for (var i = 0; i < idsToInclude.length; i++) {
      var frames = getFrames(idsToInclude[i]);
      if (frames && frames.length) {
        for (var j = 0; j < frames.length; j++) result.push(frames[j]);
      }
    }
    return result;
  }

  /**
   * Построить (или обновить) модель понимания образа для заданного finalImageId.
   *
   * Модель представляет собой массив сводных записей по всем найденным
   * сочетаниям (wellNormaBad, emotionId) для этого FinalImage:
   * [
   *   {
   *     wellNormaBad: 1|2|3,
   *     emotionId: number,
   *     meanImportance: number,
   *     samplesCount: number,
   *     lastImportance: number
   *   },
   *   ...
   * ]
   *
   * @param {number} finalImageId
   * @returns {Array<Object>} модель понимания для данного finalImageId.
   */
  function buildImageUnderstandingModel(finalImageId) {
    var frames = getSemanticFramesForFinalImage(finalImageId);
    var model = [];
    for (var i = 0; i < frames.length; i++) {
      var f = frames[i];
      model.push({
        wellNormaBad: f.wellNormaBad,
        emotionId: f.emotionId,
        meanImportance: f.meanImportance,
        samplesCount: f.samplesCount,
        lastImportance: f.lastImportance
      });
    }
    global.ImageUnderstandingModelArr[finalImageId] = model;
    return model;
  }

  /**
   * Получить уже построенную модель понимания образа, не пересчитывая её.
   * Если модели нет, она будет построена на основе текущей SemanticMemory.
   *
   * @param {number} finalImageId
   * @returns {Array<Object>} модель понимания.
   */
  function getImageUnderstandingModel(finalImageId) {
    var model = global.ImageUnderstandingModelArr[finalImageId];
    if (!model) {
      model = buildImageUnderstandingModel(finalImageId);
    }
    return model;
  }

  // Экспорт в глобальную область
  global.getSemanticFramesForFinalImage = getSemanticFramesForFinalImage;
  global.buildImageUnderstandingModel = buildImageUnderstandingModel;
  global.getImageUnderstandingModel = getImageUnderstandingModel;

})(window);


════════════════════════════════════════════════════════════

/BEAST/beast/_6_semantic_memory/about_semantic_memory.md
════════════════════════════════════════════════════════════
# Семантическая память (_6_semantic_memory)

## Назначение

Модуль реализует **семантическую память** — связь образа стимула (FinalImageId) с его **значимостью** в данных условиях (базовое состояние + эмоция). Значимость используется ориентировочным рефлексом (новизна, порог OR_well_known), моделями понимания и принятием решений.

## Условия записи

- При взаимодействии до запуска рефлексов фиксируется «ноль» (getDiffImportance).
- После срабатывания рефлексов (через пульс) снова вызывается getDiffImportance; полученное значение z — семантическая значимость данного образа в текущих условиях.
- Запись уникальна по тройке: **wellNormaBad** (1/2/3), **emotionId**, **finalImageId**. Для каждой такой тройки хранятся meanImportance, samplesCount, lastImportance; при новом опыте пересчитывается среднее.

**Лупа значимости** (`registerSemanticSample`): при уже «тихой» семантике (|meanImportance| ≤ 0,5) и **норме всех виталов** (ни один Kbad > 0) в усреднение подмешивается не сам `getDiffImportance()`, а `normalizeValue` от **сырого** diff умноженного на 5 и ограниченного по модулю 3 (`LastDiffImportanceRaw` в `prepare.js`). Так слабые сигналы сильнее выделяются на фоне почти нулевой памяти, но лупа отключается при стрессе по виталам.

## Основные компоненты

- **SemanticMemoryIndex** — индекс по (well, emotionId) → объекты по finalImageId (meanImportance, samplesCount).
- **FinalImage.js** — конечные образы восприятия (FinalImages): пары (id, stimulIds). getOrCreateFinalImageId(stimulIds) возвращает или создаёт ID образа по набору ID стимулов.
- **Emotion.js** — эмоция как ID сочетания активных базовых контекстов (getOrCreateEmotionId).
- **Understanding_model.js** — модели понимания на основе семантики.

## Файлы

- **semantic_memory.js** — класс SemanticObjectImportance, индексация, регистрация опыта (`registerSemanticSample`), сохранение/загрузка. При **восстановлении** из снимка состояния поля `meanImportance`, `lastImportance`, `samplesCount` переносятся в объект явно (с нормализацией типов); создание объекта для восстановления идёт **без** лишнего `addSample(meanImportance)`, чтобы не искажать счётчик и не оставлять заниженный `samplesCount` при дубликатах ключей или строковых значениях из хранилища.
- **FinalImage.js**, **Emotion.js**, **Understanding_model.js** — образы, эмоции и использование семантики.

Семантическая память сохраняется в IndexedDB и в файл; от неё зависят условия запуска ОР и формирования кадров ЭП (порог samplesCount >= OR_well_known).

Дополнительно: после добавления нового семантического образца (`registerSemanticSample`) `semantic_memory.js` может вызывать точечное обновление UI-индикатора «готовности к ОР» у кнопок стимула (чтобы бордер не перерасчитывался на каждом пульсе). После асинхронной загрузки семантики из IndexedDB вызывается обновление индикаторов, в т.ч. повтор на следующем тике (`setTimeout(0)`), чтобы снизить рассинхрон с уже выставленными контекстами после старта.

════════════════════════════════════════════════════════════

/BEAST/beast/_6_semantic_memory/semantic_memory.js
════════════════════════════════════════════════════════════
/* Семантическая память - основа моделей понимания.
Связь id стимула (образ) с его значимостью в данных условиях, что порождает абстракцию.
Каждый акт формирования семантической памяти требует слепка текущих условий (контекста):
id базового состояния WellNormaBad (1 / 2 / 3) и сочетания ID активных базовых контекстов (EmotionId).
Значимость берется из значимости текущего состояния z = getDiffImportance()
по сравнению с состоянием до контакта с объектом.

«Лупа» значимости (registerSemanticSample): если по данной тройке (well, emotion, finalImage) уже накопленная
средняя значимость meanImportance по модулю не превышает 0,5, и ни один витал не вышел из нормы (Kbad = 0),
в память записывается не сам z, а normalizeValue(clamp(raw × 5, ±3)), где raw — неокруглённый diff из
getDiffImportance (LastDiffImportanceRaw в prepare.js), т.е. усиление слабого сигнала при «тихой» семантике
и спокойном теле (без искажения при стрессе по виталам).

При взаимодействии с объектом:
1) До запуска внутренних рефлексов вызывается getDiffImportance() — фиксация "нуля".
2) После срабатывания рефлексов (через 1 пульс, когда оформилось воздействие) снова вызывается getDiffImportance().
   Полученное z — "семантическая значимость" данного объекта (FinalImageId) в текущих условиях
   (WellNormaBad + EmotionId).
3) Для комбинации (WellNormaBad, EmotionId, FinalImageId) создаётся / обновляется объект SemanticObjectImportance
   в массиве SemanticMemory.
*/

/* Класс семантической значимости объекта при взаимодействии с ним (в том числе пассивном).
Уникальность записи определяют:
- wellNormaBad (1, 2, 3) — базовое состояние "Плохо / Норма / Хорошо" на момент взаимодействия;
- emotionId — ID сочетания базовых контекстов (Emotion);
- finalImageId — ID сочетания активных стимулов (FinalImage).

Каждый новый опыт с таким же (wellNormaBad, emotionId, finalImageId) модифицирует итоговую значимость:
по текущему z и samplesCount пересчитывается новое среднее meanImportance.

ВАЖНО ПОНИМАТЬ, что если действие не изменяет витал (он уже в 0 или в 100) то и эффекта от действия не будет. 
Т.е. Поощрить даст эффект если Стресс имеет некоторое значение, которое уменьшится.
*/

(function (global) {
  'use strict';

  // Класс одной записи семантической памяти
  function SemanticObjectImportance(wellNormaBad, emotionId, finalImageId, initialImportance) {
    this.wellNormaBad = wellNormaBad;      // 1 / 2 / 3
    this.emotionId = emotionId;            // ID Emotion
    this.finalImageId = finalImageId;      // ID FinalImage

    this.meanImportance = initialImportance || 0; // среднее значение z
    this.samplesCount = 0;                        // сколько раз наблюдали
    this.lastImportance = 0;                      // последнее z

    if (typeof initialImportance === 'number') {
      this.meanImportance = initialImportance;
      this.lastImportance = initialImportance;
      this.samplesCount = 1;
    }
  }

  SemanticObjectImportance.prototype.addSample = function (z) {
    if (typeof z !== 'number' || isNaN(z)) {
      return;
    }
    var n = this.samplesCount;
    if (n <= 0) {
      this.meanImportance = z;
      this.samplesCount = 1;
      this.lastImportance = z;
      return;
    }
    // Обновляем среднее: новое = (старое*n + z) / (n+1)
    this.meanImportance = (this.meanImportance * n + z) / (n + 1);
    this.samplesCount = n + 1;
    this.lastImportance = z;
  };

  // Глобальный массив объектов семантической памяти
  if (!Array.isArray(global.SemanticMemory)) {
    global.SemanticMemory = [];
  }

  // Известные FinalImageId и EmotionId можно хранить как простые массивы,
  // чтобы их тоже сохранять/восстанавливать.
  if (!Array.isArray(global.KnownFinalImageIds)) {
    global.KnownFinalImageIds = [];
  }
  if (!Array.isArray(global.KnownEmotionIds)) {
    global.KnownEmotionIds = [];
  }

  // Индекс для быстрого поиска по (wellNormaBad, emotionId, finalImageId)
  // Структура: SemanticMemoryIndex[wellNormaBad][emotionId][finalImageId] = SemanticObjectImportance
  if (!global.SemanticMemoryIndex) {
    global.SemanticMemoryIndex = {};
  }

  function getIndexBucket(wellNormaBad, emotionId) {
    var wKey = String(wellNormaBad);
    var eKey = String(emotionId);
    if (!global.SemanticMemoryIndex[wKey]) {
      global.SemanticMemoryIndex[wKey] = {};
    }
    if (!global.SemanticMemoryIndex[wKey][eKey]) {
      global.SemanticMemoryIndex[wKey][eKey] = {};
    }
    return global.SemanticMemoryIndex[wKey][eKey];
  }

  /**
   * Быстрый поиск объекта семантической значимости по индексу O(1).
   * @param {number} well — базовое состояние (1/2/3).
   * @param {number} emotionId — ID эмоции.
   * @param {number} finalImageId — ID образа.
   * @returns {SemanticObjectImportance|null}
   */
  function getSemanticObjectFast(well, emotionId, finalImageId) {
    if (!well || !emotionId || !finalImageId || !global.SemanticMemoryIndex) return null;
    var bucket = global.SemanticMemoryIndex[String(well)];
    if (!bucket) return null;
    bucket = bucket[String(emotionId)];
    if (!bucket) return null;
    return bucket[String(finalImageId)] || null;
  }

  /**
   * Все кадры семантической памяти для одного finalImageId (обход по индексу, без линейного скана SemanticMemory).
   * @param {number} finalImageId
   * @returns {Object[]} массив SemanticObjectImportance.
   */
  /** true, если хотя бы один витал с Kbad > 0 (как в prepare.calculateBadVitalsValues). */
  function semanticMemory_anyVitalOutOfNorm() {
    var bad = global.BadVitalsValue;
    var arr = global.VitalsArr;
    if (!bad || !arr || !arr.length) return false;
    for (var vi = 0; vi < arr.length; vi++) {
      var vid = arr[vi].Id;
      var kb = bad[vid];
      if (kb == null) kb = bad[String(vid)];
      if (typeof kb === 'number' && kb > 0) return true;
    }
    return false;
  }

  function getSemanticFramesForFinalImageId(finalImageId) {
    var result = [];
    if (!global.SemanticMemoryIndex || finalImageId == null) return result;
    var fKey = String(finalImageId);
    var wKeys = Object.keys(global.SemanticMemoryIndex);
    for (var w = 0; w < wKeys.length; w++) {
      var byEmotion = global.SemanticMemoryIndex[wKeys[w]];
      if (!byEmotion) continue;
      var eKeys = Object.keys(byEmotion);
      for (var e = 0; e < eKeys.length; e++) {
        var obj = byEmotion[eKeys[e]] && byEmotion[eKeys[e]][fKey];
        if (obj) result.push(obj);
      }
    }
    return result;
  }

  function getOrCreateSemanticObject(wellNormaBad, emotionId, finalImageId, initialImportance) {
    if (!wellNormaBad || !emotionId || !finalImageId) {
      return null;
    }
    var bucket = getIndexBucket(wellNormaBad, emotionId);
    var fKey = String(finalImageId);
    var obj = bucket[fKey];
    if (!obj) {
      obj = new SemanticObjectImportance(wellNormaBad, emotionId, finalImageId, initialImportance);
      bucket[fKey] = obj;
      global.SemanticMemory.push(obj);

      if (global.KnownFinalImageIds.indexOf(finalImageId) === -1) {
        global.KnownFinalImageIds.push(finalImageId);
      }
      if (global.KnownEmotionIds.indexOf(emotionId) === -1) {
        global.KnownEmotionIds.push(emotionId);
      }
    } else if (typeof initialImportance === 'number') {
      obj.addSample(initialImportance);
    }
    return obj;
  }

  /**
   * Регистрация одного семантического опыта.
   * Вызывается ПОСЛЕ второго getDiffImportance(), когда есть значение z.
   *
   * «Лупа» значимости: при |meanImportance| ≤ 0,5 для этой тройки и отсутствии выхода виталов из нормы
   * в выборку идёт normalizeValue(clamp(LastDiffImportanceRaw × 5, ±3)) вместо z — см. комментарий в шапке файла.
   * Требуется, что непосредственно перед вызовом был getDiffImportance() (заполняется LastDiffImportanceRaw).
   *
   * @param {number} finalImageId - ID текущего FinalImage (сочетание активных стимулов).
   * @param {number} emotionId - ID текущего Emotion (сочетание базовых контекстов).
   * @param {number} z - значимость объекта (результат getDiffImportance(); при лупе заменяется усиленным значением).
   */
  function registerSemanticSample(finalImageId, emotionId, z) {
    if (typeof z !== 'number' || isNaN(z)) {
      return;
    }
    var well = global.BadNormWell || 0; // 1 / 2 / 3
    if (!well) {
      return;
    }

    var LOUPE_PRIOR_MEAN_ABS = 0.5;
    var LOUPE_RAW_SCALE = 5;
    var LOUPE_RAW_CLAMP = 3;

    var priorObj = getSemanticObjectFast(well, emotionId, finalImageId);
    var priorMean = priorObj && typeof priorObj.meanImportance === 'number' && !isNaN(priorObj.meanImportance)
      ? priorObj.meanImportance
      : 0;
    var zApplied = z;
    if (Math.abs(priorMean) <= LOUPE_PRIOR_MEAN_ABS && !semanticMemory_anyVitalOutOfNorm()) {
      var raw = global.LastDiffImportanceRaw;
      if (typeof raw === 'number' && !isNaN(raw)) {
        var mag = raw * LOUPE_RAW_SCALE;
        if (mag > LOUPE_RAW_CLAMP) mag = LOUPE_RAW_CLAMP;
        if (mag < -LOUPE_RAW_CLAMP) mag = -LOUPE_RAW_CLAMP;
        if (typeof global.normalizeValue === 'function') {
          zApplied = global.normalizeValue(mag);
        } else {
          zApplied = mag;
        }
      }
    }

    // Один вызов = один образец: getOrCreateSemanticObject при существующем объекте уже вызывает obj.addSample(z).
    // Дополнительный addSample здесь приводил к +2 за активацию (счётчик 0→1 верно, далее +2).
    var obj = getOrCreateSemanticObject(well, emotionId, finalImageId, zApplied);
    if (!obj) return;

    // Точечно обновляем индикатор готовности ОР для кнопок,
    // относящихся к этому finalImageId.
    // UI обновляется точечно после изменения семантики, чтобы не делать перерасчёт
    // для всех кнопок на каждом пульсе.
    if (typeof global.updateStimulusButtonsReadinessViewForFinalImageId === 'function') {
      global.updateStimulusButtonsReadinessViewForFinalImageId(finalImageId);
    }
    // Сохраняем семантическую память сразу по факту изменения (после определения последствий действий),
    // а не по каждому пульсу. Используем подпись состояния, чтобы не писать одинаковое.
    var state = serializeSemanticState();
    var sig;
    try {
      sig = JSON.stringify(state);
    } catch (e) {
      sig = '';
    }
    if (sig && sig === lastSemanticSavedSig) return;
    lastSemanticSavedSig = sig;
    saveSemanticStateToIndexedDB(state);
  }

  // ---- Сериализация / десериализация состояния для IndexedDB и файлов ----

  function serializeSemanticState() {
    var plain = [];
    for (var i = 0; i < global.SemanticMemory.length; i++) {
      var it = global.SemanticMemory[i];
      plain.push({
        wellNormaBad: it.wellNormaBad,
        emotionId: it.emotionId,
        finalImageId: it.finalImageId,
        meanImportance: it.meanImportance,
        samplesCount: it.samplesCount,
        lastImportance: it.lastImportance
      });
    }
    return {
      semanticMemory: plain,
      knownFinalImageIds: global.KnownFinalImageIds.slice(0),
      knownEmotionIds: global.KnownEmotionIds.slice(0),
      // Дополнительно сохраняем структуру FinalImages и Emotions,
      // чтобы после перезапуска восстановить соответствия finalImageId и emotionId.
      finalImages: Array.isArray(global.FinalImages) ? global.FinalImages.slice(0) : [],
      emotions: Array.isArray(global.Emotions) ? global.Emotions.slice(0) : []
    };
  }

  function clearSemanticStructures() {
    global.SemanticMemory = [];
    global.SemanticMemoryIndex = {};
    global.KnownFinalImageIds = [];
    global.KnownEmotionIds = [];
  }

  /**
   * Сбросить всю семантическую память (вызов по кнопке «Sx» в UI).
   * Очищает массивы и сохраняет пустое состояние в IndexedDB.
   */
  function clearAllSemanticMemory() {
    clearSemanticStructures();
    var state = serializeSemanticState();
    lastSemanticSavedSig = JSON.stringify(state);
    saveSemanticStateToIndexedDB(state);
  }

  /**
   * Восстановить поля объекта из сохранённой записи без логики addSample (иначе при повторном
   * совпадении ключа в массиве или при передаче meanImportance в getOrCreate искажался бы samplesCount).
   */
  function restoreSemanticObjectFromPlain(p) {
    if (!p) return;
    var w = parseInt(p.wellNormaBad, 10);
    var e = parseInt(p.emotionId, 10);
    var f = parseInt(p.finalImageId, 10);
    if (!w || !e || !f) return;
    var obj = getOrCreateSemanticObject(w, e, f, undefined);
    if (!obj) return;
    if (p.meanImportance != null && typeof p.meanImportance === 'number' && !isNaN(p.meanImportance)) {
      obj.meanImportance = p.meanImportance;
    } else if (p.meanImportance != null && !isNaN(Number(p.meanImportance))) {
      obj.meanImportance = Number(p.meanImportance);
    }
    if (p.lastImportance != null && typeof p.lastImportance === 'number' && !isNaN(p.lastImportance)) {
      obj.lastImportance = p.lastImportance;
    } else if (p.lastImportance != null && !isNaN(Number(p.lastImportance))) {
      obj.lastImportance = Number(p.lastImportance);
    }
    var sc = parseInt(p.samplesCount, 10);
    if (!isNaN(sc) && sc >= 0) {
      obj.samplesCount = sc;
    } else if (typeof p.samplesCount === 'number' && !isNaN(p.samplesCount) && p.samplesCount >= 0) {
      obj.samplesCount = p.samplesCount;
    } else {
      if (!obj.samplesCount && obj.meanImportance) {
        obj.samplesCount = 1;
      }
    }
  }

  function applySemanticState(state) {
    clearSemanticStructures();
    if (!state) {
      return;
    }
    if (Array.isArray(state.knownFinalImageIds)) {
      global.KnownFinalImageIds = state.knownFinalImageIds.slice(0);
    }
    if (Array.isArray(state.knownEmotionIds)) {
      global.KnownEmotionIds = state.knownEmotionIds.slice(0);
    }
    if (Array.isArray(state.finalImages)) {
      global.FinalImages = state.finalImages.slice(0);
    }
    if (Array.isArray(state.emotions)) {
      global.Emotions = state.emotions.slice(0);
    }

    if (Array.isArray(state.semanticMemory)) {
      for (var i = 0; i < state.semanticMemory.length; i++) {
        restoreSemanticObjectFromPlain(state.semanticMemory[i]);
      }
    }
  }

  // ---- IndexedDB: постоянное хранение ----

  var semanticDb = null;
  // Версия 4: добавлено хранилище preferences (последняя папка для Сохранить/Загрузить).
  var SEMANTIC_DB_NAME = 'BEAST_DB';
  var SEMANTIC_DB_VERSION = 4;
  var SEMANTIC_STORE = 'semantic_state';
  var SEMANTIC_KEY = 'semantic_state_singleton';
  var MEMORY_SNAPSHOT_STORE = 'memory_snapshot';
  var MEMORY_SNAPSHOT_KEY = 'memory_singleton';
  var PREFERENCES_STORE = 'preferences';
  var LAST_MEMORY_DIR_KEY = 'lastMemoryDirectory';
  var lastSemanticSavedSig = '';

  function openSemanticDb() {
    if (!global.indexedDB || !global.IndexedDBModule) {
      return Promise.reject(new Error('IndexedDB не поддерживается или модуль не загружен.'));
    }
    return global.IndexedDBModule.openDatabase({
      dbName: SEMANTIC_DB_NAME,
      version: SEMANTIC_DB_VERSION,
      stores: [
        { name: SEMANTIC_STORE, options: { keyPath: 'id' } },
        { name: MEMORY_SNAPSHOT_STORE, options: { keyPath: 'id' } },
        { name: PREFERENCES_STORE, options: { keyPath: 'id' } }
      ]
    }).then(function (db) {
      semanticDb = db;
      return db;
    });
  }

  function loadSemanticStateFromIndexedDB() {
    if (!global.IndexedDBModule) {
      return;
    }
    openSemanticDb()
      .then(function (db) {
        return global.IndexedDBModule.get({
          db: db,
          storeName: SEMANTIC_STORE,
          key: SEMANTIC_KEY
        });
      })
      .then(function (record) {
        if (record && record.state) {
          applySemanticState(record.state);
          lastSemanticSavedSig = JSON.stringify(serializeSemanticState());

          // После восстановления семантики обновляем индикаторы на кнопках один раз,
          // чтобы "готовые" бордеры соответствовали восстановленному состоянию.
          if (typeof global.updateStimulusButtonsReadinessView === 'function') {
            global.updateStimulusButtonsReadinessView();
            global.setTimeout(function () {
              if (typeof global.updateStimulusButtonsReadinessView === 'function') {
                global.updateStimulusButtonsReadinessView();
              }
            }, 0);
          }
        }
      })
      .catch(function () {
        // тихо игнорируем ошибки загрузки
      });
  }

  function saveSemanticStateToIndexedDB(state) {
    if (!global.IndexedDBModule) {
      return;
    }
    state = state || serializeSemanticState();
    function doPut(db) {
      global.IndexedDBModule.put({
        db: db,
        storeName: SEMANTIC_STORE,
        value: {
          id: SEMANTIC_KEY,
          state: state,
          ts: Date.now()
        }
      }).catch(function () {
        // ошибки записи игнорируем
      });
    }
    if (semanticDb) {
      doPut(semanticDb);
      return;
    }
    openSemanticDb().then(function (db) {
      semanticDb = db;
      doPut(db);
    }).catch(function () {
      // не удалось открыть БД — сохранить не получится
    });
  }

  /**
   * Сохранить последнюю выбранную папку для диалогов Сохранить/Загрузить память.
   * Handle хранится в IndexedDB (FileSystemHandle поддерживается в Chrome).
   * @param {FileSystemDirectoryHandle} dirHandle
   * @returns {Promise<void>}
   */
  function saveLastMemoryDirectory(dirHandle) {
    if (!dirHandle || !global.IndexedDBModule) return Promise.resolve();
    function doPut(db) {
      return global.IndexedDBModule.put({
        db: db,
        storeName: PREFERENCES_STORE,
        value: { id: LAST_MEMORY_DIR_KEY, handle: dirHandle }
      });
    }
    if (semanticDb) return doPut(semanticDb);
    return openSemanticDb().then(doPut).catch(function () {});
  }

  /**
   * Получить последнюю сохранённую папку для startIn в диалоге выбора файла.
   * @returns {Promise<FileSystemDirectoryHandle|undefined>}
   */
  function loadLastMemoryDirectory() {
    if (!global.IndexedDBModule) return Promise.resolve(undefined);
    function doGet(db) {
      return global.IndexedDBModule.get({
        db: db,
        storeName: PREFERENCES_STORE,
        key: LAST_MEMORY_DIR_KEY
      }).then(function (record) {
        return record && record.handle ? record.handle : undefined;
      });
    }
    if (semanticDb) return doGet(semanticDb);
    return openSemanticDb().then(doGet).catch(function () { return undefined; });
  }

  // Инициализация при загрузке модуля
  loadSemanticStateFromIndexedDB();

  // Экспорт в глобальное пространство имён
  global.SemanticObjectImportance = SemanticObjectImportance;
  global.getOrCreateSemanticObject = getOrCreateSemanticObject;
  global.registerSemanticSample = registerSemanticSample;
  global.serializeSemanticState = serializeSemanticState;
  global.applySemanticState = applySemanticState;
  global.saveLastMemoryDirectory = saveLastMemoryDirectory;
  global.loadLastMemoryDirectory = loadLastMemoryDirectory;
  global.clearAllSemanticMemory = clearAllSemanticMemory;
  global.getSemanticObjectFast = getSemanticObjectFast;
  global.getSemanticFramesForFinalImageId = getSemanticFramesForFinalImageId;

  function bindResetSemanticButton() {
    try {
      var btn = global.document && global.document.getElementById('btnResetSemanticMemory');
      if (btn && typeof btn.addEventListener === 'function') {
        btn.addEventListener('click', function () {
          var msg = 'Точно сбросить всю семантическую память?';
          if (typeof global.show_confirm === 'function') {
            global.show_confirm(msg, function (result) {
              if (result) clearAllSemanticMemory();
            });
          } else if (global.confirm(msg)) {
            clearAllSemanticMemory();
          }
        });
      }
    } catch (e) {}
  }
  if (global.document && global.document.readyState === 'loading') {
    global.document.addEventListener('DOMContentLoaded', bindResetSemanticButton);
  } else {
    bindResetSemanticButton();
  }

})(window);

════════════════════════════════════════════════════════════

/BEAST/beast/_7_perception_tree/about_perception_tree.md
════════════════════════════════════════════════════════════
# Дерево условий восприятия (_7_perception_tree)

## Назначение

Модуль реализует **дерево условий восприятия** — иерархию из 6 уровней, по которой определяется текущая «ветка» условий. Узлы дерева используются ориентировочным рефлексом, эпизодической памятью (perceptionNodeId в кадре) и в перспективе — более сложными рефлексами и автоматизмами.

## Уровни

1. **BaseID** — базовое состояние (1=Плохо, 2=Норма, 3=Хорошо).
2. **EmotionID** — эмоция (сочетание активных базовых контекстов).
3. **ActivityID** — ID образа из FinalImages (активные стимулы), не ID кнопки.
4. **ToneMoodID** — тон и настроение (в данной реализации не используется; вербальный ввод и вербальные деревья в проект не входят).
5. **SimbolID** — первый символ первой фразы (не используется).
6. **PhraseID** — ID распознанной фразы (не используется).

Активная ветка определяется по уровням 1–3 (BaseID, EmotionID, ActivityID). Уровни 4–6 сохранены в структуре для совместимости с теорией и могут оставаться 0 или использоваться в будущем для иных невербальных измерений.

По полному пути из 6 ID выполняется быстрый поиск узла (pathIndex); по nodeId можно восстановить весь путь.

## Персистенция и интерфейс

- Компактный формат: одна строка на узел — `ID|ParentNode|BaseID|EmotionID|ActivityID|ToneMoodID|SimbolID|PhraseID`. Сохранение в IndexedDB (по пульсу при изменениях) и в файл (Сохранить/Загрузить).
- В интерфейсе отображается **активная ветка** (жёлтый блок): BaseID → EmotionID → ActivityID, актуальный стимул, последние 10 кадров ЭП, прогресс периода ожидания ЭП, кнопки копирования и тестового режима ЭП.

## Файлы

- **perception_tree.js** — узлы (nodes), pathIndex, getOrCreateTerminalNode, обновление вида активной ветки, расшифровка имён стимулов/действий для ЭП, сохранение/загрузка дерева.

Дерево обновляется по пульсу; терминальный узел по текущему пути (baseID, emotionID, activityID, 0, 0, 0) передаётся в ОР и в save_episodic_memory.

════════════════════════════════════════════════════════════

/BEAST/beast/_7_perception_tree/perception_tree.js
════════════════════════════════════════════════════════════
/**
 * Дерево условий восприятия (Perception Tree).
 *
 * Иерархия из 6 уровней:
 *   Level 1: BaseID (1=Плохо, 2=Норма, 3=Хорошо)
 *   Level 2: EmotionID (эмоция — сочетание активных базовых контекстов)
 *   Level 3: ActivityID — ID образа из FinalImages (global.FinalImages_findFinalImageId / getOrCreateFinalImageId), не ID кнопки стимула
 *   Level 4: ToneMoodID (тон и настроение сообщения)
 *   Level 5: SimbolID (первый символ первой фразы)
 *   Level 6: PhraseID (ID распознанной фразы)
 *
 * У каждого узла: id, parentId и значения по уровням (baseID..phraseID).
 * Быстрый поиск ветки: по текущим 6 ID сразу находится узел (pathIndex).
 * По любому nodeId можно восстановить полный путь (все 6 ID узла).
 *
 * Компактный формат в памяти: одна строка на узел
 *   ID|ParentNode|BaseID|EmotionID|ActivityID|ToneMoodID|SimbolID|PhraseID
 *
 * Сохранение: IndexedDB (авто) и файл (Сохранить/Загрузить).
 */
(function (global) {
  'use strict';

  var LEVEL_NAMES = ['BaseID', 'EmotionID', 'ActivityID', 'ToneMoodID', 'SimbolID', 'PhraseID'];

  /** Все узлы: id -> { id, parentId, baseID, emotionID, activityID, toneMoodID, simbolID, phraseID } */
  var nodes = new Map();

  /** Индекс: ключ пути (6 значений через "|") -> nodeId. Обеспечивает O(1) поиск ветки по активным условиям. */
  var pathIndex = new Map();

  /** Следующий свободный id узла. */
  var nextNodeId = 1;

  /** Корневой родитель (уровень 0). */
  var ROOT_PARENT = 0;

  function pathKey(baseID, emotionID, activityID, toneMoodID, simbolID, phraseID) {
    return (baseID || 0) + '|' + (emotionID || 0) + '|' + (activityID || 0) + '|' +
      (toneMoodID || 0) + '|' + (simbolID || 0) + '|' + (phraseID || 0);
  }

  function pathKeyFromArray(arr) {
    if (!Array.isArray(arr) || arr.length < 6) {
      return pathKey(0, 0, 0, 0, 0, 0);
    }
    return pathKey(arr[0], arr[1], arr[2], arr[3], arr[4], arr[5]);
  }

  /**
   * Добавить узел в дерево.
   * @param {number} parentId — id родителя (0 для корневого уровня).
   * @param {number} baseID — 1/2/3 (Плохо/Норма/Хорошо).
   * @param {number} emotionID
   * @param {number} activityID
   * @param {number} toneMoodID
   * @param {number} simbolID
   * @param {number} phraseID
   * @returns {number} id созданного узла.
   */
  function addNode(parentId, baseID, emotionID, activityID, toneMoodID, simbolID, phraseID) {
    var id = nextNodeId++;
    var b = baseID == null ? 0 : baseID;
    var e = emotionID == null ? 0 : emotionID;
    var a = activityID == null ? 0 : activityID;
    var t = toneMoodID == null ? 0 : toneMoodID;
    var s = simbolID == null ? 0 : simbolID;
    var p = phraseID == null ? 0 : phraseID;
    var node = {
      id: id,
      parentId: parentId == null ? ROOT_PARENT : parentId,
      baseID: b,
      emotionID: e,
      activityID: a,
      toneMoodID: t,
      simbolID: s,
      phraseID: p
    };
    nodes.set(id, node);
    pathIndex.set(pathKey(b, e, a, t, s, p), id);
    scheduleTreeSave();
    return id;
  }

  /**
   * Найти активную ветку по текущим условиям (все 6 ID).
   * Как только компоненты узлов установлены, ветка сразу определяется.
   * @returns {number|null} id узла или null.
   */
  function findActiveBranch(baseID, emotionID, activityID, toneMoodID, simbolID, phraseID) {
    var key = pathKey(
      baseID == null ? 0 : baseID,
      emotionID == null ? 0 : emotionID,
      activityID == null ? 0 : activityID,
      toneMoodID == null ? 0 : toneMoodID,
      simbolID == null ? 0 : simbolID,
      phraseID == null ? 0 : phraseID
    );
    var nodeId = pathIndex.get(key);
    return nodeId !== undefined ? nodeId : null;
  }

  /**
   * Найти активную ветку по массиву из 6 ID [baseID, emotionID, activityID, toneMoodID, simbolID, phraseID].
   */
  function findActiveBranchByArray(pathArr) {
    if (!Array.isArray(pathArr) || pathArr.length < 6) return null;
    return findActiveBranch(pathArr[0], pathArr[1], pathArr[2], pathArr[3], pathArr[4], pathArr[5]);
  }

  /**
   * По конечному id узла восстановить полный путь (все 6 уровней).
   * @param {number} nodeId
   * @returns {number[]|null} [baseID, emotionID, activityID, toneMoodID, simbolID, phraseID] или null.
   */
  function getPathByNodeId(nodeId) {
    var node = nodes.get(nodeId);
    if (!node) return null;
    return [node.baseID, node.emotionID, node.activityID, node.toneMoodID, node.simbolID, node.phraseID];
  }

  /**
   * Получить узел по id.
   */
  function getNode(nodeId) {
    return nodes.get(nodeId) || null;
  }

  /**
   * Дети узла (по parentId).
   */
  function getChildren(nodeId) {
    var list = [];
    nodes.forEach(function (n) {
      if (n.parentId === nodeId) list.push(n);
    });
    return list;
  }

  /**
   * Получить или создать конечный узел для пути из 6 уровней.
   * Создаёт узлы по уровням, если их ещё нет.
   * @param {number[]} pathArr — [baseID, emotionID, activityID, toneMoodID, simbolID, phraseID]
   * @returns {number} id конечного узла.
   */
  function getOrCreateTerminalNode(pathArr) {
    var path = [];
    for (var i = 0; i < 6; i++) {
      path.push(Array.isArray(pathArr) && pathArr[i] != null ? pathArr[i] : 0);
    }
    var parentId = ROOT_PARENT;
    for (var lev = 0; lev < 6; lev++) {
      var b = path[0];
      var e = lev >= 1 ? path[1] : 0;
      var a = lev >= 2 ? path[2] : 0;
      var t = lev >= 3 ? path[3] : 0;
      var s = lev >= 4 ? path[4] : 0;
      var p = lev >= 5 ? path[5] : 0;
      var key = pathKey(b, e, a, t, s, p);
      var nodeId = pathIndex.get(key);
      if (nodeId === undefined) {
        nodeId = addNode(parentId, b, e, a, t, s, p);
      }
      parentId = nodeId;
    }
    return parentId;
  }

  /**
   * Текущий активный терминальный узел дерева по состоянию виталов и активных стимулов.
   * Используется для привязки автоматизмов и проверки при осознании.
   * @returns {number} id узла или 0
   */
  function getActiveTerminalNodeId() {
    var baseID = global.BadNormWell || 0;
    var emotionID = 0;
    if (typeof global.Emotions_getOrCreateEmotionId === 'function' && Array.isArray(global.BasicContextsActived)) {
      emotionID = global.Emotions_getOrCreateEmotionId(global.BasicContextsActived) || 0;
    }
    var activityID = 0;
    if (typeof global.FinalImages_getOrCreateFinalImageId === 'function') {
      var activeIds = typeof global.StimulusGlobalId_getActiveGlobalIds === 'function'
        ? global.StimulusGlobalId_getActiveGlobalIds()
        : [];
      if (activeIds.length) activityID = global.FinalImages_getOrCreateFinalImageId(activeIds) || 0;
    }
    var nodeId = findActiveBranch(baseID, emotionID, activityID, 0, 0, 0);
    if (nodeId == null) nodeId = getOrCreateTerminalNode([baseID, emotionID, activityID, 0, 0, 0]);
    return nodeId != null ? nodeId : 0;
  }

  // ---------- Компактная сериализация (формат как в OLD_CODE/automatizm_tree.txt) ----------

  function serializeCompact() {
    var lines = [];
    nodes.forEach(function (node) {
      lines.push(
        node.id + '|' + node.parentId + '|' + node.baseID + '|' + node.emotionID + '|' +
        node.activityID + '|' + node.toneMoodID + '|' + node.simbolID + '|' + node.phraseID
      );
    });
    return lines.join('\n');
  }

  function deserializeCompact(text) {
    nodes.clear();
    pathIndex.clear();
    var maxId = 0;
    if (!text || typeof text !== 'string') {
      nextNodeId = 1;
      return;
    }
    var lines = text.split(/\r?\n/);
    for (var i = 0; i < lines.length; i++) {
      var line = lines[i].trim();
      if (!line) continue;
      var parts = line.split('|');
      if (parts.length < 8) continue;
      var id = parseInt(parts[0], 10);
      var parentId = parseInt(parts[1], 10);
      var baseID = parseInt(parts[2], 10) || 0;
      var emotionID = parseInt(parts[3], 10) || 0;
      var activityID = parseInt(parts[4], 10) || 0;
      var toneMoodID = parseInt(parts[5], 10) || 0;
      var simbolID = parseInt(parts[6], 10) || 0;
      var phraseID = parseInt(parts[7], 10) || 0;
      if (isNaN(id)) continue;
      var node = {
        id: id,
        parentId: isNaN(parentId) ? ROOT_PARENT : parentId,
        baseID: baseID,
        emotionID: emotionID,
        activityID: activityID,
        toneMoodID: toneMoodID,
        simbolID: simbolID,
        phraseID: phraseID
      };
      nodes.set(id, node);
      pathIndex.set(pathKey(baseID, emotionID, activityID, toneMoodID, simbolID, phraseID), id);
      if (id > maxId) maxId = id;
    }
    nextNodeId = maxId + 1;
  }

  /**
   * Состояние для сохранения (объект: compactText + nextNodeId для целостности).
   */
  function serializeState() {
    return {
      compact: serializeCompact(),
      nextId: nextNodeId
    };
  }

  function applyState(state) {
    if (!state) return;
    if (state.compact) deserializeCompact(state.compact);
    if (typeof state.nextId === 'number' && state.nextId > 0) {
      nextNodeId = state.nextId;
    }
  }

  // ---------- IndexedDB ----------

  var treeDb = null;
  var TREE_DB_NAME = 'BEAST_PerceptionTree_DB';
  var TREE_DB_VERSION = 1;
  var TREE_STORE = 'perception_tree_state';
  var TREE_KEY = 'tree_singleton';
  var treeDirty = false;
  var lastTreeSavedSig = '';

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

  function saveTreeToIndexedDB(state) {
    if (!global.IndexedDBModule) return;
    state = state || serializeState();
    function doPut(db) {
      global.IndexedDBModule.put({
        db: db,
        storeName: TREE_STORE,
        value: { id: TREE_KEY, state: state, ts: Date.now() }
      }).catch(function () {});
    }
    if (treeDb) {
      doPut(treeDb);
      return;
    }
    openTreeDb().then(doPut).catch(function () {});
  }

  function scheduleTreeSave() {
    treeDirty = true;
  }

  function loadTreeFromIndexedDB() {
    if (!global.IndexedDBModule) return;
    openTreeDb()
      .then(function (db) {
        return global.IndexedDBModule.get({
          db: db,
          storeName: TREE_STORE,
          key: TREE_KEY
        });
      })
      .then(function (record) {
        if (record && record.state) {
          applyState(record.state);
          var s = serializeState();
          lastTreeSavedSig = (s.compact || '') + '|' + (s.nextId || 0);
        }
      })
      .catch(function () {});
  }

  // ---------- Экспорт ----------

  global.PerceptionTree_addNode = addNode;
  global.PerceptionTree_findActiveBranch = findActiveBranch;
  global.PerceptionTree_findActiveBranchByArray = findActiveBranchByArray;
  global.PerceptionTree_getPathByNodeId = getPathByNodeId;
  global.PerceptionTree_getNode = getNode;
  global.PerceptionTree_getChildren = getChildren;
  global.PerceptionTree_serializeCompact = serializeCompact;
  global.PerceptionTree_deserializeCompact = deserializeCompact;
  global.PerceptionTree_serializeState = serializeState;
  global.PerceptionTree_applyState = applyState;
  global.PerceptionTree_getOrCreateTerminalNode = getOrCreateTerminalNode;
  global.PerceptionTree_getActiveTerminalNodeId = getActiveTerminalNodeId;
  global.PerceptionTree_PATH_LEVEL_NAMES = LEVEL_NAMES;
  global.PerceptionTree_ROOT_PARENT = ROOT_PARENT;

  /**
   * Расшифровка BaseID для show_alert.
   */
  function getBaseIDLabel(baseID) {
    if (baseID === 1) return 'Плохо';
    if (baseID === 2) return 'Норма';
    if (baseID === 3) return 'Хорошо';
    return '—';
  }

  /**
   * Расшифровка EmotionID: список имён активных базовых контекстов.
   */
  function getEmotionIDLabel(emotionId) {
    if (!emotionId || !Array.isArray(global.Emotions) || !Array.isArray(global.BasicContexts)) return '—';
    var ctxIds = null;
    for (var i = 0; i < global.Emotions.length; i++) {
      if (global.Emotions[i] && global.Emotions[i].id === emotionId) {
        ctxIds = global.Emotions[i].contextIds || [];
        break;
      }
    }
    if (!ctxIds || !ctxIds.length) return '—';
    var names = [];
    for (var j = 0; j < ctxIds.length; j++) {
      for (var k = 0; k < global.BasicContexts.length; k++) {
        if (global.BasicContexts[k] && global.BasicContexts[k].id === ctxIds[j]) {
          names.push(global.BasicContexts[k].name || '');
          break;
        }
      }
    }
    return names.filter(Boolean).length ? names.join(', ') : '—';
  }

  /**
   * Список имён активных стимулов (для расшифровки по клику на ActivityID).
   * ActivityID в дереве — это ID образа (FinalImages), при расшифровке показываем имена текущих активных стимулов.
   * Имена берутся только из того массива, в котором стимул активен (один Id может встречаться в разных типах).
   */
  function getActiveStimulusNames() {
    var names = [];
    function addNamesFrom(activeIds, stimulArray) {
      if (!Array.isArray(activeIds) || !Array.isArray(stimulArray)) return;
      for (var i = 0; i < activeIds.length; i++) {
        var id = activeIds[i];
        if (typeof id !== 'number') continue;
        for (var j = 0; j < stimulArray.length; j++) {
          if (stimulArray[j] && stimulArray[j].Id === id) {
            names.push(stimulArray[j].name || ('Id ' + id));
            break;
          }
        }
      }
    }
    addNamesFrom(global.ActiveMotivationStimuls, global.StimulMotivarion);
    addNamesFrom(global.ActivePositivStimuls, global.StimulPositiv);
    addNamesFrom(global.ActiveNegativeStimuls, global.StimulNegative);
    addNamesFrom(global.ActiveNeutralStimuls, global.StimulNeutral);
    return names.length ? names.join(', ') : '—';
  }

  /**
   * Текст детализации автоматизма по ID (для show_alert по щелчку).
   */
  function getAutomatizmDetailText(automatizmId) {
    if (!automatizmId || !Array.isArray(global.Automatizms)) return '—';
    var a = null;
    for (var i = 0; i < global.Automatizms.length; i++) {
      if (global.Automatizms[i] && global.Automatizms[i].id === automatizmId) {
        a = global.Automatizms[i];
        break;
      }
    }
    if (!a) return '—';
    var actionNames = getActionNamesForActionImage(a.actionsImageId);
    var lines = [
      'ID: ' + a.id,
      'Ветка (BranchID): ' + a.branchId,
      'Образ действия (ОД ' + a.actionsImageId + '): ' + actionNames,
      'Полезность (Usefulness): ' + (typeof a.usefulness === 'number' ? a.usefulness.toFixed(3) : '—'),
      'Надёжность (Count): ' + (a.count != null ? a.count : 0),
      'Штатный: ' + (a.isDefault ? 'да' : 'нет')
    ];
    return lines.join('<br>');
  }

  /**
   * Имена действий по ID образа действия (ОД).
   */
  function getActionNamesForActionImage(odId) {
    if (!odId || typeof global.ActionImages_getActionImage !== 'function') return '—';
    var od = global.ActionImages_getActionImage(odId);
    if (!od || !Array.isArray(od.actionIds) || !od.actionIds.length) return '—';
    var names = [];
    var actions = global.BasicActions;
    if (!actions || !Array.isArray(actions)) return od.actionIds.join(', ');
    for (var i = 0; i < od.actionIds.length; i++) {
      var aid = od.actionIds[i];
      var found = false;
      for (var j = 0; j < actions.length; j++) {
        if (actions[j] && actions[j].Id === aid) {
          names.push(actions[j].name || ('Id ' + aid));
          found = true;
          break;
        }
      }
      if (!found) names.push('Id ' + aid);
    }
    return names.length ? names.join(', ') : '—';
  }

  /**
   * Расшифровка ID образа (FinalImage id): по image id находим образ, по его stimulIds — имена.
   * stimulIds хранятся как глобальные id (type*1000+localId), расшифровка через StimulusGlobalId_getName устраняет коллизии.
   */
  function getStimulusNamesForFinalImageId(finalImageId) {
    if (!finalImageId || !Array.isArray(global.FinalImages)) return '—';
    var stimulIds = null;
    for (var i = 0; i < global.FinalImages.length; i++) {
      if (global.FinalImages[i] && global.FinalImages[i].id === finalImageId) {
        stimulIds = global.FinalImages[i].stimulIds;
        break;
      }
    }
    if (!stimulIds || !stimulIds.length) return '—';
    var names = [];
    var getName = typeof global.StimulusGlobalId_getName === 'function' ? global.StimulusGlobalId_getName : null;
    for (var g = 0; g < stimulIds.length; g++) {
      var sid = stimulIds[g];
      var n = getName ? getName(sid) : '';
      if (n) names.push(n); else names.push('Id ' + sid);
    }
    return names.length ? names.join(', ') : '—';
  }

  /**
   * Расшифровка конечного узла ветки: путь по уровням (BaseID, EmotionID, ActivityID и т.д.).
   */
  function getTerminalNodeDecoding(nodeId) {
    var node = nodes.get(nodeId);
    if (!node) return '—';
    var baseLabel = getBaseIDLabel(node.baseID);
    var emotionLabel = getEmotionIDLabel(node.emotionID);
    var activityLabel = '—';
    if (node.activityID && typeof global.FinalImages === 'object') {
      for (var fi = 0; fi < global.FinalImages.length; fi++) {
        if (global.FinalImages[fi] && global.FinalImages[fi].id === node.activityID) {
          var sids = global.FinalImages[fi].stimulIds;
          if (sids && sids.length) {
            var gs = [global.StimulMotivarion, global.StimulPositiv, global.StimulNegative, global.StimulNeutral];
            var nms = [];
            for (var g = 0; g < sids.length; g++) {
              for (var h = 0; h < gs.length; h++) {
                if (!gs[h] || !Array.isArray(gs[h])) continue;
                for (var k = 0; k < gs[h].length; k++) {
                  if (gs[h][k] && gs[h][k].Id === sids[g]) {
                    nms.push(gs[h][k].name || ('Id ' + sids[g]));
                    break;
                  }
                }
              }
            }
            activityLabel = nms.length ? nms.join(', ') : String(node.activityID);
          }
          break;
        }
      }
      if (activityLabel === '—') activityLabel = String(node.activityID);
    } else if (node.activityID) activityLabel = String(node.activityID);
    var parts = [
      'BaseID: ' + baseLabel,
      'EmotionID: ' + emotionLabel,
      'ActivityID: ' + activityLabel,
      'ToneMoodID: ' + (node.toneMoodID || 0),
      'SimbolID: ' + (node.simbolID || 0),
      'PhraseID: ' + (node.phraseID || 0)
    ];
    return parts.join('\n');
  }

  /**
   * Обновить блок индикации активной ветки дерева (первая строка: BaseID -> EmotionID -> ActivityID, ID узла, актуальный стимул; по клику — расшифровка).
   */
  function updateActiveBranchView() {
    var doc = global.document;
    var el = doc && doc.getElementById('perceptionTreeActiveBranchText');
    if (!el) return;

    var progressWrap = doc && doc.getElementById('epWaitingProgressWrap');
    var progressFill = doc && doc.getElementById('epWaitingProgressFill');
    var inWaiting = (global.EpisodicMemory_waitingStartedAt != null) ||
      (typeof global.EpisodicMemory_barShowCompletePulsesLeft === 'number' && global.EpisodicMemory_barShowCompletePulsesLeft > 0);
    if (progressWrap) {
      progressWrap.style.display = inWaiting ? '' : 'none';
    }
    if (progressFill) {
      var period = (typeof global.EpisodicMemory_response_waiting_period === 'number' && global.EpisodicMemory_response_waiting_period > 0)
        ? global.EpisodicMemory_response_waiting_period
        : 15;
      var pct = 0;
      if (typeof global.EpisodicMemory_barShowCompletePulsesLeft === 'number' && global.EpisodicMemory_barShowCompletePulsesLeft > 0) {
        pct = 100;
      } else if (typeof global.EpisodicMemory_pulsesWaiting === 'number' && period > 0) {
        pct = Math.min(100, (global.EpisodicMemory_pulsesWaiting / period) * 100);
      }
      progressFill.style.width = pct.toFixed(1) + '%';
    }

    var baseID = global.BadNormWell || 0;
    var emotionID = 0;
    if (typeof global.Emotions_getOrCreateEmotionId === 'function' && Array.isArray(global.BasicContextsActived)) {
      emotionID = global.Emotions_getOrCreateEmotionId(global.BasicContextsActived) || 0;
    }
    var activityID = 0;
    if (typeof global.FinalImages_getOrCreateFinalImageId === 'function') {
      // Глобальные id стимулов (type*1000+localId), см. stimulus_global_id.js.
      var activeIds = typeof global.StimulusGlobalId_getActiveGlobalIds === 'function'
        ? global.StimulusGlobalId_getActiveGlobalIds()
        : [];
      if (activeIds.length) activityID = global.FinalImages_getOrCreateFinalImageId(activeIds) || 0;
    }
    var terminalNodeId = findActiveBranch(baseID, emotionID, activityID, 0, 0, 0);
    if (terminalNodeId == null) {
      terminalNodeId = 0;
    }
    // Не вызываем getOrCreateTerminalNode здесь — обновление вида не должно создавать узлы и ставить treeDirty каждый пульс.
    // Узлы создаются при активации стимулов (orientation_reflex → getOrCreateTerminalNode).
    var actualId = (typeof global.actual_stimul_ID === 'number' && global.actual_stimul_ID > 0) ? global.actual_stimul_ID : 0;
    var html = 'Активная ветка дерева восприятия (ID=<a href="#" class="pt-branch-link" data-type="node" data-value="' + terminalNodeId + '">' + terminalNodeId + '</a>): BaseID=<a href="#" class="pt-branch-link" data-type="base" data-value="' + baseID + '">' + baseID + '</a> → EmotionID=<a href="#" class="pt-branch-link" data-type="emotion" data-value="' + emotionID + '">' + emotionID + '</a> → ActivityID=<a href="#" class="pt-branch-link" data-type="activity" data-value="' + activityID + '">' + activityID + '</a> Актуальный стимул=<a href="#" class="pt-branch-link" data-type="actual" data-value="' + actualId + '">' + actualId + '</a>';
    var automatismsList = (typeof global.Automatizms_getAutomatizmsByBranchId === 'function')
      ? global.Automatizms_getAutomatizmsByBranchId(terminalNodeId) : [];
    if (automatismsList && automatismsList.length) {
      var aParts = [];
      for (var ai = 0; ai < automatismsList.length; ai++) {
        var aid = automatismsList[ai].id;
        aParts.push('<a href="#" class="pt-branch-link" data-type="automatism" data-value="' + aid + '">' + aid + '</a>');
      }
      html += ' Автоматизмы: ' + aParts.join(', ');
    } else {
      html += ' Автоматизмы: нет';
    }
    el.innerHTML = html;
    var links = el.querySelectorAll('a.pt-branch-link');
    for (var i = 0; i < links.length; i++) {
      (function (link) {
        link.onclick = function (e) {
          e.preventDefault();
          var type = link.getAttribute('data-type');
          var value = link.getAttribute('data-value');
          var text = '—';
          if (type === 'base') text = getBaseIDLabel(parseInt(value, 10));
          else if (type === 'emotion') text = getEmotionIDLabel(parseInt(value, 10));
          else if (type === 'activity') text = getActiveStimulusNames();
          else if (type === 'actual') {
            var actualVal = parseInt(value, 10);
            var curActivityID = 0;
            if (typeof global.FinalImages_getOrCreateFinalImageId === 'function' && typeof global.StimulusGlobalId_getActiveGlobalIds === 'function') {
              var aids = global.StimulusGlobalId_getActiveGlobalIds();
              if (aids.length) curActivityID = global.FinalImages_getOrCreateFinalImageId(aids) || 0;
            }
            if (actualVal === curActivityID) {
              text = getActiveStimulusNames();
            } else {
              text = getStimulusNamesForFinalImageId(actualVal);
            }
          } else if (type === 'node') text = getTerminalNodeDecoding(parseInt(value, 10));
          else if (type === 'automatism') text = getAutomatizmDetailText(parseInt(value, 10));
          if (typeof global.show_alert === 'function') global.show_alert(text);
        };
      })(links[i]);
    }

    var epLine = global.document && global.document.getElementById('perceptionTreeEpisodesLine');
    var btnClearEp = global.document && global.document.getElementById('btnClearEpisodicMemory');
    var totalLabel = global.document && global.document.getElementById('epTotalCountLabel');
    if (epLine && Array.isArray(global.Episodes)) {
      var ep = global.Episodes;
      if (btnClearEp) {
        btnClearEp.style.display = ep.length ? '' : 'none';
        btnClearEp.textContent = '✕';
      }
      if (totalLabel) {
        totalLabel.textContent = ep.length ? ('(всего кадров ЭП: ' + String(ep.length) + ')') : '';
      }
      var last10 = ep.slice(-10).reverse();
      var parts = [];
      for (var ei = 0; ei < last10.length; ei++) {
        var num = ei + 1;
        parts.push('<span class="pt-ep-frame" data-ep-index="' + ei + '" title="Кадр ' + num + ' (щелкните для детализации)">' + num + '</span>');
      }
      epLine.innerHTML = parts.length
        ? ('Последние 10 кадров эпизодической памяти: ' + parts.join(', '))
        : '';
      var frameSpans = epLine.querySelectorAll('.pt-ep-frame');
      for (var si = 0; si < frameSpans.length && si < last10.length; si++) {
        (function (f) {
          frameSpans[si].onclick = function (e) {
            e.preventDefault();
            var node = getNode(f.perceptionNodeId);
            var baseLabel = node ? getBaseIDLabel(node.baseID) : '—';
            var emotionLabel = node ? getEmotionIDLabel(node.emotionID) : '—';
            var stimLabel = getStimulusNamesForFinalImageId(f.stimulusImageId);
            var actLabel = getActionNamesForActionImage(f.actionImageId);
            var lines = ['<b>Условия: </b><br>Базовое состояние: ' + baseLabel + '<br>Эмоция: ' + emotionLabel + '<br>Актуальный стимул: ' + stimLabel + '<br>Ответное действие: ' + actLabel];
            if (f.effect !== null && f.effect !== undefined && !isNaN(f.effect)) {
              var effStr = f.effect >= 0 ? '+' + String(f.effect) : String(f.effect);
              lines.push('Эффект: ' + effStr);
            }
            if (f.responseStimulusImageId != null) {
              var respLabel = getStimulusNamesForFinalImageId(f.responseStimulusImageId);
              lines.push('Ответ оператора: ' + respLabel);
            }
            var msg = lines.join('<br>');
            if (typeof global.show_alert === 'function') global.show_alert(msg);
          };
        })(last10[si]);
      }
    } else {
      if (btnClearEp) {
        btnClearEp.style.display = 'none';
      }
      if (totalLabel) {
        totalLabel.textContent = '';
      }
    }
    if (typeof global.scheduleStimulusReadinessRefreshIfNeeded === 'function') {
      global.scheduleStimulusReadinessRefreshIfNeeded();
    }
  }

  global.PerceptionTree_updateActiveBranchView = updateActiveBranchView;
  global.PerceptionTree_getStimulusNamesForFinalImageId = getStimulusNamesForFinalImageId;
  global.PerceptionTree_getActionNamesForActionImage = getActionNamesForActionImage;

  if (typeof global.registerIndexedDBSaver === 'function') {
    global.registerIndexedDBSaver(function () {
      if (!treeDirty) return;
      treeDirty = false;
      var state = serializeState();
      var sig = (state.compact || '') + '|' + (state.nextId || 0);
      if (sig === lastTreeSavedSig) return;
      lastTreeSavedSig = sig;
      saveTreeToIndexedDB(state);
    });
  }

  loadTreeFromIndexedDB();
  if (global.document && global.document.readyState === 'complete') {
    global.setTimeout(updateActiveBranchView, 0);
  } else if (global.document) {
    global.document.addEventListener('DOMContentLoaded', function () {
      global.setTimeout(updateActiveBranchView, 0);
    });
  }
})(window);

════════════════════════════════════════════════════════════

/BEAST/beast/_8_Hippocampus/about_hippocampus.md
════════════════════════════════════════════════════════════
# Гиппокамп и ориентировочный рефлекс (_8_Hippocampus)

## Назначение

Модуль объединяет **оперативную память стимулов** (гиппокамп) и **ориентировочный рефлекс (ОР)** — конкуренцию активных стимулов и выбор наиболее актуального. ОР запускает осознание, эпизодическую память и блокировку рефлексов на 2 пульса; от условий ОР зависит, создаётся ли кадр ЭП и формируется ли условный рефлекс.

## Гиппокамп (hippocampus.js)

- **active_stimulsArr_ID** — массив ID образов (FinalImageId), вызвавших ОР.
- **actual_stimul_ID** — наиболее актуальный стимул (ID образа). Заполняется при вызове Hippocampus_holdStimulusFromOR(winnerId) из ОР.

## Ориентировочный рефлекс (orientation_reflex.js)

- **Условия запуска:** есть активные стимулы, есть терминальный узел дерева восприятия, выявлен победитель конкуренции, у победителя **samplesCount >= OR_well_known** (по умолчанию 10). Иначе ОР не создаёт кадр ЭП и при тестовом режиме выдаёт причину отказа.
- UI-индикатор «готовности к ОР» на кнопках стимула использует тот же порог `OR_well_known` (оставшиеся активации считаются как `max(0, OR_well_known - samplesCount)`).
- **Актуальность** = новизна × значимость; гистерезис для предыдущего победителя (усилен при `ImportantProblem`). При **периоде ожидания ЭП** активация обрабатывается как ответ оператора (закрытие кадра, новый кадр по ответу), затем **второй полный проход ОР** — снова `stimuls_consciousness` и `save_episodic_memory` (см. `about_episodic_memory_rules.md`, §4).
- **Действия ОР** при успешном проходе порогов: блокировка рефлекса на 2 пульса (`setBlockedReflex`), `Hippocampus_holdStimulusFromOR`, **`stimuls_consciousness(orContext)`** (в контексте — признак победителя ОР), **`save_episodic_memory`** — независимо от тестового режима и наличия семантической записи.

Подробные правила формирования кадров ЭП и связь с условными рефлексами описаны в _10_Episodic_memory/about_episodic_memory_rules.md.

## Файлы

- **hippocampus.js** — хранение active_stimulsArr_ID, actual_stimul_ID, holdStimulusFromOR.
- **orientation_reflex.js** — runOROnStimulusActivation, условия отказа, порог OR_well_known, проверка периода ожидания ЭП, вызовы ЭП и сознания.

════════════════════════════════════════════════════════════

/BEAST/beast/_8_Hippocampus/hippocampus.js
════════════════════════════════════════════════════════════
/**
 * Гиппокамп: оперативная память стимулов, вызвавших ориентировочный рефлекс.
 *
 * - active_stimulsArr_ID — массив ID удерживаемых стимулов (FinalImageId),
 *   которые вызывали ОР.
 * - actual_stimul_ID — глобальная ссылка на наиболее актуальный стимул (ID образа/комбинации).
 */
(function (global) {
  'use strict';

  /** Оперативная память: массив ID стимулов (FinalImageId), вызвавших ОР. */
  if (!Array.isArray(global.active_stimulsArr_ID)) {
    global.active_stimulsArr_ID = [];
  }

  /** Наиболее актуальный стимул (ID образа — FinalImageId). Заполняется при ОР. */
  if (typeof global.actual_stimul_ID !== 'number') {
    global.actual_stimul_ID = 0;
  }

  /**
   * Удержать в памяти стимул, вызвавший ОР (добавить в active_stimulsArr_ID и установить actual_stimul_ID).
   * @param {number} finalImageId — ID образа (FinalImageId) наиболее актуального стимула.
   */
  function holdStimulusFromOR(finalImageId) {
    if (typeof finalImageId !== 'number' || finalImageId <= 0) return;
    if (global.active_stimulsArr_ID.indexOf(finalImageId) === -1) {
      global.active_stimulsArr_ID.push(finalImageId);
    }
    global.actual_stimul_ID = finalImageId;
  }

  global.Hippocampus_holdStimulusFromOR = holdStimulusFromOR;
})(window);

════════════════════════════════════════════════════════════

/BEAST/beast/_8_Hippocampus/orientation_reflex.js
════════════════════════════════════════════════════════════
/**
 * Ориентировочный рефлекс (ОР) — конкуренция среди активных стимулов.
 *
 * Условие запуска ОР: ID конечного узла активной ветки дерева восприятия
 * (дерево условий восприятия, а не только базовое состояние + эмоция).
 *
 * Алгоритм:
 * - Актуальность = новизна × значимость стимула.
 * - Если нет значимости в семантической модели для данных условий:
 *   значимость = (VitalsArr.importance)/20, новизна = 1.
 * - Новизна при наличии значимости: Н = (СЗ * samplesCount) / 10.
 * - Множители новизны при отсутствии узлов уровня: нет 6-го → Н*=2, нет 5-го → Н*=3, 4-го → Н*=4 и т.д.
 *
 * Действия ОР:
 * 1. Гистерезис: понижение актуальности остальных стимулов (вычитание константы).
 *    Стимулы из связной цепочки кадров ЭП (суффикс с реакциями на каждом кадре) гистерезису не подвергаются
 *    и получают повышение актуальности — см. EpisodicMemory_getChainPriorityStimulusSet, about_episodic_memory_rules.md.
 * 2. Блокировка генетического/условного рефлекса на 2 пульса (задержка решения).
 * 3. Гиппокамп: удержание в памяти стимула (active_stimulsArr_ID, actual_stimul_ID).
 * 4. Осознание: вызов stimuls_consciousness(actual_stimul_ID).
 * 5. Эпизодическая память: вызов save_episodic_memory().
 */
(function (global) {
  'use strict';

  /** Постоянная гистерезиса ОР: вычитается из актуальности остальных стимулов (подбор при оптимизации). */
  var OR_HYSTERESIS = 0.5;

  /** Блокировка рефлекса на столько пульсов после ОР. */
  var OR_BLOCK_PULSES = 2;

  /** Порог «хорошо известного» опыта: при samplesCount < OR_well_known стимул не вызывает ОР и не входит в ЭП. Подбор при оптимизации. */
  var OR_well_known = 10;
  global.OR_well_known = OR_well_known;

  /** Хранилище блокировок: { key: string (finalImageId или stimId), untilPulse: number, actionId?: number } */
  var reflexBlockedUntil = [];

  /**
   * Текущий путь дерева восприятия: [baseID, emotionID, 0, 0, 0, 0].
   * baseID = BadNormWell, emotionID = Emotions_getOrCreateEmotionId(BasicContextsActived).
   */
  function getCurrentTreePath() {
    var baseID = global.BadNormWell || 0;
    var emotionID = 0;
    if (typeof global.Emotions_getOrCreateEmotionId === 'function' && Array.isArray(global.BasicContextsActived)) {
      emotionID = global.Emotions_getOrCreateEmotionId(global.BasicContextsActived) || 0;
    }
    return [baseID, emotionID, 0, 0, 0, 0];
  }

  /**
   * Получить объект семантической значимости для (well, emotionId, finalImageId).
   * @returns {{ meanImportance: number, samplesCount: number } | null}
   */
  function getSemanticForConditions(finalImageId, well, emotionId) {
    if (!finalImageId || !well || !global.SemanticMemoryIndex) return null;
    var wKey = String(well);
    var eKey = String(emotionId);
    var bucket = global.SemanticMemoryIndex[wKey] && global.SemanticMemoryIndex[wKey][eKey];
    if (!bucket) return null;
    var obj = bucket[String(finalImageId)];
    if (!obj) return null;
    return {
      meanImportance: obj.meanImportance || 0,
      samplesCount: obj.samplesCount || 0
    };
  }

  /**
   * Уровень готовности по опыту (порог OR_well_known) для конкретного FinalImageId.
   * Возвращает число оставшихся «активаций» до выполнения порога (max(0, wellKnown - samplesCount)).
   * В состоянии Плохо фактический старт ОР порог не блокирует (см. runOROnStimulusActivation), но для UI
   * остаток считается по семантике — как в Норме/Хорошо.
   *
   * @param {number} finalImageId
   * @returns {{remainingActivations:number, samplesCount:number, wellKnown:number, well:number, emotionId:number}}
   */
  function getOrReadinessForFinalImageId(finalImageId) {
    var well = global.BadNormWell || 0;
    var wellKnown = (typeof global.OR_well_known === 'number' && global.OR_well_known >= 0) ? global.OR_well_known : OR_well_known;

    // Если базовое состояние ещё не рассчитано (BadNormWell=0) — показатель зрелости вычислить нельзя.
    if (well !== 1 && well !== 2 && well !== 3) {
      return {
        remainingActivations: null,
        samplesCount: null,
        wellKnown: wellKnown,
        well: well,
        emotionId: null
      };
    }

    // В Плохо порог OR_well_known для фактического старта ОР не применяется (см. runOROnStimulusActivation),
    // но индикатор «готовности по опыту» для UI считаем так же, как в Норме/Хорошо — иначе при well===1
    // у всех стимулов remaining=0 и все кнопки подсвечиваются как готовые к ОР.

    var emotionId = 0;
    if (typeof global.Emotions_getOrCreateEmotionId === 'function' && Array.isArray(global.BasicContextsActived)) {
      emotionId = global.Emotions_getOrCreateEmotionId(global.BasicContextsActived) || 0;
    }
    if (!emotionId) {
      return {
        remainingActivations: null,
        samplesCount: null,
        wellKnown: wellKnown,
        well: well,
        emotionId: 0
      };
    }

    var samplesCount = 0;
    if (finalImageId && well && emotionId) {
      var sem = getSemanticForConditions(finalImageId, well, emotionId);
      samplesCount = (sem && typeof sem.samplesCount === 'number') ? sem.samplesCount : 0;
    }

    var remaining = Math.max(0, wellKnown - samplesCount);
    return {
      remainingActivations: remaining,
      samplesCount: samplesCount,
      wellKnown: wellKnown,
      well: well,
      emotionId: emotionId
    };
  }

  /** Средняя важность виталов для значимости при отсутствии семантической записи (importance/20). */
  function getDefaultSignificanceFromVitals() {
    if (!Array.isArray(global.VitalsArr) || !global.VitalsArr.length) return 0.05;
    var sum = 0;
    for (var i = 0; i < global.VitalsArr.length; i++) {
      sum += (global.VitalsArr[i].importance || 0);
    }
    return (sum / global.VitalsArr.length) / 20;
  }

  /**
   * Множитель новизны по отсутствующим уровням узла дерева (pathArr — путь из 6 элементов).
   * Нет 6-го уровня → *2, нет 5-го → *3, 4-го → *4, 3-го → *5, 2-го → *6.
   */
  function noveltyLevelMultiplier(pathArr) {
    if (!Array.isArray(pathArr) || pathArr.length < 6) return 1;
    var m = 1;
    if (!pathArr[5]) m *= 2; // phraseID
    if (!pathArr[4]) m *= 3; // simbolID
    if (!pathArr[3]) m *= 4; // toneMoodID
    if (!pathArr[2]) m *= 5; // activityID
    if (!pathArr[1]) m *= 6; // emotionID
    return m;
  }

  /**
   * Вычислить новизну стимула для данных условий.
   * Если нет значимости в СЗ: новизна = 1.
   * Иначе Н = (СЗ * samplesCount) / 10, затем Н *= множитель по отсутствующим уровням.
   */
  function computeNovelty(finalImageId, well, emotionId, pathArr) {
    var sem = getSemanticForConditions(finalImageId, well, emotionId);
    var n;
    if (!sem || sem.samplesCount <= 0) {
      n = 1;
    } else {
      n = (sem.meanImportance * sem.samplesCount) / 10;
      if (n <= 0) n = 0.1;
    }
    n *= noveltyLevelMultiplier(pathArr || getCurrentTreePath());
    return n;
  }

  /**
   * Вычислить значимость стимула для данных условий.
   * Если нет записи в СЗ: (VitalsArr.importance)/20.
   */
  function computeSignificance(finalImageId, well, emotionId) {
    var sem = getSemanticForConditions(finalImageId, well, emotionId);
    if (!sem || sem.samplesCount <= 0) return getDefaultSignificanceFromVitals();
    return sem.meanImportance;
  }

  /**
   * Актуальность = новизна × значимость.
   */
  function computeActuality(finalImageId, well, emotionId, pathArr) {
    var sig = computeSignificance(finalImageId, well, emotionId);
    var nov = computeNovelty(finalImageId, well, emotionId, pathArr);
    return nov * sig;
  }

  /**
   * Среди кандидатов (finalImageId[]) выявить наиболее актуальный.
   * lastWinnerId — ID прошлого победителя; для остальных из актуальности вычитается гистерезис.
   * При ImportantProblem гистерезис усилен (понижается значимость остальных стимулов в конкуренции).
   * Стимулы из связной цепочки кадров ЭП (реакция на каждом кадре суффикса) гистерезису не подвергаются и
   * усиливаются по актуальности — преимущество перед «обрывающими» цепочку стимулами без такой связи.
   * @returns {{ winnerId: number, actuality: number } | null}
   */
  function findMostActualStimulus(candidateFinalImageIds, lastWinnerId) {
    if (!candidateFinalImageIds || !candidateFinalImageIds.length) return null;
    var well = global.BadNormWell || 0;
    var emotionId = 0;
    if (typeof global.Emotions_getOrCreateEmotionId === 'function' && Array.isArray(global.BasicContextsActived)) {
      emotionId = global.Emotions_getOrCreateEmotionId(global.BasicContextsActived) || 0;
    }
    var pathArr = getCurrentTreePath();
    var importantProblem = !!(global.InfoPicture && global.InfoPicture.ImportantProblem);
    var hysteresis = importantProblem ? OR_HYSTERESIS * 2 : OR_HYSTERESIS;
    var chainSet = typeof global.EpisodicMemory_getChainPriorityStimulusSet === 'function'
      ? global.EpisodicMemory_getChainPriorityStimulusSet() : null;

    var bestId = null;
    var bestAct = -Infinity;
    for (var i = 0; i < candidateFinalImageIds.length; i++) {
      var fid = candidateFinalImageIds[i];
      if (fid == null || fid <= 0) continue;
      var inChain = !!(chainSet && chainSet[String(fid)]);
      var act = computeActuality(fid, well, emotionId, pathArr);
      if (inChain) {
        act = act * 2 + 1;
      } else if (lastWinnerId && lastWinnerId !== fid) {
        act -= hysteresis;
      }
      if (act > bestAct) {
        bestAct = act;
        bestId = fid;
      }
    }
    return bestId != null ? { winnerId: bestId, actuality: bestAct } : null;
  }

  /** Последний победитель конкуренции (для гистерезиса). */
  var lastORWinnerId = 0;

  /**
   * Заблокировать рефлекс по данному образу/стимулу на OR_BLOCK_PULSES пульсов.
   */
  function setBlockedReflex(finalImageIdOrStimId, actionId) {
    var pulse = typeof global.Cur_puls_val === 'number' ? global.Cur_puls_val : 0;
    reflexBlockedUntil.push({
      key: String(finalImageIdOrStimId),
      actionId: actionId,
      untilPulse: pulse + OR_BLOCK_PULSES
    });
  }

  /**
   * Проверить, заблокирован ли рефлекс по данному образу/стимулу и действию (ОР задерживает на 2 пульса).
   */
  function isReflexBlockedByOR(finalImageIdOrStimId, actionId) {
    var pulse = typeof global.Cur_puls_val === 'number' ? global.Cur_puls_val : 0;
    for (var i = reflexBlockedUntil.length - 1; i >= 0; i--) {
      var b = reflexBlockedUntil[i];
      if (pulse >= b.untilPulse) {
        reflexBlockedUntil.splice(i, 1);
        continue;
      }
      if (b.key === String(finalImageIdOrStimId) && (actionId == null || b.actionId === actionId)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Детализация стимула по FinalImageId. stimulIds в FinalImages хранятся как глобальные id (type*1000+localId),
   * расшифровка через StimulusGlobalId_getName устраняет коллизии имён между типами стимулов.
   */
  /**
   * Активны стимулы «Еда» / «Вода» (глобальные id 1001, 1002): для них обходим порог OR_well_known,
   * чтобы при слабой семантике всё равно запускались ОР, ФО и генетические рефлексы «Ест»/«Пьет».
   */
  function activeIdsIncludeFoodOrWaterRelief(activeIds) {
    if (!activeIds || !activeIds.length) return false;
    for (var i = 0; i < activeIds.length; i++) {
      var g = activeIds[i];
      if (g === 1001 || g === 1002) return true;
      if (typeof global.StimulusGlobalId_fromGlobal === 'function') {
        var p = global.StimulusGlobalId_fromGlobal(g);
        if (p && p.type === 1 && (p.localId === 1 || p.localId === 2)) return true;
      }
    }
    return false;
  }

  function getStimulusDetailForAlert(finalImageId) {
    if (!finalImageId) return '';
    var stimulIds = null;
    if (Array.isArray(global.FinalImages)) {
      for (var i = 0; i < global.FinalImages.length; i++) {
        if (global.FinalImages[i] && global.FinalImages[i].id === finalImageId) {
          stimulIds = global.FinalImages[i].stimulIds;
          break;
        }
      }
    }
    if (!stimulIds || !stimulIds.length) return 'ID=' + finalImageId;
    var names = [];
    var getName = typeof global.StimulusGlobalId_getName === 'function' ? global.StimulusGlobalId_getName : null;
    for (var g = 0; g < stimulIds.length; g++) {
      var sid = stimulIds[g];
      var n = getName ? getName(sid) : '';
      if (n) names.push(n); else names.push('Id ' + sid);
    }
    return names.length ? names.join(', ') : 'ID=' + finalImageId;
  }

  /**
   * Запуск конкуренции и ОР при активации стимула (например, при щелчке).
   * Вызывать после того как активные стимулы и FinalImage обновлены.
   * Кандидаты: FinalImageId полного набора активных стимулов + по одному FinalImageId на каждый активный стимул.
   */
  function runOROnStimulusActivation() {
    if (typeof global.FinalImages_getOrCreateFinalImageId !== 'function' ||
        typeof global.PerceptionTree_getOrCreateTerminalNode !== 'function') {
      if (global.EpisodicMemory_testMode && typeof global.show_alert === 'function') {
        global.show_alert('<b>Почему не начат процесс ЭП:</b> недоступны функции дерева восприятия или образов. Ориентировочный рефлекс не вызван. Кадр ЭП не создается. Прогресс-бар не запускается.', 0);
      }
      return false;
    }

    // Глобальные id стимулов (type*1000+localId) — устраняют коллизии имён между массивами (см. stimulus_global_id.js).
    var activeIds = typeof global.StimulusGlobalId_getActiveGlobalIds === 'function'
      ? global.StimulusGlobalId_getActiveGlobalIds()
      : [];

    // Деактивация стимулов (все сняты) — нормальное действие; кадр ЭП, уже начатый при активации, не отменяется.
    if (!activeIds.length) {
      // При отсутствии активных стимулов сбрасываем детектор стимула в инфокартине.
      if (typeof global.Info_updateFromStimulus === 'function') {
        global.Info_updateFromStimulus(0, 'external');
      }
      if (typeof global.InfoDet_updateConsoleStimulus === 'function') {
        global.InfoDet_updateConsoleStimulus(0);
      }
      return false;
    }

    var fullId = global.FinalImages_getOrCreateFinalImageId(activeIds);
    var pathArr = getCurrentTreePath();
    // Терминальный узел — тот же, что и в processGeneticReflexes (getActiveTerminalNodeId), иначе branchId не совпадёт при аннулировании отложенного рефлекса.
    var terminalNodeId = typeof global.PerceptionTree_getActiveTerminalNodeId === 'function'
      ? global.PerceptionTree_getActiveTerminalNodeId()
      : global.PerceptionTree_getOrCreateTerminalNode(pathArr);
    if (!terminalNodeId) {
      // Формирование ЭП не должно мешать учёту семантической значимости — регистрируем экспозицию и при раннем выходе.
      var wellEarly = global.BadNormWell || 0;
      var emotionIdEarly = 0;
      if (typeof global.Emotions_getOrCreateEmotionId === 'function' && Array.isArray(global.BasicContextsActived)) {
        emotionIdEarly = global.Emotions_getOrCreateEmotionId(global.BasicContextsActived) || 0;
      }
      if (wellEarly && emotionIdEarly && fullId && typeof global.registerSemanticSample === 'function' && typeof global.getDiffImportance === 'function') {
        var zEarly = global.getDiffImportance();
        if (typeof zEarly === 'number' && !isNaN(zEarly)) global.registerSemanticSample(fullId, emotionIdEarly, zEarly);
      }
      if (global.EpisodicMemory_testMode && typeof global.show_alert === 'function') {
        var det = getStimulusDetailForAlert(fullId);
        global.show_alert('<b>Стимул:</b> ' + (det || 'ID=' + fullId) + '.<br><b>Почему не начат процесс ЭП:</b> не найдена или не создана ветка дерева восприятия. Ориентировочный рефлекс не вызван. Кадр ЭП не создается. Прогресс-бар периода ожидания не запускается.', 0);
      }
      return true;
    }

    // Обновляем информационную картину: текущая ветка и стимул.
    if (typeof global.Info_updateFromPerceptionTree === 'function') {
      global.Info_updateFromPerceptionTree();
    }
    if (typeof global.Info_updateFromStimulus === 'function') {
      global.Info_updateFromStimulus(fullId, 'external');
    }
    if (typeof global.Info_thinkingFlash === 'function') {
      global.Info_thinkingFlash();
    }

    var candidates = [];
    if (fullId) candidates.push(fullId);
    for (var j = 0; j < activeIds.length; j++) {
      var oneGlobalId = activeIds[j];
      var oneId = global.FinalImages_getOrCreateFinalImageId([oneGlobalId]);
      if (oneId && candidates.indexOf(oneId) === -1) candidates.push(oneId);
    }

    var result = findMostActualStimulus(candidates, lastORWinnerId);
    if (!result) {
      var wellNoRes = global.BadNormWell || 0;
      var emotionIdNoRes = 0;
      if (typeof global.Emotions_getOrCreateEmotionId === 'function' && Array.isArray(global.BasicContextsActived)) {
        emotionIdNoRes = global.Emotions_getOrCreateEmotionId(global.BasicContextsActived) || 0;
      }
      if (wellNoRes && emotionIdNoRes && fullId && typeof global.registerSemanticSample === 'function' && typeof global.getDiffImportance === 'function') {
        var zNoRes = global.getDiffImportance();
        if (typeof zNoRes === 'number' && !isNaN(zNoRes)) global.registerSemanticSample(fullId, emotionIdNoRes, zNoRes);
      }
      if (global.EpisodicMemory_testMode && typeof global.show_alert === 'function') {
        var det2 = getStimulusDetailForAlert(fullId);
        global.show_alert('<b>Стимул:</b> ' + (det2 || 'ID=' + fullId) + '.<br><b>Почему не начат процесс ЭП:</b> не выявлен наиболее актуальный стимул среди кандидатов. Ориентировочный рефлекс не вызван. Кадр ЭП не создается. Прогресс-бар периода ожидания не запускается.', 0);
      }
      return false;
    }

    var winnerId = result.winnerId;
    if (activeIds.length === 1 && fullId) {
      winnerId = fullId;
    }

    var well = global.BadNormWell || 0;
    var emotionId = 0;
    if (typeof global.Emotions_getOrCreateEmotionId === 'function' && Array.isArray(global.BasicContextsActived)) {
      emotionId = global.Emotions_getOrCreateEmotionId(global.BasicContextsActived) || 0;
    }
    var semWinner = getSemanticForConditions(winnerId, well, emotionId);
    var samplesCount = (semWinner && typeof semWinner.samplesCount === 'number') ? semWinner.samplesCount : 0;
    var wellKnown = (typeof global.OR_well_known === 'number' && global.OR_well_known >= 0) ? global.OR_well_known : OR_well_known;

    // Регистрируем экспозицию стимула при каждой активации, иначе samplesCount растёт только при срабатывании рефлекса
    // (напр. «Пение» без рефлекса никогда не увеличивал бы счётчик).
    if (well && emotionId && typeof global.registerSemanticSample === 'function' && typeof global.getDiffImportance === 'function') {
      var z = global.getDiffImportance();
      if (typeof z === 'number' && !isNaN(z)) {
        global.registerSemanticSample(winnerId, emotionId, z);
      }
    }
    // Обновляем счётчик после регистрации (он мог быть 0 до первого вызова registerSemanticSample).
    semWinner = getSemanticForConditions(winnerId, well, emotionId);
    samplesCount = (semWinner && typeof semWinner.samplesCount === 'number') ? semWinner.samplesCount : 0;

    // Второй проход ОР: после закрытия кадра по ответному стимулу — полный ориентировочный рефлекс и новый цикл ФО (без укороченной ветки ожидания).
    var resumeFullOr = !!global.OrientationReflex_resumeFullOrAfterWaitingResponse;
    if (resumeFullOr) {
      global.OrientationReflex_resumeFullOrAfterWaitingResponse = false;
      if (typeof global.Consciousness_getMainCycle === 'function' && typeof global.Consciousness_cycleAppendLog === 'function') {
        var orMain = global.Consciousness_getMainCycle();
        if (orMain && orMain.isMainCycle) {
          global.Consciousness_cycleAppendLog(orMain, 'ОР: второй полный проход после ответа оператора в периоде ожидания ЭП.', typeof global.cpuls === 'number' ? global.cpuls : 0);
        }
      }
    }

    // Сначала проверяем период ожидания: ответный стимул оператора не требует порога известности (OR_well_known).
    if (!resumeFullOr && typeof global.EpisodicMemory_isInWaitingPeriod === 'function' && global.EpisodicMemory_isInWaitingPeriod()) {
      if (typeof global.EpisodicMemory_closeLastFrameWithResponse === 'function') {
        global.EpisodicMemory_closeLastFrameWithResponse(winnerId);
      }
      // Ответ оператора получен — режим "игнорирование" снимается и детектор ответа обновляется.
      if (typeof global.InfoDet_updateOperatorIgnoring === 'function') {
        global.InfoDet_updateOperatorIgnoring(false);
      }
      if (typeof global.InfoDet_updateOperatorAnswer === 'function') {
        global.InfoDet_updateOperatorAnswer(true);
      }
      global.EpisodicMemory_skipConditionalReflexFormation = true;
      global.actual_stimul_ID = winnerId;
      var branchIdResponse = (typeof global.PerceptionTree_getActiveTerminalNodeId === 'function')
        ? global.PerceptionTree_getActiveTerminalNodeId() : 0;
      // Цепочка кадров ЭП: открываем новый кадр по ответному стимулу, затем безусловный второй проход ОР —
      // полный путь (hippocampus, orContext, stimuls_consciousness, save_episodic), новый цикл осмысления.
      if (typeof global.save_episodic_memory === 'function' && branchIdResponse) {
        global.save_episodic_memory(branchIdResponse, winnerId);
      }
      global.OrientationReflex_resumeFullOrAfterWaitingResponse = true;
      if (typeof global.Consciousness_getMainCycle === 'function' && typeof global.Consciousness_cycleAppendLog === 'function') {
        var orM2 = global.Consciousness_getMainCycle();
        if (orM2 && orM2.isMainCycle) {
          global.Consciousness_cycleAppendLog(orM2, 'ОР: ответ в ожидании — открыт кадр ЭП, запланирован повторный полный ОР.', typeof global.cpuls === 'number' ? global.cpuls : 0);
        }
      }
      return runOROnStimulusActivation();
    }

    // Порог известности только для начала нового процесса ОР/ЭП (не для ответного стимула в периоде ожидания).
    // В состоянии Плохо (well === 1) порог не применяем: всегда запускаем ОР и ФО, иначе при резком
    // ухудшении (например, кнопка «Наказание») новый цикл не создаётся из-за малого samplesCount для (Плохо, эмоция, стимул).
    // Второй проход после ответа в ожидании (resumeFullOr) порог также обходим — нужен полный ОР и новый ФО.
    var skipWellKnownInBadState = (well === 1);
    var skipWellKnownFoodWater = activeIdsIncludeFoodOrWaterRelief(activeIds);
    if (!resumeFullOr && !skipWellKnownInBadState && !skipWellKnownFoodWater && samplesCount < wellKnown) {
      if (global.EpisodicMemory_testMode && typeof global.show_alert === 'function') {
        var detWeak = (activeIds.length === 1 && typeof global.StimulusGlobalId_getName === 'function')
          ? (global.StimulusGlobalId_getName(activeIds[0]) || getStimulusDetailForAlert(winnerId))
          : getStimulusDetailForAlert(winnerId);
        global.show_alert('<b>Стимул:</b> ' + (detWeak || 'ID=' + winnerId) + '.<br><b>Почему не начат процесс ЭП:</b> опыт семантической значимости слабый (samplesCount=' + samplesCount + ' < ' + wellKnown + '). Ориентировочный рефлекс не вызван. Кадр ЭП не создается. Прогресс-бар периода ожидания не запускается.', 0);
      }
      return false;
    }

    lastORWinnerId = winnerId;
    global.actual_stimul_ID = winnerId;

    var hasSignificance = semWinner != null;

    if (typeof global.Hippocampus_holdStimulusFromOR === 'function') {
      global.Hippocampus_holdStimulusFromOR(winnerId);
    }

    setBlockedReflex(winnerId, undefined);
    if (typeof global.addBlockedReflexToQueue === 'function') {
      global.addBlockedReflexToQueue(terminalNodeId);
    }

    var activityID = (typeof global.FinalImages_getOrCreateFinalImageId === 'function' && activeIds && activeIds.length)
      ? (global.FinalImages_getOrCreateFinalImageId(activeIds) || 0) : 0;
    var pathArr = [well, emotionId, activityID, 0, 0, 0];
    var novelty = computeNovelty(winnerId, well, emotionId, pathArr);
    var significance = computeSignificance(winnerId, well, emotionId);
    var orContext = {
      actual_stimul_ID: winnerId,
      branchId: terminalNodeId,
      novelty: novelty,
      significance: significance,
      samplesCount: samplesCount,
      pathArr: pathArr,
      well: well,
      emotionId: emotionId,
      activityID: activityID,
      isResponseStimulus: false,
      /** Победитель уже выбран конкуренцией ОР — канал ФО не должен отменять запуск порогом «тишины мысли». */
      orientationReflexWinner: true
    };
    if (typeof global.stimuls_consciousness === 'function') {
      global.stimuls_consciousness(orContext);
    }
    // Кадр ЭП и прогресс-бар запускаем всегда при срабатывании ОР (если начался процесс сборки кадра).
    // Тестовый режим влияет только на пошаговые сообщения, не на наличие прогресс-бара.
    if (typeof global.save_episodic_memory === 'function') {
      global.save_episodic_memory(terminalNodeId, winnerId);
      if (global.EpisodicMemory_testMode && typeof global.show_alert === 'function') {
        var detStart = getStimulusDetailForAlert(winnerId);
        global.show_alert('<b>Стимул:</b> ' + (detStart || 'ID=' + winnerId) + '.<br><b>Результат:</b> начат процесс формирования кадра ЭП. Ожидание ответа или реакции. Прогресс-бар периода ожидания запущен.', 0);
      }
    }
    if (typeof global.PerceptionTree_updateActiveBranchView === 'function') {
      global.PerceptionTree_updateActiveBranchView();
    }
    return true;
  }

  global.OrientationReflex_runOnStimulusActivation = runOROnStimulusActivation;
  global.OrientationReflex_isReflexBlockedByOR = isReflexBlockedByOR;
  global.OrientationReflex_setBlockedReflex = setBlockedReflex;
  global.OrientationReflex_findMostActualStimulus = findMostActualStimulus;
  global.OrientationReflex_OR_HYSTERESIS = OR_HYSTERESIS;
  global.OrientationReflex_getOrReadinessForFinalImageId = getOrReadinessForFinalImageId;

  /**
   * Счётчик кликов по кнопке «Тест»: каждый следующий вызов передаёт бо́льшую значимость,
   * чтобы новый клик гарантированно прерывал текущий цикл (порог: currentThinkingImportance + 2).
   */
  var runTestNegativeInterruptCounter = 0;

  /**
   * Тестовый запуск прерывания цикла осмысления. Создаёт цикл с небольшой значимостью 5,
   * при каждом следующем клике значимость увеличивается (10, 15, 20…), чтобы каждый новый
   * клик давал новое прерывание (текущий главный уходит в фон, создаётся новый цикл).
   * Это не стимул, а жёсткий вызов осмысления для проверки диспетчера и стека фоновых.
   */
  function runTestNegativeInterrupt() {
    runTestNegativeInterruptCounter += 1;
    var significance = 5 + (runTestNegativeInterruptCounter - 1) * 5;
    var branchId = (typeof global.PerceptionTree_getActiveTerminalNodeId === 'function')
      ? global.PerceptionTree_getActiveTerminalNodeId()
      : 0;
    var ctx = {
      actual_stimul_ID: 0,
      branchId: branchId,
      well: 1,              // Плохо
      emotionId: 0,
      activityID: 0,
      significance: significance
    };
    if (typeof global.stimuls_consciousness === 'function') {
      global.stimuls_consciousness(ctx);
    }
  }

  global.runTestNegativeInterrupt = runTestNegativeInterrupt;
})(window);

════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/about_adaptivity_levels_2_4.md
════════════════════════════════════════════════════════════
# Адаптивность ФО: уровни 2 и 4 (констатация реализованного)

Документ фиксирует **фактическое состояние кода** по взаимосвязи второго и четвёртого уровней осознания, семантики, эпизодической памяти и смежных контуров. Не является спецификацией будущих задач; отдельные «зазоры» указаны как **текущие границы реализации**.

---

## 1. Уровень 2 (`conscience_level_2.js`)

Реализован как слой **постановки и пересмотра целей** и **переработки последствий действия**, когда сработал мотор и есть обратная связь по эпизодической памяти (ЭП).

### 1.1. Ветки (как в коде)

| Ветка | Реализованное поведение |
|--------|-------------------------|
| Ответ оператора / кадр ЭП | `AbstractImages_applyEpisodeEffectToSemanticContextAbstractions`; при числовом `effect > 0` — `MentalAutomatizms_tryCreateFromSemanticStructureRule`; корректировка полезности цели (`Goals_applyWaitingPeriodEffectToGoalUsefulness`, `Goals_mergeUsefulnessSampleForGoalId`) с учётом детекторов (`negativeMotorEffect`, `positiveActionEffect`, `lowAutomatismUsefulness`). Защита от повторного входа: `operatorAnswerPipelineConsumed`. |
| Выживание | Условия: `conscience_level_2_isSurvivalContext`, критичность виталов; цели из жизненного опыта (`Goals_addOrUpdateLifeGoalFromFrame` и связанная логика); цепочки по `Target_rule_stack` / `EpisodicMemory_findChainForGoalStack` при `usedRule`; запуск автоматизма с ожиданием ЭП (`info_automatismWithEpisodeFeedbackAndExpireCycle`, `conscience_level_2_startSurvivalAutomatism`). |
| Цель «значение» | `conscience_level_2_tryMeaningGoalForecast`: выборка кадров ЭП по стимулу/ветке, прогноз; `Gestalt_bumpFromMeaningForecast`; `AbstractImages_boostSemanticRulesForAction`. |
| Завершение фазы ожидания | `conscience_level_2_finalizeAutomatismAnswerCycle` — снятие флагов ожидания, `expired` цикла. |

### 1.2. Связи с другими модулями

- **ЭП**: кадры, effect, закрытие кадра → вход в пайплайн ответа оператора.
- **`abstract_images.js`**: усреднение по контексту семантики при feedback; усиление правил по ОД для цели «значение».
- **`mental_automatism.js`**: создание ментального автоматизма при положительном effect и совпадении с `semanticStructure`.
- **`goals.js`**: образы целей жизненного опыта, полезность, цель «значение».
- **`gestalt.js`**: вызовы из ветки «значение» (`Gestalt_bumpFromMeaningForecast`).

---

## 2. Уровень 4 (`conscience_level_4.js`)

Реализован как **тонкая обёртка** над гештальт-модулем: основной рабочий вызов в тике канала — `Gestalt_onConsciousnessTick(cycle)` (см. `gestalt.js`). Дополнительно объявлен `conscience_level_4(context)` с логом и **TODO** на расширения (мета-рефлексия и т.д.) — полноценная логика там **не разведена**.

### 2.1. Зафиксированное в коде назначение ур. 4

- Обработка **главного** running-цикла в `Gestalt_onConsciousnessTick` (фоновые циклы отсекаются внутри гештальта).
- Привязка ведущего гештальта к цели (`cycle.goalImageId` / `InfoPicture.currentGoalId` в целевом режиме мышления главного цикла).
- Разбор **`GestaltResonanceBuffer`** (FIFO сигнатур закрытых кадров ЭП), эвристика score → инсайт аналогии, обновление `analogyPool` и статуса гештальта.
- Повторный вызов `Gestalt_prepareContextForLevel3` после возможных инсайтов.
- Пополнение буфера резонанса **не** в этом тике: при закрытии кадра ЭП (`EpisodicMemory_closeLastFrame` → `Gestalt_onEpisodeFeedbackClosed`).

### 2.2. Связь с уровнем 3

- В начале ур. 3 вызывается `Gestalt_prepareContextForLevel3` — в `GestaltContextForLevel3` попадает снимок открытого гештальта для использования как контекст; ур. 3 не заменяет собственный поиск правил этим контекстом полностью — см. комментарии в `conscience_level_3.js` и `conscience_level_4.js`.

---

## 3. Согласованность ур. 2 ↔ ур. 3 ↔ ур. 4 (факты)

**Реализовано:**

1. Замкнутый контур: мотор / автоматизм → ЭП → обработка ответа в ур. 2 → при необходимости дальнейшие тики `runOneStep` с ур. 3 и 4.
2. Гейт ожидания ЭП: пока `awaitingAutomatismEpisodeFeedback` без ответа оператора, повторный полный проход ур. 3 не выполняется (`conscience_level_3_shouldSkipBecauseAwaitingEpisodeFeedback` и смежная логика).
3. Проверка кадра `semanticStructure` перед мотором: `conscience_level_3_semanticStructureFrameAllowsMotor` (учительский кадр и положительный episodic effect).
4. Ур. 4 не дублирует поиск правил ур. 3; добавляет слой гештальта и резонанса по уже произошедшим закрытиям кадров.

**Текущие границы (констатация, не багрепорт):**

1. Основная адаптивная логика ур. 4 сосредоточена в **`gestalt.js`**, а не в теле `conscience_level_4.js`.
2. Правила с `ruleKind === 'fantasy'` (после `info_new_abstract_semantic` / `AbstractImages_upsertSemanticRuleFromFantasyFrame`) в явном виде в `conscience_level_3_semanticStructureFrameAllowsMotor` **не перечислены** — политика мотора по fantasy отделена от episodic/teacher на уровне явных условий в этом гейте.
3. Пассивный режим мышления цикла (`cycle.thinkingMode === 'passive'`) и `info_fantasizming` в **`Consciousness_runOneStep`** обходят ур. 3–4 в том же тике; консолидация сюжета — `info_new_abstract_semantic` в `infofunctions.js` — **отдельный** вызов, не встроенный в тик гештальта по умолчанию.
4. Два источника пополнения `semanticStructure`: реальные кадры ЭП и fantasy-слоты; учительский слот при конфликте **не перезаписывается** fantasy-апдейтом (реализовано в `AbstractImages_upsertSemanticRuleFromFantasyFrame`).

---

## 4. Оценка адаптивных качеств (по текущей реализации)

| Аспект | Что зафиксировано в системе |
|--------|-----------------------------|
| Обучение на последствиях | Ур. 2: feedback по ЭП, обновление семантического контекста и целей. |
| Приоритет угроз | Отдельная ветка выживания в ур. 2. |
| Долгая память о нерешённом | Гештальты, цель «значение», прогноз по ЭП в ур. 2. |
| Аналогии и перенос | Резонансный буфер, эвристика score, `analogyPool` в `gestalt.js`. |
| Внутренняя симуляция | Пассивное фантазирование, перенос в семантику fantasy через `info_new_abstract_semantic` — отдельный контур от тика ур. 3–4. |

---

## 5. Связанные файлы

| Файл | Роль |
|------|------|
| `conscience_level_2.js` | Ур. 2 |
| `conscience_level_3.js` | Ур. 3 (канал, гейты семантики/ЭП) |
| `conscience_level_4.js` | Ур. 4 (вызов гештальта) |
| `gestalt.js` | Гештальты, резонанс, инсайты аналогии |
| `abstract_images.js` | Клоны, `semanticStructure`, fantasy-upsert |
| `infofunctions.js` | `info_new_abstract_semantic`, прочие info_* |
| `conscious_attention_channel.js` | `runOneStep`, пассивная ветка |
| `consciousness_dispatcher.js` | Диспетчер циклов, стагнация, сигнатуры |

---

*Текст согласован с обзором архитектуры адаптивности ур. 2 и 4 и приведён к форме документации «что сделано в коде».*

════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/about_conscience.md
════════════════════════════════════════════════════════════
# Канал осознанного внимания (_9_Conscience)

## Назначение

Модуль реализует **осознание стимула** после ориентировочного рефлекса: **stimuls_consciousness(context)** вызывается из ОР. На вход передаётся контекст с полной детализацией новизны условий (**actual_stimul_ID**, **branchId**, **novelty**, **significance**, **samplesCount**, **pathArr**, **well**, **emotionId**, **activityID**, **isResponseStimulus**, **orientationReflexWinner**) для анализа ситуации. Если **orientationReflexWinner** истинен, порог «тишины мыслей» (сравнение с текущей значимостью главного цикла) **не блокирует** запуск нового цикла ФО — победитель уже выбран конкуренцией ОР.

**Тишина мыслей** (понижение эффективной значимости конкурирующего стимула: несовпадение темы/ситуации с главным циклом, штраф при `ImportantProblem`, порог `INTERRUPT_THRESHOLD_ATTENTION_RATIO`) применяется **только пока существует главный цикл осмысления и он ещё не помечен `expired`**. Для стимулов **приоритета цепочки** ЭП отдельные правила см. `about_episodic_memory_rules.md`, §7. Проверяется наличие штатного автоматизма у активного узла дерева (branchId); при наличии — запускается образ действия (runActionImage). При запуске автоматизма вызывается removeDelayedReflexesForBranch(branchId): из очереди отложенных рефлексов удаляются все записи по этой ветке (без сравнения действий). Здесь предусмотрено место для блока оценки приемлемости запуска ActionsImageID при данной новизне условий.

**Независимость автоматизма от рефлекса:** первоначальный автоматизм в функции осознания может быть произвольно модифицирован с изменением его действия (ОД). Никакой связи между первичным рефлексом и вторичным автоматизмом не предполагается: автоматизм — самостоятельное правило по ветке, его образ действия может отличаться от изначально зафиксированного рефлексом.

## Диспетчер циклов мышления

Циклы осознания выполняются **итеративно по пульсу**, без рекурсивных вызовов ФО (по образцу OLD_CODE/GO: understanding.go, understanding_cycle.go).

- **consciousness_dispatcher.js** — реестр циклов (ConsciousnessCycles), создание/отмена/удаление, назначение главного (Consciousness_setMainCycle). По каждому пульсу вызывается **dispetchConsciousnessThinking(pulsCount)**: главному циклу даётся один шаг (Consciousness_runOneStep), фоновым — раз в **5** пульсов (см. `consciousness_dispatcher.js`); при отсутствии главного поднимается следующий контекст из ConsciousnessBackgroundStack (**promoteFromBackgroundCycle** / стек прерываний). Без диспетчера невозможна надёжная реализация мышления. **Стагнация:** если после шага главный помечается **expired** (три итерации без изменений инфокартины) и на этом же пульсе уже отработал тик ур.4/гештальта (`lastGestaltConsciencePuls`), инфокартина переводится в **пассивный** режим, на цикле — флаг `passiveWorkAfterGestaltExpire` (детализация цикла в `informing.js`, `formatCycleOrStackInfo`).
- **conscious_attention_channel.js** — точка входа **stimuls_consciousness(context)**. При более значимом вызове текущий главный цикл пушится в стек и отменяется; создаётся новый цикл и выполняется элементарный проход (Consciousness_runElementary). Рекурсии нет: продолжение и «возврат» к прерванному контексту выполняет диспетчер.

## Текущая реализация

- **conscious_attention_channel.js** — stimuls_consciousness создаёт цикл, запускает уровень 1 и 2 (поиск по ЭП, штатный автоматизм, при неудаче — conscience_level_2). Consciousness_runElementary и Consciousness_runOneStep экспортируются для диспетчера; шаг ур. 3–4 в runOneStep делегируется в **conscience_level_3_runFromChannel** / **conscience_level_4_runFromChannel**.
- Вызов из `orientation_reflex.js` при **полном** срабатывании ОР (в т.ч. на **втором** проходе после ответного стимула в периоде ожидания); на 3-м пульсе — начальный цикл без стимула (`actualId: 0`, контекст без `orientationReflexWinner`).
- **puls.js** — каждый пульс вызывает dispetchConsciousnessThinking(global.cpuls).

## Файлы

- **consciousness_dispatcher.js** — реестр циклов, диспетчер, promoteFromBackgroundStack.
- **infofunctions.js** — `info_automatismWithEpisodeFeedbackAndExpireCycle`; `info_bestEpisodicRule` (поиск лучшего правила ЭП с опциями: фильтр по стимулу как для ур.3, fallback по стимулу как для ур.2 выживание); `info_bestEpisodicRuleForStimulus` — обёртка.
- **conscious_attention_channel.js** — stimuls_consciousness, Consciousness_runElementary, Consciousness_runOneStep.
- **conscience_level_3.js** — третий уровень осознания (вызов из `conscience_level_3_runFromChannel` за тик канала). **Порядок:** готовый ментальный автоматизм по умолчанию (если кадр `semanticStructure` согласован с МАИ) → shortcut по `AbstractImages_findRuleFrameMatchingContext` → семантика стимула + `info_bestEpisodicRule` (тот же стимул, положительный effect) → учительский fallback (`info_teacherEpisodicRuleForAction`, зеркала 88/89). Во всех успешных ветках мотор — через `conscience_level_3_startAutomatismFromEpisodicRule` → `info_automatismWithEpisodeFeedbackAndExpireCycle`. **Устойчивость:** пока цикл ждёт кадр ЭП после запуска автоматизма (`awaitingAutomatismEpisodeFeedback` без ответа оператора), полный пайплайн не выполняется; при нескольких триггерах за тик второй/третий вызовы пропускаются с записью в лог (`Consciousness_cycleAppendLog`, часто с `{ force: true }` из‑за `expired`). **Значимость перед мотором:** shortcut и ментальный путь — только если кадр `semanticStructure` «положителен» (`conscience_level_3_semanticStructureFrameAllowsMotor`: teacher или episodic с числовым `effect > 0`); путь ЭП дополнительно блокируется при `meanImportance < 0` у лучшего семантического кадра стимула; учительский путь — если у выбранного кадра числовой `effect ≤ 0`.
- **conscience_level_4.js** — четвёртый уровень: тик канала вызывает **`Gestalt_onConsciousnessTick`** (`gestalt.js`) — привязка ведущего гештальта к цели главного цикла, разбор **буфера резонанса** (сигнатуры закрытых кадров ЭП), повторное обновление контекста. Подробно: **`about_gestalt.md`**.
- **gestalt.js** — записи **гештальтов** (доминанты по `goalImageId`), **GestaltResonanceBuffer**, вызовы из ур.3 (**`Gestalt_prepareContextForCycle`** → `GestaltContextForLevel3`) и из **`closeLastFrame`** (**`Gestalt_onEpisodeFeedbackClosed`**). Персистенция IndexedDB `BEAST_Gestalt_DB`. На главной странице в строке «Информационная картина» — индикатор **«гешт: N»** и `Info_showGestaltsList` / `Info_showGestaltDetail` (`informing.js`).

## См. также

- **`about_adaptivity_levels_2_4.md`** — констатация реализованной взаимосвязи ур. 2 и ур. 4 с семантикой, ЭП, пассивным фантазированием и границами реализации.

════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/about_gestalt.md
════════════════════════════════════════════════════════════
# Гештальт и уровень 4 — полное описание (BEAST)

Полная теоретическая и проектная документация. Исполняемая логика и структуры данных — в `gestalt.js`; этот файл — справочник для чтения и правок без разбора только исходников.

---

## 1. Теоретическая суть

В психологии «гештальт» — целостный образ ситуации, который стремится к завершению (замыканию). В BEAST под гештальтом понимается устойчивая запись о **нерешённой задаче**: связка «образ цели ↔ попытки ↔ степень продвижения». Это не копия эпизода памяти и не одно правило ЭП: это **доминанта**, которая может жить дольше одного цикла осмысления и направляет мета-уровень (ур.4), пока задача не снята с повестки или не обобщена в готовый опыт.

**Зачем отдельная сущность**

- ЭП фиксирует прошедшие события; гештальт фиксирует **открытую** проблему и накопленные гипотезы решения (ID автоматизмов, статус).
- Ур.3 решает «здесь и сейчас»; ур.4 умеет **держать** задачу в фоне, сопоставлять её с успехом в других циклах и сужать неопределённость.

Центральное явление — **инсайт** (раздел 2 ниже).

---

## 2. Инсайты

### 2.1 Что считать инсайтом в BEAST

Инсайт — это **событие** (не только смена числа в `status`), в котором система связывает **образ цели гештальта** с **конкретным кандидатом решения** так, что задача становится осмысливаемой в терминах действия — вплоть до проверки в ЭП.

### 2.2 Виды инсайта

1. **Локальный** — внутри цикла-хозяина.
2. **Кросс-цикловой / аналогический** — сигнатура успеха из другого цикла резонирует с гештальтом (см. буфер резонанса в `gestalt.js`).
3. **На обратной связи** — после закрытия кадра ЭП (`Gestalt_onEpisodeFeedbackClosed`).

### 2.3 Фазы (α–ε)

См. комментарии в `gestalt.js` и поля `lastInsightKind`, `insightChainCount`.

---

## 3. Модель данных

| Поле | Смысл |
|------|--------|
| `id` | ID записи |
| `problemTreeId` | Узел дерева проблем / ветка (в коде часто совпадает с `perceptionNodeId`) |
| `situationId` | Ситуация инфокартины |
| `goalImageId` | Образ цели (уникальность среди открытых — см. код) |
| `automatismIds` | Опробованные автоматизмы |
| `status` | 0…4 |
| `lastInsightPuls`, `lastInsightKind`, `insightChainCount` | Учёт инсайтов |
| `analogyPool` | Хвост предложений аналогии (ограниченный размер) |

---

## 4. Статусы

- **0** — заведено, нет устойчивого решения.
- **1** — начато (кандидаты, в т.ч. после резонанса).
- **2** — частичное (позитивный фидбек ЭП, уточнены ветка/ситуация).
- **3** — успешное закрытие (сильный позитивный эффект в реализации — порог настраивается в коде).
- **4** — архив / неактуально (зарезервировано).

---

## 5. Реализованные структуры (код)

### 5.1 `GestaltResonanceBuffer`

FIFO-очередь сигнатур успешных/закрытых эпизодов (после `closeLastFrame`). Каждая запись: `id`, `atPuls`, `sourceCycleId`, `goalImageId`, `problemTreeId`, `situationId`, `perceptionNodeId`, `automatismId`, `actionImageId`, `effect`, `kind`. Верхняя граница длины — константа в `gestalt.js`.

### 5.2 Сборка (analogyPool)

У открытого гештальта накапливаются записи о совпадениях с буфером (оценка `score`); детальная «гештальт-сборка» и имитация — зона расширения (TODO в коде помечены явно).

---

## 6. Точки интеграции

| Место | Роль |
|-------|------|
| `conscience_level_3_runFromChannel` | `Gestalt_prepareContextForCycle` — контекст для ур.3 до пайплайна |
| `conscience_level_4_runFromChannel` | `Gestalt_onConsciousnessTick` — резонанс, ведущий гештальт |
| `episodic_memory.closeLastFrame` | `Gestalt_onEpisodeFeedbackClosed` — фидбек, закрытие, пополнение буфера |

---

## 7. Согласованность с уровнями осмысления

- Ур.1–3 **не** переписываются: добавляются только вызовы подготовки контекста и логи.
- Гештальт **не** запускает мотор сам по себе в этой итерации: только статусы, буфер, контекст для последующих тиков.

---

## 8. Интерфейс оператора (`index.html`, `informing.js`)

- В строке заголовка **«Информационная картина:»** после индикатора «ситуации» выводится бейдж **`гешт: N`** (`#infoGestaltsIndicator`), стиль как у «цели:», «авто:» и т.д.
- **`Info_updateView`** обновляет число `N` и подсказку (число записей и длина буфера резонанса).
- По щелчку вызывается **`Info_showGestaltsList`** — список ID гештальтов; по ID — **`Info_showGestaltDetail`** (связи на образ цели, ветку, ситуацию, автоматизмы, JSON `analogyPool`).

---

*При изменении контракта обновляйте `beast/about_global_variables.md` и при необходимости `PROJECT_STATE_FOR_NEW_CHAT.md` и `beast/_9_Conscience/about_conscience.md`.*

**См. также:** констатация связки ур. 2 и ур. 4 с адаптивностью и смежными контурами — `about_adaptivity_levels_2_4.md`.

════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/about_info_detectors_algorithms.html
════════════════════════════════════════════════════════════
<!DOCTYPE html>
                <html lang="en">
                <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>Markdown Export</title>
                <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.3.0/github-markdown.min.css">
                <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
                <style>
                    body {
                        background-color: #ffffff;
                        color: #24292e;
                    }
                    .markdown-body {
                        box-sizing: border-box;
                        min-width: 200px;
                        max-width: 980px;
                        margin: 0 auto;
                        padding: 45px;
                        background-color: #ffffff;
                        color: #24292e;
                    }
                    @media (max-width: 767px) {
                        .markdown-body {
                            padding: 15px;
                        }
                    }
                </style>
                </head>
                <body>
                <article class="markdown-body">
                    <h1>Инфо-детекторы: алгоритмы активации, удержания и сброса</h1>
<p>В этом документе перечислены все инфо-детекторы инфокартины, их числовые поля в <code>InfoPicture</code> и три группы условий для каждого:</p>
<ul>
<li><strong>Активация</strong> — когда детектор становится активным (поле ≠ 0).</li>
<li><strong>Удержание</strong> — когда детектор продолжает оставаться активным.</li>
<li><strong>Сброс</strong> — когда детектор обязан вернуться в 0.</li>
</ul>
<h2>Общие принципы</h2>
<ul>
<li>Значения детекторов <strong>целочисленные</strong> (или числа с плавающей точкой для «уровней»). Ноль всегда означает «не активно / не обнаружено».</li>
<li>Каждая функция <code>InfoDet_updateXxx(...)</code> в <code>info_detectors.js</code>:<ul>
<li>принимает параметры из «места сбора информации» (виталы, контексты, эффекты ЭП, состояние ожидания и т.п.);</li>
<li><strong>сама</strong> решает, поставить ненулевое значение или сбросить в 0, исходя из трёх условий.</li>
</ul>
</li>
<li>Вызывающий код обязан передавать детектору <strong>актуальные данные</strong> при каждом существенном изменении (пульс, ОР, закрытие кадра ЭП, запуск/завершение ожидания и т.д.), даже если событие «не случилось» — чтобы детектор мог сам себя сбросить.</li>
</ul>
<p>Детекторы условно делятся на:</p>
<ul>
<li><strong>Снимковые (состояния)</strong> — могут пересчитываться по текущему снимку состояния (BadNormWell, BasicContextsActived, семантика и т.п.).</li>
<li><strong>Событийные</strong> — фиксируют момент/факт события во времени (конец ожидания без ответа, скачок новизны и т.п.). Для них условия удержания и сброса важны особенно.</li>
</ul>
<hr>
<h2>1. Режим осмысления, Тема, Ситуация</h2>
<h3>1.1. Целевой режим осмысления (главный цикл ФО + ячейка «Режим»)</h3>
<ul>
<li><strong>Источник:</strong> <code>thinkingMode</code> у главного цикла; UI — <code>Consciousness_getMainCycleThinkingMode()</code>.</li>
<li><strong>Активация:</strong>  <ul>
<li>При запуске реакции/автоматизма, который требует целевого осмысления (например, при OR с семантически значимым стимулом или при запуске цикла осмысления по <code>unsolved_problem</code>): вызывается <code>InfoDet_updateTargetMode(true)</code>.</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Пока идёт целевой цикл осмысления или активен процесс оценки последствий автоматизма (период ожидания).</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При завершении целевого цикла (проблема решена или признана неактуальной) и отсутствии условий для нового целевого осмысления → <code>InfoDet_updateTargetMode(false)</code>.</li>
</ul>
</li>
</ul>
<h3>1.2. Текущая Тема (<code>InfoPicture.themeId</code>, ячейка «Тема»)</h3>
<ul>
<li><strong>Поле:</strong> <code>themeId</code>: <code>0</code> — нет темы, &gt;0 — ID образа темы из дерева ситуаций.</li>
<li><strong>Активация:</strong>  <ul>
<li>При каждом вызове <code>SituationsTree_onPerceptionTreeActivated()</code> находящий best‑match готовый образ темы для текущего режима и <code>tree.BaseID/EmotionID/ActivityID</code>:<ul>
<li>если найден готовый образ (ready и score&gt;0) → <code>InfoDet_updateCurrentTheme(themeId)</code>.</li>
</ul>
</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Пока текущая комбинация (Base, Emotion, Activity) остаётся ближе всего к тому же готовому образу темы.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>Если при очередной активации дерева ситуаций:<ul>
<li>нет готовых образов (все count ≤ 3),  </li>
<li>или ни один готовый образ не даёт ненулевого score,<br>→ <code>InfoDet_updateCurrentTheme(0)</code>.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3>1.3. Текущая Ситуация (<code>InfoPicture.situationId</code>, ячейка «Ситуация»)</h3>
<ul>
<li><strong>Поле:</strong> <code>situationId</code>: <code>0</code> — нет, &gt;0 — ID образа ситуации из дерева ситуаций.</li>
<li><strong>Активация:</strong>  <ul>
<li>В <code>SituationsTree_onPerceptionTreeActivated()</code> из детекторных полей инфокартины формируется вектор компонент; среди готовых образов выбирается best‑match:<ul>
<li>при score&gt;0 и ready=true → <code>InfoDet_updateCurrentSituation(situationId)</code>.</li>
</ul>
</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Пока текущий набор детекторов остаётся ближе всего к той же ситуации.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При следующем вызове, если:<ul>
<li>нет готовых ситуаций для текущего режима,</li>
<li>или score==0 для всех,<br>→ <code>InfoDet_updateCurrentSituation(0)</code>.</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<h2>2. Виталы и базовые контексты</h2>
<h3>2.1. Критический витал (<code>InfoPicture.criticalVital</code>, ячейка «Крит. витал»)</h3>
<ul>
<li><strong>Поле:</strong> <code>criticalVital</code>:  <ul>
<li><code>0</code> — нет критического витала;  </li>
<li><code>&gt;0</code> — ID витала, значение которого &gt;70 и максимально среди всех.</li>
</ul>
</li>
<li><strong>Активация:</strong>  <ul>
<li>В <code>updateImportanceMetrics(badVitals)</code> или <code>getActiveBasicContexts(cpuls)</code>:<ul>
<li>сканировать <code>VitalsArr</code> и найти витал с <code>value &gt; 70</code> и максимальным значением;</li>
<li>если такой есть → <code>InfoPicture.criticalVital = vitalId</code>.</li>
</ul>
</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Пока при очередных вызовах остаётся витал с <code>value &gt; 70</code>; при смене «лидера» детектор обновляется на новый ID.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>Если при следующем вызове ни один витал не превышает 70 → <code>criticalVital = 0</code>.</li>
</ul>
</li>
</ul>
<h3>2.2. Состояние Плохо (<code>InfoPicture.badState</code>, ячейка «Плохо»)</h3>
<ul>
<li><strong>Поле:</strong> <code>badState</code>: <code>0</code> — не Плохо, <code>1</code> — Плохо (BadNormWell=1).</li>
<li><strong>Активация:</strong>  <ul>
<li>В <code>InfoDet_updateBadState(lastBadNormWell, badNormWell)</code>:<ul>
<li>при <code>badNormWell === 1</code> → <code>badState = 1</code>.</li>
</ul>
</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Пока <code>BadNormWell</code> остаётся равным 1.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При любом значении <code>BadNormWell != 1</code> в следующем вызове → <code>badState = 0</code>.</li>
</ul>
</li>
</ul>
<h3>2.3. Резкое ухудшение состояния (<code>InfoPicture.suddenChange</code>, ячейка «Резк. изм.»)</h3>
<ul>
<li><strong>Поле:</strong> <code>suddenChange</code>:  <ul>
<li><code>0</code> — нет;  </li>
<li><code>1</code> — момент смены на Плохо (по PDF); можно хранить и более детальный уровень.</li>
</ul>
</li>
<li><strong>Активация:</strong>  <ul>
<li>В <code>InfoDet_updateBadState(lastBadNormWell, badNormWell)</code>:<ul>
<li>если <code>lastBadNormWell !== 1</code> и <code>badNormWell === 1</code> → <code>suddenChange = 1</code>.</li>
</ul>
</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Это событийный детектор: можно держать <code>1</code> в течение одного шага цикла осмысления или одного/нескольких пульсов (на ваше усмотрение), если нужно «увидеть» событие.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При следующем вызове без повторной смены в Плохо → <code>suddenChange = 0</code>.</li>
</ul>
</li>
</ul>
<h3>2.4. Игровой режим (<code>InfoPicture.playMode</code>, ячейка «Игра»)</h3>
<ul>
<li><strong>Поле:</strong> <code>playMode</code>: <code>0</code> — нет, <code>1</code> — активен базовый контекст Игра.</li>
<li><strong>Активация:</strong>  <ul>
<li>В <code>getActiveBasicContexts</code> после получения <code>BasicContextsActived</code>:<ul>
<li>если <code>BasicContextsActived</code> содержит контекст с <code>id == 11</code> → <code>playMode = 1</code>.</li>
</ul>
</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Пока Игра остаётся в <code>BasicContextsActived</code>.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При следующем вызове, если контекст Игра не активен → <code>playMode = 0</code>.</li>
</ul>
</li>
</ul>
<h3>2.5. Режим обучения (<code>InfoPicture.learningMode</code>, ячейка «Обучение»)</h3>
<ul>
<li><strong>Поле:</strong> <code>learningMode</code>: <code>0</code> — нет, <code>1</code> — активен режим обучения.</li>
<li><strong>Активация:</strong>  <ul>
<li>В <code>InfoDet_updateLearningAndSearch(BasicContextsActived, VitalsArr)</code>:<ul>
<li>если активен базовый контекст Любопытство (id=12) <strong>или</strong> витал «Интерес» имеет <code>value &gt; 70</code> → <code>learningMode = 1</code>.</li>
</ul>
</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Пока сохраняется хотя бы одно из условий (Любопытство или высокий Интерес).</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При отсутствии Любопытства и Интерес≤70 → <code>learningMode = 0</code>.</li>
</ul>
</li>
</ul>
<h3>2.6. Поисковый интерес (<code>InfoPicture.searchInterest</code>, ячейка «Поиск»)</h3>
<ul>
<li><strong>Поле:</strong> <code>searchInterest</code>: <code>0</code> — нет, <code>1</code> — витал Интерес&gt;70.</li>
<li><strong>Активация:</strong>  <ul>
<li>В <code>InfoDet_updateLearningAndSearch</code>:<ul>
<li>если витал с Id «Интерес» (договорённый Id, например 10) имеет <code>value &gt; 70</code> → <code>searchInterest = 1</code>.</li>
</ul>
</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Пока Интерес&gt;70.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При следующем вызове при Интерес≤70 → <code>searchInterest = 0</code>.</li>
</ul>
</li>
</ul>
<h3>2.7. Страх (<code>InfoPicture.fear</code>, ячейка «Страх»)</h3>
<ul>
<li><strong>Поле:</strong> <code>fear</code>: <code>0</code> — нет, <code>1</code> — активен базовый контекст Страх.</li>
<li><strong>Активация:</strong>  <ul>
<li>В <code>getActiveBasicContexts</code>:<ul>
<li>если <code>BasicContextsActived</code> содержит контекст id=6 (Страх) → <code>fear = 1</code>.</li>
</ul>
</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Пока контекст Страх остаётся активным.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При следующем вызове без id=6 → <code>fear = 0</code>.</li>
</ul>
</li>
</ul>
<h3>2.8. Агрессия (<code>InfoPicture.aggression</code>, ячейка «Агрессия»)</h3>
<ul>
<li><strong>Поле:</strong> <code>aggression</code>: <code>0</code> — нет, <code>1</code> — активен базовый контекст Агрессия.</li>
<li><strong>Активация:</strong>  <ul>
<li>Если <code>BasicContextsActived</code> содержит контекст id=7 (Агрессия) → <code>aggression = 1</code>.</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Пока контекст Агрессия активен.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При следующем вызове без id=7 → <code>aggression = 0</code>.</li>
</ul>
</li>
</ul>
<h3>2.9. Защита (<code>InfoPicture.defense</code>, ячейка «Защита»)</h3>
<ul>
<li><strong>Поле:</strong> <code>defense</code>: <code>0</code> — нет, <code>1</code> — активен режим защиты.</li>
<li><strong>Активация:</strong>  <ul>
<li>Если <code>fear == 1</code> или <code>aggression == 1</code> → <code>defense = 1</code>.</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Пока страх или агрессия активны.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При <code>fear == 0</code> и <code>aggression == 0</code> → <code>defense = 0</code>.</li>
</ul>
</li>
</ul>
<hr>
<h2>3. Семантика и ориентировочный рефлекс</h2>
<h3>3.1. Стимул с Пульта (<code>InfoPicture.consoleStimulus</code>, ячейка «С пульта»)</h3>
<ul>
<li><strong>Поле:</strong> <code>consoleStimulus</code>:  <ul>
<li><code>0</code> — нет стимула;  </li>
<li><code>&gt;0</code> — <code>FinalImageId</code> победившего образа стимула.</li>
</ul>
</li>
<li><strong>Активация:</strong>  <ul>
<li>В <code>runOROnStimulusActivation</code> после выбора <code>winnerId</code>:<ul>
<li><code>InfoDet_updateConsoleStimulus(winnerId)</code>.</li>
</ul>
</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Можно удерживать до следующего срабатывания ОР (последний актуальный стимул).</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При явном отсутствии активных стимулов (все сняты) или по решению при смене ветки:<ul>
<li>вызывать <code>InfoDet_updateConsoleStimulus(0)</code>.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3>3.2. Высокая семантическая значимость (<code>InfoPicture.highSemanticImportance</code>)</h3>
<ul>
<li><strong>Поле:</strong> <code>highSemanticImportance</code>: <code>0</code> — нет; <code>&gt;0</code> — средняя значимость <code>meanImportance</code> в текущих условиях.</li>
<li><strong>Активация:</strong>  <ul>
<li>В <code>orientation_reflex.js</code> после получения <code>semWinner</code>:<ul>
<li>если <code>meanImportance &gt; 0</code> → <code>highSemanticImportance = meanImportance</code>.</li>
</ul>
</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Пока текущий актуальный стимул (<code>winnerId</code>) и условия (well, emotionId) не сменились.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При смене стимула/контекста (новое срабатывание ОР) или отсутствии семантики → <code>highSemanticImportance = 0</code>.</li>
</ul>
</li>
</ul>
<h3>3.3. Есть объект высокой значимости (<code>InfoPicture.highImportanceObject</code>)</h3>
<ul>
<li><strong>Поле:</strong> <code>highImportanceObject</code>: <code>0</code> — нет; <code>&gt;0</code> — ID актуального образа (FinalImageId), имеющего |semantics|&gt;5.</li>
<li><strong>Активация:</strong>  <ul>
<li>В <code>orientation_reflex.js</code>:<ul>
<li>если <code>Math.abs(meanImportance) &gt; 5</code> → <code>highImportanceObject = winnerId</code>.</li>
</ul>
</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Пока текущий стимул тот же и условия близки.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При смене стимула/контекста или при <code>|meanImportance| ≤ 5</code> → <code>highImportanceObject = 0</code>.</li>
</ul>
</li>
</ul>
<h3>3.4. Новизна образа (<code>InfoPicture.imageNovelty</code>)</h3>
<ul>
<li><strong>Поле:</strong> <code>imageNovelty</code>: <code>0</code> — нет; <code>&gt;0</code> — значение novelty, если выше порога.</li>
<li><strong>Активация:</strong>  <ul>
<li>В <code>orientation_reflex.js</code> после расчёта <code>novelty</code>:<ul>
<li>если <code>novelty &gt; threshold</code> (порог из кода) → <code>imageNovelty = novelty</code>.</li>
</ul>
</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>До следующего срабатывания ОР.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При новом ОР, когда <code>novelty ≤ threshold</code> → <code>imageNovelty = 0</code>.</li>
</ul>
</li>
</ul>
<h3>3.5. Противоречивая семантика (<code>InfoPicture.conflictingSemantics</code>)</h3>
<ul>
<li><strong>Поле:</strong> <code>conflictingSemantics</code>: <code>0</code> — нет; <code>1</code> — обнаружена противоречивая семантика.</li>
<li><strong>Активация:</strong>  <ul>
<li>Код анализа семантики (в модуле понимания) обнаруживает:<ul>
<li>средняя значимость близка к 0 при большом <code>samplesCount</code>,</li>
<li>для разных условий есть как значительные положительные, так и значительные отрицательные значения.</li>
</ul>
</li>
<li>В этом случае → <code>conflictingSemantics = 1</code>.</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Пока ситуация с противоречивой семантикой не разрешена.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>Если при следующем анализе не выполняются признаки конфликта → <code>conflictingSemantics = 0</code>.</li>
</ul>
</li>
</ul>
<hr>
<h2>4. Эффекты действий и автоматизмы</h2>
<h3>4.1. Положительный / Отрицательный эффект действия (<code>positiveActionEffect</code>, <code>negativeActionEffect</code>)</h3>
<ul>
<li><strong>Поля:</strong>  <ul>
<li><code>positiveActionEffect</code>: <code>0</code> или &gt;0 — величина последнего положительного эффекта;  </li>
<li><code>negativeActionEffect</code>: <code>0</code> или &lt;0 — величина последнего отрицательного эффекта.</li>
</ul>
</li>
<li><strong>Активация:</strong>  <ul>
<li>В <code>closeLastFrame</code> (<code>episodic_memory.js</code>) после вычисления <code>last.effect</code>:<ul>
<li>если <code>effect &gt; 0</code> → <code>positiveActionEffect = effect</code>, <code>negativeActionEffect = 0</code>;</li>
<li>если <code>effect &lt; 0</code> → <code>negativeActionEffect = effect</code>, <code>positiveActionEffect = 0</code>;</li>
<li>если <code>effect == 0</code> → оба 0.</li>
</ul>
</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Хранит значение последнего завершённого кадра ЭП.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При новом кадре без эффекта или при явном сбросе истории → оба = 0.</li>
</ul>
</li>
</ul>
<h3>4.2. Негативный эффект моторного автоматизма (<code>negativeMotorEffect</code>)</h3>
<ul>
<li><strong>Поле:</strong> <code>negativeMotorEffect</code>: <code>0</code> — нет; <code>&lt;0</code> — негативный z в периоде ожидания моторного автоматизма.</li>
<li><strong>Активация:</strong>  <ul>
<li>При срабатывании моторного автоматизма и измерении <code>z = getDiffImportance()</code> в периоде ожидания:<ul>
<li>если <code>z &lt; 0</code> → <code>negativeMotorEffect = z</code>.</li>
</ul>
</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Пока не произведена новая оценка <code>z</code> для другого моторного автоматизма.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При следующем измерении <code>z &gt;= 0</code> или завершении периода ожидания без негативного эффекта → <code>negativeMotorEffect = 0</code>.</li>
</ul>
</li>
</ul>
<h3>4.3. Негативный эффект ментального автоматизма (<code>negativeMentalEffect</code>)</h3>
<ul>
<li>Аналогично моторному, но для автоматизмов, запущенных из функции осмысления (ментальных):<ul>
<li>при <code>z &lt; 0</code> в периоде ожидания → <code>negativeMentalEffect = z</code>,</li>
<li>при отсутствии/завершении — → 0.</li>
</ul>
</li>
</ul>
<h3>4.4. Низкая полезность автоматизма (<code>lowAutomatismUsefulness</code>)</h3>
<ul>
<li><strong>Поле:</strong> <code>lowAutomatismUsefulness</code>: <code>0</code> — нет; |value| ≤ 1 — низкая полезность.</li>
<li><strong>Активация:</strong>  <ul>
<li>При каждом новом сэмпле <code>effectSample</code>, записываемом в полезность автоматизма:<ul>
<li>если <code>0 &lt; |effectSample| ≤ 1</code> → <code>lowAutomatismUsefulness = effectSample</code>.</li>
</ul>
</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Пока следующий сэмпл не показывает нормальный по модулю эффект (&gt;1).</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При <code>|effectSample| &gt; 1</code> или <code>effectSample == 0</code> → <code>lowAutomatismUsefulness = 0</code>.</li>
</ul>
</li>
</ul>
<h3>4.5. Пока найден подходящий автоматизм (<code>foundSuitableAutomatism</code>)</h3>
<ul>
<li><strong>Поле:</strong> <code>foundSuitableAutomatism</code>: <code>0</code> — нет; <code>1</code> — цикл осмысления в целевом режиме нашёл и использует автоматизм.</li>
<li><strong>Активация:</strong>  <ul>
<li>При выборе и запуске подходящего автоматизма в целевом режиме → <code>foundSuitableAutomatism = 1</code>.</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Пока цикл осмысления продолжает использовать этот автоматизм.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При завершении цикла или при отказе от автоматизма → <code>foundSuitableAutomatism = 0</code>.</li>
</ul>
</li>
</ul>
<hr>
<h2>5. Ожидание и ответы оператора</h2>
<h3>5.1. Нет реакции на стимул (<code>noReactionToStimulus</code>)</h3>
<ul>
<li><strong>Поле:</strong> <code>noReactionToStimulus</code>: <code>0</code> — нет; <code>1</code> — зафиксировано отсутствие реакции на стимул.</li>
<li><strong>Активация:</strong>  <ul>
<li>В <code>EpisodicMemory_onPulse</code>, когда <code>last.actionImageId === null</code> и период ожидания истёк без реакции → <code>noReactionToStimulus = 1</code>.</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>До следующего запуска ожидания или явного сброса.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При появлении реакции или при запуске нового ожидания → <code>noReactionToStimulus = 0</code>.</li>
</ul>
</li>
</ul>
<h3>5.2. Игнорирование оператором (<code>operatorIgnoring</code>)</h3>
<ul>
<li><strong>Поле:</strong> <code>operatorIgnoring</code>: <code>0</code> — нет; <code>1</code> — оператор проигнорировал действие.</li>
<li><strong>Активация:</strong>  <ul>
<li>В <code>EpisodicMemory_onPulse</code>, якщо <code>last.actionImageId != null</code> (была реакция/автоматизм) и период ожидания закончился без ответного стимула → <code>operatorIgnoring = 1</code>.</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Пока не получен ответный стимул в новом периоде ожидания.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>В <code>orientation_reflex.js</code>, при ответном стимуле в период ожидания (<code>EpisodicMemory_isInWaitingPeriod</code>):<ul>
<li><code>operatorIgnoring = 0</code>.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3>5.3. Ответ оператора получен (<code>operatorAnswerReceived</code>)</h3>
<ul>
<li><strong>Поле:</strong> <code>operatorAnswerReceived</code>: <code>0</code> — нет; <code>1</code> — ответ оператора получен.</li>
<li><strong>Активация:</strong>  <ul>
<li>В <code>orientation_reflex.js</code>, при срабатывании ОР в период ожидания и закрытии кадра с ответным стимулом → <code>operatorAnswerReceived = 1</code>.</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>До следующего кадра ожидания или явного сброса.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При запуске нового периода ожидания или при обработке ответа → <code>operatorAnswerReceived = 0</code>.</li>
</ul>
</li>
</ul>
<h3>5.4. Активное ожидание ответа (<code>activeWaitingAnswer</code>)</h3>
<ul>
<li><strong>Поле:</strong> <code>activeWaitingAnswer</code>: <code>0</code> — нет; <code>1</code> — период ожидания результата автоматизма активен.</li>
<li><strong>Активация:</strong>  <ul>
<li>При создании кадра ЭП, связанного с автоматизмом (особенно ментальным, запущенным из осмысления) → <code>activeWaitingAnswer = 1</code>.</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Пока период ожидания не завершён (ответ/таймаут).</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При закрытии кадра (с ответом или без) → <code>activeWaitingAnswer = 0</code>.</li>
</ul>
</li>
</ul>
<h3>5.5. Серия эпизодов без эффекта (<code>seriesWithoutEffect</code>)</h3>
<ul>
<li><strong>Поле:</strong> <code>seriesWithoutEffect</code>: <code>0</code> — нет; &gt;0 — длина последней серии кадров с <code>effect == 0</code>.</li>
<li><strong>Активация / удержание:</strong>  <ul>
<li>Логика в ЭП увеличивает счётчик при каждом закрываемом кадре с <code>effect == 0</code> и обнуляет при появлении ненулевого эффекта; при каждом изменении длины вызывается <code>InfoDet_updateSeriesWithoutEffect(length)</code>.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  </li>
<li>При появлении эффекта (<code>effect != 0</code>) или при явном сбросе ЭП → <code>seriesWithoutEffect = 0</code>.</li>
</ul>
<hr>
<h2>6. Осмысление и трудные проблемы</h2>
<p>Эти детекторы зависят от реализации цикла осмысления (<code>conscience_level_2</code>) и работы с <code>unsolved_problem</code>.</p>
<h3>6.1. Обучение с учителем (<code>teacherLearning</code>)</h3>
<ul>
<li><strong>Поле:</strong> <code>teacherLearning</code>: <code>0</code> — нет; <code>1</code> — включён режим обучения с учителем.</li>
<li><strong>Активация:</strong>  <ul>
<li>Когда функция осмысления фиксирует, что по конкретному образу имеется <strong>нерешённая проблема</strong>, которая остаётся нерешённой в течение непрекращающегося цикла попыток решения (несколько шагов целевого осмысления подряд).</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Пока продолжается режим «передачи инициативы» оператору: система отслеживает его действия и провоцирует на ответы.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При решении проблемы (ответ оператора или найдено внутреннее решение), либо по истечении заданного лимита шагов → <code>teacherLearning = 0</code>.</li>
</ul>
</li>
</ul>
<h3>6.2. Неудовлетворённость существующим (<code>dissatisfaction</code>)</h3>
<ul>
<li><strong>Поле:</strong> <code>dissatisfaction</code>: <code>0</code> — нет; <code>1</code> — есть неудовлетворённость.</li>
<li><strong>Активация:</strong>  <ul>
<li>Когда базовый контекст Покой активен, нет актуальных стимулов и завершён цикл осмысления в пассивном режиме (дошёл до конца без новых идей/изменений).</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Пока внешние условия не изменятся или не появится новый стимул/интерес.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При активизации новых стимулов, смене контекста или запуске целевого цикла → <code>dissatisfaction = 0</code>.</li>
</ul>
</li>
</ul>
<h3>6.3. Непонимание (<code>misunderstanding</code>)</h3>
<ul>
<li><strong>Поле:</strong> <code>misunderstanding</code>: <code>0</code> — нет; <code>1</code> — состояние непонимания.</li>
<li><strong>Активация:</strong>  <ul>
<li>Если цикл осмысления дошёл до конца в целевом режиме <strong>или</strong> число шагов итерации превысило заданный предел (например, 10), и проблема не решена.</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Пока цикл не перезапущен с иными условиями или проблема не решена.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При решении проблемы или перезапуске цикла с новой конфигурацией → <code>misunderstanding = 0</code>.</li>
</ul>
</li>
</ul>
<h3>6.4. Сомнение в штатном автоматизме (<code>doubtInDefaultAutomatism</code>)</h3>
<ul>
<li><strong>Поле:</strong> <code>doubtInDefaultAutomatism</code>: <code>0</code> — нет; <code>1</code> — есть сомнение.</li>
<li><strong>Активация:</strong>  <ul>
<li>При старте второго уровня осмысления в целевом режиме для ветки, где штатный автоматизм даёт неоднозначные/неприемлемые результаты.</li>
</ul>
</li>
<li><strong>Удержание:</strong>  <ul>
<li>Пока второй уровень рассматривает альтернативы штатному автоматизму.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При выборе нового штатного автоматизма или подтверждении старого → <code>doubtInDefaultAutomatism = 0</code>.</li>
</ul>
</li>
</ul>
<h3>6.5. Трудная проблема (<code>hardProblem</code>)</h3>
<ul>
<li><strong>Поле:</strong> <code>hardProblem</code>: <code>0</code> — нет; &gt;0 — уровень/счётчик «трудности» проблемы по образу.</li>
<li><strong>Активация / удержание:</strong>  <ul>
<li>Для каждого образа (стимула/ситуации) ведётся счётчик случаев, когда:<ul>
<li>есть <code>unsolved_problem</code> по этому образу,</li>
<li>целевой цикл доходит до конца без решения,</li>
<li>число шагов цикла осмысления превышает порог.</li>
</ul>
</li>
<li>При каждом таком случае <code>hardProblem</code> для текущего образа увеличивается и передаётся в <code>InfoDet_updateHardProblem(level)</code>.</li>
</ul>
</li>
<li><strong>Сброс:</strong>  <ul>
<li>При решении проблемы (образ помечается решённым) → уровень обнуляется, образ удаляется из массива «трудных проблем», <code>hardProblem = 0</code>.</li>
</ul>
</li>
</ul>
<hr>
<p>Этот документ задаёт целевые алгоритмы для всех инфо-детекторов; реализации функций <code>InfoDet_update...</code> в <code>info_detectors.js</code> должны следовать этим правилам, а вызовы этих функций в коде (в prepare.js, orientation_reflex.js, episodic_memory.js, conscience_level_2.js и др.) должны подавать им нужные параметры при каждом изменении релевантного состояния. 
*** End Patch***</p>
<p>*** End Patch</p>

                </article>
                </body>
                </html>
════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/about_info_detectors_algorithms.md
════════════════════════════════════════════════════════════
# Инфо-детекторы: алгоритмы активации, удержания и сброса

В этом документе перечислены все инфо-детекторы инфокартины, их числовые поля в `InfoPicture` и три группы условий для каждого:

- **Активация** — когда детектор становится активным (поле ≠ 0).
- **Удержание** — когда детектор продолжает оставаться активным.
- **Сброс** — когда детектор обязан вернуться в 0.

## Общие принципы

- Значения детекторов **целочисленные** (или числа с плавающей точкой для «уровней»). Ноль всегда означает «не активно / не обнаружено».
- Каждая функция `InfoDet_updateXxx(...)` в `info_detectors.js`:
  - принимает параметры из «места сбора информации» (виталы, контексты, эффекты ЭП, состояние ожидания и т.п.);
  - **сама** решает, поставить ненулевое значение или сбросить в 0, исходя из трёх условий.
- Вызывающий код обязан передавать детектору **актуальные данные** при каждом существенном изменении (пульс, ОР, закрытие кадра ЭП, запуск/завершение ожидания и т.д.), даже если событие «не случилось» — чтобы детектор мог сам себя сбросить.

Детекторы условно делятся на:

- **Снимковые (состояния)** — могут пересчитываться по текущему снимку состояния (BadNormWell, BasicContextsActived, семантика и т.п.).
- **Событийные** — фиксируют момент/факт события во времени (конец ожидания без ответа, скачок новизны и т.п.). Для них условия удержания и сброса важны особенно.

---

## 1. Режим осмысления, Тема, Ситуация

### 1.1. Целевой режим осмысления (главный цикл ФО + ячейка «Режим»)

- **Источник:** `thinkingMode` у **главного** цикла (`'passive'` \| `'target'`); обновление через `InfoDet_updateTargetMode` / `Info_setMode` / инсайт. Ячейка «Режим» в UI читает `Consciousness_getMainCycleThinkingMode()`.
- **Активация:**  
  - При запуске реакции/автоматизма, который требует целевого осмысления (например, при OR с семантически значимым стимулом или при запуске цикла осмысления по `unsolved_problem`): вызывается `InfoDet_updateTargetMode(true)`.
- **Удержание:**  
  - Пока идёт целевой цикл осмысления или активен процесс оценки последствий автоматизма (период ожидания).
- **Сброс:**  
  - При завершении целевого цикла (проблема решена или признана неактуальной) и отсутствии условий для нового целевого осмысления → `InfoDet_updateTargetMode(false)`.

### 1.2. Текущая Тема (`InfoPicture.themeId`, ячейка «Тема»)

- **Поле:** `themeId`: `0` — нет темы, >0 — ID образа темы из дерева ситуаций.
- **Активация:**  
  - При каждом вызове `SituationsTree_onPerceptionTreeActivated()` находящий best‑match готовый образ темы для текущего режима и `tree.BaseID/EmotionID/ActivityID`:
    - если найден готовый образ (ready и score>0) → `InfoDet_updateCurrentTheme(themeId)`.
- **Удержание:**  
  - Пока текущая комбинация (Base, Emotion, Activity) остаётся ближе всего к тому же готовому образу темы.
- **Сброс:**  
  - Если при очередной активации дерева ситуаций:
    - нет готовых образов (все count ≤ 3),  
    - или ни один готовый образ не даёт ненулевого score,  
    → `InfoDet_updateCurrentTheme(0)`.

### 1.3. Текущая Ситуация (`InfoPicture.situationId`, ячейка «Ситуация»)

- **Поле:** `situationId`: `0` — нет, >0 — ID образа ситуации из дерева ситуаций.
- **Активация:**  
  - В `SituationsTree_onPerceptionTreeActivated()` из детекторных полей инфокартины формируется вектор компонент; среди готовых образов выбирается best‑match:
    - при score>0 и ready=true → `InfoDet_updateCurrentSituation(situationId)`.
- **Удержание:**  
  - Пока текущий набор детекторов остаётся ближе всего к той же ситуации.
- **Сброс:**  
  - При следующем вызове, если:
    - нет готовых ситуаций для текущего режима,
    - или score==0 для всех,  
    → `InfoDet_updateCurrentSituation(0)`.

---

## 2. Виталы и базовые контексты

### 2.1. Критический витал (`InfoPicture.criticalVital`, ячейка «Крит. витал»)

- **Поле:** `criticalVital`:  
  - `0` — нет критического витала;  
  - `>0` — ID витала, значение которого >70 и максимально среди всех.
- **Активация:**  
  - В `updateImportanceMetrics(badVitals)` или `getActiveBasicContexts(cpuls)`:
    - сканировать `VitalsArr` и найти витал с `value > 70` и максимальным значением;
    - если такой есть → `InfoPicture.criticalVital = vitalId`.
- **Удержание:**  
  - Пока при очередных вызовах остаётся витал с `value > 70`; при смене «лидера» детектор обновляется на новый ID.
- **Сброс:**  
  - Если при следующем вызове ни один витал не превышает 70 → `criticalVital = 0`.

### 2.2. Состояние Плохо (`InfoPicture.badState`, ячейка «Плохо»)

- **Поле:** `badState`: `0` — не Плохо, `1` — Плохо (BadNormWell=1).
- **Активация:**  
  - В `InfoDet_updateBadState(lastBadNormWell, badNormWell)`:
    - при `badNormWell === 1` → `badState = 1`.
- **Удержание:**  
  - Пока `BadNormWell` остаётся равным 1.
- **Сброс:**  
  - При любом значении `BadNormWell != 1` в следующем вызове → `badState = 0`.

### 2.3. Резкое ухудшение состояния (`InfoPicture.suddenChange`, ячейка «Резк. изм.»)

- **Поле:** `suddenChange`:  
  - `0` — нет;  
  - `1` — момент смены на Плохо (по PDF); можно хранить и более детальный уровень.
- **Активация:**  
  - В `InfoDet_updateBadState(lastBadNormWell, badNormWell)`:
    - если `lastBadNormWell !== 1` и `badNormWell === 1` → `suddenChange = 1`.
- **Удержание:**  
  - Это событийный детектор: можно держать `1` в течение одного шага цикла осмысления или одного/нескольких пульсов (на ваше усмотрение), если нужно «увидеть» событие.
- **Сброс:**  
  - При следующем вызове без повторной смены в Плохо → `suddenChange = 0`.

### 2.4. Игровой режим (`InfoPicture.playMode`, ячейка «Игра»)

- **Поле:** `playMode`: `0` — нет, `1` — активен базовый контекст Игра.
- **Активация:**  
  - В `getActiveBasicContexts` после получения `BasicContextsActived`:
    - если `BasicContextsActived` содержит контекст с `id == 11` → `playMode = 1`.
- **Удержание:**  
  - Пока Игра остаётся в `BasicContextsActived`.
- **Сброс:**  
  - При следующем вызове, если контекст Игра не активен → `playMode = 0`.

### 2.5. Режим обучения (`InfoPicture.learningMode`, ячейка «Обучение»)

- **Поле:** `learningMode`: `0` — нет, `1` — активен режим обучения.
- **Активация:**  
  - В `InfoDet_updateLearningAndSearch(BasicContextsActived, VitalsArr)`:
    - если активен базовый контекст Любопытство (id=12) **или** витал «Интерес» имеет `value > 70` → `learningMode = 1`.
- **Удержание:**  
  - Пока сохраняется хотя бы одно из условий (Любопытство или высокий Интерес).
- **Сброс:**  
  - При отсутствии Любопытства и Интерес≤70 → `learningMode = 0`.

### 2.6. Поисковый интерес (`InfoPicture.searchInterest`, ячейка «Поиск»)

- **Поле:** `searchInterest`: `0` — нет, `1` — витал Интерес>70.
- **Активация:**  
  - В `InfoDet_updateLearningAndSearch`:
    - если витал с Id «Интерес» (договорённый Id, например 10) имеет `value > 70` → `searchInterest = 1`.
- **Удержание:**  
  - Пока Интерес>70.
- **Сброс:**  
  - При следующем вызове при Интерес≤70 → `searchInterest = 0`.

### 2.7. Страх (`InfoPicture.fear`, ячейка «Страх»)

- **Поле:** `fear`: `0` — нет, `1` — активен базовый контекст Страх.
- **Активация:**  
  - В `getActiveBasicContexts`:
    - если `BasicContextsActived` содержит контекст id=6 (Страх) → `fear = 1`.
- **Удержание:**  
  - Пока контекст Страх остаётся активным.
- **Сброс:**  
  - При следующем вызове без id=6 → `fear = 0`.

### 2.8. Агрессия (`InfoPicture.aggression`, ячейка «Агрессия»)

- **Поле:** `aggression`: `0` — нет, `1` — активен базовый контекст Агрессия.
- **Активация:**  
  - Если `BasicContextsActived` содержит контекст id=7 (Агрессия) → `aggression = 1`.
- **Удержание:**  
  - Пока контекст Агрессия активен.
- **Сброс:**  
  - При следующем вызове без id=7 → `aggression = 0`.

### 2.9. Защита (`InfoPicture.defense`, ячейка «Защита»)

- **Поле:** `defense`: `0` — нет, `1` — активен режим защиты.
- **Активация:**  
  - Если `fear == 1` или `aggression == 1` → `defense = 1`.
- **Удержание:**  
  - Пока страх или агрессия активны.
- **Сброс:**  
  - При `fear == 0` и `aggression == 0` → `defense = 0`.

---

## 3. Семантика и ориентировочный рефлекс

### 3.1. Стимул с Пульта (`InfoPicture.consoleStimulus`, ячейка «С пульта»)

- **Поле:** `consoleStimulus`:  
  - `0` — нет стимула;  
  - `>0` — `FinalImageId` победившего образа стимула.
- **Активация:**  
  - В `runOROnStimulusActivation` после выбора `winnerId`:
    - `InfoDet_updateConsoleStimulus(winnerId)`.
- **Удержание:**  
  - Можно удерживать до следующего срабатывания ОР (последний актуальный стимул).
- **Сброс:**  
  - При явном отсутствии активных стимулов (все сняты) или по решению при смене ветки:
    - вызывать `InfoDet_updateConsoleStimulus(0)`.

### 3.2. Высокая семантическая значимость (`InfoPicture.highSemanticImportance`)

- **Поле:** `highSemanticImportance`: `0` — нет; `>0` — средняя значимость `meanImportance` в текущих условиях.
- **Активация:**  
  - В `orientation_reflex.js` после получения `semWinner`:
    - если `meanImportance > 0` → `highSemanticImportance = meanImportance`.
- **Удержание:**  
  - Пока текущий актуальный стимул (`winnerId`) и условия (well, emotionId) не сменились.
- **Сброс:**  
  - При смене стимула/контекста (новое срабатывание ОР) или отсутствии семантики → `highSemanticImportance = 0`.

### 3.3. Есть объект высокой значимости (`InfoPicture.highImportanceObject`)

- **Поле:** `highImportanceObject`: `0` — нет; `>0` — ID актуального образа (FinalImageId), имеющего |semantics|>5.
- **Активация:**  
  - В `orientation_reflex.js`:
    - если `Math.abs(meanImportance) > 5` → `highImportanceObject = winnerId`.
- **Удержание:**  
  - Пока текущий стимул тот же и условия близки.
- **Сброс:**  
  - При смене стимула/контекста или при `|meanImportance| ≤ 5` → `highImportanceObject = 0`.

### 3.4. Новизна образа (`InfoPicture.imageNovelty`)

- **Поле:** `imageNovelty`: `0` — нет; `>0` — значение novelty, если выше порога.
- **Активация:**  
  - В `orientation_reflex.js` после расчёта `novelty`:
    - если `novelty > threshold` (порог из кода) → `imageNovelty = novelty`.
- **Удержание:**  
  - До следующего срабатывания ОР.
- **Сброс:**  
  - При новом ОР, когда `novelty ≤ threshold` → `imageNovelty = 0`.

### 3.5. Противоречивая семантика (`InfoPicture.conflictingSemantics`)

- **Поле:** `conflictingSemantics`: `0` — нет; `1` — обнаружена противоречивая семантика.
- **Активация:**  
  - Код анализа семантики (в модуле понимания) обнаруживает:
    - средняя значимость близка к 0 при большом `samplesCount`,
    - для разных условий есть как значительные положительные, так и значительные отрицательные значения.
  - В этом случае → `conflictingSemantics = 1`.
- **Удержание:**  
  - Пока ситуация с противоречивой семантикой не разрешена.
- **Сброс:**  
  - Если при следующем анализе не выполняются признаки конфликта → `conflictingSemantics = 0`.

---

## 4. Эффекты действий и автоматизмы

### 4.1. Положительный / Отрицательный эффект действия (`positiveActionEffect`, `negativeActionEffect`)

- **Поля:**  
  - `positiveActionEffect`: `0` или >0 — величина последнего положительного эффекта;  
  - `negativeActionEffect`: `0` или <0 — величина последнего отрицательного эффекта.
- **Активация:**  
  - В `closeLastFrame` (`episodic_memory.js`) после вычисления `last.effect`:
    - если `effect > 0` → `positiveActionEffect = effect`, `negativeActionEffect = 0`;
    - если `effect < 0` → `negativeActionEffect = effect`, `positiveActionEffect = 0`;
    - если `effect == 0` → оба 0.
- **Удержание:**  
  - Хранит значение последнего завершённого кадра ЭП.
- **Сброс:**  
  - При новом кадре без эффекта или при явном сбросе истории → оба = 0.

### 4.2. Негативный эффект моторного автоматизма (`negativeMotorEffect`)

- **Поле:** `negativeMotorEffect`: `0` — нет; `<0` — негативный z в периоде ожидания моторного автоматизма.
- **Активация:**  
  - При срабатывании моторного автоматизма и измерении `z = getDiffImportance()` в периоде ожидания:
    - если `z < 0` → `negativeMotorEffect = z`.
- **Удержание:**  
  - Пока не произведена новая оценка `z` для другого моторного автоматизма.
- **Сброс:**  
  - При следующем измерении `z >= 0` или завершении периода ожидания без негативного эффекта → `negativeMotorEffect = 0`.

### 4.3. Негативный эффект ментального автоматизма (`negativeMentalEffect`)

- Аналогично моторному, но для автоматизмов, запущенных из функции осмысления (ментальных):
  - при `z < 0` в периоде ожидания → `negativeMentalEffect = z`,
  - при отсутствии/завершении — → 0.

### 4.4. Низкая полезность автоматизма (`lowAutomatismUsefulness`)

- **Поле:** `lowAutomatismUsefulness`: `0` — нет; |value| ≤ 1 — низкая полезность.
- **Активация:**  
  - При каждом новом сэмпле `effectSample`, записываемом в полезность автоматизма:
    - если `0 < |effectSample| ≤ 1` → `lowAutomatismUsefulness = effectSample`.
- **Удержание:**  
  - Пока следующий сэмпл не показывает нормальный по модулю эффект (>1).
- **Сброс:**  
  - При `|effectSample| > 1` или `effectSample == 0` → `lowAutomatismUsefulness = 0`.

### 4.5. Пока найден подходящий автоматизм (`foundSuitableAutomatism`)

- **Поле:** `foundSuitableAutomatism`: `0` — нет; `1` — цикл осмысления в целевом режиме нашёл и использует автоматизм.
- **Активация:**  
  - При выборе и запуске подходящего автоматизма в целевом режиме → `foundSuitableAutomatism = 1`.
- **Удержание:**  
  - Пока цикл осмысления продолжает использовать этот автоматизм.
- **Сброс:**  
  - При завершении цикла или при отказе от автоматизма → `foundSuitableAutomatism = 0`.

---

## 5. Ожидание и ответы оператора

### 5.1. Нет реакции на стимул (`noReactionToStimulus`)

- **Поле:** `noReactionToStimulus`: `0` — нет; `1` — зафиксировано отсутствие реакции на стимул.
- **Активация:**  
  - В `EpisodicMemory_onPulse`, когда `last.actionImageId === null` и период ожидания истёк без реакции → `noReactionToStimulus = 1`.
- **Удержание:**  
  - До следующего запуска ожидания или явного сброса.
- **Сброс:**  
  - При появлении реакции или при запуске нового ожидания → `noReactionToStimulus = 0`.

### 5.2. Игнорирование оператором (`operatorIgnoring`)

- **Поле:** `operatorIgnoring`: `0` — нет; `1` — оператор проигнорировал действие.
- **Активация:**  
  - В `EpisodicMemory_onPulse`, якщо `last.actionImageId != null` (была реакция/автоматизм) и период ожидания закончился без ответного стимула → `operatorIgnoring = 1`.
- **Удержание:**  
  - Пока не получен ответный стимул в новом периоде ожидания.
- **Сброс:**  
  - В `orientation_reflex.js`, при ответном стимуле в период ожидания (`EpisodicMemory_isInWaitingPeriod`):
    - `operatorIgnoring = 0`.

### 5.3. Ответ оператора получен (`operatorAnswerReceived`)

- **Поле:** `operatorAnswerReceived`: `0` — нет; `1` — ответ оператора получен.
- **Активация:**  
  - В `orientation_reflex.js`, при срабатывании ОР в период ожидания и закрытии кадра с ответным стимулом → `operatorAnswerReceived = 1`.
- **Удержание:**  
  - До следующего кадра ожидания или явного сброса.
- **Сброс:**  
  - При запуске нового периода ожидания или при обработке ответа → `operatorAnswerReceived = 0`.

### 5.4. Активное ожидание ответа (`activeWaitingAnswer`)

- **Поле:** `activeWaitingAnswer`: `0` — нет; `1` — период ожидания результата автоматизма активен.
- **Активация:**  
  - При создании кадра ЭП, связанного с автоматизмом (особенно ментальным, запущенным из осмысления) → `activeWaitingAnswer = 1`.
- **Удержание:**  
  - Пока период ожидания не завершён (ответ/таймаут).
- **Сброс:**  
  - При закрытии кадра (с ответом или без) → `activeWaitingAnswer = 0`.

### 5.5. Серия эпизодов без эффекта (`seriesWithoutEffect`)

- **Поле:** `seriesWithoutEffect`: `0` — нет; >0 — длина последней серии кадров с `effect == 0`.
- **Активация / удержание:**  
  - Логика в ЭП увеличивает счётчик при каждом закрываемом кадре с `effect == 0` и обнуляет при появлении ненулевого эффекта; при каждом изменении длины вызывается `InfoDet_updateSeriesWithoutEffect(length)`.
- **Сброс:**  
- При появлении эффекта (`effect != 0`) или при явном сбросе ЭП → `seriesWithoutEffect = 0`.

---

## 6. Осмысление и трудные проблемы

Эти детекторы зависят от реализации цикла осмысления (`conscience_level_2`) и работы с `unsolved_problem`.

### 6.1. Обучение с учителем (`teacherLearning`)

- **Поле:** `teacherLearning`: `0` — нет; `1` — включён режим обучения с учителем.
- **Активация:**  
  - Когда функция осмысления фиксирует, что по конкретному образу имеется **нерешённая проблема**, которая остаётся нерешённой в течение непрекращающегося цикла попыток решения (несколько шагов целевого осмысления подряд).
- **Удержание:**  
  - Пока продолжается режим «передачи инициативы» оператору: система отслеживает его действия и провоцирует на ответы.
- **Сброс:**  
  - При решении проблемы (ответ оператора или найдено внутреннее решение), либо по истечении заданного лимита шагов → `teacherLearning = 0`.

### 6.2. Неудовлетворённость существующим (`dissatisfaction`)

- **Поле:** `dissatisfaction`: `0` — нет; `1` — есть неудовлетворённость.
- **Активация:**  
  - Когда базовый контекст Покой активен, нет актуальных стимулов и завершён цикл осмысления в пассивном режиме (дошёл до конца без новых идей/изменений).
- **Удержание:**  
  - Пока внешние условия не изменятся или не появится новый стимул/интерес.
- **Сброс:**  
  - При активизации новых стимулов, смене контекста или запуске целевого цикла → `dissatisfaction = 0`.

### 6.3. Непонимание (`misunderstanding`)

- **Поле:** `misunderstanding`: `0` — нет; `1` — состояние непонимания.
- **Активация:**  
  - Если цикл осмысления дошёл до конца в целевом режиме **или** число шагов итерации превысило заданный предел (например, 10), и проблема не решена.
- **Удержание:**  
  - Пока цикл не перезапущен с иными условиями или проблема не решена.
- **Сброс:**  
  - При решении проблемы или перезапуске цикла с новой конфигурацией → `misunderstanding = 0`.

### 6.4. Сомнение в штатном автоматизме (`doubtInDefaultAutomatism`)

- **Поле:** `doubtInDefaultAutomatism`: `0` — нет; `1` — есть сомнение.
- **Активация:**  
  - При старте второго уровня осмысления в целевом режиме для ветки, где штатный автоматизм даёт неоднозначные/неприемлемые результаты.
- **Удержание:**  
  - Пока второй уровень рассматривает альтернативы штатному автоматизму.
- **Сброс:**  
  - При выборе нового штатного автоматизма или подтверждении старого → `doubtInDefaultAutomatism = 0`.

### 6.5. Трудная проблема (`hardProblem`)

- **Поле:** `hardProblem`: `0` — нет; >0 — уровень/счётчик «трудности» проблемы по образу.
- **Активация / удержание:**  
  - Для каждого образа (стимула/ситуации) ведётся счётчик случаев, когда:
    - есть `unsolved_problem` по этому образу,
    - целевой цикл доходит до конца без решения,
    - число шагов цикла осмысления превышает порог.
  - При каждом таком случае `hardProblem` для текущего образа увеличивается и передаётся в `InfoDet_updateHardProblem(level)`.
- **Сброс:**  
  - При решении проблемы (образ помечается решённым) → уровень обнуляется, образ удаляется из массива «трудных проблем», `hardProblem = 0`.

---

Этот документ задаёт целевые алгоритмы для всех инфо-детекторов; реализации функций `InfoDet_update...` в `info_detectors.js` должны следовать этим правилам, а вызовы этих функций в коде (в prepare.js, orientation_reflex.js, episodic_memory.js, conscience_level_2.js и др.) должны подавать им нужные параметры при каждом изменении релевантного состояния. 
*** End Patch***

*** End Patch

════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/about_passive_fantasizming.md
════════════════════════════════════════════════════════════
# Пассивный режим и фантазирование (`info_fantasizming`)

## Роли режимов

- Режим мышления (**`target`** | **`passive`**) хранится на **цикле ФО** (`cycle.thinkingMode`), **не** в `InfoPicture`. У главного и фонового цикла он может различаться; инфокартина — общий «экран» и ячейки данных без поля «режим».
- Целевой и пассивный режимы **антагонистичны на уровне шага цикла**: при **`thinkingMode === 'passive'`** не вызываются ур. 3–4; выполняется **`info_fantasizming(cycle)`** (если не сработал антистагнационный гейт).
- **Главный цикл** в пассиве дописывает сюжет в **`InfoPicture.fantasmPlotRules`** («Сюжет фантазма») и использует **`fantazmingPlot` / `lastGeneratedActionId`** на цикле как контекст продолжения.
- **Фоновый цикл** в пассиве **не** пишет в `fantasmPlotRules`; накапливает контекст в **`cycle.fantasizmArr`** и **`cycle.fantazmingPlot`** (цепочка и якорь для следующего шага).

## Опорный кадр и «EpisodicTreeNode»

В коде кадры хранятся в массиве **`Episodes`**. Для шага фантазирования точка входа — **идентификатор стимула** `currentStimulusId` (см. `info_fantasizming`); для главного при отсутствии якоря допускается **`InfoPicture.currentGoalId`**, для фонового — нет.

## Антистагнация

После шага в **`cycle.passiveFantasyLastSig`** пишется сигнатура прогресса. **Главный** цикл: **`Consciousness_getInfoPictureSignature()`** (изменения глобальной инфокартины). **Фоновый**: локальная строка по **`fantazmingPlot` + `fantasizmArr`** (без записи в инфокартину). При **streak ≥ 3** и совпадении с последним снимком вызов **`info_fantasizming`** пропускается.

## Алгоритм одного шага `info_fantasizming(cycle)`

1. **`cycle.thinkingMode === 'passive'`**; иначе выход.
2. **Текущий стимул** — приоритет: `lastGeneratedActionId` → `context.actual_stimul_ID` → (только главный) `InfoPicture.currentGoalId`.
3. Кадры ЭП с **`responseStimulusImageId === currentStimulusId`**, лучший по **|effect|**.
4. В **`fantazmingPlot`** и **`fantasizmArr`** — всегда; в **`fantasmPlotRules`** — **только если цикл главный**.
5. Инсайт по порогу → **`info_insight`** (ставит **`thinkingMode = 'target'`** у цикла).
6. Иначе: **главный** — `runActionImage`, `Info_updateFromStimulus`, шаг инфокартины; **фоновый** — без изменения глобальной инфокартины и без моторной имитации.

## Связь с гештальтами и резонансом

Дальнейшее развитие — приоритеты объектов, `curConditions`, гештальты / **`GestaltResonanceBuffer`**.

════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/abstract_images.js
════════════════════════════════════════════════════════════
/* Абстракции
Образы неокортекса (_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);


════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/conscience_level_2.js
════════════════════════════════════════════════════════════
/**
 * Второй уровень осознания (conscience_level_2).
 * На втором уровне при обработке определяется цель — целевое или пассивное продолжение.
 *
 * Вызывается из stimuls_consciousness, когда первый уровень не смог подобрать
 * приемлемое автоматическое действие (ни по ЭП, ни по штатному автоматизму)
 * и установлена нерешённая проблема unsolved_problem = актуальный стимул.
 * Это переход ко второму уровню в целевом режиме (проблема с автоматизмом).
 *
 * Целевой режим, итерация второго уровня:
 *   — Сначала обрабатывается ответ оператора (isOperatorAnswerReceived), если есть.
 *   — Затем определение цели и действий: ветка «выживания» (criticalVital / shockState / suddenChange /
 *     isShockState) — Goals_findBestLifeGoalPositiveUsefulness, цепочка EpisodicMemory_findChainForGoalStack
 *     при usedRule, иначе поиск ExactRule / InaccurateRule по ЭП и запуск автоматизма или отложенный ур.3;
 *     без выживания — Goals_determineGoalInSituation (в т.ч. умолчательная цель «значение» при значимости
 *     стимула >3 и отсутствии цели по ветке), затем при currentGoalType === 'meaning' — прогноз по кадрам ЭП,
 *     Gestalt_bumpFromMeaningForecast, AbstractImages_boostSemanticRulesForAction; флаги isPlayMode / isLearningMode для ур.3.
 *   — В ходе поиска в эпизодической памяти новые образы цели накапливаются через Goals_* и ЭП.
 *
 * Ответ оператора на реакцию после автоматизма (ветка «ожидание обратной связи по кадру ЭП»):
 *   • В инфокартине поле isOperatorAnswerReceived (и связанные детекторы) поднимается при закрытии
 *     кадра ответным стимулом — см. EpisodicMemory (closeLastFrame / ориентировочный рефлекс в ожидании).
 *   • Объект цикла осмысления той же сессии получает automatismOperatorAnswerReceived и
 *     awaitingAutomatismEpisodeFeedback; пока цикл «ждёт», диспетчер крутит runOneStep по пульсам.
   *   • Первый исполняемый блок conscience_level_2 (не после постановки цели) обрабатывает этот ответ:
   *     по effect закрытого кадра — базовая корректировка usefulness цели цикла (cycle.goalImageId),
   *     затем доработка по детекторам; завершение цикла ожидания (expired).
 *   • Вызов из runOneStep передаёт _operatorAnswerCycleOnly: тогда после блока ответа выходим,
 *     не смешивая с постановкой ImportantProblem/целей в том же тике.
 *
 * @param {Object} context
 *   ожидаются как минимум:
 *   - actualId  — FinalImageId актуального стимула;
 *   - branchId  — текущий терминальный узел дерева восприятия;
 *   - well      — базовое состояние (1/2/3);
 *   - emotionId — текущая эмоция;
 *   - activityID — FinalImageId для условий (как в дереве);
 *   - sourceCycle — объект цикла осмысления (для ветки ответа на автоматизм);
 *   - _operatorAnswerCycleOnly — если true: только блок ответа оператора и выход (вызов из runOneStep).
 *   а также любые поля исходного контекста ОР.
 */
(function (global) {
  'use strict';

  var puls = function () { return typeof global.cpuls === 'number' ? global.cpuls : 0; };

  /** Лог цикла ФО: главный цикл или force (например, перед expired). */
  function appendCycleLog(cycle, text, force) {
    if (!cycle || typeof global.Consciousness_cycleAppendLog !== 'function') return;
    if (!force && !cycle.isMainCycle) return;
    global.Consciousness_cycleAppendLog(cycle, text, puls(), force ? { force: true } : undefined);
  }

  /**
   * Однократная обработка ответа оператора на автоматизм (уровень 2, «смысловая» реакция на фидбек).
   *
   * Смысл: по закрытому кадру ЭП уже известен effect; в инфокартине выставлены детекторы ситуации.
   * Сначала при числовом effect: AbstractImages_applyEpisodeEffectToSemanticContextAbstractions — усреднение meanImportance
   * клонов восприятия/действия при совпадающем кадре semanticStructure (effect>0 усиливает, effect<0 ослабляет).
   * При effect > 0 дополнительно: MentalAutomatizms_tryCreateFromSemanticStructureRule — ментальный автоматизм;
   * оба шага даже при goalImageId === 0 (ур.3).
   * Далее в образ цели цикла (cycle.goalImageId) записывается итог ожидания: Goals_applyWaitingPeriodEffectToGoalUsefulness
   * (при count цели === 1 usefulness = effect, иначе усреднение). Затем — корректировки через Goals_mergeUsefulnessSampleForGoalId.
   *
   * Защита от повторного входа: operatorAnswerPipelineConsumed — иначе при нескольких пульсах
   * или двойном вызове usefulness удвоился бы.
   *
   * Детекторы (компоненты ситуации, situatioms_tree.js):
   *   negativeMotorEffect (6), positiveActionEffect (26), lowAutomatismUsefulness (32).
   */
  function conscience_level_2_operatorAnswerPipeline(cycle) {
    if (!cycle) return;
    if (cycle.operatorAnswerPipelineConsumed) return;
    cycle.operatorAnswerPipelineConsumed = true;
    var goalId = typeof cycle.goalImageId === 'number' ? cycle.goalImageId : 0;
    var info = (typeof global.Info_initIfNeeded === 'function')
      ? global.Info_initIfNeeded()
      : global.InfoPicture;
    if (!info) return;

    var episodeEffect = cycle.lastAutomatismEpisodeEffect;
    if ((episodeEffect === null || episodeEffect === undefined) &&
        typeof global.EpisodicMemory_lastClosedFrameEffect === 'number') {
      episodeEffect = global.EpisodicMemory_lastClosedFrameEffect;
    }

    var effIsNum = (typeof episodeEffect === 'number' && !isNaN(episodeEffect));
    if (effIsNum) {
      var tcx = cycle.context || {};
      var mb = parseInt(tcx.branchId, 10) || 0;
      var mst = parseInt(tcx.actual_stimul_ID, 10) || 0;
      if (!mst && info.stimulus && info.stimulus.finalImageId) {
        mst = info.stimulus.finalImageId;
      }
      var msit = typeof info.situationId === 'number' ? info.situationId : 0;
      if (mb && mst) {
        if (typeof global.AbstractImages_applyEpisodeEffectToSemanticContextAbstractions === 'function') {
          global.AbstractImages_applyEpisodeEffectToSemanticContextAbstractions({
            perceptionNodeId: mb,
            situationId: msit,
            actualStimulusImageId: mst,
            effect: episodeEffect
          });
        }
        if (episodeEffect > 0 && typeof global.MentalAutomatizms_tryCreateFromSemanticStructureRule === 'function') {
          if (global.MentalAutomatizms_tryCreateFromSemanticStructureRule({
            perceptionNodeId: mb,
            situationId: msit,
            actualStimulusImageId: mst,
            episodeEffect: episodeEffect
          })) {
            appendCycleLog(cycle, 'Ур.2: effect>0 — ментальный автоматизм по правилу semanticStructure (клон восприятия).', true);
          }
        }
      }
    }

    if (!goalId) {
      if (typeof global.Info_updateView === 'function') global.Info_updateView();
      return;
    }
    if (effIsNum && typeof global.Goals_applyWaitingPeriodEffectToGoalUsefulness === 'function') {
      global.Goals_applyWaitingPeriodEffectToGoalUsefulness(goalId, episodeEffect);
    }

    if (typeof global.Goals_mergeUsefulnessSampleForGoalId !== 'function') {
      if (typeof global.Info_updateView === 'function') global.Info_updateView();
      return;
    }

    // negativeMotorEffect — компонент 6: снижаем полезность цели.
    if (info.negativeMotorEffect) {
      global.Goals_mergeUsefulnessSampleForGoalId(goalId, -2);
    }
    // positiveActionEffect — компонент 26: усиливаем по величине детектора (ограничение сверху).
    if (info.positiveActionEffect) {
      var pe = typeof info.positiveActionEffect === 'number' ? info.positiveActionEffect : 0;
      if (pe > 0) {
        global.Goals_mergeUsefulnessSampleForGoalId(goalId, Math.min(pe, 3));
      }
    }
    var effZero = !effIsNum || episodeEffect === 0;
    // lowAutomatismUsefulness — компонент 32: при нулевом effect кадра.
    if (effZero && info.lowAutomatismUsefulness) {
      global.Goals_mergeUsefulnessSampleForGoalId(goalId, -1);
    }

    if (typeof global.Info_updateView === 'function') global.Info_updateView();
  }

  /** Закрытие фазы ожидания (снятие флагов, expired). Когда фаза начинается — см. conscious_attention_channel.js, runElementary, ветка automatismStarted. */
  function conscience_level_2_finalizeAutomatismAnswerCycle(cycle) {
    if (!cycle || cycle.automatismAnswerLevel2Done) return;
    if (!cycle.awaitingAutomatismEpisodeFeedback || !cycle.automatismOperatorAnswerReceived) return;
    cycle.automatismAnswerLevel2Done = true;
    cycle.awaitingAutomatismEpisodeFeedback = false;
    cycle.expired = true;
    cycle.isMainCycle = false;
    if (typeof global.Info_setField === 'function') {
      global.Info_setField('isOperatorAnswerReceived', 0);
    } else if (global.InfoPicture) {
      global.InfoPicture.isOperatorAnswerReceived = 0;
    }
    appendCycleLog(cycle, 'Ур.2: ответ на автоматизм обработан, период ожидания завершён — цикл expired.', true);
    if (typeof global.Info_updateView === 'function') global.Info_updateView();
  }

  /** Выживание: шок / резкое ухудшение / критический витал (глобальный isShockState учитывается). */
  function conscience_level_2_isSurvivalContext(info) {
    if (!info) return !!global.isShockState;
    return !!(
      global.isShockState ||
      info.shockState ||
      (info.suddenChange && info.suddenChange !== 0) ||
      (info.criticalVital && info.criticalVital !== 0)
    );
  }

  /**
   * Критичность виталов «раньше Стресса» по порядку VitalsArr (Id 1…6 важнее 7-Стресс):
   * либо active criticalVital < 7, либо BadVitalsValue > 70 для любого из Id 1…6.
   */
  /**
   * id образа цели по кадру ЭП для выживания: addOrUpdate при effect &gt; 0; если id=0, но ключ цели полный —
   * повтор с минимальным положительным effect (правило всё равно фиксируется как образ цели цикла).
   */
  function conscience_level_2_survivalLifeGoalIdFromFrame(frame) {
    if (!frame || typeof global.Goals_addOrUpdateLifeGoalFromFrame !== 'function') return 0;
    var gid = global.Goals_addOrUpdateLifeGoalFromFrame(frame);
    if (gid) return gid;
    var pn = frame.perceptionNodeId != null ? frame.perceptionNodeId : 0;
    var ai = frame.actionImageId != null ? frame.actionImageId : 0;
    if (!pn || !ai) return 0;
    var eff = typeof frame.effect === 'number' ? frame.effect : 0;
    if (eff > 0) return 0;
    var f2 = {
      perceptionNodeId: pn,
      stimulusImageId: frame.stimulusImageId != null ? frame.stimulusImageId : 0,
      actionImageId: ai,
      situationId: frame.situationId != null ? frame.situationId : 0,
      effect: 1,
      usefulness: frame.usefulness
    };
    return global.Goals_addOrUpdateLifeGoalFromFrame(f2) || 0;
  }

  /**
   * Цель «что для меня означает это?»: прогноз последствий по кадрам ЭП, усиление гештальта и абстракций с правилом по ОД.
   */
  function conscience_level_2_tryMeaningGoalForecast(context, info, cycle) {
    var meaningId = typeof global.Goals_getDefaultMeaningGoalId === 'function'
      ? global.Goals_getDefaultMeaningGoalId() : 0;
    if (!meaningId) return;
    var branchId = context.branchId != null ? context.branchId : 0;
    var situationId = info && typeof info.situationId === 'number' ? info.situationId : 0;
    var stimId = context.actualId != null ? context.actualId : 0;
    if (!stimId && info && info.stimulus && info.stimulus.finalImageId) {
      stimId = info.stimulus.finalImageId;
    }
    stimId = parseInt(stimId, 10) || 0;

    var frames = [];
    if (typeof global.EpisodicMemory_getFramesByStimulus === 'function' && stimId) {
      frames = global.EpisodicMemory_getFramesByStimulus(stimId);
    }
    if ((!frames || !frames.length) && branchId && typeof global.EpisodicMemory_getFramesByPerceptionAndSituation === 'function') {
      frames = global.EpisodicMemory_getFramesByPerceptionAndSituation(branchId, situationId);
    }
    if (!frames || !frames.length) {
      if (cycle) appendCycleLog(cycle, 'Ур.2 (цель «значение»): прогноз по ЭП — нет кадров последствий.', false);
      return;
    }

    var candidates = [];
    for (var i = 0; i < frames.length; i++) {
      var f = frames[i];
      if (!f) continue;
      var aid = f.actionImageId != null ? parseInt(f.actionImageId, 10) : 0;
      if (!aid) continue;
      candidates.push(f);
    }
    candidates.sort(function (a, b) {
      return (typeof b.effect === 'number' ? b.effect : 0) - (typeof a.effect === 'number' ? a.effect : 0);
    });
    var taken = candidates.slice(0, 5);
    if (cycle) {
      appendCycleLog(cycle, 'Ур.2 (цель «значение»): прогноз по кадрам ЭП — ' + String(taken.length) + ' правил(а) с ОД.', false);
    }

    for (var j = 0; j < taken.length; j++) {
      var fr = taken[j];
      var actionId = fr.actionImageId != null ? parseInt(fr.actionImageId, 10) : 0;
      if (!actionId) continue;
      var eff = typeof fr.effect === 'number' ? fr.effect : 0;
      if (typeof global.Gestalt_bumpFromMeaningForecast === 'function') {
        global.Gestalt_bumpFromMeaningForecast(meaningId, actionId, fr, cycle);
      }
      if (typeof global.AbstractImages_boostSemanticRulesForAction === 'function') {
        var delta = eff > 0 ? Math.min(3, Math.max(0.5, eff / 3)) : 0.5;
        global.AbstractImages_boostSemanticRulesForAction(actionId, delta);
      }
    }
  }

  function conscience_level_2_hasCriticalVitalBeforeStress(info) {
    var cv = info && info.criticalVital ? parseInt(info.criticalVital, 10) : 0;
    if (cv > 0 && cv < 7) return true;
    var bad = global.BadVitalsValue;
    var vitals = global.VitalsArr;
    if (!bad || !Array.isArray(vitals)) return false;
    var TH = 70;
    for (var i = 0; i < vitals.length; i++) {
      if (vitals[i].Id >= 7) break;
      var k = bad[vitals[i].Id] || bad[String(vitals[i].Id)] || 0;
      if (k > TH) return true;
    }
    return false;
  }

  /**
   * Запуск автоматизма из ур.2 (выживание). Включает ту же фазу ожидания кадра ЭП, что и при запуске
   * автоматизма с ур.1 (info_automatismWithEpisodeFeedbackAndExpireCycle в infofunctions.js).
   * Завершение фазы после ответа оператора — conscience_level_2_finalizeAutomatismAnswerCycle.
   */
  function conscience_level_2_startSurvivalAutomatism(cycle, branchId, actionImageId, goalId) {
    if (!cycle || !branchId || !actionImageId) return false;
    var automatizmId = 0;
    if (typeof global.Automatizms_createAutomatizm === 'function') {
      automatizmId = global.Automatizms_createAutomatizm(branchId, actionImageId);
      if (automatizmId && typeof global.Automatizms_setDefaultAutomatizm === 'function') {
        global.Automatizms_setDefaultAutomatizm(automatizmId);
      }
    }
    if (typeof global.info_automatismWithEpisodeFeedbackAndExpireCycle === 'function') {
      return global.info_automatismWithEpisodeFeedbackAndExpireCycle(
        cycle,
        branchId,
        actionImageId,
        automatizmId,
        goalId || 0,
        'Ур.2 (выживание): запущен автоматизм по цели/правилу, ожидание ЭП.'
      );
    }
    return false;
  }

  /**
   * Режим выживания: определение цели/правила и действие по условному алгоритму.
   * Цепочка по Target_rule_stack — как партия в шахматах: ход → ответ → следующий ход опирается на цепочку
   * уже произошедших в ЭП правил (необязательно соседние кадры; порядок фиксируется в ленте Episodes).
   */
  function conscience_level_2_trySurvivalGoalAndRules(context, info, cycle) {
    var branchId = context.branchId != null ? context.branchId : 0;
    var situationId = info && typeof info.situationId === 'number' ? info.situationId : 0;
    var stimulusImageId = context.actualId != null ? context.actualId : 0;
    if (!stimulusImageId && info && info.stimulus && info.stimulus.finalImageId) {
      stimulusImageId = info.stimulus.finalImageId;
    }
    if (!branchId || !cycle) return;

    appendCycleLog(cycle, 'Ур.2 выживание: анализ цели и правил ЭП (ветка perception/stimulus/situation).');

    var lifeGoal = typeof global.Goals_findBestLifeGoalPositiveUsefulness === 'function'
      ? global.Goals_findBestLifeGoalPositiveUsefulness(branchId, situationId) : null;
    if (lifeGoal && lifeGoal.actionImageId) {
      appendCycleLog(cycle, 'Ур.2 выживание: образ цели с положительной usefulness, goalId=' + String(lifeGoal.id) + ', действие ОД=' + String(lifeGoal.actionImageId) + '.');
      if (lifeGoal.id && typeof global.TargetRuleStack_push === 'function') global.TargetRuleStack_push(lifeGoal.id);
      if (typeof global.Info_setField === 'function') global.Info_setField('usedRule', lifeGoal.id || 0);
      if (typeof global.Goals_setCurrentGoalInInfoPicture === 'function') {
        global.Goals_setCurrentGoalInInfoPicture('life', lifeGoal.id, 0);
      }
      conscience_level_2_startSurvivalAutomatism(cycle, branchId, lifeGoal.actionImageId, lifeGoal.id);
      return;
    }

    var usedRuleVal = info && typeof info.usedRule === 'number' ? info.usedRule : (parseInt(info.usedRule, 10) || 0);
    if (usedRuleVal && Array.isArray(global.Target_rule_stack) && global.Target_rule_stack.length &&
        typeof global.EpisodicMemory_findChainForGoalStack === 'function') {
      var chainGoals = global.Target_rule_stack.slice();
      var chainFrames = global.EpisodicMemory_findChainForGoalStack(chainGoals);
      if (chainFrames && chainFrames.length) {
        appendCycleLog(cycle, 'Ур.2 выживание: найдена цепочка кадров ЭП по Target_rule_stack (' + String(chainFrames.length) + ' звеньев).');
        var lastFr = chainFrames[chainFrames.length - 1];
        var gidChain = conscience_level_2_survivalLifeGoalIdFromFrame(lastFr);
        if (gidChain && lastFr.actionImageId) {
          if (typeof global.TargetRuleStack_push === 'function') global.TargetRuleStack_push(gidChain);
          if (typeof global.Info_setField === 'function') global.Info_setField('usedRule', gidChain);
          if (typeof global.Goals_setCurrentGoalInInfoPicture === 'function') {
            global.Goals_setCurrentGoalInInfoPicture('life', gidChain, 0);
          }
          conscience_level_2_startSurvivalAutomatism(cycle, branchId, lastFr.actionImageId, gidChain);
          return;
        }
      }
      appendCycleLog(cycle, 'Ур.2 выживание: цепочка по usedRule/Target_rule_stack не собрана — переход к поиску одиночного правила.');
    }

    if (typeof global.TargetRuleStack_clear === 'function') global.TargetRuleStack_clear();
    if (typeof global.Info_setField === 'function') global.Info_setField('usedRule', 0);
    appendCycleLog(cycle, 'Ур.2 выживание: сброшены Target_rule_stack и usedRule; выборка ExactRule/InaccurateRule из ЭП.');

    var ruleBundle = typeof global.info_bestEpisodicRule === 'function'
      ? global.info_bestEpisodicRule(branchId, situationId, {
        filterByStimulus: false,
        stimulusImageId: stimulusImageId,
        allowStimulusFallback: true
      })
      : null;
    var bestFrame = ruleBundle && ruleBundle.frame ? ruleBundle.frame : null;
    var exactRule = !!(ruleBundle && ruleBundle.exactRule);

    var inaccurate = !!bestFrame && !exactRule;

    function applyFromFrame(frame) {
      var gid = conscience_level_2_survivalLifeGoalIdFromFrame(frame);
      if (!gid || !frame.actionImageId) return false;
      if (typeof global.TargetRuleStack_push === 'function') global.TargetRuleStack_push(gid);
      if (typeof global.Info_setField === 'function') global.Info_setField('usedRule', gid);
      if (typeof global.Goals_setCurrentGoalInInfoPicture === 'function') {
        global.Goals_setCurrentGoalInInfoPicture('life', gid, 0);
      }
      conscience_level_2_startSurvivalAutomatism(cycle, branchId, frame.actionImageId, gid);
      return true;
    }

    if (exactRule && bestFrame) {
      appendCycleLog(cycle, 'Ур.2 выживание: ExactRule — лучший кадр ЭП (ветка+ситуация), effect=' + String(bestFrame.effect) + '.');
      applyFromFrame(bestFrame);
      return;
    }
    if (inaccurate && conscience_level_2_hasCriticalVitalBeforeStress(info) && bestFrame) {
      appendCycleLog(cycle, 'Ур.2 выживание: InaccurateRule при крит. виталах до Стресса — запуск действия по кадру ЭП, effect=' + String(bestFrame.effect) + '.');
      applyFromFrame(bestFrame);
      return;
    }
    if (inaccurate && bestFrame && !conscience_level_2_hasCriticalVitalBeforeStress(info)) {
      appendCycleLog(cycle, 'Ур.2 выживание: InaccurateRule без срочных виталов — без опрометчивого действия, запланирован ур.3.');
      cycle.survivalDeferredLevel3Planning = true;
      return;
    }

    if (!bestFrame) {
      var cryOd = 0;
      var cryGoalId = 0;
      var critV = info && info.criticalVital ? parseInt(info.criticalVital, 10) : 0;
      if (critV && critV !== 0) {
        if (typeof global.Goals_ensureDefaultCryGoal === 'function') {
          global.Goals_ensureDefaultCryGoal();
        }
        if (typeof global.Goals_getDefaultCryGoalId === 'function') {
          cryGoalId = global.Goals_getDefaultCryGoalId();
        }
        if (cryGoalId && typeof global.Goals_getGoalById === 'function') {
          var cryG = global.Goals_getGoalById(cryGoalId);
          if (cryG && cryG.actionImageId) {
            cryOd = cryG.actionImageId;
          }
        }
      }
      if (!cryOd && typeof global.ActionImages_getOrCreateActionImageId === 'function') {
        cryOd = global.ActionImages_getOrCreateActionImageId(stimulusImageId || 0, [1], '');
      }
      if (cryOd) {
        if (typeof global.Info_setField === 'function') global.Info_setField('usedRule', 0);
        if (cryGoalId && typeof global.Goals_setCurrentGoalInInfoPicture === 'function') {
          global.Goals_setCurrentGoalInInfoPicture('life', cryGoalId, 0);
        }
        appendCycleLog(
          cycle,
          cryGoalId
            ? 'Ур.2 выживание: критический витал, других целей нет — умолчательная цель «Плачь», автоматизм по ОД.'
            : 'Ур.2 выживание: позитивных правил в ЭП нет — автоматизм «Плачет» (ОД), затем ур.3.',
          !!cryGoalId
        );
        conscience_level_2_startSurvivalAutomatism(cycle, branchId, cryOd, cryGoalId || 0);
      } else {
        appendCycleLog(cycle, 'Ур.2 выживание: нет правил в ЭП и не удалось создать ОД «Плачет» — только ур.3.', true);
      }
      cycle.survivalCryLevel3Pending = true;
    }
  }

  function conscience_level_2(context) {
    if (!context) return;

    var info = (typeof global.Info_initIfNeeded === 'function')
      ? global.Info_initIfNeeded()
      : global.InfoPicture;

    // ========== Блок isOperatorAnswerReceived (обязательно в начале ур.2) ==========
    // Условие в инфокартине: оператор дал ответный стимул в периоде ожидания ЭП после реакции
    // (значение приходит из эпизодики/ОР; см. также InfoDet_updateOperatorAnswer при снятии).
    //
    // Почему блок здесь, а не после целей/ImportantProblem: ответ оператора — отдельное событие,
    // его нужно учесть до дальнейшей логики второго уровня в том же вызове, если контекст полный;
    // при вызове только из runOneStep флаг _operatorAnswerCycleOnly ограничивает работу этим блоком.
    //
    // Привязка к циклу:
    //   • Надёжный путь — context.sourceCycle (так передаёт runOneStep для того же цикла, который ждал).
    //   • Запасной — EpisodicMemory_waitingAutomatismCycleId + Consciousness_getCycle: имеет смысл,
    //     если id ещё не обнулён в closeLastFrame; после закрытия кадра с ответом id часто сбрасывается,
    //     поэтому основной контракт — всегда передавать sourceCycle при известном цикле.
    //
    // Доп.ограничения на цикл: и awaitingAutomatismEpisodeFeedback (мы в ветке «ждём кадр»),
    // и automatismOperatorAnswerReceived (эпизодика отметила ответ на этом цикле). Иначе поднятый
    // isOperatorAnswerReceived мог бы относиться к другой сессии — не трогаем чужой цикл.
    if (info && info.isOperatorAnswerReceived) {
      var opCycle = context.sourceCycle;
      if (!opCycle && typeof global.EpisodicMemory_waitingAutomatismCycleId === 'number' && global.EpisodicMemory_waitingAutomatismCycleId &&
          typeof global.Consciousness_getCycle === 'function') {
        opCycle = global.Consciousness_getCycle(global.EpisodicMemory_waitingAutomatismCycleId);
      }
      if (opCycle && opCycle.awaitingAutomatismEpisodeFeedback && opCycle.automatismOperatorAnswerReceived) {
        // Сначала смысловые поправки цели (один раз), затем архивирование состояния цикла ожидания.
        if (!opCycle.operatorAnswerPipelineConsumed) {
          conscience_level_2_operatorAnswerPipeline(opCycle);
        }
        conscience_level_2_finalizeAutomatismAnswerCycle(opCycle);
      }
      // Режим диспетчера: один тик — только ответ на автоматизм, без дальнейшего тела conscience_level_2.
      if (context._operatorAnswerCycleOnly) return;
    }

    if (info) {
      info.ImportantProblem = (info.criticalVital && info.criticalVital !== 0) ? 1 : 0;
    }

    // --- Определение цели и действия (после обработки ответа оператора): см. ветку выживания / режимы ---
    var cycle = context.sourceCycle;
    var survival = conscience_level_2_isSurvivalContext(info);
    if (survival && cycle) {
      appendCycleLog(cycle, 'Ур.2: активен контекст выживания (крит. витал / шок / резкое ухудшение / isShockState).');
      conscience_level_2_trySurvivalGoalAndRules(context, info, cycle);
    } else if (info) {
      // Не выживание: неудовлетворённость — флаг isDissatisfaction / блок ур.3 в runOneStep;
      // игровой и обучающий режимы — явные флаги для ур.3 (синхрон с playMode / learningMode).
      info.isPlayMode = info.playMode ? 1 : 0;
      info.isLearningMode = info.learningMode ? 1 : 0;
      if (cycle) {
        appendCycleLog(cycle, 'Ур.2: не выживание — Goals_determineGoalInSituation, isPlayMode=' + String(info.isPlayMode) + ', isLearningMode=' + String(info.isLearningMode) + '.');
      }
      if (typeof global.Goals_determineGoalInSituation === 'function') {
        global.Goals_determineGoalInSituation(context);
      }
      if (cycle && info && (
        info.currentGoalType === 'meaning' ||
        (typeof global.Goals_isDefaultMeaningGoalId === 'function' && global.Goals_isDefaultMeaningGoalId(info.currentGoalId))
      )) {
        conscience_level_2_tryMeaningGoalForecast(context, info, cycle);
      }
      if (typeof global.Info_updateView === 'function') global.Info_updateView();
    }
  }

  global.conscience_level_2 = conscience_level_2;
  global.conscience_level_2_operatorAnswerPipeline = conscience_level_2_operatorAnswerPipeline;
})(window);

════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/conscience_level_3.js
════════════════════════════════════════════════════════════
/**
 * Третий уровень осознания (conscience_level_3).
 *
 * Алгоритм поиска действия по семантике и ЭП:
 *  — выборка getSemanticFramesForFinalImageId(актуальный стимул);
 *  — среди кадров семантики с приоритетом текущих (well, emotion) выбирается наиболее позитивный (max meanImportance);
 *  — выборка кадров ЭП: сначала perceptionNodeId + situationId, при отсутствии правила — только perceptionNodeId (situationId = 0);
 *  — из выборки — лучшее закрытое правило с effect > 0 и тем же stimulusImageId, что актуальный стимул;
 *  — при нахождении: info_automatismWithEpisodeFeedbackAndExpireCycle (ожидание кадра ЭП, цикл expired). Ответ —
 *    conscience_level_2 из episodic_memory.closeLastFrame; finalize — conscience_level_2_finalizeAutomatismAnswerCycle.
 *  — если такого правила нет: учительский fallback — info_teacherEpisodicRuleForAction по штатному ОД ветки;
 *    зеркала «Хочет играть»/«Хочет учить» (BasicMirrorActions 88, 89); при нахождении учительского кадра —
 *    AbstractImages_activateTeacherRuleOnAbstractions (клоны под актуальный стимул и ОД, правило в semanticStructure),
 *    затем info_automatismWithEpisodeFeedbackAndExpireCycle. У учительского кадра при поиске effect не ранжирует;
 *    мотор не запускается, если у кадра числовой effect ≤ 0 (согласование с осью «значимость»).
 *
 * Старт ур.3: при готовом ментальном автоматизме по умолчанию (ветка+ситуация), совпадающем с текущим кадром
 * semanticStructure (тот же ОД, что sourceActionImageId ментального образа) — сначала ментальный прогон
 * (MentalAutomatizms_run), затем запуск моторного автоматизма; дальнейший ур.3 не выполняется.
 * Иначе: при точном совпадении кадра в semanticStructure — автоматизм с ожиданием ЭП; далее семантика+ЭП,
 * затем учительский fallback.
 *
 * Вызов из канала ФО: conscience_level_3_runFromChannel(cycle).
 *
 * Устойчивость и согласованность значимости (реализовано в коде):
 *  — Пока цикл ждёт закрытия кадра ЭП после запуска автоматизма (`awaitingAutomatismEpisodeFeedback` без ответа
 *    оператора), повторный полный проход ур.3 не выполняется: иначе во втором/третьем вызове за тик канала
 *    срабатывала бы ветка «правило не найдено». Сообщения в лог цикла — через Consciousness_scheduler
 *    Consciousness_cycleAppendLog (часто с { force: true }: цикл после запуска автоматизма уже `expired`, иначе
 *    consciousness_dispatcher отбрасывает не-force записи).
 *  — Мотор по кадру semanticStructure (shortcut и ментальный путь): только «положительная значимость» кадра —
 *    учитель (`ruleKind === 'teacher'` или ненулевой ответ оператора на ОД), либо episodic с числовым effect > 0.
 *  — Путь семантика+ЭП: при meanImportance лучшего семантического кадра стимула < 0 мотор по найденному правилу
 *    ЭП не запускается (семантика стимула как демотиватор).
 *
 * Порядок попыток в conscience_level_3: ментальный автоматизм → shortcut semanticStructure → эпизодика → учитель.
 *
 * @param {Object} context
 * @param {Object} context.sourceCycle
 * @param {string} [context.reason]
 */
(function (global) {
  'use strict';

  /**
   * Узел «уже запущен автоматизм, ждём ЭП». Повторный вход в ур.3 в том же или следующем шаге не должен
   * снова искать правило и доходить до «правило не найдено» — отсюда ранний выход в conscience_level_3 и в
   * conscience_level_3_runFromChannel.runLevel3.
   */
  function conscience_level_3_shouldSkipBecauseAwaitingEpisodeFeedback(cycle) {
    return !!(cycle && cycle.awaitingAutomatismEpisodeFeedback && !cycle.automatismOperatorAnswerReceived);
  }

  /**
   * Единая проверка значимости кадра semanticStructure перед мотором (shortcut, ментальный путь).
   * Согласовано с abstract_images.js: teacher / ответ оператора на ОД трактуются как доверенный положительный
   * сигнал; episodic-слот без положительного числового effect не даёт права на мотор (нуль/отрицание/нет числа).
   * @param {Object|null} fr — кадр из AbstractImages_findRuleFrameMatchingContext
   */
  function conscience_level_3_semanticStructureFrameAllowsMotor(fr) {
    if (!fr) return false;
    if (fr.ruleKind === 'teacher') return true;
    var resp = fr.responseStimulusImageId != null ? parseInt(fr.responseStimulusImageId, 10) : 0;
    if (resp > 0) return true;
    var eff = fr.effect;
    if (typeof eff === 'number' && !isNaN(eff)) return eff > 0;
    return false;
  }

  function puls() {
    return typeof global.cpuls === 'number' ? global.cpuls : 0;
  }

  /**
   * Запись в debugLog цикла через consciousness_dispatcher.Consciousness_cycleAppendLog(cycle, text, puls, opts).
   * Без force: только главный цикл (как раньше в ФО). С force: запись и для фонового, и после expired цикла
   * (см. cycleAppendLog в dispatcher: expired && !opts.force → не пишем).
   */
  function appendCycleLog(cycle, text, force) {
    if (!cycle || typeof global.Consciousness_cycleAppendLog !== 'function') return;
    if (!force && !cycle.isMainCycle) return;
    global.Consciousness_cycleAppendLog(cycle, text, puls(), force ? { force: true } : undefined);
  }

  function reasonLabel(reason) {
    switch (reason) {
      case 'level2_survival_inaccurate_no_rush':
        return 'InaccurateRule без срочных виталов — отложенное планирование';
      case 'level2_survival_cry':
        return 'ветка «Плачет» / нет правил в ЭП';
      case 'dissatisfaction':
        return 'isDissatisfaction';
      default:
        return reason || '(без reason)';
    }
  }

  /**
   * Среди кадров SemanticObjectImportance для стимула: при наличии записей в текущем (well, emotion) — только они;
   * иначе весь массив. Возвращает объект с максимальным meanImportance.
   */
  function conscience_level_3_pickBestSemanticFrame(semFrames, well, emotionId) {
    if (!Array.isArray(semFrames) || !semFrames.length) return null;
    var narrowed = [];
    if (well && emotionId) {
      for (var i = 0; i < semFrames.length; i++) {
        var s = semFrames[i];
        if (s && s.wellNormaBad === well && s.emotionId === emotionId) narrowed.push(s);
      }
    }
    var pool = narrowed.length ? narrowed : semFrames;
    var best = null;
    var bestM = null;
    for (var j = 0; j < pool.length; j++) {
      var p = pool[j];
      if (!p) continue;
      var m = typeof p.meanImportance === 'number' && !isNaN(p.meanImportance) ? p.meanImportance : 0;
      if (best === null || m > bestM) {
        best = p;
        bestM = m;
      }
    }
    return best;
  }

  /**
   * Единая точка моторного запуска ур.3: создание/дефолт автоматизма + info_automatismWithEpisodeFeedbackAndExpireCycle.
   * До вызова сюда должны пройти все гейты значимости по выбранному пути (semanticStructure / ЭП / учитель).
   */
  function conscience_level_3_startAutomatismFromEpisodicRule(cycle, branchId, actionImageId) {
    if (!cycle || !branchId || !actionImageId) return false;
    var automatizmId = 0;
    if (typeof global.Automatizms_createAutomatizm === 'function') {
      automatizmId = global.Automatizms_createAutomatizm(branchId, actionImageId);
      if (automatizmId && typeof global.Automatizms_setDefaultAutomatizm === 'function') {
        global.Automatizms_setDefaultAutomatizm(automatizmId);
      }
    }
    if (typeof global.info_automatismWithEpisodeFeedbackAndExpireCycle === 'function') {
      return global.info_automatismWithEpisodeFeedbackAndExpireCycle(
        cycle,
        branchId,
        actionImageId,
        automatizmId,
        0,
        'Ур.3: автоматизм по правилу ЭП запущен, ожидание кадра ЭП.'
      );
    }
    if (typeof global.Consciousness_cycleAppendLog === 'function') {
      global.Consciousness_cycleAppendLog(cycle, 'Ур.3: info_automatismWithEpisodeFeedbackAndExpireCycle недоступна — мотор не запущен.', puls(), { force: true });
    }
    return false;
  }

  /**
   * @returns {boolean} true если автоматизм запущен
   */
  function conscience_level_3_trySemanticEpisodicAutomatism(cycle) {
    if (!cycle || cycle.status !== 'running') return false;
    if (cycle.awaitingAutomatismEpisodeFeedback && !cycle.automatismOperatorAnswerReceived) return false;

    var cx = cycle.context || {};
    var branchId = parseInt(cx.branchId, 10) || 0;
    var actualStim = parseInt(cx.actual_stimul_ID, 10) || 0;
    var info = typeof global.Info_initIfNeeded === 'function' ? global.Info_initIfNeeded() : global.InfoPicture;
    if (!actualStim && info && info.stimulus && info.stimulus.finalImageId) {
      actualStim = info.stimulus.finalImageId;
    }
    var situationId = info && typeof info.situationId === 'number' ? info.situationId : 0;
    var well = global.BadNormWell || 0;
    var emotionId = 0;
    if (typeof global.Emotions_getOrCreateEmotionId === 'function' && Array.isArray(global.BasicContextsActived)) {
      emotionId = global.Emotions_getOrCreateEmotionId(global.BasicContextsActived) || 0;
    }

    if (!branchId || !actualStim) {
      appendCycleLog(cycle, 'Ур.3: нет branchId или актуального стимула — пропуск поиска правила.');
      return false;
    }

    var semFrames = typeof global.getSemanticFramesForFinalImageId === 'function'
      ? global.getSemanticFramesForFinalImageId(actualStim) : [];
    if (!semFrames.length) {
      appendCycleLog(cycle, 'Ур.3: нет кадров семантики для FinalImageId=' + String(actualStim) + '.');
      return false;
    }

    var semBest = conscience_level_3_pickBestSemanticFrame(semFrames, well, emotionId);
    if (!semBest) {
      appendCycleLog(cycle, 'Ур.3: не удалось выбрать кадр семантики.');
      return false;
    }

    appendCycleLog(cycle, 'Ур.3: кадр семантики с max meanImportance=' + String(semBest.meanImportance) +
      ' (well=' + String(semBest.wellNormaBad) + ', emotionId=' + String(semBest.emotionId) + ').');

    if (typeof global.info_bestEpisodicRule !== 'function') {
      appendCycleLog(cycle, 'Ур.3: info_bestEpisodicRule недоступна.');
      return false;
    }

    var ruleBundle = global.info_bestEpisodicRule(branchId, situationId, {
      filterByStimulus: true,
      stimulusImageId: actualStim
    });
    var bestFrame = ruleBundle && ruleBundle.frame ? ruleBundle.frame : null;
    if (bestFrame) {
      appendCycleLog(cycle, 'Ур.3: правило ЭП для стимула ' + String(actualStim) + ', effect=' + String(bestFrame.effect) + ', ОД=' + String(bestFrame.actionImageId) + '.');
    }

    if (!bestFrame || !bestFrame.actionImageId) {
      appendCycleLog(cycle, 'Ур.3: нет закрытого правила с положительным effect для стимула ' + String(actualStim) + '.');
      return false;
    }

    // Значимость стимула в семантической памяти: отрицательная — демотиватор; не глушим эпизодический effect
    // положительным правилом, если контекст стимула уже «плохой» по усреднённой значимости.
    var semZ = typeof semBest.meanImportance === 'number' && !isNaN(semBest.meanImportance) ? semBest.meanImportance : null;
    if (semZ !== null && semZ < 0) {
      if (typeof global.Consciousness_cycleAppendLog === 'function') {
        global.Consciousness_cycleAppendLog(cycle, 'Ур.3: семантика стимула отрицательна (meanImportance=' + String(semZ) + ') — мотор по ЭП не запускаем.', puls(), { force: true });
      }
      return false;
    }

    return conscience_level_3_startAutomatismFromEpisodicRule(cycle, branchId, bestFrame.actionImageId);
  }

  /**
   * Учительское правило ЭП для штатного ОД ветки: при поиске effect не ранжирует; мотор блокируется, если в кадре
   * явно числовой effect ≤ 0. Зеркала 88, 89 — провокация режима обучения.
   * @returns {boolean}
   */
  function conscience_level_3_tryTeacherEpisodicAutomatism(cycle) {
    if (!cycle || cycle.status !== 'running') return false;
    if (cycle.awaitingAutomatismEpisodeFeedback && !cycle.automatismOperatorAnswerReceived) return false;

    var cx = cycle.context || {};
    var branchId = parseInt(cx.branchId, 10) || 0;
    if (!branchId) {
      appendCycleLog(cycle, 'Ур.3 (учитель): нет branchId.', true);
      return false;
    }

    var info = typeof global.Info_initIfNeeded === 'function' ? global.Info_initIfNeeded() : global.InfoPicture;
    var situationId = info && typeof info.situationId === 'number' ? info.situationId : 0;
    var actualStim = parseInt(cx.actual_stimul_ID, 10) || 0;
    if (!actualStim && info && info.stimulus && info.stimulus.finalImageId) {
      actualStim = info.stimulus.finalImageId;
    }

    var def = typeof global.Automatizms_getDefaultAutomatizmForBranch === 'function'
      ? global.Automatizms_getDefaultAutomatizmForBranch(branchId)
      : null;
    var odId = def && def.actionsImageId != null ? parseInt(def.actionsImageId, 10) : 0;
    if (!odId) {
      appendCycleLog(cycle, 'Ур.3 (учитель): у ветки нет штатного ОД — поиск учительского правила пропущен.', true);
      return false;
    }

    var bundle = typeof global.info_teacherEpisodicRuleForAction === 'function'
      ? global.info_teacherEpisodicRuleForAction(branchId, situationId, odId)
      : null;

    if (typeof global.activateStimulusForBasicMirrorActionId === 'function') {
      global.activateStimulusForBasicMirrorActionId(88);
      global.activateStimulusForBasicMirrorActionId(89);
    }

    if (!bundle || !bundle.frame || !bundle.frame.actionImageId) {
      appendCycleLog(cycle, 'Ур.3 (учитель): в ЭП нет кадра с ответом оператора на ОД ' + String(odId) + '.', true);
      return false;
    }

    var tEff = bundle.frame.effect;
    if (typeof tEff === 'number' && !isNaN(tEff) && tEff <= 0) {
      if (typeof global.Consciousness_cycleAppendLog === 'function') {
        global.Consciousness_cycleAppendLog(cycle, 'Ур.3 (учитель): кадр ЭП с effect ≤ 0 — запуск автоматизма пропущен.', puls(), { force: true });
      }
      return false;
    }

    if (actualStim && typeof global.AbstractImages_activateTeacherRuleOnAbstractions === 'function') {
      var ar = global.AbstractImages_activateTeacherRuleOnAbstractions({
        perceptionNodeId: branchId,
        situationId: situationId,
        actualStimulusImageId: actualStim,
        teacherEpisodicFrame: bundle.frame
      });
      if (ar && (ar.abstractPerceptionId || ar.abstractActionId)) {
        appendCycleLog(cycle, 'Ур.3 (учитель): абстракции #' + String(ar.abstractPerceptionId || 0) + ' (воспр.) / #' +
          String(ar.abstractActionId || 0) + ' (действ.) — слот учителя в semanticStructure.', true);
      }
    } else {
      appendCycleLog(cycle, 'Ур.3 (учитель): нет актуального FinalImageId — абстракции и semanticStructure не трогаем.', true);
    }

    appendCycleLog(cycle, 'Ур.3 (учитель): ' + (bundle.exactRule ? 'полное условие (ветка+ситуация)' : 'только ветка') +
      ', ОД=' + String(bundle.frame.actionImageId) + ', ответ оператора FinalImageId=' +
      String(bundle.frame.responseStimulusImageId) + ' — запуск автоматизма.', true);

    return conscience_level_3_startAutomatismFromEpisodicRule(cycle, branchId, bundle.frame.actionImageId);
  }

  /**
   * Готовый ментальный автоматизм: кадр semanticStructure совпадает с sourceActionImageId МАИ;
   * мотор — только при положительной значимости кадра (см. conscience_level_3_semanticStructureFrameAllowsMotor).
   * @returns {boolean}
   */
  function conscience_level_3_tryReadyMentalAutomatismThenMotor(cycle) {
    if (!cycle || cycle.status !== 'running') return false;
    if (cycle.awaitingAutomatismEpisodeFeedback && !cycle.automatismOperatorAnswerReceived) return false;

    var cx = cycle.context || {};
    var branchId = parseInt(cx.branchId, 10) || 0;
    var actualStim = parseInt(cx.actual_stimul_ID, 10) || 0;
    var info = typeof global.Info_initIfNeeded === 'function' ? global.Info_initIfNeeded() : global.InfoPicture;
    if (!actualStim && info && info.stimulus && info.stimulus.finalImageId) {
      actualStim = info.stimulus.finalImageId;
    }
    var situationId = info && typeof info.situationId === 'number' ? info.situationId : 0;

    if (!branchId || !actualStim) return false;
    if (typeof global.MentalAutomatizms_getDefaultForThemeSituation !== 'function') return false;
    if (typeof global.AbstractImages_findRuleFrameMatchingContext !== 'function') return false;

    var fr = global.AbstractImages_findRuleFrameMatchingContext({
      perceptionNodeId: branchId,
      situationId: situationId,
      actualStimulusImageId: actualStim
    });
    if (!fr || fr.actionImageId == null) return false;
    var frameOd = parseInt(fr.actionImageId, 10) || 0;
    if (!frameOd) return false;

    var ma = global.MentalAutomatizms_getDefaultForThemeSituation(branchId, situationId);
    if (!ma || !ma.mentalActionImageId) return false;

    if (typeof global.MentalActionImages_get !== 'function') return false;
    var mai = global.MentalActionImages_get(ma.mentalActionImageId);
    if (!mai) return false;
    var srcOd = parseInt(mai.sourceActionImageId, 10) || 0;
    if (!srcOd || srcOd !== frameOd) return false;

    if (!conscience_level_3_semanticStructureFrameAllowsMotor(fr)) {
      if (typeof global.Consciousness_cycleAppendLog === 'function') {
        global.Consciousness_cycleAppendLog(cycle, 'Ур.3: ментальный автоматизм — кадр semanticStructure без положительной значимости, мотор не запускаем.', puls(), { force: true });
      }
      return false;
    }

    if (typeof global.Consciousness_cycleAppendLog === 'function') {
      global.Consciousness_cycleAppendLog(cycle, 'Ур.3: готовый ментальный автоматизм #' + String(ma.id) + ' (МАИ #' +
        String(ma.mentalActionImageId) + ') → моторный ОД ' + String(srcOd) + '.', puls(), { force: true });
    }

    if (typeof global.MentalAutomatizms_run === 'function') {
      global.MentalAutomatizms_run(ma.id);
    }

    return conscience_level_3_startAutomatismFromEpisodicRule(cycle, branchId, srcOd);
  }

  /**
   * Правило в semanticStructure клона восприятия под ветку/ситуацию/стимул; мотор при положительной значимости кадра.
   * @returns {boolean}
   */
  function conscience_level_3_tryAbstractSemanticStructureShortcut(cycle) {
    if (!cycle || cycle.status !== 'running') return false;
    if (cycle.awaitingAutomatismEpisodeFeedback && !cycle.automatismOperatorAnswerReceived) return false;

    var cx = cycle.context || {};
    var branchId = parseInt(cx.branchId, 10) || 0;
    var actualStim = parseInt(cx.actual_stimul_ID, 10) || 0;
    var info = typeof global.Info_initIfNeeded === 'function' ? global.Info_initIfNeeded() : global.InfoPicture;
    if (!actualStim && info && info.stimulus && info.stimulus.finalImageId) {
      actualStim = info.stimulus.finalImageId;
    }
    var situationId = info && typeof info.situationId === 'number' ? info.situationId : 0;

    if (!branchId || !actualStim) return false;
    if (typeof global.AbstractImages_findRuleFrameMatchingContext !== 'function') return false;

    var fr = global.AbstractImages_findRuleFrameMatchingContext({
      perceptionNodeId: branchId,
      situationId: situationId,
      actualStimulusImageId: actualStim
    });
    if (!fr || fr.actionImageId == null) return false;

    var aid = parseInt(fr.actionImageId, 10) || 0;
    if (!aid) return false;

    if (!conscience_level_3_semanticStructureFrameAllowsMotor(fr)) {
      if (typeof global.Consciousness_cycleAppendLog === 'function') {
        global.Consciousness_cycleAppendLog(cycle, 'Ур.3: semanticStructure есть, но значимость кадра не положительная — shortcut к мотору пропущен.', puls(), { force: true });
      }
      return false;
    }

    if (typeof global.Consciousness_cycleAppendLog === 'function') {
      global.Consciousness_cycleAppendLog(cycle, 'Ур.3: semanticStructure восприятия — точное совпадение (ruleKind=' + String(fr.ruleKind || '') +
        '), ОД=' + String(aid) + '.', puls(), { force: true });
    }

    return conscience_level_3_startAutomatismFromEpisodicRule(cycle, branchId, aid);
  }

  function conscience_level_3(context) {
    var cycle = context && context.sourceCycle;
    var reason = (context && context.reason) || '';
    if (!cycle) return;

    if (conscience_level_3_shouldSkipBecauseAwaitingEpisodeFeedback(cycle)) {
      if (typeof global.Consciousness_cycleAppendLog === 'function') {
        global.Consciousness_cycleAppendLog(cycle, 'Ур.3: полный пайплайн не выполняется — ожидание кадра ЭП по ранее запущенному автоматизму.', puls(), { force: true });
      }
      return;
    }

    if (typeof global.Consciousness_cycleAppendLog === 'function' && cycle.isMainCycle) {
      var msg;
      switch (reason) {
        case 'level2_survival_inaccurate_no_rush':
          msg = 'Ур.3: вызов после ур.2 (InaccurateRule без срочных виталов).';
          break;
        case 'level2_survival_cry':
          msg = 'Ур.3: вызов после ур.2 (ветка «Плачет» / нет правил в ЭП).';
          break;
        case 'dissatisfaction':
          msg = 'Триггер isDissatisfaction: открывается 3-й уровень осознания.';
          break;
        default:
          msg = 'Ур.3: ' + reasonLabel(reason) + '.';
      }
      global.Consciousness_cycleAppendLog(cycle, msg, puls());
    }

    if (conscience_level_3_tryReadyMentalAutomatismThenMotor(cycle)) {
      return;
    }

    if (conscience_level_3_tryAbstractSemanticStructureShortcut(cycle)) {
      return;
    }

    if (conscience_level_3_trySemanticEpisodicAutomatism(cycle)) {
      return;
    }

    if (conscience_level_3_tryTeacherEpisodicAutomatism(cycle)) {
      return;
    }

    // Ни один путь не дал мотор; цикл остаётся без нового автоматизма — дальше заготовка планирования ур.3.
    if (typeof global.Consciousness_cycleAppendLog === 'function') {
      global.Consciousness_cycleAppendLog(cycle, 'Ур.3: правило не найдено — продолжение (заготовка).', puls(), { force: true });
    }
  }

  function conscience_level_3_runFromChannel(cycle) {
    if (!cycle || cycle.status !== 'running') return;

    /**
     * Гештальт (ур.4): до пайплайна ур.3 выставляется global.GestaltContextForLevel3 — образ цели и status
     * открытой доминанты (если есть). Не меняет ветвление ур.3.
     */
    if (typeof global.Gestalt_prepareContextForCycle === 'function') {
      try {
        global.Gestalt_prepareContextForCycle(cycle);
      } catch (eGestaltPrep) {}
    }

    /**
     * Один вызов conscience_level_3 за причину; если предыдущая причина в том же тике уже запустила автоматизм,
     * не входим в пайплайн повторно и фиксируем это в логе цикла (Consciousness_cycleAppendLog с force).
     */
    function runLevel3(reason) {
      if (!cycle || cycle.status !== 'running') return;
      if (conscience_level_3_shouldSkipBecauseAwaitingEpisodeFeedback(cycle)) {
        if (typeof global.Consciousness_cycleAppendLog === 'function') {
          global.Consciousness_cycleAppendLog(cycle, 'Ур.3: повторный вход пропущен (reason=' + String(reason) + ') — уже ожидание кадра ЭП по автоматизму.', puls(), { force: true });
        }
        return;
      }
      conscience_level_3({ sourceCycle: cycle, reason: reason });
    }

    if (cycle.survivalDeferredLevel3Planning) {
      runLevel3('level2_survival_inaccurate_no_rush');
      cycle.survivalDeferredLevel3Planning = false;
    }

    if (cycle.survivalCryLevel3Pending) {
      runLevel3('level2_survival_cry');
      cycle.survivalCryLevel3Pending = false;
    }

    if (global.InfoPicture && global.InfoPicture.isDissatisfaction) {
      runLevel3('dissatisfaction');
    }
  }

  global.conscience_level_3 = conscience_level_3;
  global.conscience_level_3_runFromChannel = conscience_level_3_runFromChannel;
})(window);

════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/conscience_level_4.js
════════════════════════════════════════════════════════════
/**
 * Четвёртый уровень осознания (conscience_level_4).
 *
 * Реализовано: гештальтный уровень — буфер резонанса, обновление гештальтов по ЭП, тик Gestalt_onConsciousnessTick
 * (см. beast/_9_Conscience/gestalt.js, about_gestalt.md). Дополнительные триггеры (metaReflection и т.д.) —
 * через conscience_level_4({ sourceCycle, reason }).
 *
 * Вызов из канала ФО: conscience_level_4_runFromChannel(cycle) — после ур.3 в том же тике runOneStep.
 *
 * @param {Object} context
 * @param {Object} context.sourceCycle
 * @param {string} [context.reason]
 */
(function (global) {
  'use strict';

  function puls() {
    return typeof global.cpuls === 'number' ? global.cpuls : 0;
  }

  function appendCycleLog(cycle, text, force) {
    if (!cycle || typeof global.Consciousness_cycleAppendLog !== 'function') return;
    if (!force && !cycle.isMainCycle) return;
    global.Consciousness_cycleAppendLog(cycle, text, puls(), force ? { force: true } : undefined);
  }

  function conscience_level_4(context) {
    var cycle = context && context.sourceCycle;
    var reason = (context && context.reason) || '';
    if (!cycle) return;

    appendCycleLog(
      cycle,
      'Ур.4: ' + (reason || 'ручной вызов') + ' — расширение (conscience_level_4.js).',
      false
    );

    // TODO: условия входа (флаги цикла / инфокартины), иерархия целей, долгие планы — вызывать отдельные подсистемы.
  }

  /**
   * Один тик канала ФО для ур.4 (вызывается из Consciousness_runOneStep *после* ур.3 в том же пульсе).
   *
   * Гештальты на этом уровне (реализация в beast/_9_Conscience/gestalt.js):
   *
   * 1) Кто крутится здесь
   *    Обработка гештальта и буфера резонанса выполняется только для *главного* running-цикла
   *    (Gestalt_onConsciousnessTick внутри сам отсекает фоновые циклы). Так на пульс не дублируется
   *    разбор резонанса по числу фоновых процессов.
   *
   * 2) Что уже сделано *до* ур.4 в этом тике
   *    В начале ур.3 вызван Gestalt_prepareContextForCycle: в global.GestaltContextForLevel3 лежит снимок
   *    открытой доминанты по образу цели (если цель есть). Ур.3 использует это только как контекст;
   *    поиск правил и мотор идут по своим правилам.
   *
   * 3) Что делает вызов Gestalt_onConsciousnessTick(cycle)
   *    • Привязка «ведущего» открытого гештальта к цели главного цикла (cycle.goalImageId или
   *      InfoPicture.currentGoalId в режиме target): обновляются Gestalt_activeGestaltId и связанные поля.
   *    • Если у этого гештальта статус ещё не «частичное решение» (ниже PARTIAL), просматривается
   *      GestaltResonanceBuffer — FIFO сигнатур успешных/закрытых кадров ЭП. Для каждой пары
   *      (сигнатура × открытый гештальт) считается эвристический score; при превышении порога —
   *      инсайт аналогии, дополнение списка автоматизмов и analogyPool, возможный переход status 0→1.
   *    • Повторно вызывается Gestalt_prepareContextForCycle: после возможных инсайтов по резонансу
   *      контекст для следующего пульса и для чтения из других модулей обновлён.
   *
   * 4) Что *не* делается в этом вызове
   *    Закрытие кадра ЭП и запись новой сигнатуры в буфер выполняются в episodic_memory.closeLastFrame →
   *    Gestalt_onEpisodeFeedbackClosed (не здесь). Ур.4 не запускает мотор и не подменяет ур.3.
   *
   * 5) Расширения
   *    Дополнительные ветки (мета-рефлексия, флаги цикла) можно добавлять ниже, рядом с условными
   *    вызовами conscience_level_4({ sourceCycle, reason }).
   */
  function conscience_level_4_runFromChannel(cycle) {
    if (!cycle || cycle.status !== 'running') return;
    if (typeof global.Gestalt_onConsciousnessTick === 'function') {
      try {
        global.Gestalt_onConsciousnessTick(cycle);
      } catch (e) {}
    }
    // Пример будущего условия:
    // if (cycle.metaReflectionPending) { conscience_level_4({ sourceCycle: cycle, reason: 'meta' }); }
  }

  global.conscience_level_4 = conscience_level_4;
  global.conscience_level_4_runFromChannel = conscience_level_4_runFromChannel;
})(window);

════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/conscious_attention_channel.js
════════════════════════════════════════════════════════════
/**
 * Канал осознанного внимания: осознание стимула после ориентировочного рефлекса.
 *
 * stimuls_consciousness(context) — вызывается из ОР. context может быть числом (actual_stimul_ID)
 * или объектом с полной детализацией: actual_stimul_ID, branchId, novelty, significance,
 * samplesCount, pathArr, well, emotionId, activityID, isResponseStimulus.
 * Новизна условий (ОР) передаётся для анализа и будущей оценки приемлемости запуска автоматизма.
 */
(function (global) {
  'use strict';

  // ========== Оптимизация «тишины мысли» (порог прерывания текущего мышления) ==========
  /**
   * Коэффициент порога внимания: эффективная значимость нового стимула должна быть
   * ≥ (значимость главного цикла × этот коэффициент). Больше — труднее прервать, «тише» мысль.
   * Пример: 1.5 = нужно на 50% выше значимости главного процесса.
   */
  var INTERRUPT_THRESHOLD_ATTENTION_RATIO = 1.5;

  /**
   * При несовпадении темы/ситуации инфокартины с темой/ситуацией главного цикла (при создании)
   * значимость конкурирующего стимула для сравнения умножается на этот множитель (< 1 — сложнее переключить).
   * К стимулам из связной цепочки кадров ЭП с реакциями множитель и понижение при ImportantProblem не применяются.
   */
  var INTERRUPT_THEME_MISMATCH_COMPETITOR_FACTOR = 0.5;

  // Стек фоновых контекстов: при прерывании более значимым стимулом контекст текущего главного цикла
  // сохраняется сюда; диспетчер при завершении главного поднимает следующий из стека (promoteFromBackgroundStack).
  /** Очередь прерываний: стек ID циклов, ставших фоновыми при прерывании (LIFO — последний прерванный возвращается первым). */
  if (!Array.isArray(global.ConsciousnessBackgroundStack)) {
    global.ConsciousnessBackgroundStack = [];
  }
  var MAX_INTERRUPTED_STACK = 10;

  /**
   * Осознание стимула (вызывается из ОР).
   * Осознание стимула (вызывается из ОР).
   *
   * Первый уровень осознания:
   * 1) Берётся семантическая значимость стимула в текущих условиях (well, emotionId, activityID).
   *    Если meanImportance < 0, выполняется поиск по ЭП в более общих условиях (родительский узел дерева)
   *    для того же стимула. Если найден кадр с положительным effect, из его actionImageId берётся «позитивное»
   *    действие, для текущей ветки создаётся/обновляется автоматизм и это действие выполняется.
   *    Функция завершается.
   * 2) Если шаг 1 не сработал, проверяется штатный автоматизм текущей ветки.
   *    Если usefulness не отрицательная (>= 0 или ещё не накоплена, count == 0), его действие выполняется.
   *    Если usefulness < 0, первый уровень осознания не принимает решение (заготовка для второго уровня).
   *
   * При запуске любого автоматизма вызывается removeDelayedReflexesForBranch(branchId) и выставляется флаг
   * automatismRanForBranchIds[branchId], чтобы по этой ветке не запускался генетический рефлекс
   * из processGeneticReflexes после истечения блокировки ОР.
   *
   * @param {number|Object} context — ID образа (FinalImageId) или объект контекста ОР с branchId, novelty,
   *    orientationReflexWinner (если true — конкуренция ОР уже отдала победу этому стимулу; порог прерывания ФО не применяется).
   */
  function stimuls_consciousness(context) {
    var actualId = 0;      // FinalImageId стимула (0 — начальный цикл без стимула)
    var branchId = 0;      // текущий терминальный узел дерева
    var well = 0;
    var emotionId = 0;
    var activityID = 0;    // FinalImageId для условий (как в дереве)
    var callImportance = 0; // значимость данного вызова ФО (для конкуренции потоков осмысления)

    if (typeof context === 'number') {
      if (context <= 0) return;
      actualId = context;
      branchId = (typeof global.PerceptionTree_getActiveTerminalNodeId === 'function')
        ? global.PerceptionTree_getActiveTerminalNodeId() : 0;
      well = global.BadNormWell || 0;
      if (typeof global.Emotions_getOrCreateEmotionId === 'function' && Array.isArray(global.BasicContextsActived)) {
        emotionId = global.Emotions_getOrCreateEmotionId(global.BasicContextsActived) || 0;
      }
      activityID = actualId;
    } else if (context && typeof context === 'object') {
      actualId = context.actual_stimul_ID;
      branchId = context.branchId != null
        ? context.branchId
        : ((typeof global.PerceptionTree_getActiveTerminalNodeId === 'function')
          ? global.PerceptionTree_getActiveTerminalNodeId() : 0);
      well = context.well != null ? context.well : (global.BadNormWell || 0);
      emotionId = context.emotionId != null ? context.emotionId : 0;
      activityID = context.activityID != null ? context.activityID : (actualId || 0);
      if (typeof context.significance === 'number') {
        callImportance = Math.abs(context.significance);
      }
    }
    var orWinner = !!(context && typeof context === 'object' && context.orientationReflexWinner === true);
    var vitalEmergency = !!(context && typeof context === 'object' && context.vitalEmergency === true);
    if (typeof branchId !== 'number' && (branchId == null || branchId === undefined)) return;

    // --- Конкуренция потоков осмысления: см. INTERRUPT_THRESHOLD_ATTENTION_RATIO, INTERRUPT_THEME_MISMATCH_COMPETITOR_FACTOR вверху файла ---
    var info = (typeof global.Info_initIfNeeded === 'function')
      ? global.Info_initIfNeeded()
      : (global.InfoPicture || null);
    var currentThinkingImportance = info && typeof info.thinkingImportance === 'number'
      ? info.thinkingImportance
      : 0;

    var mainForInterrupt = typeof global.Consciousness_getMainCycle === 'function'
      ? global.Consciousness_getMainCycle() : null;
    /** Тишина мыслей и понижение «слабых» стимулов относительно главного — только пока главный цикл ещё не expired. */
    var mainBlocksThoughtSilence = !!(mainForInterrupt && !mainForInterrupt.expired);
    var isMainExpired = !!(mainForInterrupt && mainForInterrupt.expired);
    var importantProblem = !!(info && info.ImportantProblem);
    var themeSituationMismatch = false;
    if (mainBlocksThoughtSilence && mainForInterrupt && info) {
      var mt = mainForInterrupt.createdThemeId != null ? mainForInterrupt.createdThemeId : 0;
      var ms = mainForInterrupt.createdSituationId != null ? mainForInterrupt.createdSituationId : 0;
      var it = info.themeId != null ? info.themeId : 0;
      var is = info.situationId != null ? info.situationId : 0;
      themeSituationMismatch = (mt !== it || ms !== is);
    }

    var episodicChainPriority = typeof global.EpisodicMemory_isStimulusEpisodicChainPriority === 'function' &&
      global.EpisodicMemory_isStimulusEpisodicChainPriority(actualId);

    var effectiveCallImportance = callImportance;
    if (mainBlocksThoughtSilence && !episodicChainPriority) {
      effectiveCallImportance *= (themeSituationMismatch ? INTERRUPT_THEME_MISMATCH_COMPETITOR_FACTOR : 1);
      if (importantProblem) {
        effectiveCallImportance *= 0.5;
      }
    }
    var ratio = (mainBlocksThoughtSilence && !episodicChainPriority)
      ? INTERRUPT_THRESHOLD_ATTENTION_RATIO
      : 1.0;
    var requiredMin = currentThinkingImportance * ratio;

    var skipImportanceCheck = !mainBlocksThoughtSilence || episodicChainPriority || orWinner || vitalEmergency;
    if (!skipImportanceCheck && currentThinkingImportance > 0 && effectiveCallImportance < requiredMin) {
      if (typeof global.show_alert === 'function') {
        var stimIdWeak = (context && typeof context.actual_stimul_ID === 'number') ? context.actual_stimul_ID : actualId;
        global.show_alert(
          'Стимул FinalImageId=' + String(stimIdWeak) +
          ': значимость стимула ' + String(callImportance) +
          (themeSituationMismatch
            ? ', для сравнения ×0.5 (тема/ситуация ≠ главного цикла) → эффективно ' + String(effectiveCallImportance)
            : '') +
          '<br>Нужно ≥ ' + String(Math.round(requiredMin * 100) / 100) +
          ' (' + String(ratio * 100) + '% от значимости главного цикла ' + String(currentThinkingImportance) + (isMainExpired ? ', главный expired' : '') + ').',
          1500
        );
      }
      return;
    }

    // Прерывание: текущий главный переводим в фоновый (isMainCycle=false), его ID пушим в стек очереди прерываний.
    // Цикл остаётся в реестре, диспетчер даёт ему шаг раз в 5 пульсов. После завершения нового главного
    // возвращаем в главные прерванные по порядку (pop из стека).
    var createCycle = global.Consciousness_createCycle;
    var setMainCycle = global.Consciousness_setMainCycle;
    var getMainCycle = global.Consciousness_getMainCycle;
    if (typeof getMainCycle === 'function') {
      var main = getMainCycle();
      if (main && Array.isArray(global.ConsciousnessBackgroundStack) && global.ConsciousnessBackgroundStack.length < MAX_INTERRUPTED_STACK) {
        global.ConsciousnessBackgroundStack.push(main.id);
      }
    }

    var runContext = {
      actual_stimul_ID: actualId,
      branchId: branchId,
      well: well,
      emotionId: emotionId,
      activityID: activityID,
      significance: context && typeof context.significance === 'number' ? context.significance : callImportance
    };
    if (typeof createCycle !== 'function') return;
    var cycle = createCycle(runContext);
    if (!cycle) return;
    runContext.processId = cycle.id;
    if (typeof setMainCycle === 'function') setMainCycle(cycle.id);
    global.ConsciousnessCurrentProcessId = cycle.id;

    // Новый актуальный стимул передаёт свою значимость главному циклу (в ходе процесса weight может меняться).
    if (info && callImportance) {
      info.thinkingImportance = callImportance;
      info.thinkingProcessId = cycle.id;
      if (typeof global.Info_updateView === 'function') global.Info_updateView();
    }
    if (cycle && typeof callImportance === 'number' && callImportance > 0) {
      cycle.weight = callImportance;
    }

    if (typeof global.Consciousness_cycleAppendLog === 'function' && cycle && cycle.isMainCycle) {
      global.Consciousness_cycleAppendLog(cycle, 'ФО: новый главный цикл #' + String(cycle.id) + ', FinalImageId=' + String(actualId) + ', ветка=' + String(branchId) + '.', typeof global.cpuls === 'number' ? global.cpuls : 0);
    }

    if (typeof global.Consciousness_runElementary === 'function') {
      global.Consciousness_runElementary(cycle);
    }
  }

  /**
   * Элементарный проход осознания (уровни 1 и 2): один вызов по контексту цикла.
   * Вызывается при создании цикла и при подъёме из стека фоновых. По завершении либо
   * cycle.status = 'finished' (автоматизм запущен), либо cycle.step = 1 и передача на уровень 2.
   * @param {Object} cycle - объект цикла из реестра (cycle.context, cycle.id)
   */
  function runElementary(cycle) {
    if (!cycle || !cycle.context) return;
    if (cycle.status !== 'running') return;
    var ctx = cycle.context;
    var actualId = ctx.actual_stimul_ID != null ? ctx.actual_stimul_ID : 0;
    var branchId = ctx.branchId != null ? ctx.branchId : 0;
    var well = ctx.well != null ? ctx.well : (global.BadNormWell || 0);
    var emotionId = ctx.emotionId != null ? ctx.emotionId : 0;
    var activityID = ctx.activityID != null ? ctx.activityID : actualId;

    global.EpisodicMemory_pendingFeedbackAutomatizmId = 0;
    cycle.waitingFeedbackAutomatizmId = 0;

    var automatismStarted = false;

/////////////////////////////////// ПЕРВЫЙ уровень глубины осознания //////////////////
// Поиск «позитивного» действия по семантике и ЭП, затем штатный автоматизм ветки.
    if (actualId && branchId) {
      if (typeof global.Consciousness_cycleAppendLog === 'function' && cycle && cycle.isMainCycle) {
        global.Consciousness_cycleAppendLog(cycle, 'Уровень 1: анализ семантики/ЭП и запуск автоматизма (если возможно).', typeof global.cpuls === 'number' ? global.cpuls : 0);
      }
      var semanticObj = (typeof global.getSemanticObjectFast === 'function' && well && (emotionId || activityID))
        ? global.getSemanticObjectFast(well, emotionId, activityID)
        : null;
      if (semanticObj && typeof semanticObj.meanImportance === 'number' && semanticObj.meanImportance < 0) {
        var parentNodeId = 0;
        if (typeof global.PerceptionTree_getNode === 'function') {
          var node = global.PerceptionTree_getNode(branchId);
          if (node && typeof node.parentId === 'number') parentNodeId = node.parentId;
        }
        if (parentNodeId && Array.isArray(global.Episodes)) {
          var bestActionImageId = null;
          var bestEffect = 0;
          for (var i = 0; i < global.Episodes.length; i++) {
            var f = global.Episodes[i];
            if (!f || f.perceptionNodeId !== parentNodeId || f.stimulusImageId !== actualId) continue;
            if (typeof f.effect !== 'number' || f.effect <= 0 || f.actionImageId == null) continue;
            if (bestActionImageId === null || f.effect > bestEffect) {
              bestActionImageId = f.actionImageId;
              bestEffect = f.effect;
            }
          }
          if (bestActionImageId != null) {
            if (typeof global.Consciousness_cycleAppendLog === 'function' && cycle && cycle.isMainCycle) {
              global.Consciousness_cycleAppendLog(cycle, 'Найдена позитивная реакция по ЭП: запускаю действие (ОД) ' + String(bestActionImageId) + '.', typeof global.cpuls === 'number' ? global.cpuls : 0);
            }
            var automatizmIdEp = 0;
            if (typeof global.Automatizms_createAutomatizm === 'function') {
              automatizmIdEp = global.Automatizms_createAutomatizm(branchId, bestActionImageId);
              if (automatizmIdEp && typeof global.Automatizms_setDefaultAutomatizm === 'function') {
                global.Automatizms_setDefaultAutomatizm(automatizmIdEp);
              }
            }
            if (typeof global.info_automatismWithEpisodeFeedbackAndExpireCycle === 'function') {
              automatismStarted = global.info_automatismWithEpisodeFeedbackAndExpireCycle(
                cycle,
                branchId,
                bestActionImageId,
                automatizmIdEp,
                0,
                'Найдена позитивная реакция по ЭП: запускаю действие (ОД) ' + String(bestActionImageId) + '.'
              );
            }
          }
        }
      }

      if (!automatismStarted) {
        var defaultAutomatizm = (typeof global.Automatizms_getDefaultAutomatizmForBranch === 'function')
          ? global.Automatizms_getDefaultAutomatizmForBranch(branchId) : null;
        if (defaultAutomatizm && defaultAutomatizm.actionsImageId) {
          var u = typeof defaultAutomatizm.usefulness === 'number' ? defaultAutomatizm.usefulness : 0;
          var c = typeof defaultAutomatizm.count === 'number' ? defaultAutomatizm.count : 0;
          if (!(c > 0 && u < 0)) {
            if (typeof global.Consciousness_cycleAppendLog === 'function' && cycle && cycle.isMainCycle) {
              global.Consciousness_cycleAppendLog(cycle, 'Штатный автоматизм ветки: запускаю действие (ОД) ' + String(defaultAutomatizm.actionsImageId) + '.', typeof global.cpuls === 'number' ? global.cpuls : 0);
            }
            if (typeof global.info_automatismWithEpisodeFeedbackAndExpireCycle === 'function') {
              automatismStarted = global.info_automatismWithEpisodeFeedbackAndExpireCycle(
                cycle,
                branchId,
                defaultAutomatizm.actionsImageId,
                defaultAutomatizm.id,
                0,
                'Штатный автоматизм ветки: запускаю действие (ОД) ' + String(defaultAutomatizm.actionsImageId) + '.'
              );
            }
          }
        }
      }
    }

/////////////////////////////////// ВТОРОЙ уровень глубины осознания //////////////////
// Если автоматизм не запущен на 1-м уровне, фиксируем проблему и передаём её во 2-й уровень.
    /*
     * Фаза ожидания обратной связи по автоматизму (запуск автоматизма с ур.1).
     *
     * Здесь возникает событие ожидания: цикл остаётся running, step = 1, awaitingAutomatismEpisodeFeedback = true,
     * пока по ЭП не придёт результат (ответ оператора в периоде ожидания и т.д.). Эпизодическая память и
     * инфокартина связывают этот цикл с автоматизмом (EpisodicMemory_waitingAutomatismCycleId, activeWaitingAnswer).
     *
     * Запуск автоматизма и ожидание: info_automatismWithEpisodeFeedbackAndExpireCycle — цикл сразу expired и не главный;
     * ответ оператора тянет conscience_level_2 из episodic_memory.closeLastFrame. Финализация фазы —
     * conscience_level_2_finalizeAutomatismAnswerCycle (awaiting снимается, isOperatorAnswerReceived обнуляется и т.д.).
     */
    if (automatismStarted) {
      return;
    }

    global.unsolved_problem = actualId;
    if (typeof global.Info_setProblemFromUnsolved === 'function') global.Info_setProblemFromUnsolved(actualId);
    // Сомнение в штатном автоматизме: 1-й уровень не смог запустить автоматизм, переходим на 2-й уровень.
    if (typeof global.InfoDet_updateDoubtInDefaultAutomatism === 'function') {
      global.InfoDet_updateDoubtInDefaultAutomatism(true);
    }
    if (typeof global.Consciousness_cycleAppendLog === 'function' && cycle && cycle.isMainCycle) {
      global.Consciousness_cycleAppendLog(cycle, 'Автоматизм не запущен. Уровень 2: постановка нерешённой проблемы и выбор цели.', typeof global.cpuls === 'number' ? global.cpuls : 0);
    }
    if (typeof global.conscience_level_2 === 'function') {
      global.conscience_level_2({
        actualId: actualId,
        branchId: branchId,
        well: well,
        emotionId: emotionId,
        activityID: activityID,
        sourceContext: cycle.context,
        sourceCycle: cycle
      });
    }
    cycle.step = 1;
  }

  /**
   * Локальная сигнатура прогресса пассивного фантазма для фонового цикла (без записи в глобальную инфокартину).
   */
  function passiveFantasyLocalSig(cycle) {
    var plot = cycle && cycle.fantazmingPlot;
    var ch = plot && plot.chain ? plot.chain.length : 0;
    var last = plot && plot.lastGeneratedActionId != null ? plot.lastGeneratedActionId : 0;
    var fa = cycle && Array.isArray(cycle.fantasizmArr) ? cycle.fantasizmArr.length : 0;
    return String(ch) + '|' + String(last) + '|' + String(fa);
  }

  /**
   * Один шаг цикла мышления (для диспетчера). Вызывается по каждому пульсу для главного цикла.
   * step === 0 уже обработан элементарным проходом при создании/подъёме; здесь — шаги мышления.
   *
   * Если у цикла thinkingMode === 'passive', уровни 3–4 не выполняются: только info_fantasizming.
   * У главного — антистагнация по сигнатуре инфокартины; у фонового — по локальному сюжету (fantazmingPlot / fantasizmArr).
   *
   * @param {Object} cycle
   */
  function runOneStep(cycle) {
    if (!cycle || cycle.status !== 'running') return;
    if (cycle.step === 0) return;
    if (cycle.step >= 1) {
      // Цикл после автоматизма ждёт кадр ЭП; когда оператор ответил стимулом, эпизодика закрывает кадр
      // и выставляет automatismOperatorAnswerReceived. Инфокартина: isOperatorAnswerReceived.
      // Пока automatismAnswerLevel2Done ложно, на каждом пульсе можно попасть сюда — фактическая
      // обработка один раз (пайплайн + finalize) внутри conscience_level_2 по флагам цикла/инфокартины.
      if (cycle.awaitingAutomatismEpisodeFeedback && cycle.automatismOperatorAnswerReceived && !cycle.automatismAnswerLevel2Done) {
        var inf = global.InfoPicture;
        // Двойная проверка: флаг цикла (от эпизодики) + флаг инфокартины (ответ оператора в UI/детекторах).
        if (inf && inf.isOperatorAnswerReceived && typeof global.conscience_level_2 === 'function') {
          if (typeof global.Consciousness_cycleAppendLog === 'function' && cycle.isMainCycle) {
            global.Consciousness_cycleAppendLog(cycle, 'ФО runOneStep: ответ оператора получен — вход в ур.2 (пайплайн).', typeof global.cpuls === 'number' ? global.cpuls : 0);
          }
          var cx = cycle.context || {};
          global.conscience_level_2({
            sourceCycle: cycle,
            actualId: cx.actual_stimul_ID,
            branchId: cx.branchId,
            well: cx.well,
            emotionId: cx.emotionId,
            activityID: cx.activityID,
            // Только блок isOperatorAnswerReceived в начале conscience_level_2; цели/ImportantProblem здесь не трогаем.
            _operatorAnswerCycleOnly: true
          });
        }
        // Всегда return на этой ветке для «ожидающего» цикла: не спускаться в уровень 3+ за тот же тик.
        return;
      }

      // Ожидание ответа по кадру ЭП без ещё пришедшего ответа — ур. 3–4 не крутим (иначе повторный поиск правил каждый пульс).
      if (cycle.awaitingAutomatismEpisodeFeedback && !cycle.automatismOperatorAnswerReceived) {
        return;
      }

      // --- Пассивный режим мышления цикла (thinkingMode === 'passive') — антагонист целевого пайплайна ур.3–4.
      // Режим хранится на цикле, не в InfoPicture. Главный цикл пишет «Сюжет фантазма» в инфокартину; фоновый — только
      // fantasizmArr + fantazmingPlot на объекте цикла (см. info_fantasizming).
      if (cycle.thinkingMode === 'passive') {
        var sigBefore;
        var sigAfter;
        if (cycle.isMainCycle) {
          var sigGet = global.Consciousness_getInfoPictureSignature;
          sigBefore = typeof sigGet === 'function' ? sigGet() : '';
          if ((cycle.passiveFantasyNoChangeStreak || 0) >= 3 && sigBefore === cycle.passiveFantasyLastSig) {
            return;
          }
          if (typeof global.info_fantasizming === 'function') {
            global.info_fantasizming(cycle);
          }
          sigAfter = typeof sigGet === 'function' ? sigGet() : '';
        } else {
          sigBefore = passiveFantasyLocalSig(cycle);
          if ((cycle.passiveFantasyNoChangeStreak || 0) >= 3 && sigBefore === cycle.passiveFantasyLastSig) {
            return;
          }
          if (typeof global.info_fantasizming === 'function') {
            global.info_fantasizming(cycle);
          }
          sigAfter = passiveFantasyLocalSig(cycle);
        }
        if (sigBefore === sigAfter) {
          cycle.passiveFantasyNoChangeStreak = (cycle.passiveFantasyNoChangeStreak || 0) + 1;
        } else {
          cycle.passiveFantasyNoChangeStreak = 0;
        }
        cycle.passiveFantasyLastSig = sigAfter;
        return;
      }

/////////////////////////////////// ТРЕТИЙ уровень глубины осознания //////////////////
// Условия тика и отработка — conscience_level_3.js (conscience_level_3_runFromChannel).
      if (typeof global.conscience_level_3_runFromChannel === 'function') {
        global.conscience_level_3_runFromChannel(cycle);
      }

/////////////////////////////////// ЧЕТВЕРТЫЙ уровень глубины осознания //////////////////
// Заготовка тика — conscience_level_4.js (conscience_level_4_runFromChannel).
      if (typeof global.conscience_level_4_runFromChannel === 'function') {
        global.conscience_level_4_runFromChannel(cycle);
      }
    }
  }

  global.Consciousness_runElementary = runElementary;
  global.Consciousness_runOneStep = runOneStep;

  /**
   * Фоновая версия функции осознания.
   * Запускается, когда новый, более значимый стимул прерывает текущий основной цикл.
   * В этой заготовке фоновая функция ничего не меняет в инфокартине и автоматизмах,
   * а только фиксирует факт вызова. Реальная логика будет добавлена отдельно.
   */
  function stimuls_consciousness_background(context) {
    if (!context) return;
    if (global.console && typeof global.console.log === 'function') {
      global.console.log('stimuls_consciousness_background invoked', context);
    }
  }

  global.stimuls_consciousness = stimuls_consciousness;
  global.stimuls_consciousness_background = stimuls_consciousness_background;
})(window);

════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/consciousness_dispatcher.js
════════════════════════════════════════════════════════════
/**
 * Диспетчер циклов осмысления (по образцу OLD_CODE/GO brain/psychic understanding.go + understanding_cycle.go).
 * Один цикл = один процесс осмысления с уникальным ID. По каждому пульсу диспетчер даёт главному циклу
 * один шаг (runOneStep); фоновые циклы — реже. Рекурсивные вызовы ФО заменены на итеративное выполнение
 * через реестр циклов и диспетчер. Без этого невозможна корректная реализация мышления.
 */
(function (global) {
  'use strict';

  /**
   * Максимум циклов в реестре (память о прошедших мышлениях).
   * Если накопилось более MAX_CYCLES, при появлении нового удаляем самый старый cycle с expired=true.
   * Это — простая и «дубовая» альтернатива конкурентному латеральному торможению.
   */
  var MAX_CYCLES = 100;

  /** Порядок создания цикла (всегда уникален) */
  var curCycleOrder = 0;

  /** Реестр циклов: id -> cycleInfo */
  if (typeof global.ConsciousnessCycles !== 'object' || global.ConsciousnessCycles === null) {
    global.ConsciousnessCycles = Object.create(null);
  }

  /** Счётчик для присвоения ID новому циклу */
  if (typeof global.ConsciousnessProcessNextId !== 'number' || global.ConsciousnessProcessNextId <= 0) {
    global.ConsciousnessProcessNextId = 1;
  }

  /** ID текущего главного цикла (осознаваемого) */
  if (typeof global.ConsciousnessCurrentProcessId !== 'number') {
    global.ConsciousnessCurrentProcessId = 0;
  }

  /**
   * Флаг «главный цикл может модифицировать инфокартину».
   * Для фоновых циклов диспетчер выставляет false перед шагом цикла.
   */
  if (typeof global.ConsciousnessCanModifyInfo !== 'boolean') {
    global.ConsciousnessCanModifyInfo = true;
  }

  /** ID первого созданного главного цикла после загрузки страницы (для условного авто-лога). */
  if (typeof global.ConsciousnessFirstMainCycleId !== 'number') {
    global.ConsciousnessFirstMainCycleId = 0;
  }
  /** См. stimuls_ui / OrientationReflex_getOrReadinessForFinalImageId; выставляется на 2-м пульсе. */
  if (typeof global.OrientationReflex_anyStimulusReadyForOr !== 'boolean') {
    global.OrientationReflex_anyStimulusReadyForOr = false;
  }

  /**
   * Структура цикла осмысления (аналог cycleInfo в Go).
   * @typedef {Object} CycleInfo
   * @property {number} id - уникальный ID цикла
   * @property {number} order - порядок создания (для сортировки/конкуренции)
   * @property {boolean} isMainCycle - главный цикл (осознаваемый)
   * @property {string} status - 'running' | 'finished' | 'cancelled'
   * @property {number} step - номер шага (0 = элементарный проход выполнен, 1+ = мышление)
   * @property {number} count - число вызовов runOneStep диспетчером (не лимит остановки; у фонового цикла шаг реже).
   * @property {number} birthPuls - значение global.cpuls на момент создания цикла (для «возраста» в пульсах).
   * @property {number} weight - значимость (для выбора главного при завершении другого)
   * @property {Object} context - контекст вызова (actualId, branchId, well, emotionId, ...)
   * @property {number} createdAt - timestamp создания
   * @property {number} lastPuls - номер пульса последнего запуска (для очистки старых)
   * @property {boolean} expired - цикл отработал и остаётся как память (не удаляется автоматически)
   * @property {number} noInfoChangeStreak - сколько итераций подряд инфокартина не менялась (для главного цикла)
   * @property {string} lastInfoSig - сигнатура инфокартины на предыдущей итерации
   * @property {Array<Object>} events - важнейшие события процесса (по мере конкретизации)
   * @property {number} createdThemeId - ID темы инфокартины на момент создания цикла
   * @property {number} createdSituationId - ID ситуации инфокартины на момент создания цикла
   * @property {string} thinkingMode - 'target' | 'passive' — режим мышления цикла (не хранится в инфокартине).
   * @property {number} goalImageId - ID образа цели (GoalImagesLife); при thinkingMode passive — 0; при target — currentGoalId
   * @property {number} waitingFeedbackAutomatizmId - ID автоматизма, запущенного на 1-м уровне ФО; ожидается оценка effect в кадре ЭП (0 — нет)
   * @property {number} lastGestaltConsciencePuls - номер пульса последнего тика ур.4/гештальта (Gestalt_onConsciousnessTick) для этого цикла; 0 — не было
   * @property {boolean} passiveWorkAfterGestaltExpire - true, если цикл ушёл в expired сразу после тика гештальта на том же пульсе (стагнация) и инфокартина переведена в passive
   * @property {Array<Object>} fantasizmArr - снимки правил ЭП (кадры); синхронизируется с шагами info_fantasizming
   * @property {boolean} fantasizmPassiveFillStarted - зарезервировано (раньше — первичное заполнение)
   * @property {Object} fantazmingPlot - { chain: Array, lastGeneratedActionId }: chain — звенья { fromStimulusId, actionImageId,
   *   episodeIndex, effect }; lastGeneratedActionId — последний ОД шага (на следующем пульсе ищутся кадры ЭП с
   *   responseStimulusImageId, совпадающим с этим id — см. info_fantasizming).
   * @property {number} currentFantazmingStimulus - дубль фокуса для отладки/UI: как правило тот же id, что lastGeneratedActionId
   *   после успешного шага.
   * @property {number} passiveFantasyNoChangeStreak - сколько раз подряд сигнатура инфокартины не изменилась на пассивном
   *   шаге (сравнение sig до и после info_fantasizming в runOneStep); при ≥3 и совпадении с passiveFantasyLastSig
   *   фантазм на пульс не вызывается.
   * @property {string} passiveFantasyLastSig - снимок Consciousness_getInfoPictureSignature() после последнего пассивного шага
   */
  function snapshotGoalImageIdForCycle() {
    var info = global.InfoPicture;
    if (!info || typeof info !== 'object') return 0;
    var main = getMainCycle();
    if (main && main.thinkingMode === 'passive') return 0;
    return typeof info.currentGoalId === 'number' ? info.currentGoalId : 0;
  }

  function createCycleInfo(id, context) {
    curCycleOrder += 1;
    var weight = 0;
    if (context && typeof context.significance === 'number') {
      weight = Math.abs(context.significance);
    }
    var info = global.InfoPicture;
    var createdThemeId = 0;
    var createdSituationId = 0;
    if (info && typeof info === 'object') {
      createdThemeId = typeof info.themeId === 'number' ? info.themeId : (info.themeId || 0);
      createdSituationId = typeof info.situationId === 'number' ? info.situationId : (info.situationId || 0);
    }
    return {
      id: id,
      order: curCycleOrder,
      isMainCycle: true,
      status: 'running',
      step: 0,
      count: 0,
      weight: weight,
      createdThemeId: createdThemeId,
      createdSituationId: createdSituationId,
      goalImageId: snapshotGoalImageIdForCycle(),
      thinkingMode: 'target',
      waitingFeedbackAutomatizmId: 0,
      awaitingAutomatismEpisodeFeedback: false,
      automatismOperatorAnswerReceived: false,
      automatismAnswerLevel2Done: false,
      lastAutomatismEpisodeEffect: null,
      context: context || {},
      createdAt: typeof Date.now === 'function' ? Date.now() : 0,
      lastPuls: 0,
      birthPuls: typeof global.cpuls === 'number' ? global.cpuls : 0,
      expired: false,
      noInfoChangeStreak: 0,
      lastInfoSig: '',
      events: []
      ,
      lastGestaltConsciencePuls: 0,
      passiveWorkAfterGestaltExpire: false,
      // Человекочитаемый лог работы цикла (для оператора).
      debugLog: [],
      // Если оператор закрыл окно лога — больше не показывать авто-лог для этого цикла.
      debugLogSuppressed: false,
      fantasizmArr: [],
      fantasizmPassiveFillStarted: false,
      /** Сюжет фантазирования: цепочка звеньев { fromStimulusId, actionImageId, episodeIndex, effect }; lastGeneratedActionId — якорь следующего поиска в ЭП. */
      fantazmingPlot: { chain: [], lastGeneratedActionId: 0 },
      /** Последний фокус фантазии (для отображения/отладки; обычно совпадает с lastGeneratedActionId после шага). */
      currentFantazmingStimulus: 0,
      /** Сколько пульсов подряд сигнатура инфокартины не менялась на пассивном шаге (антистагнация вместе с passiveFantasyLastSig). */
      passiveFantasyNoChangeStreak: 0,
      /** Последняя сигнатура инфокартины после обработки пассивной ветки runOneStep. */
      passiveFantasyLastSig: ''
    };
  }

  function escapeHtml(s) {
    s = String(s == null ? '' : s);
    return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  }

  function cycleAppendLog(cycle, text, pulsCount, opts) {
    if (!cycle) return;
    opts = opts && typeof opts === 'object' ? opts : {};
    // После expired лог больше не продолжаем (кроме принудительных системных сообщений).
    if (cycle.expired && !opts.force) return;
    if (!Array.isArray(cycle.debugLog)) cycle.debugLog = [];
    var msg = (typeof text === 'string') ? text : String(text);
    cycle.debugLog.push({
      atPuls: typeof pulsCount === 'number' ? pulsCount : 0,
      atMs: typeof Date.now === 'function' ? Date.now() : 0,
      text: msg
    });
    // ограничим размер, чтобы не разрастался
    if (cycle.debugLog.length > 200) {
      cycle.debugLog.splice(0, cycle.debugLog.length - 200);
    }
  }

  function formatCycleLogHtml(cycle) {
    if (!cycle || !Array.isArray(cycle.debugLog) || !cycle.debugLog.length) {
      return '<i>лог пуст</i>';
    }
    var lines = [];
    var from = Math.max(0, cycle.debugLog.length - 40);
    for (var i = from; i < cycle.debugLog.length; i++) {
      var e = cycle.debugLog[i];
      var p = (e && typeof e.atPuls === 'number') ? e.atPuls : 0;
      var t = e && e.text != null ? e.text : '';
      lines.push('[' + String(p) + '] ' + String(t));
    }
    return '<pre style="white-space:pre-wrap;margin:6px 0 0 0;font-size:10pt">' + escapeHtml(lines.join('\n')) + '</pre>';
  }

  function maybeShowMainCycleLog(main, pulsCount) {
    if (!main || !main.isMainCycle || main.status !== 'running') return;
    if (main.debugLogSuppressed) return;
    if (main.expired) return;
    var isFirstMainAfterLoad = (global.ConsciousnessFirstMainCycleId > 0 && main.id === global.ConsciousnessFirstMainCycleId);
    if (isFirstMainAfterLoad && !global.OrientationReflex_anyStimulusReadyForOr) return;
    if (typeof global.show_alert !== 'function') return;
    var curLen = Array.isArray(main.debugLog) ? main.debugLog.length : 0;
    if (typeof main.debugLogLastShownLen === 'number' && main.debugLogLastShownLen === curLen) {
      return;
    }
    main.debugLogLastShownLen = curLen;
    // Не мешаем другим модальным окнам.
    if (typeof global.Alert_getActiveDialogType === 'function') {
      var tp = global.Alert_getActiveDialogType();
      if (tp && tp !== 'cyclelog') return;
    }
    var stimId = (main.context && typeof main.context.actual_stimul_ID === 'number') ? main.context.actual_stimul_ID : 0;
    var title = '<b>Главный цикл #' + String(main.id) + '</b>' +
      (stimId ? (' (стимул FinalImageId=' + String(stimId) + ')') : '') +
      (main.expired ? ' <b>expired</b>' : '');
    var body = title +
      '<br><small>Окно обновляется каждый пульс. Закроете — для этого цикла больше не показывается.</small>' +
      '<br>' + formatCycleLogHtml(main);
    global.show_alert(body, 0, {
      dialogType: 'cyclelog',
      onClose: function (info) {
        if (info && info.userInitiated) {
          main.debugLogSuppressed = true;
        }
      }
    });
  }

  /**
   * Оперативная память фоновых циклов: после решения проблемы / исчерпания сценария
   * значимость (weight) всех не-главных running-циклов умножается на factor (по умолчанию 0.5).
   */
  function decayBackgroundOperativeMemory(factor) {
    factor = typeof factor === 'number' && factor > 0 && factor < 1 ? factor : 0.5;
    var registry = global.ConsciousnessCycles;
    for (var k in registry) {
      if (!Object.prototype.hasOwnProperty.call(registry, k)) continue;
      var c = registry[k];
      if (!c || c.isMainCycle || c.status !== 'running') continue;
      var w = typeof c.weight === 'number' ? c.weight : 0;
      c.weight = Math.max(0.01, w * factor);
    }
  }

  function addCycleEvent(cycle, type, data, pulsCount) {
    if (!cycle) return;
    if (!Array.isArray(cycle.events)) cycle.events = [];
    cycle.events.push({
      atPuls: typeof pulsCount === 'number' ? pulsCount : 0,
      atMs: typeof Date.now === 'function' ? Date.now() : 0,
      type: type || 'event',
      data: data || null
    });
    // ограничим журнал событий, чтобы не разрастался бесконечно
    if (cycle.events.length > 50) {
      cycle.events.splice(0, cycle.events.length - 50);
    }
  }

  function getInfoPictureSignature() {
    var info = global.InfoPicture;
    if (!info || typeof info !== 'object') return '';
    // Не включаем thinkingImportance/thinkingProcessId — они меняются при переключении циклов.
    var snap = {
      themeId: info.themeId || 0,
      situationId: info.situationId || 0,
      problemFinalImageId: info.problemFinalImageId || 0,
      step: info.step || 0,
      stimulusFinalImageId: info.stimulus && info.stimulus.finalImageId ? info.stimulus.finalImageId : 0,
      tree: info.tree ? [
        info.tree.baseID || 0,
        info.tree.emotionID || 0,
        info.tree.activityID || 0,
        info.tree.toneMoodID || 0,
        info.tree.simbolID || 0,
        info.tree.phraseID || 0
      ] : [0, 0, 0, 0, 0, 0]
    };
    // Детекторы: все числовые поля, кроме перечисленных выше и служебных.
    for (var k in info) {
      if (!Object.prototype.hasOwnProperty.call(info, k)) continue;
      if (k === 'themeId' || k === 'situationId' || k === 'problemFinalImageId' || k === 'step' ||
          k === 'tree' || k === 'stimulus' || k === 'thinkingImportance' || k === 'thinkingProcessId' ||
          k === 'currentGoalType' || k === 'currentGoalId' || k === 'currentGoalVitalId') {
        continue;
      }
      var v = info[k];
      if (typeof v === 'number') {
        snap[k] = v;
      }
    }
    try {
      return JSON.stringify(snap);
    } catch (e) {
      return '';
    }
  }

  function pruneCyclesIfNeeded(pulsCount) {
    var registry = global.ConsciousnessCycles;
    var keys = Object.keys(registry);
    if (keys.length <= MAX_CYCLES) return;

    // Сначала удаляем самый старый expired=true.
    var oldestExpired = null;
    for (var i = 0; i < keys.length; i++) {
      var c = registry[keys[i]];
      if (!c || !c.expired) continue;
      if (!oldestExpired || (c.order || 0) < (oldestExpired.order || 0)) oldestExpired = c;
    }
    if (oldestExpired) {
      deleteCycle(oldestExpired.id);
      return;
    }

    // Если expired ещё нет — удаляем самый старый не главный цикл (крайняя мера, чтобы не блокировать создание нового).
    var oldestBg = null;
    for (var j = 0; j < keys.length; j++) {
      var c2 = registry[keys[j]];
      if (!c2 || c2.isMainCycle) continue;
      if (!oldestBg || (c2.order || 0) < (oldestBg.order || 0)) oldestBg = c2;
    }
    if (oldestBg) {
      deleteCycle(oldestBg.id);
    }
  }

  /**
   * Создать новый цикл осмысления и зарегистрировать его.
   * @param {Object} context - контекст (actualId, branchId, significance, ...)
   * @returns {CycleInfo|null} созданный цикл или null при переполнении
   */
  function createConsciousnessCycle(context) {
    var registry = global.ConsciousnessCycles;
    var id = global.ConsciousnessProcessNextId++;
    var cycle = createCycleInfo(id, context);
    registry[id] = cycle;
    if (cycle.isMainCycle && !(global.ConsciousnessFirstMainCycleId > 0)) {
      global.ConsciousnessFirstMainCycleId = id;
    }
    addCycleEvent(cycle, 'created', { context: context || null }, 0);
    cycleAppendLog(cycle, 'Создан цикл. Стимул FinalImageId=' + String(context && context.actual_stimul_ID ? context.actual_stimul_ID : 0) + '.', typeof global.cpuls === 'number' ? global.cpuls : 0);
    pruneCyclesIfNeeded(typeof global.cpuls === 'number' ? global.cpuls : 0);
    return cycle;
  }

  /**
   * Установить цикл с заданным ID главным. Остальные получают isMainCycle = false.
   * @param {number} cycleId - ID цикла
   */
  function setMainCycle(cycleId) {
    var registry = global.ConsciousnessCycles;
    for (var k in registry) {
      if (Object.prototype.hasOwnProperty.call(registry, k)) {
        registry[k].isMainCycle = (Number(k) === Number(cycleId));
      }
    }
    global.ConsciousnessCurrentProcessId = cycleId;
  }

  /**
   * Получить текущий главный цикл (со статусом running).
   * @returns {CycleInfo|null}
   */
  function getMainCycle() {
    var registry = global.ConsciousnessCycles;
    for (var k in registry) {
      if (!Object.prototype.hasOwnProperty.call(registry, k)) continue;
      var c = registry[k];
      if (c && c.isMainCycle && c.status === 'running') return c;
    }
    return null;
  }

  /**
   * Получить цикл по ID.
   * @param {number} id
   * @returns {CycleInfo|null}
   */
  function getCycle(id) {
    var c = global.ConsciousnessCycles[Number(id)];
    return c || null;
  }

  /**
   * Отменить цикл (установить status = 'cancelled'). RunOneStep будет его игнорировать.
   * @param {number} id
   */
  function cancelConsciousnessProcess(id) {
    var c = getCycle(id);
    if (c) c.status = 'cancelled';
  }

  /**
   * Удалить цикл из реестра. Если удалялся главный — выбрать нового главного по весу (макс. weight среди running).
   * @param {number} id
   */
  function deleteCycle(id) {
    var registry = global.ConsciousnessCycles;
    var wasMain = false;
    var cycle = registry[id];
    if (cycle) {
      wasMain = cycle.isMainCycle;
      delete registry[id];
    }
    if (wasMain) {
      var nextMain = null;
      var maxWeight = -1;
      for (var k in registry) {
        if (!Object.prototype.hasOwnProperty.call(registry, k)) continue;
        var c = registry[k];
        if (c && c.status === 'running' && c.weight > maxWeight) {
          maxWeight = c.weight;
          nextMain = c;
        }
      }
      if (nextMain) {
        setMainCycle(nextMain.id);
      } else {
        global.ConsciousnessCurrentProcessId = 0;
      }
    }
  }

  /**
   * Выполнить один шаг цикла. Вызывает глобальную Consciousness_runOneStep(cycle), если она задана.
   * @param {CycleInfo} cycle
   * @param {number} pulsCount - текущий номер пульса
   */
  function runOneStep(cycle, pulsCount) {
    if (!cycle || cycle.status !== 'running') return;
    cycle.lastPuls = pulsCount;
    cycle.count += 1;
    if (typeof global.Consciousness_runOneStep === 'function') {
      global.Consciousness_runOneStep(cycle);
    }
  }

  /**
   * Поднять фоновый цикл в главный: сначала по очереди прерываний (стек ID), иначе по макс. weight.
   * Только сменить вывеску и дать права редактировать инфокартину; ID цикла не меняется.
   */
  function promoteFromBackgroundCycle() {
    var stack = global.ConsciousnessBackgroundStack;
    if (Array.isArray(stack) && stack.length > 0) {
      while (stack.length > 0) {
        var id = stack.pop();
        var c = getCycle(id);
        if (c && c.status === 'running' && !c.expired) {
          setMainCycle(c.id);
          if (typeof global.InfoPicture !== 'undefined' && global.InfoPicture) {
            global.InfoPicture.thinkingImportance = c.weight;
            global.InfoPicture.thinkingProcessId = c.id;
            if (typeof global.Info_updateView === 'function') global.Info_updateView();
          }
          if (typeof global.show_alert === 'function') {
            var stimId = (c.context && typeof c.context.actual_stimul_ID === 'number') ? c.context.actual_stimul_ID : 0;
            global.show_alert(
              'Возврат к прерванному процессу ID=' + String(c.id) + ' в главный (стимул FinalImageId=' + String(stimId) + ', значимость=' + String(c.weight || 0) + ').',
              1500
            );
          }
          return;
        }
      }
    }
    var registry = global.ConsciousnessCycles;
    var best = null;
    var bestWeight = -1;
    for (var k in registry) {
      if (!Object.prototype.hasOwnProperty.call(registry, k)) continue;
      var c = registry[k];
      if (!c || c.isMainCycle || c.status !== 'running' || c.expired) continue;
      var w = typeof c.weight === 'number' ? c.weight : 0;
      if (w > bestWeight) {
        bestWeight = w;
        best = c;
      }
    }
    if (!best) return;
    setMainCycle(best.id);
    if (typeof global.InfoPicture !== 'undefined' && global.InfoPicture) {
      global.InfoPicture.thinkingImportance = best.weight;
      global.InfoPicture.thinkingProcessId = best.id;
      if (typeof global.Info_updateView === 'function') global.Info_updateView();
    }
    if (typeof global.show_alert === 'function') {
      var stimId = (best.context && typeof best.context.actual_stimul_ID === 'number') ? best.context.actual_stimul_ID : 0;
      global.show_alert(
        'Возврат к фоновому процессу ID=' + String(best.id) + ' в главный (стимул FinalImageId=' + String(stimId) + ', значимость=' + String(best.weight || 0) + ').',
        1500
      );
    }
  }

  /**
   * Поднять контекст из стека фоновых (устаревший путь: создание нового цикла по сохранённому контексту).
   * Оставлено для совместимости; основной путь — promoteFromBackgroundCycle().
   */
  function promoteFromBackgroundStack() {
    var stack = global.ConsciousnessBackgroundStack;
    if (!Array.isArray(stack) || stack.length === 0) return;
    var bg = stack.pop();
    if (!bg || !bg.context) return;
    var cycle = createConsciousnessCycle(bg.context);
    if (!cycle) return;
    setMainCycle(cycle.id);
    if (typeof global.InfoPicture !== 'undefined' && global.InfoPicture && bg.importance != null) {
      global.InfoPicture.thinkingImportance = bg.importance;
      global.InfoPicture.thinkingProcessId = cycle.id;
      if (typeof global.Info_updateView === 'function') global.Info_updateView();
    }
    if (typeof global.Consciousness_runElementary === 'function') {
      global.Consciousness_runElementary(cycle);
    }
  }

  /**
   * Поддержание реестра циклов: фоновые циклы автоматически не закрываем (это память).
   * Единственная «очистка» — ограничение размера реестра через pruneCyclesIfNeeded().
   * @param {number} pulsCount
   */
  function cleanupCycles(pulsCount) {
    pruneCyclesIfNeeded(pulsCount);
  }

  /**
   * Диспетчер циклов мышления. Вызывается по каждому пульсу.
   * 1) Если нет главного цикла — поднять один фоновый в главный (promoteFromBackgroundCycle).
   * 2) Главному циклу выполнить один шаг (Consciousness_runOneStep).
   * 3) Если после шага главный завершён — удалить его, поднять следующий фоновый в главный.
   * 4) Фоновым циклам (running, не main) дать один шаг раз в 5 пульсов; ID сохраняются.
   * 5) Очистить завершённые/отменённые и старые фоновые.
   * @param {number} pulsCount - текущее значение счётчика пульсов (например global.cpuls)
   */
  function dispetchConsciousnessThinking(pulsCount) {
    pulsCount = typeof pulsCount === 'number' ? pulsCount : 0;

    var main = getMainCycle();

    if (!main) {
      promoteFromBackgroundCycle();
      cleanupCycles(pulsCount);
      return;
    }

    // Главный цикл имеет право модифицировать инфокартину.
    global.ConsciousnessCanModifyInfo = true;
    // Для логики expired: снимок инфокартины до/после итерации главного цикла.
    var sigBefore = getInfoPictureSignature();
    runOneStep(main, pulsCount);
    var sigAfter = getInfoPictureSignature();
    if (sigBefore && sigAfter && sigBefore === sigAfter) {
      main.noInfoChangeStreak = (main.noInfoChangeStreak || 0) + 1;
    } else {
      main.noInfoChangeStreak = 0;
    }
    main.lastInfoSig = sigAfter;
    var skipStagnationExpire = !!(main.awaitingAutomatismEpisodeFeedback && !main.automatismAnswerLevel2Done);
    if (!main.expired && main.noInfoChangeStreak >= 3 && !skipStagnationExpire) {
      cycleAppendLog(main, 'Цикл помечен expired (3 итерации без изменений инфокартины).', pulsCount, { force: true });
      main.expired = true;
      main.isMainCycle = false; // чтобы getMainCycle() больше не возвращал этот цикл — простаивающий цикл перестаёт получать шаги
      addCycleEvent(main, 'expired', { reason: '3 iterations without InfoPicture changes' }, pulsCount);
      var gestaltTickSamePuls = (typeof main.lastGestaltConsciencePuls === 'number' &&
        main.lastGestaltConsciencePuls === pulsCount);
      if (gestaltTickSamePuls) {
        main.passiveWorkAfterGestaltExpire = true;
        addCycleEvent(main, 'passiveAfterGestaltStagnation', { puls: pulsCount }, pulsCount);
        main.thinkingMode = 'passive';
        if (typeof global.Info_updateView === 'function') global.Info_updateView();
        cycleAppendLog(main, 'После тика ур.4/гештальта: expired по стагнации — главный цикл переведён в пассивный режим мышления (thinkingMode).', pulsCount, { force: true });
      }
      decayBackgroundOperativeMemory(0.5);
      var mw = typeof main.weight === 'number' ? main.weight : 0;
      main.weight = Math.max(0.01, mw * 0.5);
      if (typeof global.InfoPicture !== 'undefined' && global.InfoPicture) {
        global.InfoPicture.thinkingImportance = main.weight;
        if (typeof global.Info_updateView === 'function') global.Info_updateView();
      }
    }

    // Детектор «Неудовлетворённость существующим»:
    // если главный цикл expired и более 20 пульсов подряд активен базовый контекст «Покой» (id=14),
    // то включаем InfoPicture.isDissatisfaction (и dissatisfaction) через InfoDet_updateDissatisfaction.
    (function () {
      var info = global.InfoPicture;
      var peaceActive = Array.isArray(global.BasicContextsActived) && global.BasicContextsActived.indexOf(14) !== -1;
      if (main && main.expired && peaceActive) {
        main.expiredPeacePulses = (typeof main.expiredPeacePulses === 'number' ? main.expiredPeacePulses : 0) + 1;
      } else if (main) {
        main.expiredPeacePulses = 0;
      }
      var should = !!(main && main.expiredPeacePulses > 20);
      if (typeof global.InfoDet_updateDissatisfaction === 'function') {
        global.InfoDet_updateDissatisfaction(should);
      } else if (info) {
        info.dissatisfaction = should ? 1 : 0;
        info.isDissatisfaction = should ? 1 : 0;
        if (typeof global.Info_updateView === 'function') global.Info_updateView();
      }
    })();

    // Детектор «Непонимание»:
    // целевой режим мышления главного цикла + слишком длинная итерация или главный expired в целевом режиме.
    (function () {
      var info = global.InfoPicture;
      var isTarget = !!(main && main.thinkingMode !== 'passive');
      var tooLong = !!(main && typeof main.count === 'number' && main.count > 10);
      var expiredTarget = !!(main && main.expired && isTarget);
      var flag = !!(isTarget && (tooLong || expiredTarget));
      if (typeof global.InfoDet_updateMisunderstanding === 'function') {
        global.InfoDet_updateMisunderstanding(flag);
      } else if (info) {
        info.misunderstanding = flag ? 1 : 0;
        if (typeof global.Info_updateView === 'function') global.Info_updateView();
      }
    })();

    // Авто-показ/обновление лога главного цикла (если оператор не закрыл для этого цикла).
    maybeShowMainCycleLog(main, pulsCount);

    if (main.status === 'finished' || main.status === 'cancelled') {
      addCycleEvent(main, main.status, null, pulsCount);
      cycleAppendLog(main, 'Цикл завершён: ' + String(main.status) + '.', pulsCount, { force: true });
      decayBackgroundOperativeMemory(0.5);
      deleteCycle(main.id);
      promoteFromBackgroundCycle();
    }

    var registry = global.ConsciousnessCycles;
    for (var k in registry) {
      if (!Object.prototype.hasOwnProperty.call(registry, k)) continue;
      var c = registry[k];
      if (!c || c.isMainCycle || c.status !== 'running') continue;
      var fastBgAwait = !!(c.awaitingAutomatismEpisodeFeedback && c.automatismOperatorAnswerReceived && !c.automatismAnswerLevel2Done);
      if (fastBgAwait || pulsCount % 5 === 0) {
        // Фоновые циклы могут только читать инфокартину, без модификаций.
        global.ConsciousnessCanModifyInfo = false;
        runOneStep(c, pulsCount);
        // Сюжет фонового фантазирования — в info_fantasizming (conscious_attention_channel, пассивный режим).
        // Фоновые циклы не закрываем автоматически — это память о прошедших мышлениях.
      }
    }

    cleanupCycles(pulsCount);
  }

  /**
   * Число фоновых циклов (running, isMainCycle=false) в реестре — для индикации «фон: N».
   * @returns {number}
   */
  function getBackgroundCyclesCount() {
    var registry = global.ConsciousnessCycles;
    var n = 0;
    for (var k in registry) {
      if (!Object.prototype.hasOwnProperty.call(registry, k)) continue;
      var c = registry[k];
      if (c && !c.isMainCycle && c.status === 'running') n += 1;
    }
    return n;
  }

  /**
   * Список фоновых циклов (для UI: детализация по клику «фон: N»).
   * @returns {Array<CycleInfo>}
   */
  function getBackgroundCyclesList() {
    var registry = global.ConsciousnessCycles;
    var list = [];
    for (var k in registry) {
      if (!Object.prototype.hasOwnProperty.call(registry, k)) continue;
      var c = registry[k];
      if (c && !c.isMainCycle && c.status === 'running') list.push(c);
    }
    list.sort(function (a, b) { return (b.weight || 0) - (a.weight || 0); });
    return list;
  }

  /**
   * Можно ли в текущем шаге цикла модифицировать инфокартину (InfoPicture)?
   * true — для главного цикла; false — для фонового шага.
   * Код внутри функций уровней осознания должен учитывать этот флаг.
   * @returns {boolean}
   */
  function canModifyInfoPicture() {
    return !!global.ConsciousnessCanModifyInfo;
  }

  /**
   * Режим мышления текущего главного цикла ('target' | 'passive'). Без главного — 'target'.
   * Режим не хранится в InfoPicture; инфокартина получает только следы (сюжет фантазма и т.д.).
   * @returns {'target'|'passive'}
   */
  function getMainCycleThinkingMode() {
    var c = getMainCycle();
    if (!c) return 'target';
    return c.thinkingMode === 'passive' ? 'passive' : 'target';
  }

  global.Consciousness_createCycle = createConsciousnessCycle;
  global.Consciousness_setMainCycle = setMainCycle;
  global.Consciousness_getMainCycle = getMainCycle;
  global.Consciousness_getMainCycleThinkingMode = getMainCycleThinkingMode;
  global.Consciousness_getCycle = getCycle;
  global.Consciousness_cancelProcess = cancelConsciousnessProcess;
  global.Consciousness_deleteCycle = deleteCycle;
  global.Consciousness_promoteFromBackgroundCycle = promoteFromBackgroundCycle;
  global.Consciousness_promoteFromBackgroundStack = promoteFromBackgroundStack;
  global.Consciousness_getBackgroundCyclesCount = getBackgroundCyclesCount;
  global.Consciousness_getBackgroundCyclesList = getBackgroundCyclesList;
  global.Consciousness_canModifyInfoPicture = canModifyInfoPicture;
  global.Consciousness_decayBackgroundOperativeMemory = decayBackgroundOperativeMemory;
  global.Consciousness_cycleAppendLog = function (cycle, text, pulsCount, opts) {
    cycleAppendLog(cycle, text, pulsCount, opts);
  };
  global.dispetchConsciousnessThinking = dispetchConsciousnessThinking;
  global.Consciousness_getInfoPictureSignature = getInfoPictureSignature;
})(window);

════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/dispetcheron.js
════════════════════════════════════════════════════════════
/* Диспетчер выбора следующего шага осмысления.


*/
════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/fantasizming.js
════════════════════════════════════════════════════════════
/**
 * Пассивное фантазирование: один шаг построения ассоциативной цепочки по кадрам эпизодической памяти (ЭП).
 *
 * Идея: в учительском кадре зафиксировано «оператор ответил стимулом X» → responseStimulusImageId = X.
 * Мы задаём X как текущий фокус (опорный стимул), находим такие кадры, выбираем самый «сильный» по |effect|,
 * из него берём образ действия (ОД) и делаем его следующим X для следующего пульса — получается сюжет «если
 * в жизни было так, то дальше…» без реального стимула среды (имитация через runActionImage + обновление инфокартины).
 *
 * Режим «пассивного» мышления задаётся полем цикла thinkingMode === 'passive' (не инфокартиной).
 * Главный цикл дописывает сюжет в InfoPicture.fantasmPlotRules; фоновый — только в cycle.fantasizmArr и
 * fantazmingPlot. Пока цикл пассивен, ур.3–4 не крутятся (Consciousness_runOneStep). См. about_passive_fantasizming.md.
 *
 * Зависимости: EpisodicMemory_getFramesByResponseStimulus, EpisodicMemory_pickBestFrameByAbsEffect,
 * runActionImage, Info_updateFromStimulus, Consciousness_cycleAppendLog, info_insight (порог FANTASMI_INSITE_COMPARE).
 */
(function (global) {
  'use strict';

  /** Порог |effect| по умолчанию: выше — считаем кадр настолько значимым, что нужен инсайт (переход в target). */
  var DEFAULT_INSITE_COMPARE = 5;

  function puls() {
    return typeof global.cpuls === 'number' ? global.cpuls : 0;
  }

  /** Лог цикла ФО: фоновые циклы — с force, чтобы сообщение не терялось. */
  function appendCycleLog(cycle, text, opts) {
    if (typeof global.Consciousness_cycleAppendLog !== 'function') return;
    var o = opts && typeof opts === 'object' ? opts : {};
    if (o.force === undefined) o.force = !cycle.isMainCycle;
    global.Consciousness_cycleAppendLog(cycle, text, puls(), o);
  }

  /** Инициализация структуры сюжета на цикле: цепочка звеньев и последний ОД как «якорь» для следующего поиска по ЭП. */
  function ensureFantazmingPlot(cycle) {
    if (!cycle) return { chain: [], lastGeneratedActionId: 0 };
    if (!cycle.fantazmingPlot || typeof cycle.fantazmingPlot !== 'object') {
      cycle.fantazmingPlot = { chain: [], lastGeneratedActionId: 0 };
    }
    if (!Array.isArray(cycle.fantazmingPlot.chain)) cycle.fantazmingPlot.chain = [];
    if (cycle.fantazmingPlot.lastGeneratedActionId == null) cycle.fantazmingPlot.lastGeneratedActionId = 0;
    return cycle.fantazmingPlot;
  }

  /**
   * Добавить в инфокартину фрагмент «Сюжет фантазма» (ячейка fantasmPlotRules) для отображения оператору.
   * Обрезка хвоста — чтобы массив не разрастался бесконечно при длинной цепочке.
   */
  function appendFantasmSnapshotToInfoPicture(frameLike) {
    var info = typeof global.Info_initIfNeeded === 'function' ? global.Info_initIfNeeded() : global.InfoPicture;
    if (!info) return;
    if (!Array.isArray(info.fantasmPlotRules)) info.fantasmPlotRules = [];
    info.fantasmPlotRules.push({
      perceptionNodeId: frameLike.perceptionNodeId || 0,
      stimulusImageId: frameLike.stimulusImageId || 0,
      situationId: frameLike.situationId || 0,
      actionImageId: frameLike.actionImageId != null ? frameLike.actionImageId : null,
      effect: frameLike.effect != null ? frameLike.effect : null,
      responseStimulusImageId: frameLike.responseStimulusImageId != null ? frameLike.responseStimulusImageId : null,
      episodeIndex: frameLike._episodeIndex != null ? frameLike._episodeIndex : null
    });
    while (info.fantasmPlotRules.length > 80) info.fantasmPlotRules.shift();
  }

  /** Дублирование снимка кадра в cycle.fantasizmArr — оперативная лента для отладки/будущей связи с другими циклами. */
  function syncFantasizmArr(cycle, frameLike) {
    if (!Array.isArray(cycle.fantasizmArr)) cycle.fantasizmArr = [];
    cycle.fantasizmArr.push({
      perceptionNodeId: frameLike.perceptionNodeId || 0,
      stimulusImageId: frameLike.stimulusImageId || 0,
      situationId: frameLike.situationId || 0,
      actionImageId: frameLike.actionImageId != null ? frameLike.actionImageId : null,
      effect: frameLike.effect != null ? frameLike.effect : null,
      responseStimulusImageId: frameLike.responseStimulusImageId != null ? frameLike.responseStimulusImageId : null
    });
  }

  /**
   * Один шаг итерации пассивного режима для данного цикла.
   *
   * Возврат true означает, что цепочка логически продолжена (в т.ч. ветка инсайта без мотора).
   * false — нет опорного стимула, нет кадров в ЭП или нет подходящих функций памяти.
   *
   * @param {Object} cycle — активный цикл сознания (status === 'running')
   * @returns {boolean}
   */
  function info_fantasizming(cycle) {
    if (!cycle || cycle.status !== 'running') return false;
    if (cycle.thinkingMode !== 'passive') return false;

    var inf = global.InfoPicture;
    var isMain = cycle.isMainCycle === true;

    var plot = ensureFantazmingPlot(cycle);
    // --- Шаг 1: опорный стимул (FinalImageId), с которым ищем «учительские» кадры по responseStimulusImageId.
    // Приоритет: продолжение цепочки (последний ОД шага как новый «ответный» идентификатор) → контекст создания цикла
    // → текущая цель осмысления в инфокартине (часто 0 в пассиве).
    var currentStimulusId = 0;
    if (plot.lastGeneratedActionId) {
      currentStimulusId = parseInt(plot.lastGeneratedActionId, 10) || 0;
    }
    if (!currentStimulusId) {
      var cx = cycle.context || {};
      if (cx.actual_stimul_ID != null) currentStimulusId = parseInt(cx.actual_stimul_ID, 10) || 0;
    }
    if (!currentStimulusId && isMain && inf && typeof inf.currentGoalId === 'number' && inf.currentGoalId > 0) {
      currentStimulusId = inf.currentGoalId;
    }
    if (!currentStimulusId) {
      appendCycleLog(cycle, 'Фантазм: нет опорного стимула (currentStimulusId) — шаг пропущен.');
      return false;
    }

    // --- Шаг 2: все кадры ЭП, где операторский ответ совпал с нашим currentStimulusId; лучший по модулю эффекта.
    var getFrames = global.EpisodicMemory_getFramesByResponseStimulus;
    var pickBest = global.EpisodicMemory_pickBestFrameByAbsEffect;
    if (typeof getFrames !== 'function' || typeof pickBest !== 'function') {
      appendCycleLog(cycle, 'Фантазм: нет EpisodicMemory_getFramesByResponseStimulus / pickBestFrameByAbsEffect — сюжет не обновлён.');
      return false;
    }

    var candidates = getFrames(currentStimulusId);
    var bestFrame = pickBest(candidates);
    if (!bestFrame || !bestFrame.actionImageId) {
      appendCycleLog(cycle, 'Фантазм: нет кадра с responseStimulusImageId=' + currentStimulusId + ' — цепочка прервана.');
      return false;
    }

    var actionId = parseInt(bestFrame.actionImageId, 10) || 0;
    if (!actionId) {
      appendCycleLog(cycle, 'Фантазм: кадр ЭП без валидного actionImageId — сюжет не обновлён.');
      return false;
    }

    var eff = typeof bestFrame.effect === 'number' ? bestFrame.effect : 0;
    var threshold = typeof global.FANTASMI_INSITE_COMPARE === 'number' ? global.FANTASMI_INSITE_COMPARE : DEFAULT_INSITE_COMPARE;

    // --- Шаг 3: зафиксировать звено сюжета; lastGeneratedActionId = ОД — на следующем пульсе поиск пойдёт
    // от «как будто оператор ответил этим действием-образом» (сопоставление с полем responseStimulusImageId в данных).
    plot.chain.push({
      fromStimulusId: currentStimulusId,
      actionImageId: actionId,
      episodeIndex: bestFrame._episodeIndex != null ? bestFrame._episodeIndex : null,
      effect: eff
    });
    plot.lastGeneratedActionId = actionId;
    cycle.currentFantazmingStimulus = actionId;

    syncFantasizmArr(cycle, bestFrame);
    if (isMain) {
      appendFantasmSnapshotToInfoPicture(bestFrame);
    }

    // Каждое успешное обновление сюжета (цепочка + fantasizmArr [+ fantasmPlotRules у главного]) — отдельная запись в лог цикла.
    var storyLog = 'Фантазм [сюжет]: ответный стимул=' + currentStimulusId + ' → ОД=' + actionId +
      ', effect=' + eff + ', эпизод #' + (bestFrame._episodeIndex != null ? bestFrame._episodeIndex : '—') +
      (isMain ? ', инфо-ячейка «Сюжет фантазма» обновлена.' : ', контекст в fantasizmArr / fantazmingPlot.');
    appendCycleLog(cycle, storyLog);

    // --- Шаг 4а: экстремальный эффект → инсайт: целевой режим цикла, цель «значение».
    // Намеренно не вызываем runActionImage: смысловой сдвиг важнее имитации мотора в этом тике.
    if (Math.abs(eff) >= threshold && typeof global.info_insight === 'function') {
      global.info_insight(cycle, { fantasyInsight: true, effect: eff });
      if (isMain && global.InfoPicture) {
        global.InfoPicture.thinkingSource = 'fantasy';
      }
      if (isMain && typeof global.Info_incrementStep === 'function') {
        global.Info_incrementStep();
      }
      appendCycleLog(cycle, 'Фантазм: инсайт |effect|≥' + String(threshold) + ' — цель «значение», переход в целевой режим.', { force: true });
      return true;
    }

    // --- Шаг 4б: главный цикл — имитация ответа и обновление глобальной инфокартины; фоновый — только контекст в цикле.
    if (isMain) {
      if (typeof global.runActionImage === 'function') {
        global.runActionImage(actionId);
      }
      if (typeof global.Info_updateFromStimulus === 'function') {
        var showStim = bestFrame.stimulusImageId || actionId;
        global.Info_updateFromStimulus(showStim, 'imagination');
      }
      if (global.InfoPicture) {
        global.InfoPicture.thinkingSource = 'fantasy';
      }
      if (typeof global.Info_incrementStep === 'function') {
        global.Info_incrementStep();
      }
    }

    return true;
  }

  global.info_fantasizming = info_fantasizming;
  /** Глобальный порог инсайта из фантазирования; можно переопределить до первого шага. */
  global.FANTASMI_INSITE_COMPARE = DEFAULT_INSITE_COMPARE;
})(window);

════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/gestalt.js
════════════════════════════════════════════════════════════
/**
 * Гештальты (доминанты нерешённых задач) и буфер резонанса для уровня 4.
 *
 * Полная теория: beast/_9_Conscience/about_gestalt.md
 *
 * Поток данных (кратко):
 *   1) Ур.3 в начале тика вызывает Gestalt_prepareContextForCycle — в global.GestaltContextForLevel3
 *      кладётся снимок открытого гештальта по цели главного цикла (если есть).
 *   2) Ур.4 вызывает Gestalt_onConsciousnessTick — привязка «хозяина», разбор GestaltResonanceBuffer,
 *      повторное обновление контекста после возможных инсайтов по аналогии.
 *   3) EpisodicMemory_closeLastFrame → Gestalt_onEpisodeFeedbackClosed — в буфер кладётся сигнатура
 *      закрытого кадра; при наличии goalImageId у цикла ожидания обновляется запись гештальта.
 *
 * Логи цикла: Consciousness_cycleAppendLog(cycle, text, puls [, { force: true }]).
 * Сигнатура диспетчера: см. consciousness_dispatcher.js — для цикла expired / не главного нужен force,
 * иначе cycleAppendLog отбросит запись.
 */
(function (global) {
  'use strict';

  /** Статусы гештальта: 0 новый → 1 начато → 2 частично → 3 закрыт успехом → 4 архив. */
  var GESTALT_STATUS = {
    NEW: 0,
    STARTED: 1,
    PARTIAL: 2,
    CLOSED_SUCCESS: 3,
    ARCHIVED: 4
  };

  /** Метки типа инсайта для поля lastInsightKind и строк лога. */
  var INSIGHT_KIND = {
    PRE: 'pre',
    LOCAL: 'local',
    ANALOGY: 'analogy',
    FEEDBACK: 'feedback'
  };

  /** Макс. записей в буфере резонанса (FIFO, старые выталкиваются). */
  var MAX_RESONANCE_BUFFER = 48;
  /** Макс. элементов в analogyPool у одного гештальта. */
  var MAX_ANALOGY_POOL = 16;
  /**
   * Порог «сильного» эффекта для закрытия гештальта (переход в CLOSED_SUCCESS).
   * Диапазон effect в ЭП: [-10, 10] — см. episodic_memory.closeLastFrame.
   */
  var EFFECT_STRONG_CLOSE = 6;
  /** Минимальный score для инсайта по резонансу (эвристика v1, см. scoreResonanceAgainstGestalt). */
  var RESONANCE_SCORE_INSIGHT = 0.55;

  var gestaltDirty = false;
  var lastSavedSig = '';

  if (!Array.isArray(global.GestaltArr)) global.GestaltArr = [];
  if (typeof global.GestaltNextId !== 'number' || global.GestaltNextId < 1) global.GestaltNextId = 1;
  if (!Array.isArray(global.GestaltResonanceBuffer)) global.GestaltResonanceBuffer = [];
  if (typeof global.GestaltResonanceSeq !== 'number' || global.GestaltResonanceSeq < 1) global.GestaltResonanceSeq = 1;
  if (typeof global.Gestalt_activeMasterCycleId !== 'number') global.Gestalt_activeMasterCycleId = 0;
  if (typeof global.Gestalt_activeGestaltId !== 'number') global.Gestalt_activeGestaltId = 0;

  global.GESTALT_STATUS = GESTALT_STATUS;
  global.GESTALT_INSIGHT_KIND = INSIGHT_KIND;

  function puls() {
    return typeof global.cpuls === 'number' ? global.cpuls : 0;
  }

  function markDirty() {
    gestaltDirty = true;
  }

  /**
   * Приводит объект гештальта к предсказуемой форме при создании и после загрузки из IndexedDB.
   * Копируем массивы, чтобы сериализация не делила ссылку с живым состоянием по ошибке.
   */
  function normalizeGestalt(g) {
    return {
      id: g.id,
      problemTreeId: g.problemTreeId || 0,
      situationId: g.situationId || 0,
      goalImageId: g.goalImageId || 0,
      automatismIds: Array.isArray(g.automatismIds) ? g.automatismIds.slice() : [],
      status: typeof g.status === 'number' ? g.status : 0,
      lastInsightPuls: typeof g.lastInsightPuls === 'number' ? g.lastInsightPuls : 0,
      lastInsightKind: typeof g.lastInsightKind === 'string' ? g.lastInsightKind : '',
      insightChainCount: typeof g.insightChainCount === 'number' ? g.insightChainCount : 0,
      analogyPool: Array.isArray(g.analogyPool) ? g.analogyPool.slice() : []
    };
  }

  /** Не более одной записи на goalImageId в GestaltArr (инвариант поддерживается при создании). */
  function findGestaltByGoal(goalImageId) {
    var gid = parseInt(goalImageId, 10) || 0;
    if (!gid) return null;
    for (var i = 0; i < global.GestaltArr.length; i++) {
      var g = global.GestaltArr[i];
      if (g && g.goalImageId === gid) return g;
    }
    return null;
  }

  /**
   * Открытый гештальт: status 0, 1 или 2.
   * TODO (сценарии): при «повторной» нерешённой задаче для той же цели после status 3 — либо новая запись
   * с новым id (если разрешить несколько строк на цель в архиве), либо reopen этой же записи (сброс status).
   * Сейчас: одна строка на goalImageId; после 3 обновления идут только через фидбек (редкое переоткрытие — вручную/API).
   */
  function isGestaltOpen(g) {
    return g && g.status !== GESTALT_STATUS.CLOSED_SUCCESS && g.status !== GESTALT_STATUS.ARCHIVED;
  }

  /** Добавляет ID автоматизма в список опробованных, без дубликатов. */
  function appendAutomatismIfNew(g, aid) {
    aid = parseInt(aid, 10) || 0;
    if (!aid) return false;
    if (!Array.isArray(g.automatismIds)) g.automatismIds = [];
    for (var i = 0; i < g.automatismIds.length; i++) {
      if (g.automatismIds[i] === aid) return false;
    }
    g.automatismIds.push(aid);
    return true;
  }

  /**
   * Обёртка над Consciousness_cycleAppendLog: третий аргумент — номер пульса (как в диспетчере).
   * Без force пишет только для главного цикла; force: true — для фонового и после expired (как ур.3).
   */
  function logToCycle(cycle, text, force) {
    if (!cycle || typeof global.Consciousness_cycleAppendLog !== 'function') return;
    if (!force && !cycle.isMainCycle) return;
    global.Consciousness_cycleAppendLog(cycle, text, puls(), force ? { force: true } : undefined);
  }

  /** Если цикл не передан — пробуем главный (события из ЭП без привязки к шагу ФО). */
  function logToCycleOrMain(cycle, text, force) {
    var c = cycle;
    if (!c && typeof global.Consciousness_getMainCycle === 'function') {
      c = global.Consciousness_getMainCycle();
    }
    logToCycle(c, text, force);
  }

  /**
   * Фиксирует инсайт в полях гештальта и пишет строку в лог цикла (если cycleForLog задан).
   * Всегда с force: true — инсайт важен, цикл ожидания часто expired / не главный.
   */
  function registerInsight(g, kind, detail, cycleForLog) {
    g.lastInsightPuls = puls();
    g.lastInsightKind = kind;
    g.insightChainCount = (g.insightChainCount || 0) + 1;
    var msg = 'Гештальт #' + g.id + ' (цель ' + g.goalImageId + '): инсайт [' + String(kind) + '] ' + String(detail || '');
    if (cycleForLog) logToCycle(cycleForLog, msg, true);
    markDirty();
    if (typeof global.info_insight === 'function') {
      global.info_insight(cycleForLog, { insightKind: kind, detail: detail });
    }
  }

  /**
   * Создать запись, если для goalImageId ещё нет строки.
   * @param {Object} [hint] — problemTreeId, situationId (подсказка из ветки/инфокартины)
   */
  function Gestalt_createOrGetForGoal(goalImageId, hint) {
    var gid = parseInt(goalImageId, 10) || 0;
    if (!gid) return null;
    var ex = findGestaltByGoal(gid);
    if (ex) return ex;
    hint = hint || {};
    var id = global.GestaltNextId++;
    var rec = normalizeGestalt({
      id: id,
      problemTreeId: hint.problemTreeId || 0,
      situationId: hint.situationId || 0,
      goalImageId: gid,
      automatismIds: [],
      status: GESTALT_STATUS.NEW,
      lastInsightPuls: 0,
      lastInsightKind: '',
      insightChainCount: 0,
      analogyPool: []
    });
    global.GestaltArr.push(rec);
    markDirty();
    logToCycleOrMain(null, 'Гештальт: создана запись #' + rec.id + ' для образа цели goalImageId=' + gid +
      (hint.problemTreeId ? (', ветка ' + hint.problemTreeId) : '') +
      (hint.situationId ? (', ситуация ' + hint.situationId) : '') + '.', false);
    return rec;
  }

  /**
   * Буфер резонанса: каждая запись — сигнатура закрытого кадра ЭП для кросс-цикловой аналогии.
   * @returns {Object} созданная сигнатура (для лога в вызывающем коде).
   *
   * TODO (расширение): goal_vector / precondition_mask из семантики — см. about_gestalt.md §6.
   */
  function Gestalt_pushResonanceSignature(entry) {
    var e = entry || {};
    var sig = {
      id: global.GestaltResonanceSeq++,
      atPuls: puls(),
      sourceCycleId: parseInt(e.sourceCycleId, 10) || 0,
      goalImageId: parseInt(e.goalImageId, 10) || 0,
      problemTreeId: parseInt(e.problemTreeId, 10) || 0,
      situationId: parseInt(e.situationId, 10) || 0,
      perceptionNodeId: parseInt(e.perceptionNodeId, 10) || 0,
      automatismId: parseInt(e.automatismId, 10) || 0,
      actionImageId: parseInt(e.actionImageId, 10) || 0,
      effect: typeof e.effect === 'number' && !isNaN(e.effect) ? e.effect : null,
      kind: e.kind || 'episode_close'
    };
    global.GestaltResonanceBuffer.push(sig);
    while (global.GestaltResonanceBuffer.length > MAX_RESONANCE_BUFFER) {
      global.GestaltResonanceBuffer.shift();
    }
    markDirty();
    return sig;
  }

  /**
   * Эвристика сходства сигнатуры буфера с гештальтом (v1: скалярные совпадения ID).
   * Чем выше, тем больше «это про мою задачу» для открытого гештальта.
   */
  function scoreResonanceAgainstGestalt(sig, g) {
    var score = 0;
    if (sig.goalImageId && g.goalImageId === sig.goalImageId) score += 1.0;
    var branch = sig.perceptionNodeId || sig.problemTreeId;
    if (branch && g.problemTreeId && branch === g.problemTreeId) score += 0.28;
    if (sig.situationId && g.situationId && sig.situationId === g.situationId) score += 0.35;
    if (!g.situationId && sig.situationId) score += 0.08;
    if (sig.effect !== null && sig.effect > 0) score += 0.12;
    return score;
  }

  /**
   * Обход буфера резонанса: для записей с effect > 0 и открытых гештальтов — оценка; при score ≥ порога —
   * инсайт аналогии, дополнение automatismIds и analogyPool («гештальт-сборка», упрощённо).
   *
   * @returns {number} число срабатываний (пар сигнатура×гештальт), прошедших порог и изменивших инсайт.
   *
   * TODO: помечать записи буфера consumed, чтобы не обходить повторно при неизменном буфере.
   */
  function Gestalt_processResonanceBufferForCycle(cycle) {
    var buf = global.GestaltResonanceBuffer;
    if (!buf.length) return 0;

    var applied = 0;
    for (var b = 0; b < buf.length; b++) {
      var sig = buf[b];
      if (!sig || sig.effect === null || sig.effect <= 0) continue;

      for (var i = 0; i < global.GestaltArr.length; i++) {
        var g = global.GestaltArr[i];
        if (!isGestaltOpen(g)) continue;

        var sc = scoreResonanceAgainstGestalt(sig, g);
        if (sc < RESONANCE_SCORE_INSIGHT) continue;

        if (sig.automatismId) appendAutomatismIfNew(g, sig.automatismId);

        if (!Array.isArray(g.analogyPool)) g.analogyPool = [];
        g.analogyPool.push({
          resonanceId: sig.id,
          atPuls: sig.atPuls,
          score: sc,
          automatismId: sig.automatismId || 0,
          sourceCycleId: sig.sourceCycleId || 0
        });
        while (g.analogyPool.length > MAX_ANALOGY_POOL) g.analogyPool.shift();

        if (g.status === GESTALT_STATUS.NEW) {
          g.status = GESTALT_STATUS.STARTED;
          registerInsight(g, INSIGHT_KIND.ANALOGY, 'резонанс score=' + sc.toFixed(2) + ' resonanceId=' + sig.id, cycle);
          applied++;
        } else if (g.status === GESTALT_STATUS.STARTED) {
          registerInsight(g, INSIGHT_KIND.ANALOGY, 'уточнение аналогии score=' + sc.toFixed(2), cycle);
          applied++;
        }

        if (sig.situationId && !g.situationId) g.situationId = sig.situationId;
        var br = sig.perceptionNodeId || sig.problemTreeId;
        if (br && !g.problemTreeId) g.problemTreeId = br;
      }
    }
    return applied;
  }

  /**
   * Ведущий открытый гештальт по образу цели.
   * Приоритет goalImageId: поле цикла (сессия ожидания ЭП), иначе InfoPicture.currentGoalId при целевом режиме цикла.
   */
  function Gestalt_bindActiveMaster(cycle) {
    var goalId = 0;
    if (cycle && typeof cycle.goalImageId === 'number' && cycle.goalImageId > 0) {
      goalId = cycle.goalImageId;
    }
    var info = global.InfoPicture;
    var cycleTarget;
    if (cycle) {
      cycleTarget = cycle.thinkingMode !== 'passive';
    } else if (typeof global.Consciousness_getMainCycleThinkingMode === 'function') {
      cycleTarget = global.Consciousness_getMainCycleThinkingMode() !== 'passive';
    } else {
      cycleTarget = true;
    }
    if (!goalId && cycleTarget && info && typeof info.currentGoalId === 'number' && info.currentGoalId > 0) {
      goalId = info.currentGoalId;
    }
    if (!goalId) {
      global.Gestalt_activeGestaltId = 0;
      global.Gestalt_activeMasterCycleId = 0;
      return null;
    }

    var hint = {
      situationId: info && typeof info.situationId === 'number' ? info.situationId : 0,
      problemTreeId: (cycle && cycle.context && typeof cycle.context.branchId === 'number') ? cycle.context.branchId : 0
    };
    var g = findGestaltByGoal(goalId);
    if (!g) {
      g = Gestalt_createOrGetForGoal(goalId, hint);
    } else if (isGestaltOpen(g)) {
      if (hint.situationId && !g.situationId) g.situationId = hint.situationId;
      if (hint.problemTreeId && !g.problemTreeId) g.problemTreeId = hint.problemTreeId;
    }

    if (g && isGestaltOpen(g)) {
      global.Gestalt_activeGestaltId = g.id;
      global.Gestalt_activeMasterCycleId = cycle ? cycle.id : 0;
    } else {
      global.Gestalt_activeGestaltId = 0;
      global.Gestalt_activeMasterCycleId = cycle ? cycle.id : 0;
    }
    return g && isGestaltOpen(g) ? g : null;
  }

  /**
   * Начало ур.3: заполняет GestaltContextForLevel3. Лог при смене пары id+status (без спама каждый пульс).
   */
  function Gestalt_prepareContextForCycle(cycle) {
    if (!cycle || cycle.status !== 'running') {
      global.GestaltContextForLevel3 = null;
      return;
    }
    var g = Gestalt_bindActiveMaster(cycle);
    if (g && cycle.isMainCycle) {
      global.GestaltContextForLevel3 = {
        gestaltId: g.id,
        goalImageId: g.goalImageId,
        gestaltStatus: g.status,
        problemTreeId: g.problemTreeId,
        situationId: g.situationId
      };
      var snap = String(g.id) + '_' + String(g.status);
      if (cycle._gestaltPrepareLogSnap !== snap) {
        cycle._gestaltPrepareLogSnap = snap;
        logToCycle(cycle, 'Гештальт: контекст для ур.3 — id=' + g.id + ', goal=' + g.goalImageId +
          ', status=' + g.status + ', ветка=' + g.problemTreeId + ', ситуация=' + g.situationId + '.', false);
      }
    } else {
      global.GestaltContextForLevel3 = null;
    }
  }

  /**
   * Тик ур.4: только главный цикл. Резонанс, пока status < PARTIAL; затем обновление контекста.
   */
  function Gestalt_onConsciousnessTick(cycle) {
    if (!cycle || cycle.status !== 'running' || !cycle.isMainCycle) return;

    /** Для диспетчера: совпадение с cpuls на этом шаге → expired по стагнации после гештальта (см. consciousness_dispatcher). */
    cycle.lastGestaltConsciencePuls = puls();

    var g = Gestalt_bindActiveMaster(cycle);
    var applied = 0;
    if (g && g.status < GESTALT_STATUS.PARTIAL) {
      applied = Gestalt_processResonanceBufferForCycle(cycle);
    }
    if (applied > 0) {
      logToCycle(cycle, 'Ур.4/гештальт: резонанс — обработано срабатываний по буферу: ' + applied + '.', false);
    }

    Gestalt_prepareContextForCycle(cycle);
  }

  /**
   * Закрытие кадра ЭП: буфер резонанса + обновление гештальта по goalImageId цикла ожидания.
   */
  function Gestalt_onEpisodeFeedbackClosed(payload) {
    var p = payload || {};
    var waitCycleId = parseInt(p.waitCycleId, 10) || 0;
    var last = p.lastFrame || {};
    var effect = typeof p.effect === 'number' && !isNaN(p.effect) ? p.effect : null;
    var autoId = parseInt(p.automatismId, 10) || 0;

    var perceptionNodeId = last.perceptionNodeId || 0;
    var situationId = last.situationId || 0;

    var oc = waitCycleId && typeof global.Consciousness_getCycle === 'function'
      ? global.Consciousness_getCycle(waitCycleId) : null;
    var goalId = oc && typeof oc.goalImageId === 'number' ? oc.goalImageId : 0;

    var pushedSig = Gestalt_pushResonanceSignature({
      sourceCycleId: waitCycleId,
      goalImageId: goalId,
      problemTreeId: perceptionNodeId,
      situationId: situationId,
      perceptionNodeId: perceptionNodeId,
      automatismId: autoId,
      actionImageId: last.actionImageId || 0,
      effect: effect,
      kind: effect !== null && effect > 0 ? 'positive_feedback' : 'non_positive'
    });

    if (pushedSig) {
      var logC = oc || (typeof global.Consciousness_getMainCycle === 'function' ? global.Consciousness_getMainCycle() : null);
      var forceEp = !!(logC && (!logC.isMainCycle || logC.expired));
      logToCycle(logC, 'Гештальт/ЭП: резонанс — сигнатура #' + pushedSig.id +
        ' (цикл ожидания=' + waitCycleId + ', goal=' + pushedSig.goalImageId + ', effect=' + String(pushedSig.effect) +
        ', авто=' + pushedSig.automatismId + ', len буфера=' + global.GestaltResonanceBuffer.length + ').', forceEp);
    }

    if (!goalId) {
      if (oc) {
        logToCycle(oc, 'Гештальт/ЭП: goalImageId не задан — запись гештальта не менялась (сигнатура в буфере есть).', true);
      }
      return;
    }

    var g = findGestaltByGoal(goalId);
    if (!g) {
      g = Gestalt_createOrGetForGoal(goalId, {
        problemTreeId: perceptionNodeId,
        situationId: situationId
      });
    }
    if (!g) return;

    if (!isGestaltOpen(g)) {
      logToCycle(oc, 'Гештальт: цель ' + goalId + ' уже закрыта (status≥3) — смена статуса пропущена.', true);
      return;
    }

    if (autoId) {
      var added = appendAutomatismIfNew(g, autoId);
      if (added && oc) {
        logToCycle(oc, 'Гештальт #' + g.id + ': в список опробованных добавлен автоматизм id=' + autoId + '.', true);
      }
    }

    if (effect !== null && effect > 0) {
      if (!g.problemTreeId && perceptionNodeId) g.problemTreeId = perceptionNodeId;
      if (!g.situationId && situationId) g.situationId = situationId;

      if (effect >= EFFECT_STRONG_CLOSE) {
        g.status = GESTALT_STATUS.CLOSED_SUCCESS;
        registerInsight(g, INSIGHT_KIND.FEEDBACK, 'сильный эффект ≥' + String(EFFECT_STRONG_CLOSE) + ' — гештальт закрыт (effect=' + String(effect) + ')', oc);
      } else if (g.status <= GESTALT_STATUS.STARTED) {
        g.status = GESTALT_STATUS.PARTIAL;
        registerInsight(g, INSIGHT_KIND.FEEDBACK, 'effect=' + String(effect) + ' auto=' + String(autoId) + ' — status→PARTIAL', oc);
      }
    } else if (effect !== null && effect <= 0) {
      registerInsight(g, INSIGHT_KIND.PRE, 'effect≤0 — переинтерпретация? (effect=' + String(effect) + ')', oc);
    }
    markDirty();
  }

  /**
   * Ур.2, цель «значение»: если открытый гештальт про эту цель или перечисляет автоматизм с данным ОД — повысить статус / инсайт.
   */
  function Gestalt_bumpFromMeaningForecast(meaningGoalId, actionImageId, frame, cycleForLog) {
    var mg = parseInt(meaningGoalId, 10) || 0;
    var act = parseInt(actionImageId, 10) || 0;
    if (!mg || !act) return;

    function automatismMatchesAction(automatismId, odId) {
      var aid = parseInt(automatismId, 10) || 0;
      if (!aid || !Array.isArray(global.Automatizms)) return false;
      for (var ai = 0; ai < global.Automatizms.length; ai++) {
        var az = global.Automatizms[ai];
        if (az && az.id === aid && az.actionsImageId === odId) return true;
      }
      return false;
    }

    for (var i = 0; i < global.GestaltArr.length; i++) {
      var g = global.GestaltArr[i];
      if (!g || !isGestaltOpen(g)) continue;
      var match = g.goalImageId === mg;
      if (!match && Array.isArray(g.automatismIds)) {
        for (var j = 0; j < g.automatismIds.length; j++) {
          if (automatismMatchesAction(g.automatismIds[j], act)) {
            match = true;
            break;
          }
        }
      }
      if (!match) continue;

      if (g.status === GESTALT_STATUS.NEW) {
        g.status = GESTALT_STATUS.STARTED;
        registerInsight(g, INSIGHT_KIND.LOCAL, 'прогноз по смыслу стимула, ОД=' + act, cycleForLog);
      } else if (g.status === GESTALT_STATUS.STARTED) {
        g.status = GESTALT_STATUS.PARTIAL;
        registerInsight(g, INSIGHT_KIND.LOCAL, 'уточнение прогноза, ОД=' + act, cycleForLog);
      } else if (g.status === GESTALT_STATUS.PARTIAL) {
        registerInsight(g, INSIGHT_KIND.LOCAL, 'подкрепление прогноза, ОД=' + act, cycleForLog);
      }
    }
  }

  // ---- IndexedDB: сейв по dirty + сравнение подписи состояния ----
  var db = null;
  var DB_NAME = 'BEAST_Gestalt_DB';
  var DB_VERSION = 1;
  var STORE = 'gestalt_state';
  var KEY = 'gestalt_singleton';

  function serializeState() {
    return {
      gestaltArr: global.GestaltArr.map(function (g) { return normalizeGestalt(g); }),
      nextId: global.GestaltNextId,
      resonanceBuffer: global.GestaltResonanceBuffer.slice(),
      resonanceSeq: global.GestaltResonanceSeq,
      activeMasterCycleId: global.Gestalt_activeMasterCycleId,
      activeGestaltId: global.Gestalt_activeGestaltId
    };
  }

  function applyState(state) {
    if (!state || typeof state !== 'object') return;
    global.GestaltArr = Array.isArray(state.gestaltArr) ? state.gestaltArr.map(normalizeGestalt) : [];
    global.GestaltNextId = typeof state.nextId === 'number' && state.nextId > 0 ? state.nextId : 1;
    global.GestaltResonanceBuffer = Array.isArray(state.resonanceBuffer) ? state.resonanceBuffer : [];
    global.GestaltResonanceSeq = typeof state.resonanceSeq === 'number' && state.resonanceSeq > 0 ? state.resonanceSeq : 1;
    global.Gestalt_activeMasterCycleId = typeof state.activeMasterCycleId === 'number' ? state.activeMasterCycleId : 0;
    global.Gestalt_activeGestaltId = typeof state.activeGestaltId === 'number' ? state.activeGestaltId : 0;
    lastSavedSig = JSON.stringify(serializeState());
    gestaltDirty = false;
    if (typeof global.Info_updateView === 'function') {
      try {
        global.Info_updateView();
      } catch (eInfo) {}
    }
  }

  function openDb() {
    if (!global.indexedDB || !global.IndexedDBModule) return Promise.reject(new Error('IndexedDB не доступен'));
    if (db) return Promise.resolve(db);
    return global.IndexedDBModule.openDatabase({
      dbName: DB_NAME,
      version: DB_VERSION,
      stores: [{ name: STORE, options: { keyPath: 'id' } }]
    }).then(function (d) {
      db = d;
      return d;
    });
  }

  function saveToIndexedDB(state) {
    if (!global.IndexedDBModule) return;
    state = state || serializeState();
    function put(database) {
      return global.IndexedDBModule.put({
        db: database,
        storeName: STORE,
        value: { id: KEY, state: state, ts: Date.now() }
      }).catch(function () {});
    }
    if (db) put(db);
    else openDb().then(put).catch(function () {});
  }

  function loadFromIndexedDB() {
    if (!global.IndexedDBModule) return;
    openDb()
      .then(function (d) {
        return global.IndexedDBModule.get({ db: d, storeName: STORE, key: KEY });
      })
      .then(function (record) {
        if (record && record.state) applyState(record.state);
      })
      .catch(function () {});
  }

  if (typeof global.GestaltContextForLevel3 === 'undefined') {
    global.GestaltContextForLevel3 = null;
  }

  if (typeof global.registerIndexedDBSaver === 'function') {
    global.registerIndexedDBSaver(function () {
      if (!gestaltDirty) return;
      var state = serializeState();
      var sig = JSON.stringify(state);
      if (sig === lastSavedSig) {
        gestaltDirty = false;
        return;
      }
      lastSavedSig = sig;
      gestaltDirty = false;
      saveToIndexedDB(state);
    });
  }

  loadFromIndexedDB();

  global.Gestalt_createOrGetForGoal = Gestalt_createOrGetForGoal;
  global.Gestalt_findGestaltByGoal = findGestaltByGoal;
  global.Gestalt_pushResonanceSignature = Gestalt_pushResonanceSignature;
  global.Gestalt_prepareContextForCycle = Gestalt_prepareContextForCycle;
  global.Gestalt_onConsciousnessTick = Gestalt_onConsciousnessTick;
  global.Gestalt_onEpisodeFeedbackClosed = Gestalt_onEpisodeFeedbackClosed;
  global.Gestalt_bindActiveMaster = Gestalt_bindActiveMaster;
  global.Gestalt_processResonanceBufferForCycle = Gestalt_processResonanceBufferForCycle;
  global.Gestalt_bumpFromMeaningForecast = Gestalt_bumpFromMeaningForecast;
})(window);

════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/goals.js
════════════════════════════════════════════════════════════
/* ОБРАЗЫ ЦЕЛИ
Теория.
Образ цели, по данным когнитивной нейронауки, возникает как обобщённая «модель желаемого состояния», извлечённая из эпизодических воспоминаний о связках «ситуация → действие → последствия» и переорганизованная системами контроля (ПФК) в форму, пригодную для планирования.
При повторении опыта гиппокамп строит не только пространственные, но и предсказательные карты переходов, где закодированы вероятности и ожидаемые последствия разных действий.
Обзор по RL и эпизодической памяти показывает, что люди и животные могут «прокручивать» прошлые эпизоды и комбинировать их, чтобы делать выводы о новых ситуациях — это функциональный аналог модель‑бейсд RL на эпизодическом представлении.
Лобные отделы (особенно дорсолатеральная и орбитофронтальная кора) по данным фМРТ и моделирования выступают как система управления, которая запрашивает у гиппокампа те эпизоды, которые релевантны текущей потребности/задаче, и модулирует, как они будут перекодированы.
При построении воображаемых будущих эпизодов люди сначала активируют обобщённое личное знание (семантический уровень), а потом подставляют эпизодические детали — по сути, рекомбинируют прошлые сцены в возможные будущие сценарии.
Из множества эпизодов с похожим итогом выделяется инвариантный «шаблон последствий»: что общего у всех удачных исходов в данной сфере.
ПФК абстрагирует этот шаблон от конкретных деталей эпизодов, оставляя «вектор» направления (какой результат должен быть достигнут и какие ограничения удерживать), что и есть образ цели. Этот образ хранится уже не как отдельный эпизод, а как более устойчивое семантико‑аффективное представление, связанное с личной значимостью (mPFC) и оценкой ценности (орбитофронтальная кора).
Образы разных потенциальных целей конкурируют за поведенческий контроль через сеть ценности (вентральный стриатум, орбитофронтальная кора), которая интегрирует ожидаемую награду, усилие, риск и задержку.
Симулированный образ должен получить мотивационный заряд, чтобы стать целью, а не просто фантазией.
1. Накопление: Гиппокамп и базальные ганглии фиксируют эпизоды с надежной связкой Действие → Последствие.
2. Абстракция: Префронтальная кора обобщает эпизоды в причинно-следственную модель мира.
3. Симуляция: Нейронные сети памяти рекомбинируют прошлый опыт, создавая сенсорный образ желаемого будущего состояния (Эпизодическое будущее мышление).
4. Валидация: Система вознаграждения оценивает этот образ на основе прошлого опыта удовлетворения потребностей. Если ценность высока, образ становится Целью.
5. Реализация: Исполнительные системы используют цель как референсную точку для планирования и коррекции действий.
Данные показывают, что если поврежден гиппокамп (нет эпизодической памяти), человек теряет способность формировать конкретные, личные цели на будущее, хотя может понимать социальные нормы. Это подтверждает, что образ цели неразрывно связан с реконструкцией эпизодов памяти, а не является чисто логической конструкцией.
Способность создавать новые цели из старых элементов — ключевая функция гиппокампа и префронтальной коры.
Реализация.
Создается поддержка массива образов цели следующим образом.
В основе - базовая гомеостатическая цель, если другая не активирована - ID витала, который нужно вернуть в норму. Эта самая простая цель, которая может использовать семантические модели понимания.
Жизненный опыт (перекрывает базовую) - ID образа цели, который содержит условие - ID ветки дерева (что позволяет делать условие все более общими если цель не достигается при точном соотвествии), ID образа восприятия, ID совершенного действия и значение эффекта, число опытов для усреднения эффекта. Фактически правило из эпизодов памяти или даже цепочка правил, приводящая к позитиву. Такие правила, как и базовая цель, реализуются на втором уровне осознания.
Произвольные цели - это искуссвтенно выстроенные цепочки правил, которые, предположительно, способны привести к позитиву. Так же содержит величину значимости эффекта (предположительную) и счетчик реализаций для усреднения эффекта.
Перед активацией произвольной цели система должна прогнать цепочку правил через модель мира и оценить суммарную ошибку предсказания с применением семантической модели. Всякий раз при попытке достигнуть такой цели, эффект корректируется.
Произвольная цель не должна создаваться «с нуля». Она должна валидироваться через проверку каждого звена цепочки на наличие подтвержденных правил из Уровня 2 (Жизненный опыт). Если звено цепочки не имеет эмпирического подтверждения, значимость всей цепочки должна резко падать (коэффициент ненадежности).
Если отклонение витального параметра превышает порог (аллостатическую нагрузку), система должна временно блокировать произвольные цели целью становится восстановление гомеостата. 
В систоянии Норма, если не получается выбрать или произвольно сформировать цель, то осознание переходит в пассивный режим.

Связь со вторым уровнем осмысления:
- В функции осмысления при переходе ко второму уровню в целевом режиме (проблема с автоматизмом) ставится цель — вызывается функция определения цели в данной ситуации (например Goals_determineGoalInSituation(context)).
- Новые образы цели формируются на втором уровне осмысления, при работе с эпизодами памяти (перебор/анализ кадров ЭП, выделение правил с позитивным эффектом и т.п.), а не при автоматическом закрытии кадра ЭП.

=== ПРЕДЛОЖЕНИЯ ПО РЕАЛИЗАЦИИ ===

1. Три типа целей и структуры данных
--------------------------------------
• Базовая (гомеостатическая) цель:
  - Не хранится в массиве; вычисляется на лету.
  - activeBaseGoalVitalId: number | null — ID витала, который нужно вернуть в норму.
  - Источник: при BadNormWell !== 2 по максимальному BadVitalsValue[vitalId] (или по importance)
    выбрать один витал-«мишень». Если BadNormWell === 2 и нет активной цели из опыта/произвольной —
    базовая цель не активируется (активна «норма»).

• Образ цели жизненного опыта (Life Experience Goal):
  - Массив global.GoalImagesLife = [] (или отдельный ключ в BEAST.persistentModules).
  - Цель трактуется как правило «в таких условиях → действие → позитив»: агрегаты (effect, count)
    и явное хранение происхождения из ЭП.
  - Элемент: {
      id: number,
      perceptionNodeId: number,   // ID ветки (терминального узла) дерева восприятия
      perceptionBranchId: number, // то же, что perceptionNodeId (явное имя для «ветки восприятия»)
      situationId: number,        // ID образа ситуации (дерево ситуаций), 0 — не привязано к ситуации
      stimulusImageId: number,    // образ восприятия (FinalImage)
      actionImageId: number,     // образ действия (ОД)
      effect: number,            // усреднённый эффект (как в ЭП)
      count: number,             // число опытов для усреднения
      usefulness: number,        // усреднённая полезность (отдельно от effect)
      usefulnessCount: number,  // число усреднений полезности
      lastUsedAt?: number,       // опционально — для приоритета/актуальности
      episodicRule: object|null, // один снимок правила с кадра ЭП (см. Goals_episodicRuleFromFrame)
      episodicRuleChain: array  // упорядоченная цепочка таких снимков — подряд идущие кадры ЭП,
                                // усиливающие ту же цель (индекс эпизода хранится в snapshot.episodeIndex)
    }
  - Формирование: при закрытии кадра ЭП с effect > 0 — создать или обновить запись по
    (perceptionNodeId, stimulusImageId, actionImageId, situationId): добавить effect в усреднение, count++.
  - Выбор активной цели: при conscience_level_2 по текущей ветке (и при необходимости по
    родительским узлам) искать GoalImagesLife с лучшим effect и подходящим условием (точное
    совпадение ветки или обобщённое по parentId).

• Произвольная цель (Voluntary Goal):
  - Массив global.GoalImagesVoluntary = [].
  - Элемент: {
      id: number,
      chain: Array<{ perceptionNodeId?, stimulusImageId?, actionImageId? }>,  // цепочка правил
      assumedEffect: number,     // предполагаемая значимость (корректируется по опыту)
      count: number,             // число попыток реализации для усреднения
      validated: boolean         // все звенья цепочки имеют подтверждение из GoalImagesLife/ЭП
    }
  - Валидация: перед активацией проверять каждое звено chain на наличие подтверждённого правила
    (кадр ЭП с effect > 0 или запись в GoalImagesLife). Если хотя бы одно звено без подтверждения —
    применять коэффициент ненадёжности (например, assumedEffect *= 0.2 или не выбирать цель).
  - Оценка: перед активацией «прогнать» цепочку через модель мира (текущее состояние дерева
    восприятия, семантика) и пересчитать суммарную ошибку предсказания; при каждой попытке
    реализации обновлять assumedEffect по фактическому effect.

2. Точки интеграции в существующий код
--------------------------------------
• prepare.js (или отдельный goals.js):
  - Функция getActiveBaseGoalVitalId(): при BadNormWell === 1 (Плохо) вернуть vitalId с макс.
    BadVitalsValue; при BadNormWell === 3 (Хорошо) можно не задавать базовую цель «вернуть в норму»
    или трактовать по ТЗ. При BadNormWell === 2 вернуть null.

• episodic_memory.js:
  - Образы цели в ЭП не создаются при закрытии кадра (closeLastFrame). Второй уровень осмысления
    при работе с эпизодами памяти (перебор Episodes, анализ effect) сам формирует и обновляет
    записи в GoalImagesLife (и при необходимости цепочки для произвольных целей).

• conscience_level_2.js:
  - При входе во второй уровень в целевом режиме (проблема с автоматизмом) первым делом
    вызывается функция определения цели в данной ситуации: Goals_determineGoalInSituation(context).
    Она возвращает/устанавливает текущую цель (базовая, из жизненного опыта или произвольная).
  - В начале: проверить allostatic block (см. п. 4). Если порог превышен — активная цель только
    базовая (восстановление гомеостаза), выход.
  - Выбор цели по приоритету: 1) базовая (если activeBaseGoalVitalId); 2) жизненный опыт —
    поиск по текущей ветке/родителям в GoalImagesLife, выбор с лучшим effect; 3) произвольная —
    только если validated и assumedEffect после коэффициента выше порога.
  - Реализация выбранной цели: для базовой — передать управление в существующие механизмы
    (виталы/контексты); для жизненного опыта — запустить actionImageId как при автоматизме
    (через runActionImage) и ждать оценки в ЭП; для произвольной — пошаговый запуск цепочки
    с проверкой после каждого шага.
  - Формирование новых образов цели: выполняется на втором уровне при работе с эпизодами
    памяти (анализ Episodes, выделение правил с позитивным эффектом, обобщение по веткам
    дерева), а не в closeLastFrame ЭП.
  - Если цель не выбрана и не сформирована (Норма, нет подходящих целей) — установить
    thinkingMode = 'passive' у главного цикла ФО и выйти.

• InfoPicture / informing.js:
  - Добавить поле currentGoalType: 'base' | 'life' | 'voluntary' | null и currentGoalId (или
    currentGoalVitalId для базы). Обновлять при смене цели в conscience_level_2.

3. Обобщение условия для целей жизненного опыта
----------------------------------------------
• Если при точном совпадении perceptionNodeId подходящей цели нет — искать по parentId узла
  дерева (PerceptionTree_getNode(branchId).parentId) и так рекурсивно до корня. Первая
  найденная запись в GoalImagesLife с effect > 0 и подходящим actionImageId даёт «более общее»
  правило. Это соответствует тексту «условие все более общие если цель не достигается».

4. Аллостатическая нагрузка и блок произвольных целей
----------------------------------------------------
• Ввести порог ALLOSTATIC_THRESHOLD (например, сумма BadVitalsValue по всем виталам или макс.
  значение). Если порог превышен — в conscience_level_2 не рассматривать произвольные цели и
  цели жизненного опыта, не связанные с восстановлением витала; активной считать только
  базовую цель (activeBaseGoalVitalId). Порог можно хранить в goals.js и сделать настраиваемым.

5. Порядок реализации
---------------------
a) Реализовать getActiveBaseGoalVitalId() и использование базовой цели в conscience_level_2
   (выбор «вернуть витал в норму» как единственной цели при Плохо и при превышении порога).
b) Функция Goals_determineGoalInSituation(context): вызывается при входе в conscience_level_2;
   определяет и устанавливает цель в данной ситуации (базовая / жизненный опыт / произвольная).
c) Образы цели жизненного опыта: формируются на втором уровне при работе с эпизодами (анализ
   Episodes, выделение правил с effect > 0, запись в GoalImagesLife). Сохранение/загрузка в
   BEAST.persistentModules.
d) В conscience_level_2: после определения цели — выбор и реализация (базовая / жизненный опыт
   по текущей ветке и родителям / произвольная); обновление InfoPicture (currentGoalType/currentGoalId).
e) Структура GoalImagesVoluntary + валидация цепочки по ЭП/GoalImagesLife; блок при
   превышении ALLOSTATIC_THRESHOLD; переход в passive при отсутствии выбранной цели в Норма.

6. Дополнительно
----------------
• Уточнить по ТЗ: при Хорошо (BadNormWell === 3) базовая цель «вернуть в норму» не ставится —
  тогда getActiveBaseGoalVitalId() возвращает ненулевое значение только при BadNormWell === 1.
• Цели жизненного опыта можно ограничить по count (например, не предлагать к выбору при
  count < 2) или по порогу effect, чтобы не активировать слабые правила.

*/

(function (global) {
  'use strict';

  /** Умолчательная цель смысла: ключ (perceptionNodeId=0, situationId=0, actionImageId=-1). */
  var GOAL_KIND_MEANING_DEFAULT = 'meaning_default';
  var GOALS_DEFAULT_MEANING_LABEL = 'что для меня означает это?';

  /** Умолчательная цель «Плачь»: perceptionNodeId=0, situationId=0, ОД с BasicActions Id=1 (Плачет). */
  var GOAL_KIND_CRY_DEFAULT = 'cry_default';
  var GOALS_DEFAULT_CRY_LABEL = 'Плачь';

  if (!Array.isArray(global.GoalImagesLife)) {
    global.GoalImagesLife = [];
  }

  var nextId = 1;
  /** Индекс: perceptionNodeId -> массив образов цели для этой ветки (для быстрого поиска). */
  var byPerceptionNodeId = Object.create(null);
  /** Уникальность по (perceptionNodeId, stimulusImageId, actionImageId) -> объект цели. */
  var byKey = Object.create(null);
  var goalsDirty = false;
  var lastGoalsSavedSig = '';

  function lifeGoalKey(perceptionNodeId, stimulusImageId, actionImageId, situationId) {
    return (perceptionNodeId || 0) + '|' + (stimulusImageId || 0) + '|' + (actionImageId || 0) +
      '|' + (situationId || 0);
  }

  function normalizeLifeGoalRecord(g) {
    if (!g) return g;
    var pn = g.perceptionNodeId != null ? g.perceptionNodeId : (g.perceptionBranchId || 0);
    g.perceptionNodeId = pn;
    g.perceptionBranchId = pn;
    if (g.situationId == null) g.situationId = 0;
    if (g.usefulness == null) g.usefulness = 0;
    if (g.usefulnessCount == null) g.usefulnessCount = 0;
    if (g.episodicRule !== null && g.episodicRule !== undefined && typeof g.episodicRule !== 'object') {
      g.episodicRule = null;
    }
    if (!Array.isArray(g.episodicRuleChain)) g.episodicRuleChain = [];
    if (g.goalKind == null) g.goalKind = '';
    if (g.goalLabel == null) g.goalLabel = '';
    return g;
  }

  /**
   * Один «снимок правила» из кадра ЭП: условия + действие + исход кадра (+ ответ оператора при наличии).
   * Не включает служебные поля кадра (_goalsProcessed и т.д.).
   * @param {Object} frame
   * @param {number} [episodeIndex] — индекс в global.Episodes для сборки цепочки подряд идущих кадров
   * @returns {Object|null}
   */
  function episodicRuleFromFrame(frame, episodeIndex) {
    if (!frame) return null;
    var r = {
      perceptionNodeId: frame.perceptionNodeId != null ? frame.perceptionNodeId : 0,
      stimulusImageId: frame.stimulusImageId != null ? frame.stimulusImageId : 0,
      situationId: frame.situationId != null ? frame.situationId : 0,
      actionImageId: frame.actionImageId != null ? frame.actionImageId : 0,
      effect: typeof frame.effect === 'number' && !isNaN(frame.effect) ? frame.effect : null,
      responseStimulusImageId: frame.responseStimulusImageId != null ? frame.responseStimulusImageId : 0
    };
    if (episodeIndex != null && typeof episodeIndex === 'number' && !isNaN(episodeIndex)) {
      r.episodeIndex = episodeIndex;
    }
    return r;
  }

  function cloneEpisodicRule(rule) {
    if (!rule || typeof rule !== 'object') return null;
    try {
      return JSON.parse(JSON.stringify(rule));
    } catch (e) {
      return episodicRuleFromFrame(rule, rule.episodeIndex);
    }
  }

  /**
   * Обновить снимок правила и при возможности — цепочку: новое звено только если episodeIndex
   * на 1 больше индекса последнего звена (подряд идущие кадры в Episodes).
   * Пустая цепочка (старые сохранения): первое обновление добавляет текущий снимок как первое звено.
   */
  function mergeEpisodicRuleChainOnUpdate(goal, frame, meta) {
    if (!goal || !frame) return;
    var snap = episodicRuleFromFrame(frame, meta && typeof meta.episodeIndex === 'number' ? meta.episodeIndex : undefined);
    if (!snap) return;
    goal.episodicRule = snap;
    if (!Array.isArray(goal.episodicRuleChain)) goal.episodicRuleChain = [];
    if (goal.episodicRuleChain.length === 0) {
      goal.episodicRuleChain.push(cloneEpisodicRule(snap));
      return;
    }
    var idx = meta && typeof meta.episodeIndex === 'number' ? meta.episodeIndex : null;
    var last = goal.episodicRuleChain[goal.episodicRuleChain.length - 1];
    if (idx == null || typeof last !== 'object' || last == null || typeof last.episodeIndex !== 'number') {
      return;
    }
    if (idx === last.episodeIndex + 1) {
      goal.episodicRuleChain.push(cloneEpisodicRule(snap));
    }
  }

  /** Добавить образец полезности в скользящее среднее цели. */
  function mergeUsefulnessSample(goal, sample) {
    if (!goal || typeof sample !== 'number' || isNaN(sample)) return;
    var oc = goal.usefulnessCount || 0;
    var n = oc + 1;
    goal.usefulness = ((goal.usefulness || 0) * oc + sample) / n;
    goal.usefulnessCount = n;
  }

  function rebuildIndex() {
    byPerceptionNodeId = Object.create(null);
    byKey = Object.create(null);
    var arr = global.GoalImagesLife;
    if (!Array.isArray(arr)) return;
    for (var i = 0; i < arr.length; i++) {
      var g = arr[i];
      if (!g || g.perceptionNodeId == null) continue;
      var key = String(g.perceptionNodeId);
      if (!byPerceptionNodeId[key]) byPerceptionNodeId[key] = [];
      byPerceptionNodeId[key].push(g);
      normalizeLifeGoalRecord(g);
      var k = lifeGoalKey(g.perceptionNodeId, g.stimulusImageId, g.actionImageId, g.situationId);
      byKey[k] = g;
    }
  }

  function getNextId() {
    var max = 0;
    var arr = global.GoalImagesLife;
    for (var i = 0; i < (arr && arr.length) || 0; i++) {
      if (arr[i] && arr[i].id > max) max = arr[i].id;
    }
    return max + 1;
  }

  /**
   * Добавить или обновить образ цели жизненного опыта по кадру эпизода (effect > 0).
   * Уникальность по (perceptionNodeId, stimulusImageId, actionImageId, situationId). Усреднение effect по count.
   * В цель записывается episodicRule (последний снимок кадра) и при нумерации эпизодов — наращивается episodicRuleChain.
   * @param {Object} frame — кадр ЭП
   * @param {Object} [meta] — { episodeIndex: number } индекс кадра в global.Episodes для цепочки подряд идущих правил
   * @returns {number} id образа цели (существующего или нового)
   */
  function addOrUpdateLifeGoalFromFrame(frame, meta) {
    if (!frame || typeof frame.effect !== 'number' || frame.effect <= 0) return 0;
    var perceptionNodeId = frame.perceptionNodeId != null ? frame.perceptionNodeId : 0;
    var stimulusImageId = frame.stimulusImageId != null ? frame.stimulusImageId : 0;
    var actionImageId = frame.actionImageId != null ? frame.actionImageId : 0;
    var situationId = frame.situationId != null ? frame.situationId : 0;
    if (!perceptionNodeId || !actionImageId) return 0;

    var k = lifeGoalKey(perceptionNodeId, stimulusImageId, actionImageId, situationId);
    var existing = byKey[k];
    if (existing) {
      var n = (existing.count || 0) + 1;
      existing.effect = ((existing.effect || 0) * (existing.count || 0) + frame.effect) / n;
      existing.count = n;
      mergeUsefulnessSample(existing, frame.usefulness);
      existing.lastUsedAt = Date.now();
      normalizeLifeGoalRecord(existing);
      mergeEpisodicRuleChainOnUpdate(existing, frame, meta || {});
      goalsDirty = true;
      return existing.id;
    }

    var id = getNextId();
    var snap = episodicRuleFromFrame(frame, meta && typeof meta.episodeIndex === 'number' ? meta.episodeIndex : undefined);
    var goal = {
      id: id,
      perceptionNodeId: perceptionNodeId,
      perceptionBranchId: perceptionNodeId,
      situationId: situationId,
      stimulusImageId: stimulusImageId,
      actionImageId: actionImageId,
      effect: frame.effect,
      count: 1,
      usefulness: 0,
      usefulnessCount: 0,
      lastUsedAt: Date.now(),
      episodicRule: snap,
      episodicRuleChain: snap ? [cloneEpisodicRule(snap)] : []
    };
    normalizeLifeGoalRecord(goal);
    mergeUsefulnessSample(goal, frame.usefulness);
    global.GoalImagesLife.push(goal);
    rebuildIndex();
    goalsDirty = true;
    return id;
  }

  /**
   * Явно добавить в цепочку правил образа цели ещё один снимок (порядок задаётся вызывающим кодом).
   * Обновляет episodicRule на этот снимок.
   * @param {number} goalId
   * @param {Object} frame — кадр ЭП или объект полей как у episodicRuleFromFrame
   * @param {number} [episodeIndex]
   * @returns {boolean}
   */
  function appendLifeGoalEpisodicRuleLink(goalId, frame, episodeIndex) {
    var g = getGoalById(goalId);
    if (!g || !frame) return false;
    var snap = episodicRuleFromFrame(frame, episodeIndex);
    if (!snap) return false;
    normalizeLifeGoalRecord(g);
    g.episodicRule = snap;
    g.episodicRuleChain.push(cloneEpisodicRule(snap));
    goalsDirty = true;
    return true;
  }

  /**
   * Усреднить полезность образа цели (GoalImagesLife) по одному образцу.
   * @param {number} goalId
   * @param {number} sample
   */
  function mergeUsefulnessSampleForGoalId(goalId, sample) {
    var g = getGoalById(goalId);
    if (!g || typeof sample !== 'number' || isNaN(sample)) return;
    mergeUsefulnessSample(g, sample);
    goalsDirty = true;
  }

  /**
   * Корректировка usefulness после закрытия кадра ожидания по effect эпизода (ответ оператора).
   * При count === 1 usefulness приравнивается к эффекту (без усреднения с прошлым).
   * При count &gt; 1 — mergeUsefulnessSample (накапливается среднее).
   * При таймауте ожидания без ответа функция не вызывается (пайплайн ур.2 не запускается).
   * @param {number} goalId
   * @param {number} episodeEffect
   */
  function applyWaitingPeriodEffectToGoalUsefulness(goalId, episodeEffect) {
    var g = getGoalById(goalId);
    if (!g || typeof episodeEffect !== 'number' || isNaN(episodeEffect)) return;
    if ((g.count || 0) === 1) {
      g.usefulness = episodeEffect;
      g.usefulnessCount = 1;
    } else {
      mergeUsefulnessSample(g, episodeEffect);
    }
    goalsDirty = true;
  }

  /**
   * Получить образ цели жизненного опыта по id.
   * @param {number} id
   * @returns {Object|null}
   */
  function getGoalById(id) {
    if (id == null || id === 0) return null;
    var arr = global.GoalImagesLife;
    if (!Array.isArray(arr)) return null;
    for (var i = 0; i < arr.length; i++) {
      if (arr[i] && arr[i].id === id) return arr[i];
    }
    return null;
  }

  function lifeGoalMatchesSituationFilter(g, situationFilter) {
    if (situationFilter == null || situationFilter === 0) return true;
    return (g.situationId || 0) === situationFilter;
  }

  /**
   * Найти лучший образ цели жизненного опыта для ветки: сначала точное совпадение,
   * затем по цепочке родительских узлов. Возвращает цель с максимальным effect (count >= minCount).
   * @param {number} branchId — текущий терминальный узел дерева восприятия
   * @param {number} [minCount=1] — минимальное число опытов для учёта
   * @param {number} [situationId=0] — если 0, ситуация не фильтрует; иначе только цели с этим situationId
   * @returns {Object|null}
   */
  function getBestLifeGoalForBranch(branchId, minCount, situationId) {
    if (branchId == null || branchId === 0) return null;
    minCount = minCount != null ? minCount : 1;
    var situationFilter = situationId != null ? situationId : 0;
    var getNode = global.PerceptionTree_getNode;
    if (typeof getNode !== 'function') return null;

    var nodeId = branchId;
    var best = null;
    var bestEffect = -Infinity;

    while (nodeId) {
      var list = byPerceptionNodeId[String(nodeId)];
      if (Array.isArray(list)) {
        for (var i = 0; i < list.length; i++) {
          var g = list[i];
          if (!g || (g.count || 0) < minCount || typeof g.effect !== 'number') continue;
          if (!lifeGoalMatchesSituationFilter(g, situationFilter)) continue;
          if (g.effect > bestEffect) {
            bestEffect = g.effect;
            best = g;
          }
        }
        if (best) return best;
      }
      var node = getNode(nodeId);
      nodeId = node && typeof node.parentId === 'number' && node.parentId !== 0 ? node.parentId : null;
    }
    return null;
  }

  /**
   * Поиск образа цели жизненного опыта по ветке восприятия и (опционально) образу ситуации.
   * При situationId === 0 условие по ситуации не применяется (подходят любые situationId у записей).
   * При ненулевом situationId учитываются только цели с тем же situationId.
   * Из подходящих выбирается запись с максимальным effect (при равенстве — первая по порядку в массиве).
   * @param {number} perceptionNodeId
   * @param {number} situationId
   * @returns {Object|null}
   */
  function findLifeGoalByPerceptionAndSituation(perceptionNodeId, situationId) {
    perceptionNodeId = parseInt(perceptionNodeId, 10) || 0;
    situationId = parseInt(situationId, 10) || 0;
    if (!perceptionNodeId) return null;
    var arr = global.GoalImagesLife;
    if (!Array.isArray(arr) || !arr.length) return null;
    var best = null;
    var bestEffect = -Infinity;
    for (var i = 0; i < arr.length; i++) {
      var g = arr[i];
      if (!g) continue;
      var pn = g.perceptionNodeId != null ? g.perceptionNodeId : (g.perceptionBranchId || 0);
      if (pn !== perceptionNodeId) continue;
      if (situationId !== 0 && (g.situationId || 0) !== situationId) continue;
      if (typeof g.effect !== 'number') continue;
      if (g.effect > bestEffect) {
        bestEffect = g.effect;
        best = g;
      }
    }
    return best;
  }

  function goalUsefulnessPositive(g) {
    return g && typeof g.usefulness === 'number' && !isNaN(g.usefulness) && g.usefulness > 0;
  }

  /**
   * Образ цели жизненного опыта с положительной полезностью (режим выживания, ур.2).
   * 1) Сначала perceptionNodeId и situationId (если situationId === 0 — только цели с situationId 0).
   * 2) Иначе любая цель с тем же perceptionNodeId.
   * Из кандидатов — максимум usefulness (при равенстве — больший effect).
   */
  function findBestLifeGoalPositiveUsefulness(perceptionNodeId, situationId) {
    perceptionNodeId = parseInt(perceptionNodeId, 10) || 0;
    situationId = parseInt(situationId, 10) || 0;
    if (!perceptionNodeId) return null;
    var arr = global.GoalImagesLife;
    if (!Array.isArray(arr) || !arr.length) return null;
    function pickBest(filterSit) {
      var best = null;
      var bestU = -Infinity;
      for (var i = 0; i < arr.length; i++) {
        var g = arr[i];
        if (!g || !goalUsefulnessPositive(g)) continue;
        var pn = g.perceptionNodeId != null ? g.perceptionNodeId : (g.perceptionBranchId || 0);
        if (pn !== perceptionNodeId) continue;
        var gs = g.situationId != null ? g.situationId : 0;
        if (!filterSit(gs)) continue;
        var u = g.usefulness;
        var eff = typeof g.effect === 'number' ? g.effect : 0;
        var bestEff = best && typeof best.effect === 'number' ? best.effect : 0;
        if (u > bestU || (u === bestU && eff > bestEff)) {
          bestU = u;
          best = g;
        }
      }
      return best;
    }
    var tier1 = pickBest(function (gs) {
      return situationId === 0 ? gs === 0 : gs === situationId;
    });
    if (tier1) return tier1;
    return pickBest(function () { return true; });
  }

  /**
   * Базовая гомеостатическая цель: ID витала, который нужно вернуть в норму.
   * Только при BadNormWell === 1 (Плохо); при Норма/Хорошо возвращает 0.
   * @returns {number} vitalId с максимальным BadVitalsValue или 0
   */
  function getActiveBaseGoalVitalId() {
    if (global.BadNormWell !== 1) return 0;
    var badVitals = global.BadVitalsValue;
    if (!badVitals || typeof badVitals !== 'object') return 0;
    var vitalsArr = global.VitalsArr;
    if (!Array.isArray(vitalsArr) || !vitalsArr.length) return 0;
    var maxK = 0;
    var vitalId = 0;
    for (var v = 0; v < vitalsArr.length; v++) {
      var vid = vitalsArr[v].Id;
      var k = badVitals[vid] || badVitals[String(vid)] || 0;
      if (k > maxK) {
        maxK = k;
        vitalId = vid;
      }
    }
    return vitalId;
  }

  function contextSignificance(ctx) {
    if (!ctx) return 0;
    if (typeof ctx.significance === 'number' && !isNaN(ctx.significance)) return Math.abs(ctx.significance);
    var sc = ctx.sourceContext || (ctx.sourceCycle && ctx.sourceCycle.context);
    if (sc && typeof sc.significance === 'number' && !isNaN(sc.significance)) return Math.abs(sc.significance);
    return 0;
  }

  function actualStimulusIdFromContext(ctx) {
    if (!ctx) return 0;
    var a = ctx.actualId != null ? ctx.actualId : ctx.actual_stimul_ID;
    if (a != null) {
      var n = parseInt(a, 10);
      return isNaN(n) ? 0 : n;
    }
    var sc = ctx.sourceContext || (ctx.sourceCycle && ctx.sourceCycle.context);
    if (sc && sc.actual_stimul_ID != null) {
      var n2 = parseInt(sc.actual_stimul_ID, 10);
      return isNaN(n2) ? 0 : n2;
    }
    return 0;
  }

  /**
   * Запись умолчательной цели «что для меня означает это?» (если ещё нет).
   * Ключ: perceptionNodeId=0, situationId=0, actionImageId=-1, stimulusImageId=0.
   */
  function findDefaultMeaningGoal() {
    var arr = global.GoalImagesLife;
    if (!Array.isArray(arr)) return null;
    for (var i = 0; i < arr.length; i++) {
      var g = arr[i];
      if (!g) continue;
      if (g.goalKind === GOAL_KIND_MEANING_DEFAULT) return normalizeLifeGoalRecord(g);
      var pn = g.perceptionNodeId != null ? g.perceptionNodeId : 0;
      var sn = g.situationId != null ? g.situationId : 0;
      var ai = g.actionImageId != null ? g.actionImageId : 0;
      if (pn === 0 && sn === 0 && ai === -1) return normalizeLifeGoalRecord(g);
    }
    return null;
  }

  function ensureDefaultMeaningGoal() {
    if (findDefaultMeaningGoal()) return;
    var id = getNextId();
    var goal = {
      id: id,
      perceptionNodeId: 0,
      perceptionBranchId: 0,
      situationId: 0,
      stimulusImageId: 0,
      actionImageId: -1,
      effect: 0,
      count: 1,
      usefulness: 0,
      usefulnessCount: 0,
      goalKind: GOAL_KIND_MEANING_DEFAULT,
      goalLabel: GOALS_DEFAULT_MEANING_LABEL,
      episodicRule: null,
      episodicRuleChain: []
    };
    normalizeLifeGoalRecord(goal);
    global.GoalImagesLife.push(goal);
    rebuildIndex();
    goalsDirty = true;
  }

  function findDefaultCryGoal() {
    var arr = global.GoalImagesLife;
    if (!Array.isArray(arr)) return null;
    for (var i = 0; i < arr.length; i++) {
      var g = arr[i];
      if (!g) continue;
      if (g.goalKind === GOAL_KIND_CRY_DEFAULT) return normalizeLifeGoalRecord(g);
    }
    return null;
  }

  /**
   * Образ цели «Плачь» (если ещё нет): ОД для finalImageId=0 и actionIds=[1] (BasicActions «Плачет»).
   * Требует загруженный модуль actions_image.js (ActionImages_getOrCreateActionImageId).
   */
  function ensureDefaultCryGoal() {
    if (typeof global.ActionImages_getOrCreateActionImageId !== 'function') return;
    var cryOd = global.ActionImages_getOrCreateActionImageId(0, [1], '');
    if (!cryOd) return;
    var existing = findDefaultCryGoal();
    if (existing) {
      if (existing.actionImageId !== cryOd) {
        existing.actionImageId = cryOd;
        rebuildIndex();
        goalsDirty = true;
      }
      return;
    }
    var id = getNextId();
    var goal = {
      id: id,
      perceptionNodeId: 0,
      perceptionBranchId: 0,
      situationId: 0,
      stimulusImageId: 0,
      actionImageId: cryOd,
      effect: 0,
      count: 1,
      usefulness: 0,
      usefulnessCount: 0,
      goalKind: GOAL_KIND_CRY_DEFAULT,
      goalLabel: GOALS_DEFAULT_CRY_LABEL,
      episodicRule: null,
      episodicRuleChain: []
    };
    normalizeLifeGoalRecord(goal);
    global.GoalImagesLife.push(goal);
    rebuildIndex();
    goalsDirty = true;
  }

  function getDefaultCryGoalId() {
    var g = findDefaultCryGoal();
    return g && g.id ? g.id : 0;
  }

  function isDefaultCryGoalId(goalId) {
    if (goalId == null || goalId === 0) return false;
    var g = getGoalById(goalId);
    if (!g) return false;
    return g.goalKind === GOAL_KIND_CRY_DEFAULT;
  }

  function isDefaultMeaningGoalId(id) {
    if (id == null || id === 0) return false;
    var g = getGoalById(id);
    if (!g) return false;
    if (g.goalKind === GOAL_KIND_MEANING_DEFAULT) return true;
    var pn = g.perceptionNodeId != null ? g.perceptionNodeId : 0;
    var sn = g.situationId != null ? g.situationId : 0;
    return pn === 0 && sn === 0 && g.actionImageId === -1;
  }

  function getDefaultMeaningGoalId() {
    var g = findDefaultMeaningGoal();
    return g && g.id ? g.id : 0;
  }

  /**
   * Установить текущую цель в инфокартине (currentGoalType, currentGoalId, currentGoalVitalId).
   */
  function setCurrentGoalInInfoPicture(goalType, goalId, vitalId) {
    var info = global.InfoPicture;
    if (!info) return;
    info.currentGoalType = goalType || null;
    info.currentGoalId = goalId != null ? goalId : 0;
    info.currentGoalVitalId = vitalId != null ? vitalId : 0;
    if (typeof global.Info_updateView === 'function') {
      global.Info_updateView();
    }
  }

  /**
   * Определение цели в данной ситуации (первый шаг итерации второго уровня в целевом режиме).
   * Приоритет: 1) базовая (витал при Плохо); 2) образ из жизненного опыта по ветке/родителям;
   * 3) при значимости стимула &gt; 3 и наличии актуального стимула — умолчательная цель «значение» (meaning);
   * 4) иначе сброс.
   */
  function Goals_determineGoalInSituation(context) {
    if (!context) return;

    var baseVitalId = getActiveBaseGoalVitalId();
    if (baseVitalId) {
      setCurrentGoalInInfoPicture('base', 0, baseVitalId);
      return;
    }

    var branchId = context.branchId != null ? context.branchId : 0;
    var sitId = 0;
    if (global.InfoPicture && typeof global.InfoPicture.situationId === 'number') {
      sitId = global.InfoPicture.situationId || 0;
    }
    var lifeGoal = getBestLifeGoalForBranch(branchId, 1, sitId);
    if (lifeGoal && lifeGoal.id) {
      setCurrentGoalInInfoPicture('life', lifeGoal.id, 0);
      return;
    }

    ensureDefaultMeaningGoal();
    var stim = actualStimulusIdFromContext(context);
    var sig = contextSignificance(context);
    var meaningG = findDefaultMeaningGoal();
    if (meaningG && meaningG.id && stim > 0 && sig > 3) {
      setCurrentGoalInInfoPicture('meaning', meaningG.id, 0);
      return;
    }

    setCurrentGoalInInfoPicture(null, 0, 0);
  }

  /**
   * Поиск решения в эпизодической памяти: перебор кадров, вызов Goals_considerNewGoalFromEpisode.
   */
  function Goals_runEpisodicSearch(context) {
    if (!context) return;
    var episodes = global.Episodes;
    if (!Array.isArray(episodes) || !episodes.length) return;
    var limit = Math.min(50, episodes.length);
    for (var i = episodes.length - 1; i >= 0 && episodes.length - i <= limit; i--) {
      var frame = episodes[i];
      if (frame) Goals_considerNewGoalFromEpisode(frame, context, i);
    }
  }

  /**
   * Рассмотрение кадра эпизода: при effect > 0 и наличии полей — создать/обновить GoalImagesLife.
   * Каждый кадр учитывается один раз (по флагу _goalsProcessed), чтобы не сохранять цели каждый пульс.
   * @param {number} [episodeIndex] — индекс в Episodes[] для episodicRuleChain (подряд идущие кадры).
   */
  function Goals_considerNewGoalFromEpisode(frame, context, episodeIndex) {
    if (!frame) return;
    if (frame._goalsProcessed) return;
    addOrUpdateLifeGoalFromFrame(frame, episodeIndex != null && typeof episodeIndex === 'number'
      ? { episodeIndex: episodeIndex }
      : null);
    frame._goalsProcessed = true;
  }

  /**
   * Пассивный режим: семантическая обработка моделей понимания; новые образы ситуаций — в situatioms_tree.
   */
  function PassiveMode_processUnderstandingModels() {
    if (typeof global.Consciousness_getMainCycleThinkingMode !== 'function' ||
        global.Consciousness_getMainCycleThinkingMode() !== 'passive') return;
    if (typeof global.SituationsTree_considerNewSituationFromUnderstanding === 'function') {
      global.SituationsTree_considerNewSituationFromUnderstanding();
    }
  }

  /**
   * Вызвать при исчерпании сценариев пассивного режима: понижает значимость фоновых циклов ФО на 50%
   * (оперативная память о прошлых мышлениях).
   */
  function PassiveMode_notifyScenariosExhausted() {
    if (typeof global.Consciousness_decayBackgroundOperativeMemory === 'function') {
      global.Consciousness_decayBackgroundOperativeMemory(0.5);
    }
  }

  // --- Сериализация и восстановление ---
  function serializeState() {
    var arr = global.GoalImagesLife;
    if (!Array.isArray(arr)) return { goals: [], nextId: 1 };
    var list = [];
    for (var i = 0; i < arr.length; i++) {
      var g = arr[i];
      if (!g) continue;
      var chain = [];
      if (Array.isArray(g.episodicRuleChain)) {
        for (var c = 0; c < g.episodicRuleChain.length; c++) {
          var link = cloneEpisodicRule(g.episodicRuleChain[c]);
          if (link) chain.push(link);
        }
      }
      list.push({
        id: g.id,
        perceptionNodeId: g.perceptionNodeId,
        perceptionBranchId: g.perceptionBranchId != null ? g.perceptionBranchId : (g.perceptionNodeId || 0),
        situationId: g.situationId != null ? g.situationId : 0,
        stimulusImageId: g.stimulusImageId,
        actionImageId: g.actionImageId,
        effect: g.effect != null ? g.effect : 0,
        count: g.count != null ? g.count : 0,
        usefulness: g.usefulness != null ? g.usefulness : 0,
        usefulnessCount: g.usefulnessCount != null ? g.usefulnessCount : 0,
        lastUsedAt: g.lastUsedAt || 0,
        goalKind: g.goalKind || '',
        goalLabel: g.goalLabel || '',
        episodicRule: g.episodicRule != null ? cloneEpisodicRule(g.episodicRule) : null,
        episodicRuleChain: chain
      });
    }
    return { goals: list, nextId: getNextId() };
  }

  /** Состояние без lastUsedAt — для сравнения подписи, чтобы не сохранять каждый пульс при обновлении только времени. */
  function serializeStateForSig() {
    var arr = global.GoalImagesLife;
    if (!Array.isArray(arr)) return { goals: [], nextId: 1 };
    var list = [];
    for (var i = 0; i < arr.length; i++) {
      var g = arr[i];
      if (!g) continue;
      var chainSig = [];
      if (Array.isArray(g.episodicRuleChain)) {
        for (var c2 = 0; c2 < g.episodicRuleChain.length; c2++) {
          var lk = cloneEpisodicRule(g.episodicRuleChain[c2]);
          if (lk) chainSig.push(lk);
        }
      }
      list.push({
        id: g.id,
        perceptionNodeId: g.perceptionNodeId,
        perceptionBranchId: g.perceptionBranchId != null ? g.perceptionBranchId : (g.perceptionNodeId || 0),
        situationId: g.situationId != null ? g.situationId : 0,
        stimulusImageId: g.stimulusImageId,
        actionImageId: g.actionImageId,
        effect: g.effect != null ? g.effect : 0,
        count: g.count != null ? g.count : 0,
        usefulness: g.usefulness != null ? g.usefulness : 0,
        usefulnessCount: g.usefulnessCount != null ? g.usefulnessCount : 0,
        goalKind: g.goalKind || '',
        goalLabel: g.goalLabel || '',
        episodicRule: g.episodicRule != null ? cloneEpisodicRule(g.episodicRule) : null,
        episodicRuleChain: chainSig
      });
    }
    return { goals: list, nextId: getNextId() };
  }

  function applyState(state) {
    if (!state || !Array.isArray(state.goals)) {
      global.GoalImagesLife = [];
      rebuildIndex();
      return;
    }
    global.GoalImagesLife = [];
    for (var i = 0; i < state.goals.length; i++) {
      var s = state.goals[i];
      if (!s || s.id == null) continue;
      var pn = s.perceptionNodeId != null ? s.perceptionNodeId : (s.perceptionBranchId != null ? s.perceptionBranchId : 0);
      var restChain = [];
      if (Array.isArray(s.episodicRuleChain)) {
        for (var rc = 0; rc < s.episodicRuleChain.length; rc++) {
          var rcl = cloneEpisodicRule(s.episodicRuleChain[rc]);
          if (rcl) restChain.push(rcl);
        }
      }
      var actId = 0;
      if (typeof s.actionImageId === 'number' && !isNaN(s.actionImageId)) actId = s.actionImageId;
      else if (s.actionImageId != null) {
        var ap = parseInt(s.actionImageId, 10);
        actId = isNaN(ap) ? 0 : ap;
      }
      global.GoalImagesLife.push(normalizeLifeGoalRecord({
        id: s.id,
        perceptionNodeId: pn,
        perceptionBranchId: s.perceptionBranchId != null ? s.perceptionBranchId : pn,
        situationId: s.situationId != null ? s.situationId : 0,
        stimulusImageId: s.stimulusImageId != null ? s.stimulusImageId : 0,
        actionImageId: actId,
        effect: typeof s.effect === 'number' ? s.effect : 0,
        count: (s.count != null && s.count >= 0) ? s.count : 0,
        usefulness: typeof s.usefulness === 'number' ? s.usefulness : 0,
        usefulnessCount: (s.usefulnessCount != null && s.usefulnessCount >= 0) ? s.usefulnessCount : 0,
        lastUsedAt: s.lastUsedAt || 0,
        goalKind: s.goalKind != null ? s.goalKind : '',
        goalLabel: s.goalLabel != null ? s.goalLabel : '',
        episodicRule: s.episodicRule != null ? cloneEpisodicRule(s.episodicRule) : null,
        episodicRuleChain: restChain
      }));
    }
    nextId = (typeof state.nextId === 'number' && state.nextId > 0) ? state.nextId : getNextId();
    rebuildIndex();
    ensureDefaultMeaningGoal();
    ensureDefaultCryGoal();
    lastGoalsSavedSig = JSON.stringify(serializeStateForSig());
    goalsDirty = false;
  }

  // IndexedDB (отдельная БД по аналогии с автоматизмами)
  var db = null;
  var DB_NAME = 'BEAST_Goals_DB';
  var DB_VERSION = 1;
  var STORE = 'goals_state';
  var KEY = 'goals_singleton';

  function openDb() {
    if (!global.indexedDB || !global.IndexedDBModule) return Promise.reject(new Error('IndexedDB не доступен'));
    if (db) return Promise.resolve(db);
    return global.IndexedDBModule.openDatabase({
      dbName: DB_NAME,
      version: DB_VERSION,
      stores: [{ name: STORE, options: { keyPath: 'id' } }]
    }).then(function (d) {
      db = d;
      return d;
    });
  }

  function saveToIndexedDB(state) {
    if (!global.IndexedDBModule) return;
    state = state || serializeState();
    function put(database) {
      return global.IndexedDBModule.put({
        db: database,
        storeName: STORE,
        value: { id: KEY, state: state, ts: Date.now() }
      }).catch(function () {});
    }
    if (db) put(db);
    else openDb().then(put).catch(function () {});
  }

  function loadFromIndexedDB() {
    if (!global.IndexedDBModule) return;
    openDb()
      .then(function (d) {
        return global.IndexedDBModule.get({ db: d, storeName: STORE, key: KEY });
      })
      .then(function (record) {
        if (record && record.state) {
          applyState(record.state);
          lastGoalsSavedSig = JSON.stringify(serializeStateForSig());
        }
      })
      .catch(function () {});
  }

  if (typeof global.registerIndexedDBSaver === 'function') {
    global.registerIndexedDBSaver(function () {
      if (!goalsDirty) return;
      goalsDirty = false;
      var state = serializeState();
      var sig = JSON.stringify(serializeStateForSig());
      if (sig === lastGoalsSavedSig) return;
      lastGoalsSavedSig = sig;
      saveToIndexedDB(state);
    });
  }

  function clearAllLifeGoals() {
    global.GoalImagesLife = [];
    rebuildIndex();
    nextId = 1;
    goalsDirty = true;
    if (typeof global.TargetRuleStack_clear === 'function') global.TargetRuleStack_clear();
    var info = global.InfoPicture;
    if (info && (info.currentGoalType === 'life' || info.currentGoalType === 'meaning')) {
      info.currentGoalType = null;
      info.currentGoalId = 0;
    }
    if (info) info.usedRule = 0;
    if (typeof global.Info_updateView === 'function') global.Info_updateView();
    ensureDefaultMeaningGoal();
    ensureDefaultCryGoal();
  }

  rebuildIndex();
  ensureDefaultMeaningGoal();
  ensureDefaultCryGoal();
  loadFromIndexedDB();

  global.GoalImagesLife = global.GoalImagesLife;
  global.Goals_determineGoalInSituation = Goals_determineGoalInSituation;
  global.Goals_runEpisodicSearch = Goals_runEpisodicSearch;
  global.Goals_considerNewGoalFromEpisode = Goals_considerNewGoalFromEpisode;
  global.PassiveMode_processUnderstandingModels = PassiveMode_processUnderstandingModels;
  global.PassiveMode_notifyScenariosExhausted = PassiveMode_notifyScenariosExhausted;
  global.Goals_addOrUpdateLifeGoalFromFrame = addOrUpdateLifeGoalFromFrame;
  global.Goals_episodicRuleFromFrame = episodicRuleFromFrame;
  global.Goals_appendLifeGoalEpisodicRuleLink = appendLifeGoalEpisodicRuleLink;
  global.Goals_getGoalById = getGoalById;
  global.Goals_getBestLifeGoalForBranch = getBestLifeGoalForBranch;
  global.Goals_findLifeGoalByPerceptionAndSituation = findLifeGoalByPerceptionAndSituation;
  global.Goals_findBestLifeGoalPositiveUsefulness = findBestLifeGoalPositiveUsefulness;
  global.Goals_mergeUsefulnessSampleForGoalId = mergeUsefulnessSampleForGoalId;
  global.Goals_applyWaitingPeriodEffectToGoalUsefulness = applyWaitingPeriodEffectToGoalUsefulness;
  global.getActiveBaseGoalVitalId = getActiveBaseGoalVitalId;
  global.Goals_setCurrentGoalInInfoPicture = setCurrentGoalInInfoPicture;
  global.Goals_serializeState = serializeState;
  global.Goals_applyState = applyState;
  global.Goals_clearAllLifeGoals = clearAllLifeGoals;
  global.Goals_ensureDefaultMeaningGoal = ensureDefaultMeaningGoal;
  global.Goals_findDefaultMeaningGoal = findDefaultMeaningGoal;
  global.Goals_isDefaultMeaningGoalId = isDefaultMeaningGoalId;
  global.Goals_getDefaultMeaningGoalId = getDefaultMeaningGoalId;
  global.GOAL_KIND_MEANING_DEFAULT = GOAL_KIND_MEANING_DEFAULT;
  global.GOALS_DEFAULT_MEANING_LABEL = GOALS_DEFAULT_MEANING_LABEL;
  global.Goals_ensureDefaultCryGoal = ensureDefaultCryGoal;
  global.Goals_findDefaultCryGoal = findDefaultCryGoal;
  global.Goals_getDefaultCryGoalId = getDefaultCryGoalId;
  global.Goals_isDefaultCryGoalId = isDefaultCryGoalId;
  global.GOAL_KIND_CRY_DEFAULT = GOAL_KIND_CRY_DEFAULT;
  global.GOALS_DEFAULT_CRY_LABEL = GOALS_DEFAULT_CRY_LABEL;
})(typeof window !== 'undefined' ? window : globalThis);
════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/info_detectors.js
════════════════════════════════════════════════════════════
/* Детекторы активных признаков инфокартины. Это детекторы случающихся ситуаций.
Инфо-детекторы (beast/_9_Conscience/info_detectors.js) выставляют в InfoPicture поля вроде criticalVital, badState, playMode, searchInterest и т.д.

Сбор «состава ситуации» — в beast/_9_Conscience/situatioms_tree.js, функция extractSituationComponentsFromInfo(info): по карте DETECTOR_COMPONENT_IDS для каждого ключа, если в info значение число и ≠ 0, в набор компонент ситуации попадает соответствующий числовой id компонента. Если набор пуст, но Норма/Хорошо (BadNormWell 2/3), активен контекст «Покой» (id 14) и нет крит. витала — добавляется synthetic peaceNormBaseline (36), иначе при спокойной норме ситуация не формировалась бы.

Создание/усиление образа ситуации — в SituationsTree_onPerceptionTreeActivated():
берутся situationComps, при непустом наборе вызывается findOrCreateSituationImage(mode, situationComps) (счётчик count, позже ready при count > 3).

Точка вызова — из Info_updateFromPerceptionTree() в informing.js после обновления ветки восприятия вызывается SituationsTree_onPerceptionTreeActivated().
*/

(function (global) {
  'use strict';

  // Утилита безопасной записи в InfoPicture.
  function setInfo(name, value) {
    if (typeof global.Info_setField === 'function') {
      global.Info_setField(name, value);
    } else if (global.InfoPicture) {
      global.InfoPicture[name] = value;
    }
  }

  // -------- Целевой режим / Тема / Ситуация --------

  /**
   * Целевой / пассивный режим мышления главного цикла ФО (thinkingMode), не поле инфокартины.
   * Ячейка «Режим» в UI обновляется через Info_updateView (Consciousness_getMainCycleThinkingMode).
   */
  function InfoDet_updateTargetMode(isTarget) {
    if (typeof global.Consciousness_getMainCycle === 'function') {
      var mc = global.Consciousness_getMainCycle();
      if (mc) {
        mc.thinkingMode = isTarget ? 'target' : 'passive';
      }
    }
    if (typeof global.Info_updateView === 'function') {
      global.Info_updateView();
    }
  }

  /**
   * Текущая тема осмысления.
   * Активный образ темы – ID или 0.
   */
  function InfoDet_updateCurrentTheme(themeId) {
    setInfo('themeId', themeId || 0);
  }

  /**
   * Текущая ситуация осмысления.
   * Текущий активный образ ситуации – ID или 0.
   */
  function InfoDet_updateCurrentSituation(situationId) {
    setInfo('situationId', situationId || 0);
  }

  // -------- Виталы и базовые контексты --------

  /**
   * Критический витал:
   * если какой-то витал заходит за значение 70, возвращается его ID.
   * Если таких несколько – выбирается витал с наибольшим значением и его ID записывается.
   * @param {Array<{Id:number,value:number}>} vitalsArr
   */
  function InfoDet_updateCriticalVital(vitalsArr) {
    // При «Хорошо» и активной подаче еды/воды инфокартина не должна показывать критический витал —
    // стимул сразу задаёт удержание Хорошо (SetWellForHolding) и смыслово перекрывает «критичность».
    var bnw = typeof global.BadNormWell === 'number' ? global.BadNormWell : 0;
    if (bnw === 3 && Array.isArray(global.ActiveMotivationStimuls)) {
      var am = global.ActiveMotivationStimuls;
      if (am.indexOf(1) !== -1 || am.indexOf(2) !== -1) {
        setInfo('criticalVital', 0);
        setInfo('ImportantProblem', 0);
        return;
      }
    }
    if (!Array.isArray(vitalsArr) || !vitalsArr.length) {
      setInfo('criticalVital', 0);
      setInfo('ImportantProblem', 0);
      return;
    }
    var bestId = 0;
    var bestVal = 70;
    for (var i = 0; i < vitalsArr.length; i++) {
      var v = vitalsArr[i];
      if (!v || typeof v.value !== 'number') continue;
      if (v.value > bestVal) {
        bestVal = v.value;
        bestId = v.Id || 0;
      }
    }
    setInfo('criticalVital', bestId);
    setInfo('ImportantProblem', bestId ? 1 : 0);
  }

  /**
   * Резкое ухудшение состояния / Состояние Плохо.
   * Резкое ухудшение: 1 – сменилось состояние на Плохо; иначе 0.
   * Состояние Плохо: badState = 1 при BadNormWell=1, иначе 0.
   * @param {number} lastBadNormWell - предыдущее BadNormWell
   * @param {number} badNormWell - текущее BadNormWell (1/2/3)
   */
  function InfoDet_updateBadState(lastBadNormWell, badNormWell) {
    if (typeof badNormWell !== 'number') badNormWell = 0;
    if (typeof lastBadNormWell !== 'number') lastBadNormWell = 0;
    // badState: текущее Плохо.
    setInfo('badState', badNormWell === 1 ? 1 : 0);
    // suddenChange: 1 если было не Плохо, а стало Плохо.
    var sudden = (lastBadNormWell !== 1 && badNormWell === 1) ? 1 : 0;
    setInfo('suddenChange', sudden);
  }

  /**
   * Игровой режим (ячейка «Игра»):
   * — базовый контекст Игра (id=11), или
   * — кнопка пульта «Играть» (global.isGameMode), см. stimuls_ui.js.
   * @param {number[]} basicContextsActived
   */
  function InfoDet_updatePlayMode(basicContextsActived) {
    var active = Array.isArray(basicContextsActived) &&
      basicContextsActived.indexOf(11) !== -1;
    if (global.isGameMode) {
      active = true;
    }
    setInfo('playMode', active ? 1 : 0);
  }

  /**
   * Режим обучения / Поисковый интерес:
   * Обучение (ячейка «Обучение»): контекст Любопытство (id=12), или Интерес>70,
   * или кнопка «Учить» (global.isAuthoritarianEducation).
   * Поисковый интерес: только витал Интерес > 70 (без кнопки «Учить»).
   * @param {number[]} basicContextsActived
   * @param {Array<{Id:number,value:number}>} vitalsArr
   *   (ожидается витал с Id==10 или по договорённости как "Интерес")
   */
  function InfoDet_updateLearningAndSearch(basicContextsActived, vitalsArr) {
    var curiosityActive = Array.isArray(basicContextsActived) &&
      basicContextsActived.indexOf(12) !== -1;
    var interestHigh = false;
    if (Array.isArray(vitalsArr)) {
      for (var i = 0; i < vitalsArr.length; i++) {
        var v = vitalsArr[i];
        if (!v || typeof v.value !== 'number') continue;
        // Здесь предполагаем, что витал "Интерес" имеет Id == 10 (или другой, по факту).
        if (v.Id === 10 && v.value > 70) {
          interestHigh = true;
          break;
        }
      }
    }
    setInfo('searchInterest', interestHigh ? 1 : 0);
    var learning = curiosityActive || interestHigh || !!global.isAuthoritarianEducation;
    setInfo('learningMode', learning ? 1 : 0);
  }

  // -------- Эффекты действий и автоматизмов --------

  /**
   * Негативный эффект моторного автоматизма:
   * getDiffImportance(), вызываемая при стимуле во время периода ожидания
   * последствий моторного автоматизма, выдаёт значение < 0.
   * @param {number} z - значение getDiffImportance() в период ожидания
   */
  function InfoDet_updateNegativeMotorEffect(z) {
    if (typeof z !== 'number' || isNaN(z)) {
      setInfo('negativeMotorEffect', 0);
      return;
    }
    setInfo('negativeMotorEffect', z < 0 ? z : 0);
  }

  /**
   * Негативный эффект ментального автоматизма:
   * аналогично моторному, но для ментальных автоматизмов.
   * Пока может быть заготовкой: просто принимает z и пишет, если < 0.
   * @param {number} z
   */
  function InfoDet_updateNegativeMentalEffect(z) {
    if (typeof z !== 'number' || isNaN(z)) {
      setInfo('negativeMentalEffect', 0);
      return;
    }
    setInfo('negativeMentalEffect', z < 0 ? z : 0);
  }

  /**
   * Положительный / отрицательный эффект действия:
   * при закрытии кадра ЭП после автоматизма.
   * @param {number} effect - last.effect из ЭП (diffAfter - diffBefore)
   */
  function InfoDet_updateActionEffect(effect) {
    if (typeof effect !== 'number' || isNaN(effect)) {
      setInfo('positiveActionEffect', 0);
      setInfo('negativeActionEffect', 0);
      return;
    }
    setInfo('positiveActionEffect', effect > 0 ? effect : 0);
    setInfo('negativeActionEffect', effect < 0 ? effect : 0);
  }

  /**
   * Нет реакции на стимул:
   * период ожидания результата автоматизма закончился без ответного стимула.
   * @param {boolean} noReaction
   */
  function InfoDet_updateNoReaction(noReaction) {
    setInfo('noReactionToStimulus', noReaction ? 1 : 0);
  }

  /**
   * Активное ожидание ответа:
   * запущен период ожидания результата автоматизма из функции осмысления.
   * @param {boolean} active
   */
  function InfoDet_updateActiveWaiting(active) {
    setInfo('activeWaitingAnswer', active ? 1 : 0);
  }

  /**
   * Низкая полезность автоматизма:
   * эффект в ожидании не превышает 1 (по модулю).
   * @param {number} usefulnessSample
   */
  function InfoDet_updateLowAutomatismUsefulness(usefulnessSample) {
    if (typeof usefulnessSample !== 'number' || isNaN(usefulnessSample)) {
      setInfo('lowAutomatismUsefulness', 0);
      return;
    }
    if (Math.abs(usefulnessSample) <= 1 && usefulnessSample !== 0) {
      setInfo('lowAutomatismUsefulness', usefulnessSample);
    } else {
      setInfo('lowAutomatismUsefulness', 0);
    }
  }

  /**
   * Пока найден подходящий автоматизм:
   * продолжается цикл осмысления в целевом режиме.
   * @param {boolean} active
   */
  function InfoDet_updateFoundSuitableAutomatism(active) {
    setInfo('foundSuitableAutomatism', active ? 1 : 0);
  }

  // -------- Стимулы и семантика --------

  /**
   * Стимул с Пульта:
   * появился новый актуальный стимул, вызвавший ориентировочный рефлекс.
   * Записывается ID образа стимула (FinalImageId) или 0.
   * @param {number} finalImageId
   */
  function InfoDet_updateConsoleStimulus(finalImageId) {
    if (typeof finalImageId !== 'number' || finalImageId <= 0) {
      setInfo('consoleStimulus', 0);
    } else {
      setInfo('consoleStimulus', finalImageId);
    }
  }

  /**
   * Есть объект высокой значимости:
   * текущий образ актуального стимула содержит стимул с высокой семантической значимостью
   * в данном контексте условий. Здесь ожидается, что вызывающая сторона уже обнаружила
   * такой объект и передала его ID или уровень значимости.
   * @param {number} objectIdOrLevel
   */
  function InfoDet_updateHighImportanceObject(objectIdOrLevel) {
    if (typeof objectIdOrLevel !== 'number' || objectIdOrLevel <= 0) {
      setInfo('highImportanceObject', 0);
    } else {
      setInfo('highImportanceObject', objectIdOrLevel);
    }
  }

  /**
   * Улучшение настроения:
   * появилось состояние Хорошо или улучшение более, чем на 5 единиц.
   * @param {number} diffLevel - положительная разница (нормализованная), если есть.
   */
  function InfoDet_updateMoodImproving(diffLevel) {
    if (typeof diffLevel !== 'number' || isNaN(diffLevel)) {
      setInfo('moodImproving', 0);
      return;
    }
    if (diffLevel > 5) {
      setInfo('moodImproving', diffLevel);
    } else {
      setInfo('moodImproving', 0);
    }
  }

  /**
   * Новизна образа:
   * детектор новизны ОР даёт большое значение novelty.
   * @param {number} novelty
   * @param {number} threshold
   */
  function InfoDet_updateImageNovelty(novelty, threshold) {
    if (typeof novelty !== 'number' || isNaN(novelty)) {
      setInfo('imageNovelty', 0);
      return;
    }
    if (typeof threshold !== 'number' || isNaN(threshold)) threshold = 0.5;
    setInfo('imageNovelty', novelty > threshold ? novelty : 0);
  }

  /**
   * Высокая семантическая значимость:
   * для текущего образа семантика >> 0.
   * @param {number} meanImportance
   */
  function InfoDet_updateHighSemanticImportance(meanImportance) {
    if (typeof meanImportance !== 'number' || isNaN(meanImportance)) {
      setInfo('highSemanticImportance', 0);
      return;
    }
    setInfo('highSemanticImportance', meanImportance > 0 ? meanImportance : 0);
  }

  /**
   * Противоречивая семантика:
   * для данного образа семантика около нуля при высокой силе измерений (samplesCount),
   * и при этом в других условиях есть как большие положительные, так и большие отрицательные значения.
   * Здесь ожидается, что вызывающий код уже провёл анализ и передаст 1/0.
   * @param {boolean} hasConflict
   */
  function InfoDet_updateConflictingSemantics(hasConflict) {
    setInfo('conflictingSemantics', hasConflict ? 1 : 0);
  }

  // -------- Цикл осмысления / проблемы --------

  /**
   * Обучение с учителем:
   * 1 – включается, когда функция осмысления даёт Нерешённая проблема
   * в течение непрекращающегося цикла попыток решения (режим отслеживания действий оператора).
   * @param {boolean} active
   */
  function InfoDet_updateTeacherLearning(active) {
    setInfo('teacherLearning', active ? 1 : 0);
  }

  /**
   * Игнорирование оператором:
   * активизация мотивирующего стимула «Игнорировать» или отсутствие стимула в период
   * ожидания после реакции и завершившийся период.
   * @param {boolean} active
   */
  var operatorIgnoring_waiting = false;
  var operatorIgnoring_stimulus = false;

  function InfoDet_recalcOperatorIgnoring() {
    setInfo('operatorIgnoring', (operatorIgnoring_waiting || operatorIgnoring_stimulus) ? 1 : 0);
  }

  /**
   * Источник 1: логика периода ожидания / отсутствия ответа.
   */
  function InfoDet_updateOperatorIgnoring(active) {
    operatorIgnoring_waiting = !!active;
    InfoDet_recalcOperatorIgnoring();
  }

  /**
   * Источник 2: активен мотивирующий стимул «Игнорировать» (StimulMotivarion.Id = 18).
   * Вызывать при переключении стимула или при массовом сбросе стимулов.
   */
  function InfoDet_updateOperatorIgnoringFromStimulus() {
    var arr = global.ActiveMotivationStimuls;
    operatorIgnoring_stimulus = Array.isArray(arr) && arr.indexOf(18) !== -1;
    InfoDet_recalcOperatorIgnoring();
  }

  /**
   * Неудовлетворенность существующим:
   * базовый контекст Покой активен, нет актуальных стимулов и закончен цикл
   * осмысления в пассивном режиме.
   * @param {boolean} active
   */
  function InfoDet_updateDissatisfaction(active) {
    var v = active ? 1 : 0;
    setInfo('dissatisfaction', v);
    setInfo('isDissatisfaction', v);
  }

  /**
   * Непонимание:
   * цикл осмысления дошёл до конца в целевом режиме или число шагов итерации превысило 10.
   * @param {boolean} active
   */
  function InfoDet_updateMisunderstanding(active) {
    setInfo('misunderstanding', active ? 1 : 0);
  }

  /**
   * Сомнение в штатном автоматизме:
   * начат второй уровень осмысления в целевом режиме.
   * @param {boolean} active
   */
  function InfoDet_updateDoubtInDefaultAutomatism(active) {
    setInfo('doubtInDefaultAutomatism', active ? 1 : 0);
  }

  /**
   * Защита:
   * активен базовый контекст Страх и/или Агрессия.
   * @param {boolean} active
   */
  function InfoDet_updateDefense(active) {
    setInfo('defense', active ? 1 : 0);
  }

  /**
   * Страх:
   * активен базовый контекст Страх.
   * @param {boolean} active
   */
  function InfoDet_updateFear(active) {
    setInfo('fear', active ? 1 : 0);
  }

  /**
   * Агрессия:
   * активен базовый контекст Агрессия.
   * @param {boolean} active
   */
  function InfoDet_updateAggression(active) {
    setInfo('aggression', active ? 1 : 0);
  }

  /**
   * Трудная проблема:
   * массив образов, для которых в ходе осмысления возникает нерешённая проблема
   * более чем на 10 шагов и цикл доходит до конца в целевом режиме.
   * Здесь ожидается, что вызывающий код ведёт такой массив и передаёт уровень/счётчик.
   * @param {number} level
   */
  function InfoDet_updateHardProblem(level) {
    if (typeof level !== 'number' || isNaN(level)) {
      setInfo('hardProblem', 0);
    } else {
      setInfo('hardProblem', level);
    }
  }

  /**
   * Ответ оператора получен:
   * @param {boolean} received
   */
  function InfoDet_updateOperatorAnswer(received) {
    setInfo('operatorAnswerReceived', received ? 1 : 0);
    if (!received) {
      setInfo('isOperatorAnswerReceived', 0);
    }
  }

  /**
   * Серия эпизодов без эффекта:
   * @param {number} length
   */
  function InfoDet_updateSeriesWithoutEffect(length) {
    if (typeof length !== 'number' || isNaN(length) || length <= 0) {
      setInfo('seriesWithoutEffect', 0);
    } else {
      setInfo('seriesWithoutEffect', length);
    }
  }

  // Экспорт детекторов в global
  global.InfoDet_updateTargetMode = InfoDet_updateTargetMode;
  global.InfoDet_updateCurrentTheme = InfoDet_updateCurrentTheme;
  global.InfoDet_updateCurrentSituation = InfoDet_updateCurrentSituation;
  global.InfoDet_updateCriticalVital = InfoDet_updateCriticalVital;
  global.InfoDet_updateBadState = InfoDet_updateBadState;
  global.InfoDet_updatePlayMode = InfoDet_updatePlayMode;
  global.InfoDet_updateLearningAndSearch = InfoDet_updateLearningAndSearch;
  global.InfoDet_updateNegativeMotorEffect = InfoDet_updateNegativeMotorEffect;
  global.InfoDet_updateNegativeMentalEffect = InfoDet_updateNegativeMentalEffect;
  global.InfoDet_updateActionEffect = InfoDet_updateActionEffect;
  global.InfoDet_updateNoReaction = InfoDet_updateNoReaction;
  global.InfoDet_updateActiveWaiting = InfoDet_updateActiveWaiting;
  global.InfoDet_updateLowAutomatismUsefulness = InfoDet_updateLowAutomatismUsefulness;
  global.InfoDet_updateFoundSuitableAutomatism = InfoDet_updateFoundSuitableAutomatism;
  global.InfoDet_updateConsoleStimulus = InfoDet_updateConsoleStimulus;
  global.InfoDet_updateHighImportanceObject = InfoDet_updateHighImportanceObject;
  global.InfoDet_updateMoodImproving = InfoDet_updateMoodImproving;
  global.InfoDet_updateImageNovelty = InfoDet_updateImageNovelty;
  global.InfoDet_updateHighSemanticImportance = InfoDet_updateHighSemanticImportance;
  global.InfoDet_updateConflictingSemantics = InfoDet_updateConflictingSemantics;
  global.InfoDet_updateTeacherLearning = InfoDet_updateTeacherLearning;
  global.InfoDet_updateOperatorIgnoring = InfoDet_updateOperatorIgnoring;
  global.InfoDet_updateOperatorIgnoringFromStimulus = InfoDet_updateOperatorIgnoringFromStimulus;
  global.InfoDet_updateDissatisfaction = InfoDet_updateDissatisfaction;
  global.InfoDet_updateMisunderstanding = InfoDet_updateMisunderstanding;
  global.InfoDet_updateDoubtInDefaultAutomatism = InfoDet_updateDoubtInDefaultAutomatism;
  global.InfoDet_updateDefense = InfoDet_updateDefense;
  global.InfoDet_updateFear = InfoDet_updateFear;
  global.InfoDet_updateAggression = InfoDet_updateAggression;
  global.InfoDet_updateHardProblem = InfoDet_updateHardProblem;
  global.InfoDet_updateOperatorAnswer = InfoDet_updateOperatorAnswer;
  global.InfoDet_updateSeriesWithoutEffect = InfoDet_updateSeriesWithoutEffect;

})(window);


════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/info_new_abstract_semantic_2_task.txt
════════════════════════════════════════════════════════════
Способ использования цепочек сценариев пассивного режима для развития семантики абстракций
Концептуальная основа
В пассивном режиме система может обобщать конкретный эпизодический опыт в семантические структуры абстракций. Ключевая идея: цепочки связанных кадров ЭП содержат паттерны «условия → действие → эффект», которые можно транслировать в semanticStructure абстрактных образов.
Алгоритм развития semanticStructure из цепочек ЭП
Этап 1: Выявление сценарных цепочек в ЭП
javascript
function extractScenarioChainsFromEpisodes(maxChainLength = 5) {
    var chains = [];
    var episodes = global.Episodes || [];
    
    // Ищем последовательности кадров с ненулевым effect
    for (var i = 0; i < episodes.length; i++) {
        var frame = episodes[i];
        if (!frame || typeof frame.effect !== 'number' || frame.effect === 0) continue;
        
        var chain = [frame];
        var currentPerceptionNode = frame.perceptionNodeId;
        
        // Ищем продолжения цепочки (те же perceptionNodeId или родительские узлы)
        for (var j = i + 1; j < episodes.length && chain.length < maxChainLength; j++) {
            var nextFrame = episodes[j];
            if (!nextFrame) break;
            
            // Критерии связи кадров в цепочку:
            var isRelated = (
                nextFrame.perceptionNodeId === currentPerceptionNode ||  // та же ветка
                isParentNode(nextFrame.perceptionNodeId, currentPerceptionNode) ||  // родительская ветка
                (nextFrame.stimulusImageId === frame.actionImageId)  // стимул = предыдущее действие
            );
            
            if (isRelated && typeof nextFrame.effect === 'number') {
                chain.push(nextFrame);
                currentPerceptionNode = nextFrame.perceptionNodeId;
            }
        }
        
        if (chain.length >= 2) {
            chains.push({
                startIndex: i,
                frames: chain,
                totalEffect: chain.reduce(function(sum, f) { return sum + (f.effect || 0); }, 0),
                perceptionNodeId: chain[0].perceptionNodeId
            });
        }
    }
    
    return chains;
}
Этап 2: Извлечение паттернов из цепочек
javascript
function extractPatternsFromChain(chain) {
    var patterns = [];
    
    for (var i = 0; i < chain.frames.length; i++) {
        var frame = chain.frames[i];
        
        // Паттерн для абстракции восприятия
        if (frame.stimulusImageId) {
            var absPerceptionId = global.findAbstractPerceptionRecordId(
                frame.perceptionNodeId, 
                frame.stimulusImageId
            );
            
            if (absPerceptionId) {
                patterns.push({
                    type: 'perception',
                    abstractId: absPerceptionId,
                    perceptionNodeId: frame.perceptionNodeId,
                    situationId: frame.situationId || 0,
                    ruleKind: 'episodic',
                    stimulusImageId: frame.stimulusImageId,
                    actionImageId: frame.actionImageId || null,
                    effect: frame.effect,
                    responseStimulusImageId: frame.responseStimulusImageId || null,
                    chainPosition: i,
                    chainTotalEffect: chain.totalEffect
                });
            }
        }
        
        // Паттерн для абстракции действия
        if (frame.actionImageId) {
            var absActionId = global.findAbstractActionRecordId(
                frame.perceptionNodeId, 
                frame.actionImageId
            );
            
            if (absActionId) {
                patterns.push({
                    type: 'action',
                    abstractId: absActionId,
                    perceptionNodeId: frame.perceptionNodeId,
                    situationId: frame.situationId || 0,
                    ruleKind: 'episodic',
                    stimulusImageId: frame.stimulusImageId || null,
                    actionImageId: frame.actionImageId,
                    effect: frame.effect,
                    responseStimulusImageId: frame.responseStimulusImageId || null,
                    chainPosition: i,
                    chainTotalEffect: chain.totalEffect
                });
            }
        }
    }
    
    return patterns;
}
Этап 3: Обновление semanticStructure абстракций
javascript
function updateAbstractSemanticStructureFromPatterns(patterns) {
    var updatedCount = 0;
    
    for (var p = 0; p < patterns.length; p++) {
        var pattern = patterns[p];
        var absRecord = null;
        
        // Находим запись абстракции
        if (pattern.type === 'perception') {
            absRecord = global.Info_findAbstractPerceptionImage(pattern.abstractId);
        } else if (pattern.type === 'action') {
            absRecord = global.Info_findAbstractActionImage(pattern.abstractId);
        }
        
        if (!absRecord || !absRecord.semanticStructure) continue;
        
        // Ищем существующий кадр в semanticStructure по условиям
        var existingFrameIndex = -1;
        var frames = absRecord.semanticStructure.frames || [];
        
        for (var f = 0; f < frames.length; f++) {
            var fr = frames[f];
            if (fr.perceptionNodeId === pattern.perceptionNodeId &&
                fr.situationId === pattern.situationId) {
                existingFrameIndex = f;
                break;
            }
        }
        
        // Создаём новый кадр или обновляем существующий
        var newFrame = {
            perceptionNodeId: pattern.perceptionNodeId,
            situationId: pattern.situationId,
            ruleKind: pattern.ruleKind,
            stimulusImageId: pattern.stimulusImageId,
            actionImageId: pattern.actionImageId,
            effect: pattern.effect,
            responseStimulusImageId: pattern.responseStimulusImageId,
            updatedAt: Date.now(),
            // Новые поля для цепочек:
            chainEffect: pattern.chainTotalEffect,
            chainPosition: pattern.chainPosition
        };
        
        if (existingFrameIndex >= 0) {
            // Обновляем существующий: усредняем effect, сохраняем лучший chainEffect
            var existing = frames[existingFrameIndex];
            existing.effect = (existing.effect + pattern.effect) / 2;
            if (!existing.chainEffect || pattern.chainTotalEffect > existing.chainEffect) {
                existing.chainEffect = pattern.chainTotalEffect;
            }
            existing.updatedAt = Date.now();
        } else {
            // Добавляем новый кадр
            frames.push(newFrame);
            updatedCount++;
        }
        
        absRecord.semanticStructure.frames = frames;
        global.memDirty = true;
    }
    
    return updatedCount;
}
Этап 4: Интеграция в пассивный режим
javascript
// В goals.js: PassiveMode_processUnderstandingModels()
function PassiveMode_processUnderstandingModels() {
    if (!global.InfoPicture || global.InfoPicture.mode !== 'passive') return;
    
    // 1. Извлекаем сценарные цепочки из ЭП
    var chains = extractScenarioChainsFromEpisodes(5);
    if (!chains || chains.length === 0) return;
    
    // 2. Извлекаем паттерны из цепочек
    var allPatterns = [];
    for (var c = 0; c < chains.length; c++) {
        var patterns = extractPatternsFromChain(chains[c]);
        allPatterns = allPatterns.concat(patterns);
    }
    
    // 3. Обновляем semanticStructure абстракций
    var updated = updateAbstractSemanticStructureFromPatterns(allPatterns);
    
    // 4. Логирование для отладки
    if (updated > 0 && typeof global.Consciousness_cycleAppendLog === 'function') {
        var mainCycle = global.Consciousness_getMainCycle();
        if (mainCycle) {
            global.Consciousness_cycleAppendLog(
                mainCycle,
                'Пассивный режим: обновлено ' + updated + ' кадров semanticStructure из ' + 
                chains.length + ' цепочек ЭП (' + allPatterns.length + ' паттернов).',
                global.cpuls,
                { force: true }
            );
        }
    }
    
    // 5. Сохранение изменений
    if (updated > 0 && typeof global.AbstractImages_flushIndexedDB === 'function') {
        global.AbstractImages_flushIndexedDB();
    }
}
Дополнительные механизмы развития
4.1: Приоритизация цепочек по суммарному эффекту
javascript
function prioritizeChainsByEffect(chains) {
    chains.sort(function(a, b) {
        // Положительные цепочки приоритетнее
        if (a.totalEffect > 0 && b.totalEffect <= 0) return -1;
        if (a.totalEffect <= 0 && b.totalEffect > 0) return 1;
        // Сортировка по абсолютной величине эффекта
        return Math.abs(b.totalEffect) - Math.abs(a.totalEffect);
    });
    
    // Обрабатываем только топ-3 цепочки за один проход пассивного режима
    return chains.slice(0, 3);
}
4.2: Обобщение по родительским узлам дерева восприятия
javascript
function generalizePatternToParentNodes(pattern) {
    var generalized = [pattern];
    var currentNodeId = pattern.perceptionNodeId;
    
    // Поднимаемся по дереву до 3 уровней вверх
    for (var level = 0; level < 3; level++) {
        var parentNode = global.PerceptionTree_getNode(currentNodeId);
        if (!parentNode || !parentNode.parentId || parentNode.parentId === 0) break;
        
        generalized.push({
            type: pattern.type,
            abstractId: pattern.abstractId,
            perceptionNodeId: parentNode.parentId,  // обобщённый узел
            situationId: pattern.situationId,
            ruleKind: 'episodic_generalized',
            stimulusImageId: pattern.stimulusImageId,
            actionImageId: pattern.actionImageId,
            effect: pattern.effect * 0.7,  // снижаем вес для обобщённых правил
            chainPosition: pattern.chainPosition,
            chainTotalEffect: pattern.chainTotalEffect
        });
        
        currentNodeId = parentNode.parentId;
    }
    
    return generalized;
}
4.3: Слияние дублирующихся кадров semanticStructure
javascript
function mergeDuplicateSemanticFrames(absRecord) {
    if (!absRecord || !absRecord.semanticStructure || 
        !Array.isArray(absRecord.semanticStructure.frames)) {
        return 0;
    }
    
    var frames = absRecord.semanticStructure.frames;
    var merged = 0;
    
    // Группируем по ключу (perceptionNodeId + situationId)
    var grouped = {};
    for (var i = 0; i < frames.length; i++) {
        var key = frames[i].perceptionNodeId + '|' + frames[i].situationId;
        if (!grouped[key]) {
            grouped[key] = [];
        }
        grouped[key].push(frames[i]);
    }
    
    // Сливаем дубликаты
    var newFrames = [];
    for (var k in grouped) {
        var group = grouped[k];
        if (group.length === 1) {
            newFrames.push(group[0]);
        } else {
            // Усредняем effect, сохраняем максимальный chainEffect
            var mergedFrame = Object.assign({}, group[0]);
            mergedFrame.effect = group.reduce(function(sum, f) { 
                return sum + (f.effect || 0); 
            }, 0) / group.length;
            mergedFrame.chainEffect = Math.max.apply(null, 
                group.map(function(f) { return f.chainEffect || 0; })
            );
            newFrames.push(mergedFrame);
            merged++;
        }
    }
    
    absRecord.semanticStructure.frames = newFrames;
    return merged;
}

Точки интеграции в существующий код
Модуль
Функция
Изменение
goals.js
PassiveMode_processUnderstandingModels()
Вызов extractScenarioChainsFromEpisodes() + updateAbstractSemanticStructureFromPatterns()
abstract_images.js
AbstractImages_cloneFromEpisodeFrame()
Добавление полей chainEffect, chainPosition в кадр semanticStructure
episodic_memory.js
EpisodicMemory_onPulse()
Флаг episodesChanged для триггера обработки в пассивном режиме
puls.js
sep_of_puls()
Вызов PassiveMode_processUnderstandingModels() только в пассивном режиме
════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/info_new_abstract_semantic_task.txt
════════════════════════════════════════════════════════════
После процесса использования цепочек сюжетов они обнуляются и нигде не сохраняются. Процесс использования сюжетов будет реализован для сна и реализцется как инфо-функция info_new_abstract_semantic - обработки сюжета (внести в infofunctions.js).

Использование цепочек пассивного режима для развития семантики абстракций
1. Концептуальная основа
В пассивном режиме система строит цепочки ассоциаций, перемещаясь от одного кадра ЭП к другому. Эти цепочки представляют собой потенциально новые, ранее не встречавшиеся в опыте комбинации "условие → стимул → действие → эффект". Они являются идеальным материалом для:

Обогащения существующих абстракций новыми семантическими связями

Создания новых абстракций на основе комбинаций известных элементов

Формирования ментальных правил более высокого порядка

2. Механизм извлечения цепочек из пассивного режима
2.1. Структура цепочки фантазирования
Каждая цепочка, построенная в процессе пассивного мышления, должна сохраняться в структуре, привязанной к циклу:

text
fantazmingChain = [
  {
    sourceStimulusId: number,      // исходный стимул (FinalImageId)
    actionImageId: number,          // действие Beast в ответ
    responseStimulusId: number,     // ответ оператора (новый стимул)
    effect: number,                 // эффект от этого шага в исходном кадре
    episodeFrameId: number,         // ID исходного кадра ЭП
    confidence: number              // уверенность (на основе count кадра)
  },
  // ... следующие звенья
]
2.2. Детектор ценности цепочки
При завершении цепочки (по достижении лимита длины или отсутствии продолжения) необходимо оценить ее ценность для семантического развития:

Критерии ценности:

Длина цепочки: цепочки из 2-3 звеньев наиболее ценны для формирования новых связей

Суммарный эффект: положительный суммарный эффект всей цепочки указывает на потенциально полезную стратегию

Новизна комбинаций: отсутствие в существующих абстракциях комбинаций "стимул → действие → стимул"

Контрастность переходов: резкие смены контекста (например, от страха к радости) создают более сильные семантические связи

3. Обогащение существующих абстракций
3.1. Выявление абстракций для обогащения
Для каждого звена цепочки нужно определить соответствующие абстракции:

text
function findAffectedAbstractions(chainLink):
  // 1. Абстракция восприятия для стимула
  perceptionAbs = findAbstractPerceptionByStimulus(chainLink.sourceStimulusId)
  
  // 2. Абстракция действия для действия Beast
  actionAbs = findAbstractActionByActionImage(chainLink.actionImageId)
  
  // 3. Абстракция восприятия для ответного стимула
  responseAbs = findAbstractPerceptionByStimulus(chainLink.responseStimulusId)
  
  return { perceptionAbs, actionAbs, responseAbs }
3.2. Добавление семантических связей
Для каждой найденной абстракции добавляем новую связь в ее semanticStructure.frames:

Для абстракции восприятия стимула:

Добавляем правило, связывающее текущий стимул с действием, которое привело к новому стимулу

Ключ условия: (perceptionNodeId, situationId) из контекста звена

Эффект: суммарный эффект от этого звена (или всей цепочки)

Для абстракции действия:

Добавляем правило, связывающее это действие с его последствием (новым стимулом)

Усредняем эффект с существующими записями для той же пары (ветка, ситуация)

Для абстракции ответного стимула:

Добавляем обратную связь: этот стимул может быть достигнут через данное действие из данного контекста

Усиливаем значимость стимула на основе эффекта

3.3. Обновление значимости абстракций
text
function updateAbstractionSignificance(abstraction, effect, context):
  // Используем существующий механизм усреднения из AbstractImages_upsertSemanticRuleFromEpisodeFrame
  // Но с важным отличием: правило помечается как "фантазийное" (ruleKind = 'fantasy')
  // Это позволяет отличать реальный опыт от сгенерированного
  
  if (abstraction.semanticStructure.frames中存在такойжеключ):
    усреднитьэффектспредыдущимзначением
  else:
    создатьновуюзаписьс effect
    пометить ruleKind = 'fantasy'
    установить confidence = effect * chainLength / 10 (коэффициент достоверности)
4. Создание новых абстракций
4.1. Композиция новых абстракций
Цепочки фантазирования позволяют создавать абстракции, не существовавшие в опыте:

Типы новых абстракций:

Составной стимул: комбинация исходного и ответного стимулов

text
newAbstraction = {
  type: 'composite',
  components: [chainLink.sourceStimulusId, chainLink.responseStimulusId],
  relation: 'leads_to',
  weight: суммарный_эффект_цепочки
}
Транзитивное действие: последовательность действий, приводящая к результату

text
newAbstraction = {
  type: 'sequence',
  actions: [звено1.actionImageId, звено2.actionImageId, ...],
  finalEffect: суммарный_эффект_цепочки,
  contexts: [контексты_звеньев]
}
Абстрактная ситуация: обобщение контекстов, в которых цепочка работает

text
newAbstraction = {
  type: 'situation',
  baseStates: [уникальные_базовые_состояния_из_цепочки],
  emotions: [уникальные_эмоции_из_цепочки],
  commonStimulus: chainLink.sourceStimulusId
}
4.2. Регистрация новых абстракций
Новые абстракции создаются через существующий API:

text
function createAbstractionFromChain(chain, type):
  abstractionId = Abstractions_create({
    type: type,
    sourceChain: chain,
    createdAt: Date.now(),
    confidence: calculateConfidence(chain),
    semanticStructure: buildSemanticStructureFromChain(chain)
  })
  
  // Добавляем связь с исходными абстракциями
  linkAbstractionToSources(abstractionId, chain)
5. Формирование ментальных правил
5.1. Из цепочки в ментальное правило
Каждая успешная цепочка фантазирования может стать основой для ментального автоматизма:

Структура ментального правила:

text
mentalRule = {
  perceptionNodeId: контекст_первого_звена,
  stimulusImageId: стимул_первого_звена,
  abstractionId: id_созданной_абстракции,
  meanEffect: суммарный_эффект_цепочки / длина_цепочки,
  count: 1,
  chain: fantazmingChain  // сохраняем для возможного развертывания
}
5.2. Регистрация в системе ментальных автоматизмов
text
function registerMentalRuleFromChain(chain, cycle):
  // 1. Создаем ментальный образ действия (MentalActionImage)
  maiId = MentalActionImages_getOrCreate(
    sourceActionImageId: chain[0].actionImageId,
    abstractActionIds: [извлечь_абстракции_действий_из_цепочки],
    maSequence: сериализовать_цепочку_в_строку
  )
  
  // 2. Создаем или обновляем ментальный автоматизм
  automatizmId = MentalAutomatizms_create({
    themeId: cycle.createdThemeId,
    situationId: cycle.createdSituationId,
    branchId: getBranchIdFromContext(chain),
    mentalActionImageId: maiId,
    usefulness: calculateChainUsefulness(chain),
    isDefault: false  // фантазийный автоматизм не становится штатным
  })
  
  // 3. Связываем с семантической структурой абстракций
  for each абстракции в chain:
    AbstractImages_upsertSemanticRuleFromEpisodeFrame(
      абстракция,
      построить_фрейм_из_звена(звено)
    )
6. Цикл развития семантики через пассивный режим
Предлагается следующий цикл, интегрированный в диспетчер сознания:

Аккумуляция: В процессе пассивного мышления накапливаются цепочки фантазирования

Каждая успешная итерация info_fantasizming добавляет звено в cycle.fantazmingChain

При достижении лимита длины (3-5 звеньев) цепочка считается завершенной

Валидация: Оценка ценности цепочки

Если суммарный эффект > порога (например, >3) → цепочка полезна

Если в цепочке есть новые комбинации (не встречавшиеся в ЭП) → высокая ценность

Если цепочка зацикливается (повторяет звенья) → низкая ценность

Семантическая обработка:

Обогащение существующих абстракций (создание ruleKind: 'fantasy')

Создание новых абстракций (композиты, последовательности, ситуации)

Формирование ментальных правил

Интеграция в активное мышление:

Фантазийные правила доступны для использования в активном режиме, но с пониженным приоритетом

При подтверждении реальным опытом (effect от реального кадра ЭП) ruleKind меняется на 'episodic'

При опровержении (отрицательный effect) confidence снижается, правило может быть удалено

7. Критерии успешности
Эффективность этого механизма можно оценивать по следующим метрикам:

Количество новых абстракций в единицу времени (должно расти с накоплением опыта)

Доля фантазийных правил, подтвердившихся реальным опытом (должна быть >30%)

Увеличение длины успешных цепочек фантазирования (с 2-3 до 5-7 звеньев)

Снижение времени поиска решения в новых ситуациях за счет использования фантазийных абстракций
════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/infofunctions.js
════════════════════════════════════════════════════════════
/**
 * Общие info_*-хелперы для ФО / инфокартины и связки с ЭП.
 */
(function (global) {
  'use strict';

  /**
   * Сформировать и запустить автоматизм с периодом ожидания ответа по кадру ЭП; цикл отметить expired и снять с главного.
   * Единая точка для ур.1 (после выбора автоматизма), ур.2 (выживание), ур.3 (правило из ЭП).
   *
   * Ответ оператора закрывает кадр в ЭП; пайплайн ур.2 по этому циклу вызывается из episodic_memory.closeLastFrame
   * (иначе при expired и не-главном цикле runOneStep до него не доходит).
   *
   * @param {Object} cycle — цикл осмысления
   * @param {number} branchId — узел дерева восприятия
   * @param {number} actionImageId — ОД
   * @param {number} automatizmId — ID автоматизма (уже созданного или штатного)
   * @param {number} goalImageId — образ цели цикла (0, если нет)
   * @param {string} logMessage — строка в лог цикла (force)
   * @returns {boolean}
   */
  function info_automatismWithEpisodeFeedbackAndExpireCycle(cycle, branchId, actionImageId, automatizmId, goalImageId, logMessage) {
    if (!cycle || !branchId || !actionImageId) return false;

    var aid = parseInt(automatizmId, 10) || 0;
    var gid = parseInt(goalImageId, 10) || 0;
    var pc = typeof global.cpuls === 'number' ? global.cpuls : 0;

    global.EpisodicMemory_pendingFeedbackAutomatizmId = 0;
    cycle.waitingFeedbackAutomatizmId = 0;

    global.EpisodicMemory_waitingAutomatismCycleId = cycle.id;
    global.EpisodicMemory_pendingFeedbackAutomatizmId = aid;
    cycle.waitingFeedbackAutomatizmId = aid;
    cycle.goalImageId = gid;

    if (aid && typeof global.InfoDet_updateActiveWaiting === 'function') {
      global.InfoDet_updateActiveWaiting(true);
    }
    if (typeof global.runActionImage === 'function') {
      global.runActionImage(actionImageId);
    }
    if (typeof global.removeDelayedReflexesForBranch === 'function') {
      global.removeDelayedReflexesForBranch(branchId);
    }
    if (!global.automatismRanForBranchIds) global.automatismRanForBranchIds = {};
    global.automatismRanForBranchIds[branchId] = true;

    cycle.awaitingAutomatismEpisodeFeedback = true;
    cycle.automatismOperatorAnswerReceived = false;
    cycle.automatismAnswerLevel2Done = false;
    cycle.lastAutomatismEpisodeEffect = null;
    cycle.operatorAnswerPipelineConsumed = false;
    cycle.status = 'running';
    cycle.step = 1;

    if (typeof global.InfoDet_updateDoubtInDefaultAutomatism === 'function') {
      global.InfoDet_updateDoubtInDefaultAutomatism(false);
    }
    if (global.unsolved_problem) {
      global.unsolved_problem = null;
      if (typeof global.Info_setProblemFromUnsolved === 'function') {
        global.Info_setProblemFromUnsolved(0);
      }
    }

    if (logMessage && typeof global.Consciousness_cycleAppendLog === 'function') {
      global.Consciousness_cycleAppendLog(cycle, logMessage, pc, { force: true });
    }

    cycle.expired = true;
    cycle.isMainCycle = false;

    if (typeof global.Info_updateView === 'function') {
      global.Info_updateView();
    }
    return true;
  }

  /**
   * Лучшее положительное правило в ЭП по ветке восприятия и ситуации; режим задаётся options.
   *
   * Порядок шагов (как в conscience_level_2 выживание / conscience_level_3):
   *  1) при situationId > 0 — кадры perceptionNodeId + situationId;
   *  2) иначе или если не найдено — perceptionNodeId + situation 0 (только ветка);
   *  3) опционально allowStimulusFallback: если stimulusImageId > 0 и кадр ещё null — getFramesByStimulus + лучший позитивный.
   *
   * Режим выборки в шагах 1–2:
   *  — filterByStimulus: true — только кадры с данным stimulusImageId (pickBestPositiveFrameForStimulus), ур.3;
   *  — filterByStimulus: false — любой стимул (pickBestPositiveFrame), ур.2 выживание.
   *
   * exactRule: true только если кадр найден на шаге 1 (ветка+ситуация).
   *
   * @param {number} perceptionNodeId
   * @param {number} situationId
   * @param {Object} [options]
   * @param {boolean} [options.filterByStimulus] — фильтр по стимулу в шагах 1–2 (по умолчанию false)
   * @param {number} [options.stimulusImageId] — при filterByStimulus обязателен; иначе используется для шага 3 при allowStimulusFallback
   * @param {boolean} [options.allowStimulusFallback] — третий шаг по стимулу (ур.2)
   * @returns {{ frame: Object|null, exactRule: boolean }|null} null только при отсутствии API ЭП или при filterByStimulus без stimulus
   */
  function info_bestEpisodicRule(perceptionNodeId, situationId, options) {
    perceptionNodeId = parseInt(perceptionNodeId, 10) || 0;
    situationId = parseInt(situationId, 10) || 0;
    options = options || {};
    var filterByStimulus = !!options.filterByStimulus;
    var stimulusImageId = parseInt(options.stimulusImageId, 10) || 0;
    var allowStimulusFallback = !!options.allowStimulusFallback;

    if (!perceptionNodeId) return null;
    if (filterByStimulus && !stimulusImageId) return null;

    var getFr = global.EpisodicMemory_getFramesByPerceptionAndSituation;
    var pickBest = global.EpisodicMemory_pickBestPositiveFrame;
    var pickForStim = global.EpisodicMemory_pickBestPositiveFrameForStimulus;
    var getByStim = global.EpisodicMemory_getFramesByStimulus;

    if (typeof getFr !== 'function') return null;

    function pickFromFrames(frames) {
      if (filterByStimulus) {
        if (typeof pickForStim !== 'function') return null;
        return pickForStim(frames, stimulusImageId);
      }
      if (typeof pickBest !== 'function') return null;
      return pickBest(frames);
    }

    var exactRule = false;
    var bestFrame = null;

    if (situationId > 0) {
      bestFrame = pickFromFrames(getFr(perceptionNodeId, situationId));
      if (bestFrame) exactRule = true;
    }
    if (!bestFrame) {
      bestFrame = pickFromFrames(getFr(perceptionNodeId, 0));
    }
    if (!bestFrame && allowStimulusFallback && stimulusImageId && typeof getByStim === 'function' && typeof pickBest === 'function') {
      bestFrame = pickBest(getByStim(stimulusImageId));
    }

    return { frame: bestFrame, exactRule: exactRule };
  }

  /**
   * Обёртка: только кадры с заданным стимулом в шагах 1–2 (ур.3).
   * @returns {Object|null}
   */
  function info_bestEpisodicRuleForStimulus(perceptionNodeId, situationId, stimulusImageId) {
    var r = info_bestEpisodicRule(perceptionNodeId, situationId, {
      filterByStimulus: true,
      stimulusImageId: stimulusImageId
    });
    return r && r.frame ? r.frame : null;
  }

  /**
   * Учительское правило в ЭП: кадр с заданным actionImageId (ОД) и ненулевым responseStimulusImageId
   * (ответный стимул оператора на это действие). Значение effect не ранжирует кадры — достаточно закрытого кадра
   * (effect — число); отбор «доверия безусловному правилу».
   *
   * Порядок: 1) последний подходящий кадр при perceptionNodeId и situationId (если situationId > 0);
   * 2) иначе — последний подходящий кадр только по perceptionNodeId (любая ситуация).
   *
   * @param {number} perceptionNodeId
   * @param {number} situationId
   * @param {number} actionImageId
   * @returns {{ frame: Object, exactRule: boolean }|null}
   */
  function info_teacherEpisodicRuleForAction(perceptionNodeId, situationId, actionImageId) {
    perceptionNodeId = parseInt(perceptionNodeId, 10) || 0;
    situationId = parseInt(situationId, 10) || 0;
    actionImageId = parseInt(actionImageId, 10) || 0;
    if (!perceptionNodeId || !actionImageId) return null;
    var ep = global.Episodes;
    if (!Array.isArray(ep) || !ep.length) return null;

    function isTeacherMatch(f) {
      if (!f) return false;
      if (typeof f.effect !== 'number' || isNaN(f.effect)) return false;
      var aid = f.actionImageId != null ? parseInt(f.actionImageId, 10) : 0;
      if (aid !== actionImageId) return false;
      var resp = f.responseStimulusImageId != null ? parseInt(f.responseStimulusImageId, 10) : 0;
      return resp > 0;
    }

    function scanLatest(situationFilter) {
      for (var i = ep.length - 1; i >= 0; i--) {
        var f = ep[i];
        if (!f || (f.perceptionNodeId || 0) !== perceptionNodeId) continue;
        if (situationFilter != null && (f.situationId || 0) !== situationFilter) continue;
        if (!isTeacherMatch(f)) continue;
        return {
          perceptionNodeId: f.perceptionNodeId || 0,
          stimulusImageId: f.stimulusImageId || 0,
          situationId: f.situationId || 0,
          actionImageId: f.actionImageId != null ? f.actionImageId : null,
          effect: f.effect != null ? f.effect : null,
          responseStimulusImageId: f.responseStimulusImageId != null ? f.responseStimulusImageId : null,
          createdAt: f.createdAt || 0
        };
      }
      return null;
    }

    var exactRule = false;
    var frame = null;
    if (situationId > 0) {
      frame = scanLatest(situationId);
      if (frame) exactRule = true;
    }
    if (!frame) {
      frame = scanLatest(null);
    }
    if (!frame) return null;
    return { frame: frame, exactRule: exactRule };
  }

  /**
   * Удержание одного базового контекста на столько же пульсов, сколько в UI (prepare.js: state_retention_period).
   * @param {number} basicContextId — id из window.BasicContexts
   * @returns {boolean}
   */
  function info_activateBasicContextForHolding(basicContextId) {
    var id = parseInt(basicContextId, 10);
    if (!id) return false;
    if (typeof global.ActivateBasicContextsForHolding !== 'function') return false;
    global.ActivateBasicContextsForHolding([id]);
    return true;
  }

  /**
   * Запуск удержания базового состояния Плохо (1), Норма (2), Хорошо (3) — тот же период, что у SetBad/SetWell/SetNorm в prepare.js
   * (аналог круглых кнопок B/W плюс явная Норма).
   *
   * @param {number} badNormWellId — 1 | 2 | 3
   * @returns {boolean}
   */
  function info_setBadNormWellForHolding(badNormWellId) {
    var id = parseInt(badNormWellId, 10);
    if (id === 1 && typeof global.SetBadForHolding === 'function') {
      global.SetBadForHolding();
      return true;
    }
    if (id === 2 && typeof global.SetNormForHolding === 'function') {
      global.SetNormForHolding();
      return true;
    }
    if (id === 3 && typeof global.SetWellForHolding === 'function') {
      global.SetWellForHolding();
      return true;
    }
    return false;
  }

  /**
   * Активировать зеркальное действие оператора: стимул по Id из window.BasicMirrorActions
   * (см. activateStimulusForBasicMirrorActionId в stimuls_ui.js).
   *
   * @param {number} basicMirrorActionId
   * @returns {boolean}
   */
  function info_activateBasicMirrorAction(basicMirrorActionId) {
    if (typeof global.activateStimulusForBasicMirrorActionId !== 'function') {
      return false;
    }
    return !!global.activateStimulusForBasicMirrorActionId(basicMirrorActionId);
  }

  /**
   * Общая реакция на инсайт (гештальт и др.): умолчательная цель «что для меня означает это?», `thinkingMode = 'target'` у цикла;
   * если передан цикл и он фоновый — перевод в главный (Consciousness_setMainCycle).
   *
   * @param {Object|null} cycle — цикл, в котором зафиксирован инсайт (null — только инфокартина)
   * @param {Object} [opts] — { skipPromote: true } — не менять главный цикл
   * @returns {boolean}
   */
  function info_insight(cycle, opts) {
    opts = opts || {};
    if (typeof global.Goals_ensureDefaultMeaningGoal === 'function') {
      global.Goals_ensureDefaultMeaningGoal();
    }
    var gid = typeof global.Goals_getDefaultMeaningGoalId === 'function'
      ? global.Goals_getDefaultMeaningGoalId() : 0;
    if (!gid) return false;

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

    if (cycle && cycle.status === 'running' && !cycle.expired) {
      cycle.thinkingMode = 'target';
    } else if (typeof global.Consciousness_getMainCycle === 'function') {
      var mc = global.Consciousness_getMainCycle();
      if (mc) mc.thinkingMode = 'target';
    }

    if (typeof global.Goals_setCurrentGoalInInfoPicture === 'function') {
      global.Goals_setCurrentGoalInInfoPicture('meaning', gid, 0);
    }

    if (cycle && cycle.status === 'running' && !cycle.expired) {
      var wasBackground = cycle.isMainCycle === false;
      cycle.goalImageId = gid;
      if (wasBackground && !opts.skipPromote && typeof global.Consciousness_setMainCycle === 'function') {
        global.Consciousness_setMainCycle(cycle.id);
        if (typeof global.Consciousness_cycleAppendLog === 'function') {
          var pc = typeof global.cpuls === 'number' ? global.cpuls : 0;
          var label = global.GOALS_DEFAULT_MEANING_LABEL || 'значение';
          global.Consciousness_cycleAppendLog(cycle,
            'Инсайт: цикл переведён в главный, цель «' + String(label) + '».',
            pc,
            { force: true });
        }
      }
      if (global.InfoPicture) {
        if (typeof cycle.weight === 'number') {
          global.InfoPicture.thinkingImportance = cycle.weight;
        }
        global.InfoPicture.thinkingProcessId = cycle.id;
      }
    }

    if (typeof global.Info_updateView === 'function') {
      global.Info_updateView();
    }
    return true;
  }

  /**
   * Обработка накопленного сюжета пассивного фантазирования: перенос звеньев в semanticStructure клонов
   * (AbstractImages_RULE_KIND_FANTASY), без перезаписи учительских слотов. После успеха цепочки на цикле обнуляются.
   * Предназначено для вызова из «сна» и редких консолидаций; не тянет цепочки из сырого Episodes (см. task-файлы).
   *
   * @param {Object|null} cycle — цикл ФО с fantasizmArr / fantazmingPlot; можно null при plotSnapshot в opts
   * @param {Object} [opts]
   * @param {number} [opts.minChainLength] — не обрабатывать, если chain.length < (по умолчанию 0)
   * @param {boolean} [opts.clearInfoPlot=true] — для главного цикла очистить InfoPicture.fantasmPlotRules
   * @param {{ fantasizmArr?: Array }} [opts.plotSnapshot] — снимок без cycle (например отложенный сон)
   * @param {string} [opts.source] — метка источника ('sleep' и т.д.), только для лога
   * @returns {{ ok: boolean, links: number, upserts: number, skippedTeacher: number, cleared: boolean, reason?: string }}
   */
  function info_new_abstract_semantic(cycle, opts) {
    opts = opts || {};
    var pc = typeof global.cpuls === 'number' ? global.cpuls : 0;
    var upsertF = global.AbstractImages_upsertSemanticRuleFromFantasyFrame;
    var ensureP = global.AbstractImages_ensurePerceptionAbstraction;
    var ensureA = global.AbstractImages_ensureActionAbstraction;
    var out = {
      ok: false,
      links: 0,
      upserts: 0,
      skippedTeacher: 0,
      cleared: false,
      reason: ''
    };

    if (typeof upsertF !== 'function' || typeof ensureP !== 'function' || typeof ensureA !== 'function') {
      out.reason = 'abstract_images_api';
      return out;
    }

    var minChain = typeof opts.minChainLength === 'number' ? opts.minChainLength : 0;
    if (cycle && minChain > 0) {
      var ch = cycle.fantazmingPlot && cycle.fantazmingPlot.chain ? cycle.fantazmingPlot.chain.length : 0;
      if (ch < minChain) {
        out.reason = 'chain_too_short';
        return out;
      }
    }

    var arr = [];
    if (cycle && Array.isArray(cycle.fantasizmArr) && cycle.fantasizmArr.length) {
      arr = cycle.fantasizmArr.slice();
    } else if (opts.plotSnapshot && Array.isArray(opts.plotSnapshot.fantasizmArr) && opts.plotSnapshot.fantasizmArr.length) {
      arr = opts.plotSnapshot.fantasizmArr.slice();
    }

    if (!arr.length) {
      out.reason = 'empty_plot';
      return out;
    }

    out.links = arr.length;
    var srcTag = opts.source ? '[' + String(opts.source) + '] ' : '';

    for (var i = 0; i < arr.length; i++) {
      var link = arr[i];
      if (!link) continue;
      var p = parseInt(link.perceptionNodeId, 10) || 0;
      var stim = link.stimulusImageId != null ? parseInt(link.stimulusImageId, 10) || 0 : 0;
      var act = link.actionImageId != null ? parseInt(link.actionImageId, 10) || 0 : 0;
      if (!p || !stim || !act) continue;

      var epLike = {
        perceptionNodeId: p,
        situationId: link.situationId != null ? parseInt(link.situationId, 10) || 0 : 0,
        stimulusImageId: stim,
        actionImageId: act,
        effect: typeof link.effect === 'number' && !isNaN(link.effect) ? link.effect : 0,
        responseStimulusImageId: link.responseStimulusImageId != null ? link.responseStimulusImageId : null
      };

      var apObj = ensureP(p, stim);
      var aaObj = ensureA(p, act);
      if (apObj) {
        if (upsertF(apObj, epLike)) out.upserts += 1;
        else out.skippedTeacher += 1;
      }
      if (aaObj) {
        if (upsertF(aaObj, epLike)) out.upserts += 1;
        else out.skippedTeacher += 1;
      }
    }

    out.ok = arr.length > 0;

    if (arr.length && cycle) {
      cycle.fantasizmArr = [];
      cycle.fantazmingPlot = { chain: [], lastGeneratedActionId: 0 };
      cycle.currentFantazmingStimulus = 0;
      out.cleared = true;
      var clearInfo = opts.clearInfoPlot !== false;
      if (clearInfo && cycle.isMainCycle && global.InfoPicture) {
        global.InfoPicture.fantasmPlotRules = [];
      }
    }

    if (typeof global.Consciousness_cycleAppendLog === 'function' && cycle) {
      var msg = srcTag + 'info_new_abstract_semantic: звеньев=' + out.links +
        ', upsert слотов semanticStructure=' + out.upserts + ', пропуск (teacher)=' + out.skippedTeacher +
        (out.cleared ? ', сюжет цикла обнулён' : '');
      global.Consciousness_cycleAppendLog(cycle, msg, pc, { force: !cycle.isMainCycle });
    }

    if (out.upserts > 0 && typeof global.AbstractImages_flushIndexedDB === 'function') {
      global.AbstractImages_flushIndexedDB();
    }
    if (out.ok && typeof global.Info_updateView === 'function') {
      global.Info_updateView();
    }

    return out;
  }

  global.info_automatismWithEpisodeFeedbackAndExpireCycle = info_automatismWithEpisodeFeedbackAndExpireCycle;
  global.info_bestEpisodicRule = info_bestEpisodicRule;
  global.info_bestEpisodicRuleForStimulus = info_bestEpisodicRuleForStimulus;
  global.info_teacherEpisodicRuleForAction = info_teacherEpisodicRuleForAction;
  global.info_activateBasicContextForHolding = info_activateBasicContextForHolding;
  global.info_setBadNormWellForHolding = info_setBadNormWellForHolding;
  global.info_activateBasicMirrorAction = info_activateBasicMirrorAction;
  global.info_insight = info_insight;
  global.info_new_abstract_semantic = info_new_abstract_semantic;
})(window);

════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/informing.js
════════════════════════════════════════════════════════════
(function (global) {
  'use strict';

  /**
   * Информационная картина (InfoPicture) — оперативный контекст осмысления.
   * Не сохраняется между сессиями; при перезагрузке восстанавливается по текущей активности.
   */
  function ensureInfoPicture() {
    if (!global.InfoPicture) {
      global.InfoPicture = {
        themeId: 0,
        situationId: 0,
        problemFinalImageId: 0,   // связана с unsolved_problem
        step: 0,
        tree: {
          baseID: 0,
          emotionID: 0,
          activityID: 0,
          toneMoodID: 0,
          simbolID: 0,
          phraseID: 0
        },
        stimulus: {
          source: 'external',     // 'external' | 'imagination'
          finalImageId: 0
        },
        // Детекторные переменные инфокартины (целые числа, по умолчанию 0):
        // criticalVital: 0 — нет, >0 — ID критического витала.
        criticalVital: 0,
        // shockState: 0 — нет, 1 — шоковое состояние активно.
        shockState: 0,
        // suddenChange: 0 — нет, |value| — величина резкого изменения состояния (нормализованная).
        suddenChange: 0,
        // playMode: 0 — нет, 1 — игровой режим активен.
        playMode: 0,
        // learningMode: 0 — нет, 1 — режим обучения активен.
        learningMode: 0,
        // isPlayMode / isLearningMode — дубль для ур.3 (синхрон с playMode / learningMode на ур.2).
        isPlayMode: 0,
        isLearningMode: 0,
        // negativeMotorEffect: 0 — нет, <0 — сумма/уровень негативного эффекта моторных автоматизмов.
        negativeMotorEffect: 0,
        // negativeMentalEffect: 0 — нет, <0 — сумма/уровень негативного эффекта ментальных автоматизмов.
        negativeMentalEffect: 0,
        // badState: 0 — не Плохо, 1 — состояние Плохо активно.
        badState: 0,
        // consoleStimulus: 0 — нет, >0 — ID последнего стимула с пульта.
        consoleStimulus: 0,
        // searchInterest: 0 — нет, 1 — поисковый интерес активен.
        searchInterest: 0,
        // teacherLearning: 0 — нет, 1 — идёт обучение с учителем.
        teacherLearning: 0,
        // operatorIgnoring: 0 — нет, 1 — оператор игнорирует.
        operatorIgnoring: 0,
        // dissatisfaction: 0 — нет, 1 — есть неудовлетворённость существующим.
        dissatisfaction: 0,
        // isDissatisfaction: 0 — нет, 1 — активна «неудовлетворённость существующим» (триггер 3-го уровня).
        isDissatisfaction: 0,
        // misunderstanding: 0 — нет, 1 — состояние непонимания.
        misunderstanding: 0,
        // doubtInDefaultAutomatism: 0 — нет, 1 — есть сомнение в штатном автоматизме.
        doubtInDefaultAutomatism: 0,
        // defense: 0 — нет, 1 — активен режим защиты.
        defense: 0,
        // fear: 0 — нет, 1 — страх активен.
        fear: 0,
        // aggression: 0 — нет, 1 — агрессия активна.
        aggression: 0,
        // highImportanceObject: 0 — нет, >0 — ID объекта высокой значимости.
        highImportanceObject: 0,
        // moodImproving: 0 — нет, >0 — величина улучшения настроения.
        moodImproving: 0,
        // imageNovelty: 0 — нет, >0 — уровень новизны образа.
        imageNovelty: 0,
        // highSemanticImportance: 0 — нет, >0 — величина высокой семантической значимости.
        highSemanticImportance: 0,
        // conflictingSemantics: 0 — нет, 1 — обнаружена противоречивая семантика.
        conflictingSemantics: 0,
        // hardProblem: 0 — нет, >0 — счётчик попыток/уровень трудности проблемы.
        hardProblem: 0,
        // positiveActionEffect: 0 — нет, >0 — величина последнего положительного эффекта.
        positiveActionEffect: 0,
        // negativeActionEffect: 0 — нет, <0 — величина последнего отрицательного эффекта.
        negativeActionEffect: 0,
        // noReactionToStimulus: 0 — нет, 1 — зафиксировано отсутствие реакции.
        noReactionToStimulus: 0,
        // operatorAnswerReceived: 0 — нет, 1 — ответ оператора получен.
        operatorAnswerReceived: 0,
        // isOperatorAnswerReceived: 0 — нет, 1 — ответ именно на реакцию после автоматизма ФО (в т.ч. для ур.2).
        isOperatorAnswerReceived: 0,
        // activeWaitingAnswer: 0 — нет, 1 — активное ожидание ответа.
        activeWaitingAnswer: 0,
        // seriesWithoutEffect: 0 — нет, >0 — длина последней серии эпизодов без эффекта.
        seriesWithoutEffect: 0,
        // lowAutomatismUsefulness: 0 — нет, <0 — уровень низкой полезности автоматизма.
        lowAutomatismUsefulness: 0,
        // foundSuitableAutomatism: 0 — нет, >0 — ID/признак найденного подходящего автоматизма.
        foundSuitableAutomatism: 0,
        // ImportantProblem: 0 — нет, 1 — «это важная проблема» (критические виталы); гистерезис внимания к стимулам, при 0 — облегчённый ОР/ФО.
        ImportantProblem: 0,
        // usedRule: 0 — реакция не по запомненному правилу; ≠0 — активно реагирование по правилу (флаг или id правила по договорённости вызывающего кода).
        usedRule: 0,
        // Сюжет фантазма: пополняемый массив снимков кадров ЭП (для ячейки UI); пополняется при target→passive и на каждом шаге info_fantasizming.
        fantasmPlotRules: [],
        // Кто «двигает» отображаемый ход мысли: external — реальный стимул; imagination — Info_updateFromStimulus(..., 'imagination'); fantasy — шаг info_fantasizming (пассивная цепочка).
        thinkingSource: 'external'
      };
      // Текущая цель (устанавливается на втором уровне осмысления).
      global.InfoPicture.currentGoalType = null;   // 'base' | 'life' | 'voluntary' | 'meaning' | null
      global.InfoPicture.currentGoalId = 0;       // id образа цели (life/voluntary)
      global.InfoPicture.currentGoalVitalId = 0;  // для базовой цели — ID витала
    } else {
      var pic = global.InfoPicture;
      if (pic.currentGoalType === undefined) pic.currentGoalType = null;
      if (pic.currentGoalId === undefined) pic.currentGoalId = 0;
      if (pic.currentGoalVitalId === undefined) pic.currentGoalVitalId = 0;
      if (pic.isDissatisfaction === undefined) pic.isDissatisfaction = 0;
      if (pic.ImportantProblem === undefined) pic.ImportantProblem = 0;
      if (pic.usedRule === undefined) pic.usedRule = 0;
      if (pic.isOperatorAnswerReceived === undefined) pic.isOperatorAnswerReceived = 0;
      if (pic.isPlayMode === undefined) pic.isPlayMode = 0;
      if (pic.isLearningMode === undefined) pic.isLearningMode = 0;
      if (pic.fantasmPlotRules === undefined || !Array.isArray(pic.fantasmPlotRules)) pic.fantasmPlotRules = [];
      if (pic.thinkingSource === undefined) pic.thinkingSource = 'external';
      if (pic.mode !== undefined) delete pic.mode;
    }
    return global.InfoPicture;
  }

  /** Снимки кадров ЭП как «правила» для сюжета фантазма / fantasizmArr (заглушка до финального алгоритма). */
  function snapshotEpisodicRulesForFantasm(maxN) {
    maxN = typeof maxN === 'number' && maxN > 0 ? maxN : 8;
    var out = [];
    var ep = global.Episodes;
    if (!Array.isArray(ep) || !ep.length) return out;
    for (var i = ep.length - 1; i >= 0 && out.length < maxN; i--) {
      var f = ep[i];
      if (!f) continue;
      out.push({
        perceptionNodeId: f.perceptionNodeId || 0,
        stimulusImageId: f.stimulusImageId || 0,
        situationId: f.situationId || 0,
        actionImageId: f.actionImageId != null ? f.actionImageId : null,
        effect: f.effect != null ? f.effect : null,
        responseStimulusImageId: f.responseStimulusImageId != null ? f.responseStimulusImageId : null
      });
    }
    return out;
  }

  /**
   * Переход инфокартины в пассивный режим (целевой → пассивный): заполнить сюжет фантазма.
   * TODO: заменить на целевой алгоритм подбора правил ЭП.
   */
  function Info_fillFantasmPlotOnPassiveMain() {
    var info = ensureInfoPicture();
    info.fantasmPlotRules = snapshotEpisodicRulesForFantasm(12);
  }

  /**
   * Первый шаг фонового цикла в глобальном пассивном режиме: заполнить fantasizmArr цикла.
   * TODO: заменить на целевой алгоритм.
   */
  function Info_fillFantasmPlotOnPassiveBackground(cycle) {
    if (!cycle) return;
    cycle.fantasizmArr = snapshotEpisodicRulesForFantasm(8);
  }

  function setByPath(obj, path, value) {
    if (!obj || !path) return;
    var parts = path.split('.');
    var cur = obj;
    for (var i = 0; i < parts.length - 1; i++) {
      var k = parts[i];
      if (!cur[k] || typeof cur[k] !== 'object') {
        cur[k] = {};
      }
      cur = cur[k];
    }
    cur[parts[parts.length - 1]] = value;
  }

  function getCell(id) {
    return global.document && global.document.getElementById(id);
  }

  /** Ссылка в show_alert: щелчок перезагружает диалог с детализацией по ID. */
  function infoAlertIdLink(handlerName, id) {
    var n = parseInt(id, 10);
    if (isNaN(n)) n = 0;
    return '<span style="cursor:pointer;color:#1976d2;text-decoration:underline" onclick="' + handlerName + '(' + n + ')">' + String(n) + '</span>';
  }

  /** Поля объектов Abstractions: обработчик детализации по имени ключа (для «своей» детализации ID). */
  var INFO_ABSTRACTION_FIELD_LINK = {
    perceptionNodeId: 'Info_showPerceptionNodeDetail',
    stimulusImageId: 'Info_showFinalImageStimulusDetail',
    actionImageId: 'Info_showActionImageDetail',
    sourceActionImageId: 'Info_showActionImageDetail',
    abstractPerceptionId: 'Info_showAbstractPerceptionImageDetail',
    abstractActionId: 'Info_showAbstractActionImageDetail',
    abstractionId: 'Info_showAbstractionDetail',
    mentalActionImageId: 'Info_showMentalActionImageDetail',
    themeId: 'Info_showThemeImageDetail',
    situationId: 'Info_showSituationImageDetail'
  };

  var INFO_ABSTRACTION_FIELD_LABEL = {
    perceptionNodeId: 'Узел ветки восприятия',
    stimulusImageId: 'Жёсткий стимул (клон с него)',
    actionImageId: 'Жёсткий образ действия (ОД)',
    sourceActionImageId: 'Исходный ОД',
    abstractPerceptionId: 'Клон восприятия (AbstractPerceptionImages)',
    abstractActionId: 'Клон действия (AbstractActionImages)',
    abstractionId: 'Символ Abstractions',
    mentalActionImageId: 'Ментальный ОД',
    themeId: 'Образ темы',
    situationId: 'Образ ситуации',
    meanImportance: 'Средняя значимость (уср.)',
    meanEffect: 'Средний эффект',
    count: 'Число усреднений',
    createdAt: 'Создан',
    lastUsedAt: 'Последнее использование',
    semanticStructure: 'Семантическая структура'
  };

  /** semanticStructure абстракции: кадры условий (ветка + ситуация) и правило ЭП / учительское. */
  function infoFormatAbstractSemanticStructureHtml(sem) {
    if (!sem || typeof sem !== 'object') return '<br><i>semanticStructure:</i> —';
    var frames = sem.frames;
    if (!Array.isArray(frames) || !frames.length) {
      return '<br><i>semanticStructure:</i> нет кадров (0 условий)';
    }
    var parts = ['<br><b>semanticStructure</b> — по условиям (узел восприятия + ситуация), ' + frames.length + ' кадр(ов):'];
    for (var si = 0; si < frames.length; si++) {
      var fr = frames[si];
      if (!fr) continue;
      var rk = fr.ruleKind || '—';
      parts.push('<br>— <b>' + rk + '</b>: узел ' + (fr.perceptionNodeId || 0) + ', ситуация ' + (fr.situationId || 0));
      parts.push('<br> &nbsp; стимул FinalImageId=' + (fr.stimulusImageId || 0) + ', ОД=' + (fr.actionImageId != null ? fr.actionImageId : '—'));
      if (rk === 'teacher' || (fr.responseStimulusImageId != null && fr.responseStimulusImageId > 0)) {
        parts.push(', ответ оператора FinalImageId=' + (fr.responseStimulusImageId || 0));
      }
      if (typeof fr.effect === 'number') {
        parts.push(', effect=' + fr.effect);
      }
    }
    return parts.join('');
  }

  function infoFormatAbstractionFieldValue(key, val) {
    if (key === 'abstractActionIds' && Array.isArray(val)) {
      if (!val.length) return '—';
      var parts = [];
      for (var ai = 0; ai < val.length; ai++) {
        if (ai) parts.push(', ');
        var ab = parseInt(val[ai], 10) || 0;
        parts.push(ab ? infoAlertIdLink('Info_showAbstractionDetail', ab) : '0');
      }
      return parts.join('');
    }
    if (val != null && typeof val === 'object') {
      try {
        return JSON.stringify(val);
      } catch (e) {
        return String(val);
      }
    }
    var handler = INFO_ABSTRACTION_FIELD_LINK[key];
    var n = typeof val === 'number' ? val : parseInt(val, 10);
    if (handler && !isNaN(n) && n > 0) {
      return infoAlertIdLink(handler, n);
    }
    if ((key === 'createdAt' || key === 'lastUsedAt') && typeof val === 'number' && val > 0) {
      try {
        return new Date(val).toLocaleString();
      } catch (e2) {
        return String(val);
      }
    }
    return String(val);
  }

  /**
   * Ссылка на образ ситуации: готовые (ready) обводятся синим бордером в списках детализации.
   * @param {{id:number, ready?:boolean}|null} sv
   */
  function infoSituationImageLinkFromRecord(sv) {
    if (!sv || typeof sv.id !== 'number') return '';
    var n = sv.id;
    var ready = sv.ready === true;
    var base = 'cursor:pointer;color:#1976d2;text-decoration:underline;';
    var box = ready
      ? 'border:2px solid #1976d2;border-radius:4px;padding:1px 5px;display:inline-block;margin:0 1px;vertical-align:baseline;'
      : '';
    return '<span style="' + base + box + '" onclick="Info_showSituationImageDetail(' + n + ')">' + String(n) + '</span>';
  }

  function infoSituationImageLinkById(sitId) {
    var n = parseInt(sitId, 10) || 0;
    if (!n) return '0';
    var sv = typeof global.SituationsTree_getSituationImage === 'function'
      ? global.SituationsTree_getSituationImage(n) : null;
    if (sv) return infoSituationImageLinkFromRecord(sv);
    return infoAlertIdLink('Info_showSituationImageDetail', n);
  }

  function Info_showFinalImageStimulusDetail(finalImageId) {
    if (typeof global.show_alert !== 'function') return;
    finalImageId = parseInt(finalImageId, 10) || 0;
    if (!finalImageId) {
      global.show_alert('FinalImageId = 0.', 0);
      return;
    }
    var names = '';
    if (typeof global.PerceptionTree_getStimulusNamesForFinalImageId === 'function') {
      names = global.PerceptionTree_getStimulusNamesForFinalImageId(finalImageId) || '';
    }
    var html = '<b>Конечный образ FinalImageId:</b> ' + infoAlertIdLink('Info_showFinalImageStimulusDetail', finalImageId) +
      '<br><b>Стимулы:</b> ' + (names && names !== '—' ? names : '—');
    if (typeof global.getSemanticObjectFast === 'function') {
      var well = global.BadNormWell || 0;
      var emotionId = 0;
      if (typeof global.Emotions_getOrCreateEmotionId === 'function' && Array.isArray(global.BasicContextsActived)) {
        emotionId = global.Emotions_getOrCreateEmotionId(global.BasicContextsActived) || 0;
      }
      var semObj = (well && emotionId) ? global.getSemanticObjectFast(well, emotionId, finalImageId) : null;
      if (semObj) {
        html += '<br><br><b>Семантика (текущие well/emotion):</b> z ≈ ' +
          (Math.round(semObj.meanImportance * 100) / 100) + ', обучений: ' + semObj.samplesCount;
      }
    }
    global.show_alert(html, 0);
  }

  function Info_showPerceptionNodeDetail(nodeId) {
    if (typeof global.show_alert !== 'function') return;
    nodeId = parseInt(nodeId, 10) || 0;
    if (!nodeId || typeof global.PerceptionTree_getNode !== 'function') {
      global.show_alert('Узел ветки #' + nodeId + ' не найден.', 0);
      return;
    }
    var node = global.PerceptionTree_getNode(nodeId);
    if (!node) {
      global.show_alert('Узел ветки #' + nodeId + ' не найден в дереве восприятия.', 0);
      return;
    }
    var actId = node.activityID || 0;
    var actDetail = '';
    if (actId && typeof global.PerceptionTree_getStimulusNamesForFinalImageId === 'function') {
      actDetail = global.PerceptionTree_getStimulusNamesForFinalImageId(actId) || '';
    }
    var html = '<b>Узел ветки (branchId) ' + infoAlertIdLink('Info_showPerceptionNodeDetail', nodeId) + '</b>' +
      '<br>BaseID: ' + (node.baseID || 0) +
      '<br>EmotionID: ' + (node.emotionID || 0) +
      '<br>ActivityID: ' + (actId ? infoAlertIdLink('Info_showFinalImageStimulusDetail', actId) : '0') +
      (actDetail && actDetail !== '—' ? '<br><i>Актуальный образ:</i> ' + actDetail : '') +
      '<br>ToneMoodID: ' + (node.toneMoodID || 0) +
      '<br>SimbolID: ' + (node.simbolID || 0) +
      '<br>PhraseID: ' + (node.phraseID || 0) +
      '<br>parentId: ' + (node.parentId != null ? node.parentId : '—');
    global.show_alert(html, 0);
  }

  function Info_showEmotionRecordDetail(emotionId) {
    if (typeof global.show_alert !== 'function') return;
    emotionId = parseInt(emotionId, 10) || 0;
    if (!emotionId) {
      global.show_alert('emotionId = 0.', 0);
      return;
    }
    var em = null;
    if (Array.isArray(global.Emotions)) {
      for (var ei = 0; ei < global.Emotions.length; ei++) {
        if (global.Emotions[ei] && global.Emotions[ei].id === emotionId) {
          em = global.Emotions[ei];
          break;
        }
      }
    }
    if (!em) {
      global.show_alert('Запись эмоции #' + emotionId + ' не найдена.', 0);
      return;
    }
    var parts = ['<b>Эмоция (emotionId) ' + infoAlertIdLink('Info_showEmotionRecordDetail', emotionId) + '</b>'];
    var emotionContexts = em.contextIds || [];
    if (emotionContexts.length && Array.isArray(global.BasicContexts)) {
      var names = [];
      for (var ci = 0; ci < emotionContexts.length; ci++) {
        var cid = emotionContexts[ci];
        var found = false;
        for (var bj = 0; bj < global.BasicContexts.length; bj++) {
          if (global.BasicContexts[bj] && global.BasicContexts[bj].id === cid) {
            names.push(global.BasicContexts[bj].name || ('Контекст ' + cid));
            found = true;
            break;
          }
        }
        if (!found) names.push('id ' + cid);
      }
      parts.push('<br><b>Базовые контексты:</b> ' + names.join(', '));
    } else {
      parts.push('<br>Контексты: —');
    }
    global.show_alert(parts.join(''), 0);
  }

  function Info_showCycleDetailById(cycleId) {
    if (typeof global.show_alert !== 'function') return;
    cycleId = parseInt(cycleId, 10);
    if (isNaN(cycleId) || cycleId <= 0) return;
    var getCycle = global.Consciousness_getCycle;
    var cycle = typeof getCycle === 'function' ? getCycle(cycleId) : null;
    if (!cycle) {
      global.show_alert('Цикл с ID ' + String(cycleId) + ' не найден в реестре.', 0);
      return;
    }
    var mainId = typeof global.ConsciousnessCurrentProcessId === 'number' ? global.ConsciousnessCurrentProcessId : 0;
    var prefix = (cycleId === mainId && mainId) ? 'Главный цикл' : ('Цикл №' + String(cycleId));
    global.show_alert(formatCycleOrStackInfo(cycle, prefix), 0);
  }

  var thinkingTimer = null;

  /**
   * Кратковременная индикация «ориентировочного эффекта» осмысления:
   * зажигает синюю лампочку рядом с заголовком «Информационная картина» на 0.5 секунды.
   */
  function Info_thinkingFlash() {
    if (!global.document) return;
    var el = getCell('infoThinkingIndicator');
    if (!el) return;
    el.classList.add('on');
    if (thinkingTimer !== null) {
      global.clearTimeout(thinkingTimer);
    }
    thinkingTimer = global.setTimeout(function () {
      el.classList.remove('on');
      thinkingTimer = null;
    }, 500);
  }

  /**
   * Обновить индикацию инфокартины на главной странице.
   * Вызывается автоматически при каждом изменении InfoPicture.
   */
  function Info_updateView() {
    var info = ensureInfoPicture();
    if (!global.document) return;

    var mainThinking = (typeof global.Consciousness_getMainCycleThinkingMode === 'function')
      ? global.Consciousness_getMainCycleThinkingMode()
      : 'target';
    if (info._fantasmPrevMainThinkingMode === undefined) {
      info._fantasmPrevMainThinkingMode = mainThinking;
    } else {
      if (mainThinking === 'passive' && info._fantasmPrevMainThinkingMode === 'target') {
        Info_fillFantasmPlotOnPassiveMain();
      }
      info._fantasmPrevMainThinkingMode = mainThinking;
    }

    var modeCell = getCell('infoModeCell');
    var themeCell = getCell('infoThemeCell');
    var situationCell = getCell('infoSituationCell');
    var problemCell = getCell('infoProblemCell');
    var stepCell = getCell('infoStepCell');
    var stimCell = getCell('infoStimulusCell');
    var baseCell = getCell('infoBaseCell');
    var emoCell = getCell('infoEmotionCell');
    var actCell = getCell('infoActivityCell');
    // Детекторные ячейки
    var detCriticalVital = getCell('infoDetCriticalVital');
    var detImportantProblem = getCell('infoDetImportantProblem');
    var detUsedRule = getCell('infoDetUsedRule');
    var detShockState = getCell('infoDetShockState');
    var detSuddenChange = getCell('infoDetSuddenChange');
    var detPlayMode = getCell('infoDetPlayMode');
    var detLearningMode = getCell('infoDetLearningMode');
    var detNegativeMotor = getCell('infoDetNegativeMotor');
    var detNegativeMental = getCell('infoDetNegativeMental');
    var detBadState = getCell('infoDetBadState');
    var detConsoleStimulus = getCell('infoDetConsoleStimulus');
    var detSearchInterest = getCell('infoDetSearchInterest');
    var detTeacherLearning = getCell('infoDetTeacherLearning');
    var detOperatorIgnoring = getCell('infoDetOperatorIgnoring');
    var detDissatisfaction = getCell('infoDetDissatisfaction');
    var detMisunderstanding = getCell('infoDetMisunderstanding');
    var detDoubtAuto = getCell('infoDetDoubtAuto');
    var detDefense = getCell('infoDetDefense');
    var detFear = getCell('infoDetFear');
    var detAggression = getCell('infoDetAggression');
    var detHighObj = getCell('infoDetHighObj');
    var detMoodImproving = getCell('infoDetMoodImproving');
    var detImageNovelty = getCell('infoDetImageNovelty');
    var detHighSem = getCell('infoDetHighSem');
    var detConfSem = getCell('infoDetConfSem');
    var detHardProblem = getCell('infoDetHardProblem');
    var detPosEffect = getCell('infoDetPosEffect');
    var detNegEffect = getCell('infoDetNegEffect');
    var detNoReaction = getCell('infoDetNoReaction');
    var detAnswer = getCell('infoDetAnswer');
    var detActiveWait = getCell('infoDetActiveWait');
    var detSeriesNoEffect = getCell('infoDetSeriesNoEffect');
    var detLowAutoUse = getCell('infoDetLowAutoUse');
    var detFoundAuto = getCell('infoDetFoundAuto');
    var bgCountLabel = getCell('infoBackgroundCountLabel');
    var interruptQueueLabel = getCell('infoInterruptQueueLabel');
    var bgStackIndicator = getCell('infoBackgroundStackIndicator');
    var motorAutoInd = getCell('infoMotorAutomatizmsIndicator');
    var mentalAutoInd = getCell('infoMentalAutomatizmsIndicator');
    var abstrInd = getCell('infoAbstractionsIndicator');
    var lifeGoalsInd = getCell('infoLifeGoalsIndicator');
    var situationImagesInd = getCell('infoSituationImagesIndicator');
    var mainProcessLabel = getCell('infoMainProcessLabel');

    if (modeCell) {
      var mt = (typeof global.Consciousness_getMainCycleThinkingMode === 'function')
        ? global.Consciousness_getMainCycleThinkingMode()
        : 'target';
      var modeTitle = (mt === 'target' ? 'Целевой (активный)' : 'Пассивный') + ' режим мышления (главный цикл)';
      modeCell.title = modeTitle;
      modeCell.classList.toggle('info-cell-active', true);
    }
    if (themeCell) {
      themeCell.title = info.themeId ? ('Тема #' + info.themeId) : 'Тема не активирована';
      themeCell.classList.toggle('info-cell-active', !!info.themeId);
    }
    if (situationCell) {
      situationCell.title = info.situationId ? ('Ситуация #' + info.situationId) : 'Ситуация не активирована';
      situationCell.classList.toggle('info-cell-active', !!info.situationId);
    }
    if (problemCell) {
      problemCell.title = info.problemFinalImageId
        ? ('Нерешённая проблема: FinalImageId=' + info.problemFinalImageId)
        : 'Нерешённая проблема отсутствует';
      problemCell.classList.toggle('info-cell-active', !!info.problemFinalImageId);
    }
    if (stepCell) {
      stepCell.title = 'Шаг осмысления: ' + String(info.step || 0);
      stepCell.classList.toggle('info-cell-active', info.step > 0);
    }
    if (stimCell) {
      stimCell.title = info.stimulus.finalImageId
        ? ('Актуальный стимул: FinalImageId=' + info.stimulus.finalImageId)
        : 'Актуальный стимул отсутствует';
      stimCell.classList.toggle('info-cell-active', !!info.stimulus.finalImageId);
    }
    if (baseCell || emoCell || actCell) {
      var treeTitle = 'Ветка дерева восприятия:\n' +
        'BaseID=' + (info.tree.baseID || 0) +
        ' → EmotionID=' + (info.tree.emotionID || 0) +
        ' → ActivityID=' + (info.tree.activityID || 0) +
        ' → ToneMoodID=' + (info.tree.toneMoodID || 0) +
        ' → SimbolID=' + (info.tree.simbolID || 0) +
        ' → PhraseID=' + (info.tree.phraseID || 0);
      if (baseCell) {
        baseCell.title = treeTitle;
        baseCell.classList.toggle('info-cell-active', !!info.tree.baseID);
      }
      if (emoCell) {
        emoCell.title = 'EmotionID=' + (info.tree.emotionID || 0) + '\n(щелчок — состав базовых контекстов)\n\n' + treeTitle;
        emoCell.classList.toggle('info-cell-active', !!info.tree.emotionID);
      }
      if (actCell) {
        var aId = info.tree.activityID || 0;
        var aNames = '';
        if (aId && typeof global.PerceptionTree_getStimulusNamesForFinalImageId === 'function') {
          aNames = global.PerceptionTree_getStimulusNamesForFinalImageId(aId) || '';
        }
        actCell.title = 'ActivityID=' + aId + (aNames ? ('\n' + aNames) : '') + '\n(щелчок — детализация активности)\n\n' + treeTitle;
        actCell.classList.toggle('info-cell-active', !!info.tree.activityID);
      }
    }

    // Индикаторы фоновых процессов и очереди прерываний.
    var bgCount = typeof global.Consciousness_getBackgroundCyclesCount === 'function'
      ? global.Consciousness_getBackgroundCyclesCount() : 0;
    var interruptQueueLen = Array.isArray(global.ConsciousnessBackgroundStack) ? global.ConsciousnessBackgroundStack.length : 0;
    if (bgCountLabel) {
      bgCountLabel.textContent = 'Чисто фоновых процессов: ' + String(bgCount);
    }
    if (interruptQueueLabel) {
      interruptQueueLabel.textContent = 'очередь прерываний: ' + String(interruptQueueLen);
      interruptQueueLabel.title = 'Число прерванных главных циклов в очереди на возврат (LIFO): ' + String(interruptQueueLen);
    }
    if (bgStackIndicator) {
      bgStackIndicator.textContent = 'фон: ' + String(bgCount);
      var title = 'Фоновые циклы осмысления (' + bgCount + '), по клику — детализация:';
      if (typeof global.Consciousness_getBackgroundCyclesList === 'function') {
        var list = global.Consciousness_getBackgroundCyclesList();
        for (var si = 0; si < list.length; si++) {
          var item = list[si];
          var ctx = item && item.context ? item.context : null;
          var stimId = ctx && typeof ctx.actual_stimul_ID === 'number' ? ctx.actual_stimul_ID : 0;
          title += '\nID ' + String(item && item.id != null ? item.id : '—') +
            ' — стимул FinalImageId=' + String(stimId) + ', значимость=' + String(item && typeof item.weight === 'number' ? item.weight : 0) +
            (item && item.expired ? ', expired' : '');
        }
      }
      bgStackIndicator.title = title;
    }

    var motorN = Array.isArray(global.Automatizms) ? global.Automatizms.length : 0;
    var mentalN = Array.isArray(global.MentalAutomatizms) ? global.MentalAutomatizms.length : 0;
    // «Клоны» из ЭП (abstract_images.js) — не путать с массивом Abstractions (символы mental_automatism.js).
    var apClones = Array.isArray(global.AbstractPerceptionImages) ? global.AbstractPerceptionImages.length : 0;
    var aaClones = Array.isArray(global.AbstractActionImages) ? global.AbstractActionImages.length : 0;
    var cloneAbstrN = apClones + aaClones;
    var mentalSymN = Array.isArray(global.Abstractions) ? global.Abstractions.length : 0;
    if (motorAutoInd) {
      motorAutoInd.textContent = 'авто: ' + String(motorN);
      motorAutoInd.title = 'Моторные автоматизмы (' + motorN + '), щелчок — список ID';
    }
    if (mentalAutoInd) {
      mentalAutoInd.textContent = 'м-авто: ' + String(mentalN);
      mentalAutoInd.title = 'Ментальные автоматизмы (' + mentalN + '), щелчок — список ID';
    }
    if (abstrInd) {
      var abstrLabel = String(cloneAbstrN);
      if (mentalSymN > 0) abstrLabel += '+' + String(mentalSymN);
      abstrInd.textContent = 'абстр: ' + abstrLabel;
      abstrInd.title = 'Три вида: клоны восприятия ' + apClones + ', клоны действия ' + aaClones
        + ' (abstract_images.js); символы Abstractions: ' + mentalSymN + ' (mental_automatism.js).'
        + ' Щелчок — сводка; в списке каждый ID ведёт в свою детализацию.';
    }
    var lifeGoalsArr = Array.isArray(global.GoalImagesLife) ? global.GoalImagesLife : [];
    var lifeGoalsN = 0;
    for (var gi = 0; gi < lifeGoalsArr.length; gi++) {
      if (lifeGoalsArr[gi] && typeof lifeGoalsArr[gi].id === 'number') lifeGoalsN += 1;
    }
    if (lifeGoalsInd) {
      lifeGoalsInd.textContent = 'цели: ' + String(lifeGoalsN);
      lifeGoalsInd.title = 'Образы целей жизненного опыта (GoalImagesLife), ' + lifeGoalsN + ' шт.; щелчок — список ID';
    }
    var sitImgN = 0;
    if (typeof global.SituationsTree_getAllSituationImages === 'function') {
      var sitArr = global.SituationsTree_getAllSituationImages() || [];
      sitImgN = sitArr.length;
    }
    if (situationImagesInd) {
      situationImagesInd.textContent = 'ситуации: ' + String(sitImgN);
      situationImagesInd.title = 'Образы ситуаций (SituationImages), ' + sitImgN + ' шт.; щелчок — список ID';
    }

    var gestaltInd = getCell('infoGestaltsIndicator');
    if (gestaltInd) {
      var gArr = Array.isArray(global.GestaltArr) ? global.GestaltArr : [];
      var gN = gArr.length;
      var rBuf = Array.isArray(global.GestaltResonanceBuffer) ? global.GestaltResonanceBuffer.length : 0;
      gestaltInd.textContent = 'гешт: ' + String(gN);
      gestaltInd.title = 'Гештальты (доминанты), записей: ' + gN
        + ', буфер резонанса (сигнатуры ЭП): ' + rBuf + '. Щелчок — список ID. См. beast/_9_Conscience/gestalt.js';
    }
    var fantasmInd = getCell('infoFantasmPlotIndicator');
    if (fantasmInd) {
      var fp = Array.isArray(info.fantasmPlotRules) ? info.fantasmPlotRules : [];
      var fpN = fp.length;
      fantasmInd.textContent = 'фант: ' + String(fpN);
      fantasmInd.title = 'Сюжет фантазма: правил ЭП в сюжете ' + fpN
        + '. Заполняется при переходе в пассивный режим (целевой→пассивный). Щелчок — детализация.';
    }
    var fantasmCell = getCell('infoFantasmPlotCell');
    if (fantasmCell) {
      var fp2 = Array.isArray(info.fantasmPlotRules) ? info.fantasmPlotRules : [];
      fantasmCell.title = 'Сюжет фантазма: ' + fp2.length + ' правил(а) снимков ЭП. Щелчок — список.';
      fantasmCell.classList.toggle('info-cell-active', fp2.length > 0);
    }

    // Индикатор текущего главного цикла осмысления (клик — детализация цикла).
    if (mainProcessLabel) {
      var mainId = typeof global.ConsciousnessCurrentProcessId === 'number'
        ? global.ConsciousnessCurrentProcessId
        : 0;
      var mainCyc = mainId && typeof global.Consciousness_getCycle === 'function'
        ? global.Consciousness_getCycle(mainId) : null;
      var mainExpired = !!(mainCyc && mainCyc.expired);
      mainProcessLabel.textContent = 'Главный цикл ID: ' + String(mainId) +
        (mainExpired ? ' · expired' : '');
      mainProcessLabel.title = 'Щелкните для детализации цикла' +
        (mainExpired ? '. Цикл помечен expired (отработан).' : '');
      mainProcessLabel.style.cursor = 'pointer';
      mainProcessLabel.onclick = function () {
        if (typeof global.Info_showMainCycleDetail === 'function') global.Info_showMainCycleDetail();
      };
    }

    // Детекторные индикаторы (желтая подсветка при активности)
    if (detCriticalVital) detCriticalVital.classList.toggle('info-cell-active', !!info.criticalVital);
    if (detImportantProblem) detImportantProblem.classList.toggle('info-cell-active', !!info.ImportantProblem);
    if (detUsedRule) {
      var urVal = typeof info.usedRule === 'number' ? info.usedRule : 0;
      var stArr = Array.isArray(global.Target_rule_stack) ? global.Target_rule_stack : [];
      var stTxt = stArr.length
        ? ('дно→верх: ' + stArr.map(function (x) { return String(x); }).join(', '))
        : 'стек пуст';
      detUsedRule.title = 'Реагирование по правилу (usedRule): ' + (urVal ? ('да (' + urVal + ')') : 'нет') +
        '\nTarget_rule_stack (до 3 ID цели): ' + stTxt;
      detUsedRule.classList.toggle('info-cell-active', !!urVal);
    }
    if (detShockState) detShockState.classList.toggle('info-cell-active', !!info.shockState);
    if (detSuddenChange) detSuddenChange.classList.toggle('info-cell-active', !!info.suddenChange);
    if (detPlayMode) detPlayMode.classList.toggle('info-cell-active', !!info.playMode);
    if (detLearningMode) detLearningMode.classList.toggle('info-cell-active', !!info.learningMode);
    if (detNegativeMotor) detNegativeMotor.classList.toggle('info-cell-active', !!info.negativeMotorEffect);
    if (detNegativeMental) detNegativeMental.classList.toggle('info-cell-active', !!info.negativeMentalEffect);
    if (detBadState) detBadState.classList.toggle('info-cell-active', !!info.badState);
    if (detConsoleStimulus) detConsoleStimulus.classList.toggle('info-cell-active', !!info.consoleStimulus);
    if (detSearchInterest) detSearchInterest.classList.toggle('info-cell-active', !!info.searchInterest);
    if (detTeacherLearning) detTeacherLearning.classList.toggle('info-cell-active', !!info.teacherLearning);
    if (detOperatorIgnoring) detOperatorIgnoring.classList.toggle('info-cell-active', !!info.operatorIgnoring);
    if (detDissatisfaction) detDissatisfaction.classList.toggle('info-cell-active', !!info.dissatisfaction);
    if (detMisunderstanding) detMisunderstanding.classList.toggle('info-cell-active', !!info.misunderstanding);
    if (detDoubtAuto) detDoubtAuto.classList.toggle('info-cell-active', !!info.doubtInDefaultAutomatism);
    if (detDefense) detDefense.classList.toggle('info-cell-active', !!info.defense);
    if (detFear) detFear.classList.toggle('info-cell-active', !!info.fear);
    if (detAggression) detAggression.classList.toggle('info-cell-active', !!info.aggression);
    if (detHighObj) detHighObj.classList.toggle('info-cell-active', !!info.highImportanceObject);
    if (detMoodImproving) detMoodImproving.classList.toggle('info-cell-active', !!info.moodImproving);
    if (detImageNovelty) detImageNovelty.classList.toggle('info-cell-active', !!info.imageNovelty);
    if (detHighSem) detHighSem.classList.toggle('info-cell-active', !!info.highSemanticImportance);
    if (detConfSem) detConfSem.classList.toggle('info-cell-active', !!info.conflictingSemantics);
    if (detHardProblem) detHardProblem.classList.toggle('info-cell-active', !!info.hardProblem);
    if (detPosEffect) detPosEffect.classList.toggle('info-cell-active', !!info.positiveActionEffect);
    if (detNegEffect) detNegEffect.classList.toggle('info-cell-active', !!info.negativeActionEffect);
    if (detNoReaction) detNoReaction.classList.toggle('info-cell-active', !!info.noReactionToStimulus);
    if (detAnswer) {
      detAnswer.classList.toggle('info-cell-active',
        !!(info.operatorAnswerReceived || info.isOperatorAnswerReceived));
    }
    if (detActiveWait) detActiveWait.classList.toggle('info-cell-active', !!info.activeWaitingAnswer);
    if (detSeriesNoEffect) detSeriesNoEffect.classList.toggle('info-cell-active', !!info.seriesWithoutEffect);
    if (detLowAutoUse) detLowAutoUse.classList.toggle('info-cell-active', !!info.lowAutomatismUsefulness);
    if (detFoundAuto) detFoundAuto.classList.toggle('info-cell-active', !!info.foundSuitableAutomatism);
  }

  function Info_setField(path, value) {
    var info = ensureInfoPicture();
    setByPath(info, path, value);
    Info_updateView();
  }

  /**
   * Режим мышления задаётся только у главного цикла ФО (thinkingMode), не полем инфокартины.
   */
  function Info_setMode(mode) {
    var passive = !(mode === 'target');
    if (typeof global.Consciousness_getMainCycle === 'function') {
      var mc = global.Consciousness_getMainCycle();
      if (mc) {
        mc.thinkingMode = passive ? 'passive' : 'target';
      }
    }
    if (typeof global.Info_updateView === 'function') {
      global.Info_updateView();
    }
  }

  var lastActiveBranchIdForSituations = null;

  function Info_updateFromPerceptionTree() {
    if (typeof global.PerceptionTree_getActiveTerminalNodeId !== 'function' ||
        typeof global.PerceptionTree_getPathByNodeId !== 'function') {
      return;
    }
    var nodeId = global.PerceptionTree_getActiveTerminalNodeId();
    if (!nodeId) return;
    var path = global.PerceptionTree_getPathByNodeId(nodeId);
    if (!Array.isArray(path) || path.length < 3) return;
    var info = ensureInfoPicture();
    info.tree.baseID = path[0] || 0;
    info.tree.emotionID = path[1] || 0;
    info.tree.activityID = path[2] || 0;
    info.tree.toneMoodID = path[3] || 0;
    info.tree.simbolID = path[4] || 0;
    info.tree.phraseID = path[5] || 0;
    Info_updateView();
    // Тему и ситуацию обновляем только при смене ID активной ветки дерева восприятия, а не по каждому пульсу.
    if (nodeId !== lastActiveBranchIdForSituations) {
      lastActiveBranchIdForSituations = nodeId;
      if (typeof global.SituationsTree_onPerceptionTreeActivated === 'function') {
        global.SituationsTree_onPerceptionTreeActivated();
      }
    }
  }

  function Info_updateFromStimulus(finalImageId, source) {
    var info = ensureInfoPicture();
    info.stimulus.finalImageId = finalImageId || 0;
    info.stimulus.source = source || 'external';
    Info_updateView();
  }

  function Info_setProblemFromUnsolved(finalImageId) {
    Info_setField('problemFinalImageId', finalImageId || 0);
  }

  function Info_incrementStep() {
    var info = ensureInfoPicture();
    info.step = (info.step || 0) + 1;
    Info_updateView();
  }

  /** Русские подписи детекторов ситуации (ключи как в SituationsTree_getSituationComponentName). */
  var SITUATION_DETECTOR_NAMES_RU = {
    criticalVital: 'критический витал',
    shockState: 'шоковое состояние',
    suddenChange: 'резкое изменение состояния',
    playMode: 'игровой режим',
    learningMode: 'режим обучения',
    negativeMotorEffect: 'негативный эффект моторного автоматизма',
    negativeMentalEffect: 'негативный эффект ментального автоматизма',
    badState: 'состояние «Плохо»',
    consoleStimulus: 'стимул с пульта',
    searchInterest: 'поисковый интерес',
    teacherLearning: 'обучение с учителем',
    operatorIgnoring: 'оператор игнорирует',
    dissatisfaction: 'неудовлетворённость',
    misunderstanding: 'непонимание',
    doubtInDefaultAutomatism: 'сомнение в штатном автоматизме',
    defense: 'защита',
    fear: 'страх',
    aggression: 'агрессия',
    highImportanceObject: 'объект высокой значимости',
    moodImproving: 'улучшение настроения',
    imageNovelty: 'новизна образа',
    highSemanticImportance: 'высокая семантическая значимость',
    conflictingSemantics: 'противоречивая семантика',
    hardProblem: 'трудная проблема',
    positiveActionEffect: 'положительный эффект действия',
    negativeActionEffect: 'отрицательный эффект действия',
    noReactionToStimulus: 'нет реакции на стимул',
    operatorAnswerReceived: 'ответ оператора получен',
    activeWaitingAnswer: 'активное ожидание ответа',
    seriesWithoutEffect: 'серия эпизодов без эффекта',
    lowAutomatismUsefulness: 'низкая полезность автоматизма',
    foundSuitableAutomatism: 'найден подходящий автоматизм',
    notFoundSuitableAutomatism: 'не найден подходящий автоматизм',
    usedRule: 'реагирование по правилу',
    peaceNormBaseline: 'норма виталов и покой'
  };

  function Info_formatSituationDetectorNames(sImg) {
    if (!sImg || !Array.isArray(sImg.componentIds) || !sImg.componentIds.length) {
      return '—';
    }
    var parts = [];
    for (var di = 0; di < sImg.componentIds.length; di++) {
      var cid = sImg.componentIds[di];
      var key = (typeof global.SituationsTree_getSituationComponentName === 'function')
        ? global.SituationsTree_getSituationComponentName(cid) : '';
      var label = (key && SITUATION_DETECTOR_NAMES_RU[key]) || key || ('id ' + String(cid));
      parts.push(label);
    }
    return parts.join(', ');
  }

  function Info_showDetail(key) {
    if (typeof global.show_alert !== 'function') return;
    var info = ensureInfoPicture();
    var html = '';
    if (key === 'mode') {
      var mtm = (typeof global.Consciousness_getMainCycleThinkingMode === 'function')
        ? global.Consciousness_getMainCycleThinkingMode()
        : 'target';
      html = '<b>Режим мышления (главный цикл ФО):</b> ' + (mtm === 'target' ? 'Целевой' : 'Пассивный');
    } else if (key === 'fantasmPlot') {
      var rules = Array.isArray(info.fantasmPlotRules) ? info.fantasmPlotRules : [];
      html = '<b>Сюжет фантазма</b> — пополняемый массив правил эпизодической памяти (снимки кадров).';
      html += '<br><small>При переходе режима целевой → пассивный выполняется заполнение (алгоритм будет задан отдельно; сейчас — последние кадры Episodes).</small>';
      html += '<br><br><b>Правил в сюжете:</b> ' + rules.length;
      for (var fpi = 0; fpi < rules.length; fpi++) {
        var rr = rules[fpi];
        if (!rr) continue;
        html += '<br>— узел ' + (rr.perceptionNodeId || 0) + ', ситуация ' + (rr.situationId || 0) +
          ', стимул ' + (rr.stimulusImageId || 0) + ', ОД=' + (rr.actionImageId != null ? rr.actionImageId : '—') +
          ', effect=' + (rr.effect != null ? rr.effect : '—');
      }
      if (!rules.length) html += '<br><i>пока пусто</i>';
    } else if (key === 'theme') {
      var thId = info.themeId || 0;
      html = '<b>Тема:</b> ' + (thId || 'нет');
      if (thId && typeof global.SituationsTree_getThemeImage === 'function') {
        var tImg = global.SituationsTree_getThemeImage(thId);
        if (tImg) {
          html += '<br><br><b>Детализация образа темы:</b>' +
            '<br>ID темы: ' + tImg.id +
            '<br>Режим: ' + (tImg.mode === 1 ? 'целевой' : 'пассивный') +
            '<br>Компоненты (BaseID, EmotionID, ActivityID): ' +
            (tImg.componentIds && tImg.componentIds.length ? tImg.componentIds.join(', ') : 'нет') +
            '<br>Число усреднений: ' + tImg.count +
            '<br>Готовый образ: ' + (tImg.ready ? 'да' : 'нет');
        }
      }
      if (typeof global.SituationsTree_getAllThemeImages === 'function') {
        var allT = global.SituationsTree_getAllThemeImages() || [];
        html += '<br><br><b>Всего тем:</b> ' + allT.length;
        if (allT.length) {
          html += '<br>ID тем (щелчок — детализация): ';
          for (var ti = 0; ti < allT.length; ti++) {
            if (ti > 0) html += ', ';
            html += infoAlertIdLink('Info_showThemeImageDetail', allT[ti].id);
          }
        }
      }
    } else if (key === 'situation') {
      var stId = info.situationId || 0;
      html = '<b>Ситуации</b> (образы из дерева ситуаций)';
      html += '<br><b>Активная сейчас:</b> ';
      if (stId && typeof global.SituationsTree_getSituationImage === 'function' &&
          global.SituationsTree_getSituationImage(stId)) {
        var actSit = global.SituationsTree_getSituationImage(stId);
        html += infoSituationImageLinkFromRecord(actSit);
        html += '<br><i>Детекторы:</i> ' + Info_formatSituationDetectorNames(actSit);
      } else {
        html += (stId ? ('ID ' + String(stId) + ' (образ не найден)') : 'нет');
      }
      if (typeof global.SituationsTree_getAllSituationImages !== 'function') {
        html += '<br><br>Списки ситуаций недоступны (SituationsTree).';
      } else {
        var allS = global.SituationsTree_getAllSituationImages() || [];
        var realized = [];
        var unrealized = [];
        for (var sx = 0; sx < allS.length; sx++) {
          var sv = allS[sx];
          if (!sv || typeof sv.id !== 'number') continue;
          if (sv.ready === true) {
            realized.push(sv);
          } else {
            unrealized.push(sv);
          }
        }
        realized.sort(function (a, b) { return (a.id || 0) - (b.id || 0); });
        unrealized.sort(function (a, b) {
          var ca = typeof a.count === 'number' ? a.count : 0;
          var cb = typeof b.count === 'number' ? b.count : 0;
          if (ca !== cb) return ca - cb;
          return (a.id || 0) - (b.id || 0);
        });
        html += '<br><br><b>Реализованные</b> (готовый образ, ready): ' + realized.length;
        html += '<br><small>Щелчок по ID — полная детализация</small>';
        if (realized.length) {
          for (var ri = 0; ri < realized.length; ri++) {
            html += '<br>• ' + infoSituationImageLinkFromRecord(realized[ri]) +
              ' — <i>' + Info_formatSituationDetectorNames(realized[ri]) + '</i>';
          }
        } else {
          html += '<br><i>пока нет</i>';
        }
        html += '<br><br><b>Нереализованные</b> (ещё усредняются, не ready): ' + unrealized.length;
        html += '<br><small>count — число усреднений</small>';
        if (unrealized.length) {
          for (var ui = 0; ui < unrealized.length; ui++) {
            var uc = typeof unrealized[ui].count === 'number' ? unrealized[ui].count : 0;
            html += '<br>• ' + infoSituationImageLinkFromRecord(unrealized[ui]) +
              ' <small>(' + String(uc) + ')</small> — <i>' + Info_formatSituationDetectorNames(unrealized[ui]) + '</i>';
          }
        } else {
          html += '<br><i>пока нет</i>';
        }
        html += '<br><br><small>Всего записей ситуаций: ' + String(allS.length) + '</small>';
      }
    } else if (key === 'problem') {
      html = '<b>Нерешённая проблема:</b> FinalImageId=' + (info.problemFinalImageId || 'нет');
    } else if (key === 'step') {
      html = '<b>Шаг осмысления:</b> ' + (info.step || 0);
    } else if (key === 'stimulus') {
      var fid = info.stimulus.finalImageId || 0;
      var stimDetail = '';
      if (fid && typeof global.PerceptionTree_getStimulusNamesForFinalImageId === 'function') {
        stimDetail = global.PerceptionTree_getStimulusNamesForFinalImageId(fid) || '';
      }
      html = '<b>Стимул:</b> ' + (stimDetail || ('FinalImageId=' + (fid || 'нет'))) +
        '<br><b>Источник:</b> ' + (info.stimulus.source === 'imagination' ? 'воображение' : 'внешний стимул');
      // Сравнение с "стимулом с пульта".
      var cId2 = info.consoleStimulus || 0;
      if (cId2) {
        html += '<br><b>Стимул с пульта (последний):</b> ' +
          infoAlertIdLink('Info_showFinalImageStimulusDetail', cId2) +
          (cId2 === fid ? ' (совпадает)' : ' (не совпадает)');
      } else {
        html += '<br><b>Стимул с пульта (последний):</b> 0';
      }
      // Семантика для этого стимула (если есть текущие well/emotion).
      if (fid && typeof global.getSemanticObjectFast === 'function') {
        var well = global.BadNormWell || 0;
        var emotionId = 0;
        if (typeof global.Emotions_getOrCreateEmotionId === 'function' && Array.isArray(global.BasicContextsActived)) {
          emotionId = global.Emotions_getOrCreateEmotionId(global.BasicContextsActived) || 0;
        }
        var semObj = (well && emotionId) ? global.getSemanticObjectFast(well, emotionId, fid) : null;
        if (semObj) {
          html += '<br><br><b>Семантика (в текущих условиях):</b>' +
            '<br>Средняя значимость z ≈ ' + (Math.round(semObj.meanImportance * 100) / 100) +
            '<br>Число обучений: ' + semObj.samplesCount;
        }
      }
    } else if (key === 'emotion') {
      var eId = info.tree.emotionID || 0;
      html = '<b>EmotionID:</b> ' + String(eId || 0);
      if (eId && Array.isArray(global.Emotions)) {
        var em = null;
        for (var eii = 0; eii < global.Emotions.length; eii++) {
          if (global.Emotions[eii] && global.Emotions[eii].id === eId) { em = global.Emotions[eii]; break; }
        }
        if (em) {
          var emotionContexts = em.contextIds || [];
          if (emotionContexts.length && Array.isArray(global.BasicContexts)) {
            var names = [];
            for (var ci = 0; ci < emotionContexts.length; ci++) {
              var cid = emotionContexts[ci];
              var found = false;
              for (var bj = 0; bj < global.BasicContexts.length; bj++) {
                if (global.BasicContexts[bj] && global.BasicContexts[bj].id === cid) {
                  names.push(global.BasicContexts[bj].name || ('Контекст ' + cid));
                  found = true;
                  break;
                }
              }
              if (!found) names.push('id ' + cid);
            }
            html += '<br><b>Базовые контексты:</b> ' + names.join(', ');
          } else {
            html += '<br><b>Базовые контексты:</b> —';
          }
        }
      }
      html += '<br><br><b>Полная ветка:</b><br>' +
        'BaseID=' + (info.tree.baseID || 0) +
        ' → EmotionID=' + (info.tree.emotionID || 0) +
        ' → ActivityID=' + (info.tree.activityID || 0);
    } else if (key === 'activity') {
      var actId2 = info.tree.activityID || 0;
      var actNames = '';
      if (actId2 && typeof global.PerceptionTree_getStimulusNamesForFinalImageId === 'function') {
        actNames = global.PerceptionTree_getStimulusNamesForFinalImageId(actId2) || '';
      }
      html = '<b>ActivityID:</b> ' + (actId2 ? infoAlertIdLink('Info_showFinalImageStimulusDetail', actId2) : '0');
      html += '<br><b>Актуальный образ стимула:</b> ' + (actNames && actNames !== '—' ? actNames : '—');
      if (actId2 && typeof global.getSemanticObjectFast === 'function') {
        var wellA = global.BadNormWell || 0;
        var emoA = info.tree.emotionID || 0;
        var semA = (wellA && emoA) ? global.getSemanticObjectFast(wellA, emoA, actId2) : null;
        if (semA) {
          html += '<br><br><b>Семантика (well=' + String(wellA) + ', emotionId=' + String(emoA) + '):</b>' +
            '<br>z ≈ ' + (Math.round(semA.meanImportance * 100) / 100) +
            '<br>обучений: ' + semA.samplesCount;
        }
      }
      html += '<br><br><b>Полная ветка:</b><br>' +
        'BaseID=' + (info.tree.baseID || 0) +
        ' → EmotionID=' + (info.tree.emotionID || 0) +
        ' → ActivityID=' + (info.tree.activityID || 0);
    } else if (key === 'tree') {
      // Base: Плохо / Норма / Хорошо
      var baseName = 'не определено';
      if (info.tree.baseID === 1) baseName = 'Плохо';
      else if (info.tree.baseID === 2) baseName = 'Норма';
      else if (info.tree.baseID === 3) baseName = 'Хорошо';

      // Emotion: состав базовых контекстов
      var emotionName = 'нет';
      var emotionContexts = [];
      if (info.tree.emotionID && Array.isArray(global.Emotions)) {
        for (var ei = 0; ei < global.Emotions.length; ei++) {
          if (global.Emotions[ei] && global.Emotions[ei].id === info.tree.emotionID) {
            emotionContexts = global.Emotions[ei].contextIds || [];
            break;
          }
        }
      }
      if (emotionContexts.length && Array.isArray(global.BasicContexts)) {
        var parts = [];
        for (var ci = 0; ci < emotionContexts.length; ci++) {
          var cid = emotionContexts[ci];
          for (var bj = 0; bj < global.BasicContexts.length; bj++) {
            if (global.BasicContexts[bj] && global.BasicContexts[bj].id === cid) {
              parts.push(global.BasicContexts[bj].name || ('Контекст ' + cid));
              break;
            }
          }
        }
        if (parts.length) {
          emotionName = parts.join(', ');
        }
      }

      // Activity: актуальный образ стимула + семантика
      var actId = info.tree.activityID || 0;
      var actDetail = '';
      if (actId && typeof global.PerceptionTree_getStimulusNamesForFinalImageId === 'function') {
        actDetail = global.PerceptionTree_getStimulusNamesForFinalImageId(actId) || '';
      }
      var actSem = '';
      if (actId && typeof global.getSemanticObjectFast === 'function') {
        var wellT = global.BadNormWell || 0;
        var emotionIdT = info.tree.emotionID || 0;
        var semObjT = (wellT && emotionIdT) ? global.getSemanticObjectFast(wellT, emotionIdT, actId) : null;
        if (semObjT) {
          actSem = 'Средняя значимость z ≈ ' + (Math.round(semObjT.meanImportance * 100) / 100) +
            ', число обучений: ' + semObjT.samplesCount;
        }
      }

      html = '<b>Base:</b> ' + baseName +
        '<br><b>EmotionID:</b> ' + (info.tree.emotionID || 0) +
        '<br><b>Эмоция (состав базовых контекстов):</b> ' + emotionName +
        '<br><br><b>ActivityID:</b> ' + (actId || 0) +
        '<br><b>Актуальный образ стимула:</b> ' + (actDetail || 'нет') +
        (actSem ? ('<br>' + actSem) : '') +
        '<br><br><b>Полная ветка дерева:</b><br>' +
        'BaseID=' + (info.tree.baseID || 0) +
        ' → EmotionID=' + (info.tree.emotionID || 0) +
        ' → ActivityID=' + (info.tree.activityID || 0) +
        ' → ToneMoodID=' + (info.tree.toneMoodID || 0) +
        ' → SimbolID=' + (info.tree.simbolID || 0) +
        ' → PhraseID=' + (info.tree.phraseID || 0);
    } else if (key === 'criticalVital') {
      html = '<b>Критический витал:</b> ' + (info.criticalVital ? 'активен' : 'нет');
    } else if (key === 'ImportantProblem') {
      html = '<b>Важная проблема:</b> ' + (info.ImportantProblem ? 'да (гистерезис внимания к стимулам)' : 'нет (облегчённый ОР/ФО)');
    } else if (key === 'usedRule') {
      var ur = typeof info.usedRule === 'number' ? info.usedRule : 0;
      var stk = Array.isArray(global.Target_rule_stack) ? global.Target_rule_stack.slice() : [];
      html = '<b>Реагирование по правилу</b> (usedRule): ' + (ur ? ('да, значение ' + ur) : 'нет (0)') +
        '<br><br><b>Стек целей Target_rule_stack</b> (LIFO, макс. 3 FinalImageId): ';
      if (!stk.length) {
        html += 'пусто.';
      } else {
        html += 'дно → вершина (последний добавленный справа):<br>';
        for (var si = 0; si < stk.length; si++) {
          if (si > 0) html += ' → ';
          html += infoAlertIdLink('Info_showFinalImageStimulusDetail', stk[si]);
        }
      }
    } else if (key === 'shockState') {
      html = '<b>Шоковое состояние:</b> ' + (info.shockState ? 'активно' : 'нет');
    } else if (key === 'suddenChange') {
      html = '<b>Стало резко хуже (переход в Плохо):</b> ' + (info.suddenChange ? 'да' : 'нет');
    } else if (key === 'playMode') {
      html = '<b>Игровой режим:</b> ' + (info.playMode ? 'активен' : 'нет');
    } else if (key === 'learningMode') {
      html = '<b>Режим обучения:</b> ' + (info.learningMode ? 'активен' : 'нет');
    } else if (key === 'negativeMotorEffect') {
      html = '<b>Негативный эффект моторного автоматизма:</b> ' + (info.negativeMotorEffect ? 'есть' : 'нет');
    } else if (key === 'negativeMentalEffect') {
      html = '<b>Негативный эффект ментального автоматизма:</b> ' + (info.negativeMentalEffect ? 'есть' : 'нет');
    } else if (key === 'badState') {
      html = '<b>Состояние Плохо:</b> ' + (info.badState ? 'да' : 'нет');
    } else if (key === 'consoleStimulus') {
      var cId = info.consoleStimulus || 0;
      var curId = info.stimulus && typeof info.stimulus.finalImageId === 'number' ? info.stimulus.finalImageId : 0;
      var cNames = '';
      if (cId && typeof global.PerceptionTree_getStimulusNamesForFinalImageId === 'function') {
        cNames = global.PerceptionTree_getStimulusNamesForFinalImageId(cId) || '';
      }
      html = '<b>Стимул с пульта</b> (consoleStimulus): ' + (cId ? infoAlertIdLink('Info_showFinalImageStimulusDetail', cId) : '0') +
        '<br><i>Смысл:</i> это ID конечного образа (FinalImageId) последнего стимула, пришедшего от оператора (пульта).' +
        '<br>Он может <b>не совпадать</b> с текущим стимулом осмысления.' +
        (cId ? ('<br><b>Имена стимулов:</b> ' + (cNames && cNames !== '—' ? cNames : '—')) : '') +
        '<br><br><b>Текущий стимул осмысления:</b> ' +
        (curId ? infoAlertIdLink('Info_showFinalImageStimulusDetail', curId) : '0') +
        (curId && info.stimulus && info.stimulus.source ? (' (' + (info.stimulus.source === 'imagination' ? 'воображение' : 'внешний') + ')') : '') +
        '<br><b>Совпадает?</b> ' + ((cId && curId && cId === curId) ? 'да' : 'нет');
    } else if (key === 'searchInterest') {
      html = '<b>Поисковый интерес:</b> ' + (info.searchInterest ? 'активен' : 'нет');
    } else if (key === 'teacherLearning') {
      html = '<b>Обучение с учителем:</b> ' + (info.teacherLearning ? 'идёт' : 'нет');
    } else if (key === 'operatorIgnoring') {
      html = '<b>Игнорирование оператором:</b> ' + (info.operatorIgnoring ? 'обнаружено' : 'нет');
    } else if (key === 'dissatisfaction') {
      html = '<b>Неудовлетворённость существующим:</b> ' + (info.dissatisfaction ? 'есть' : 'нет');
    } else if (key === 'misunderstanding') {
      html = '<b>Непонимание:</b> ' + (info.misunderstanding ? 'есть' : 'нет');
    } else if (key === 'doubtInDefaultAutomatism') {
      html = '<b>Сомнение в штатном автоматизме:</b> ' + (info.doubtInDefaultAutomatism ? 'есть' : 'нет');
    } else if (key === 'defense') {
      html = '<b>Защита:</b> ' + (info.defense ? 'активна' : 'нет');
    } else if (key === 'fear') {
      html = '<b>Страх:</b> ' + (info.fear ? 'активен' : 'нет');
    } else if (key === 'aggression') {
      html = '<b>Агрессия:</b> ' + (info.aggression ? 'активна' : 'нет');
    } else if (key === 'highImportanceObject') {
      var ho = typeof info.highImportanceObject === 'number' ? info.highImportanceObject : 0;
      var curStim = info.stimulus && typeof info.stimulus.finalImageId === 'number' ? info.stimulus.finalImageId : 0;
      html = '<b>Важный объект</b> (highImportanceObject): ' + String(ho || 0) +
        '<br><i>Суть:</i> это <b>ID конкретного объекта</b> (или уровень), который внешний код распознал как «особо значимый»' +
        ' в текущей ситуации и передал сюда.' +
        '<br>Это не «уровень семантики вообще», а <b>указание на найденный объект</b>.' +
        (ho ? '<br><br><b>Подсказка:</b> если это FinalImageId, можно открыть: ' + infoAlertIdLink('Info_showFinalImageStimulusDetail', ho) : '') +
        '<br><br><b>Текущий стимул осмысления:</b> ' + (curStim ? infoAlertIdLink('Info_showFinalImageStimulusDetail', curStim) : '0') +
        '<br><b>Совпадает с текущим стимулом?</b> ' + ((ho && curStim && ho === curStim) ? 'да' : 'нет');
    } else if (key === 'moodImproving') {
      html = '<b>Улучшение настроения:</b> ' + (info.moodImproving ? 'зафиксировано' : 'нет');
    } else if (key === 'imageNovelty') {
      html = '<b>Новизна образа:</b> ' + (info.imageNovelty ? 'высокая' : 'нет');
    } else if (key === 'highSemanticImportance') {
      var hs = typeof info.highSemanticImportance === 'number' ? info.highSemanticImportance : 0;
      var fidHs = info.stimulus && typeof info.stimulus.finalImageId === 'number' ? info.stimulus.finalImageId : 0;
      html = '<b>Выс. семантика</b> (highSemanticImportance): ' + String(hs || 0) +
        '<br><i>Суть:</i> это <b>численный уровень семантической значимости</b> текущего образа в данных условиях (обычно meanImportance).' +
        '<br>То есть «насколько значим стимул по семантике», а не «какой именно объект важен».' +
        '<br><br><b>Для текущего стимула:</b> ' + (fidHs ? infoAlertIdLink('Info_showFinalImageStimulusDetail', fidHs) : '0');
    } else if (key === 'conflictingSemantics') {
      html = '<b>Противоречивая семантика:</b> ' + (info.conflictingSemantics ? 'есть' : 'нет');
    } else if (key === 'hardProblem') {
      html = '<b>Трудная проблема:</b> ' + (info.hardProblem ? 'активна' : 'нет');
    } else if (key === 'positiveActionEffect') {
      html = '<b>Положительный эффект действия:</b> ' + (info.positiveActionEffect ? 'есть' : 'нет');
    } else if (key === 'negativeActionEffect') {
      html = '<b>Отрицательный эффект действия:</b> ' + (info.negativeActionEffect ? 'есть' : 'нет');
    } else if (key === 'noReactionToStimulus') {
      html = '<b>Нет реакции на стимул:</b> ' + (info.noReactionToStimulus ? 'зафиксировано' : 'нет');
    } else if (key === 'operatorAnswerReceived') {
      html = '<b>Ответ оператора получен</b> (operatorAnswerReceived): ' +
        (info.operatorAnswerReceived ? 'да' : 'нет') +
        '<br><b>Ответ на автоматизм ФО</b> (isOperatorAnswerReceived): ' +
        (info.isOperatorAnswerReceived ? 'да (обрабатывается ур.2 при необходимости)' : 'нет');
    } else if (key === 'activeWaitingAnswer') {
      html = '<b>Активное ожидание ответа:</b> ' + (info.activeWaitingAnswer ? 'идёт' : 'нет');
    } else if (key === 'seriesWithoutEffect') {
      html = '<b>Серия эпизодов без эффекта:</b> ' + (info.seriesWithoutEffect ? 'есть' : 'нет');
    } else if (key === 'lowAutomatismUsefulness') {
      html = '<b>Низкая полезность автоматизма:</b> ' + (info.lowAutomatismUsefulness ? 'обнаружена' : 'нет');
    } else if (key === 'foundSuitableAutomatism') {
      html = '<b>Пока найден подходящий автоматизм:</b> ' + (info.foundSuitableAutomatism ? 'да' : 'нет');
    }
    if (html) {
      global.show_alert(html, 0);
    }
  }

  global.Info_initIfNeeded = ensureInfoPicture;

  /** Пульс создания цикла: явное поле или первая запись лога (для старых циклов без birthPuls). */
  function resolveCycleBirthPuls(cycleOrItem, pulsNow) {
    if (cycleOrItem && typeof cycleOrItem.birthPuls === 'number') return cycleOrItem.birthPuls;
    var dl = cycleOrItem && cycleOrItem.debugLog;
    if (Array.isArray(dl) && dl.length && typeof dl[0].atPuls === 'number') return dl[0].atPuls;
    return pulsNow;
  }

  /**
   * Форматирует информацию о цикле или элементе стека фоновых в HTML для show_alert.
   * @param {Object} cycleOrItem - объект цикла (из реестра) или элемент стека { id, context, importance }
   * @param {string} prefix - подпись блока (например "Цикл" или "Фоновый #N")
   * @returns {string}
   */
  function formatCycleOrStackInfo(cycleOrItem, prefix) {
    if (!cycleOrItem) return '';
    var ctx = cycleOrItem.context || {};
    var actualId = ctx.actual_stimul_ID != null ? ctx.actual_stimul_ID : 0;
    var branchId = ctx.branchId != null ? ctx.branchId : 0;
    var well = ctx.well != null ? ctx.well : 0;
    var emotionId = ctx.emotionId != null ? ctx.emotionId : 0;
    var activityID = ctx.activityID != null ? ctx.activityID : actualId;
    var weight = cycleOrItem.weight != null ? cycleOrItem.weight : (cycleOrItem.importance != null ? cycleOrItem.importance : 0);
    var stimNames = '';
    if (typeof global.PerceptionTree_getStimulusNamesForFinalImageId === 'function' && actualId) {
      var names = global.PerceptionTree_getStimulusNamesForFinalImageId(actualId);
      if (names && names.length) stimNames = ' — ' + names;
    }
    var cycId = cycleOrItem.id != null ? cycleOrItem.id : 0;
    var idLine = 'ID: ' + (cycId ? infoAlertIdLink('Info_showCycleDetailById', cycId) : '—');
    var themeId = typeof cycleOrItem.createdThemeId === 'number' ? cycleOrItem.createdThemeId : 0;
    var themeLine = 'Тема при создании (createdThemeId): ' +
      (themeId ? infoAlertIdLink('Info_showThemeImageDetail', themeId) : '0');
    var sitId = typeof cycleOrItem.createdSituationId === 'number' ? cycleOrItem.createdSituationId : 0;
    var sitLine = 'Ситуация при создании (createdSituationId): ' +
      (sitId ? infoSituationImageLinkById(sitId) : '0');
    var goalImgId = typeof cycleOrItem.goalImageId === 'number' ? cycleOrItem.goalImageId : 0;
    var goalLine = 'Образ цели (goalImageId): ' + String(goalImgId || 0) +
      (goalImgId ? ' <small>(жизненный опыт, id записи)</small>' : ' <small>(пассивный режим или базовая цель без образа)</small>');
    var waitAutoId = typeof cycleOrItem.waitingFeedbackAutomatizmId === 'number' ? cycleOrItem.waitingFeedbackAutomatizmId : 0;
    var waitAutoLine = 'Ожидание оценки автоматизма (waitingFeedbackAutomatizmId): ' + String(waitAutoId || 0);
    var waitCycLine = 'Период ожидания после автоматизма: awaiting=' +
      (cycleOrItem.awaitingAutomatismEpisodeFeedback ? 'да' : 'нет') +
      ', ответ стимулом=' + (cycleOrItem.automatismOperatorAnswerReceived ? 'да' : 'нет') +
      ', effect кадра=' + (typeof cycleOrItem.lastAutomatismEpisodeEffect === 'number'
        ? String(cycleOrItem.lastAutomatismEpisodeEffect) : '—');
    var stimLine = 'Стимул FinalImageId: ' +
      (actualId ? infoAlertIdLink('Info_showFinalImageStimulusDetail', actualId) : '0') + stimNames;
    var branchLine = 'Ветка (branchId): ' +
      (branchId ? infoAlertIdLink('Info_showPerceptionNodeDetail', branchId) : '0');
    var emoLine = 'Эмоция (emotionId): ' +
      (emotionId ? infoAlertIdLink('Info_showEmotionRecordDetail', emotionId) : '0');
    var actLine = 'ActivityID: ' +
      (activityID ? infoAlertIdLink('Info_showFinalImageStimulusDetail', activityID) : '0');
    if (activityID && activityID === actualId) {
      actLine += ' (совпадает со стимулом)';
    }
    var effectiveState = (cycleOrItem && cycleOrItem.expired === true) ? 'expired' : 'running';
    var parts = [
      '<b>' + (prefix || 'Цикл') + '</b>',
      idLine,
      themeLine,
      sitLine,
      goalLine,
      waitAutoLine,
      waitCycLine,
      stimLine,
      branchLine,
      'Базовое состояние (well): ' + String(well) + (well === 1 ? ' (Плохо)' : well === 2 ? ' (Норма)' : well === 3 ? ' (Хорошо)' : ''),
      emoLine,
      actLine,
      'Значимость: ' + String(weight),
      'Состояние цикла: ' + effectiveState,
      'expired: ' + (cycleOrItem.expired === true
        ? 'да (отработан, остаётся в реестре)'
        : 'нет')
    ];
    if (cycleOrItem.passiveWorkAfterGestaltExpire === true) {
      parts.push('После expired (стагнация сразу после тика гештальта): инфокартина переведена в <b>пассивный режим работы</b>.');
    }
    if (typeof cycleOrItem.lastGestaltConsciencePuls === 'number' && cycleOrItem.lastGestaltConsciencePuls > 0) {
      parts.push('Последний тик ур.4/гештальта (пульс): ' + String(cycleOrItem.lastGestaltConsciencePuls));
    }
    if (cycleOrItem.status != null) {
      parts.push('Статус: ' + String(cycleOrItem.status));
    }
    if (typeof cycleOrItem.step === 'number') {
      parts.push('Шаг (уровень ФО): ' + String(cycleOrItem.step));
    }
    var pulsNow = typeof global.cpuls === 'number' ? global.cpuls : 0;
    var birthPuls = resolveCycleBirthPuls(cycleOrItem, pulsNow);
    parts.push('Пульс создания: ' + String(birthPuls));
    parts.push('Возраст цикла (пульсов с создания): ' + String(Math.max(0, pulsNow - birthPuls)));
    if (typeof cycleOrItem.count === 'number') {
      parts.push(
        'Шагов диспетчера (runOneStep): ' + String(cycleOrItem.count) +
          ' — тики dispetchConsciousnessThinking, не лимит остановки; у фонового цикла шаг реже (≈ раз в 5 пульсов).'
      );
    }
    if (typeof cycleOrItem.order === 'number') {
      parts.push('Порядок создания: ' + String(cycleOrItem.order));
    }
    if (Array.isArray(cycleOrItem.fantasizmArr)) {
      parts.push('fantasizmArr (правила ЭП, сюжет фонового цикла): ' + String(cycleOrItem.fantasizmArr.length) + ' шт.');
    }
    if (cycleOrItem.fantasizmPassiveFillStarted === true) {
      parts.push('Заполнение fantasizmArr при пассивном режиме: начато.');
    }
    // Лог цикла (если есть)
    if (Array.isArray(cycleOrItem.debugLog) && cycleOrItem.debugLog.length) {
      var from = Math.max(0, cycleOrItem.debugLog.length - 30);
      var lines = [];
      for (var i = from; i < cycleOrItem.debugLog.length; i++) {
        var e = cycleOrItem.debugLog[i];
        var p = (e && typeof e.atPuls === 'number') ? e.atPuls : 0;
        var t = e && e.text != null ? String(e.text) : '';
        lines.push('[' + String(p) + '] ' + t);
      }
      parts.push('<details style="margin-top:6px"><summary><b>Лог цикла</b> (последние ' + String(lines.length) + ')</summary>' +
        '<pre style="white-space:pre-wrap;margin:6px 0 0 0;font-size:10pt">' +
        lines.join('\n').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') +
        '</pre></details>');
    }
    return parts.join('<br>');
  }

  /**
   * Показать информацию о текущем главном цикле осмысления.
   * Вызывается по щелчку на «Главный цикл ID: N».
   */
  function Info_showMainCycleDetail() {
    if (typeof global.show_alert !== 'function') return;
    var mainId = typeof global.ConsciousnessCurrentProcessId === 'number' ? global.ConsciousnessCurrentProcessId : 0;
    if (!mainId) {
      global.show_alert('Нет активного главного цикла (ID = 0).', 0);
      return;
    }
    var getCycle = global.Consciousness_getCycle;
    var cycle = typeof getCycle === 'function' ? getCycle(mainId) : null;
    if (!cycle) {
      global.show_alert('Цикл с ID ' + String(mainId) + ' не найден в реестре (уже завершён или удалён).', 0);
      return;
    }
    var html = formatCycleOrStackInfo(cycle, 'Главный цикл');
    global.show_alert(html, 0);
  }

  /**
   * Показать подробности фоновых циклов осмысления (из реестра, isMainCycle=false, status=running).
   * Вызывается по щелчку на индикаторе «фон: N». Для каждого цикла выводится та же детализация, что и для главного.
   */
  function Info_showBackgroundStack() {
    if (typeof global.show_alert !== 'function') return;
    var list = typeof global.Consciousness_getBackgroundCyclesList === 'function'
      ? global.Consciousness_getBackgroundCyclesList() : [];
    if (!list.length) {
      global.show_alert('Фоновых циклов осмысления нет.', 0);
      return;
    }
    var blocks = ['<b>Фоновые циклы осмысления</b> (по убыванию значимости). Щелчок по ID — полная детализация:'];
    for (var i = 0; i < list.length; i++) {
      var item = list[i];
      var cid = item && item.id != null ? item.id : 0;
      var w = item && typeof item.weight === 'number' ? item.weight : 0;
      var ctx = item && item.context ? item.context : {};
      var stim = ctx && typeof ctx.actual_stimul_ID === 'number' ? ctx.actual_stimul_ID : 0;
      blocks.push('<br>');
      blocks.push(cid ? infoAlertIdLink('Info_showBackgroundCycleDetail', cid) : '—');
      blocks.push(' — значимость ' + String(w) + ', стимул FinalImageId=');
      blocks.push(stim ? infoAlertIdLink('Info_showFinalImageStimulusDetail', stim) : '0');
      if (item && item.expired === true) {
        blocks.push(', <b>expired</b>');
      }
    }
    global.show_alert(blocks.join(''), 0);
  }

  function Info_showBackgroundCycleDetail(cycleId) {
    if (typeof global.show_alert !== 'function') return;
    cycleId = parseInt(cycleId, 10);
    if (isNaN(cycleId) || cycleId <= 0) return;
    var getCycle = global.Consciousness_getCycle;
    var cycle = typeof getCycle === 'function' ? getCycle(cycleId) : null;
    if (!cycle) {
      global.show_alert('Цикл с ID ' + String(cycleId) + ' не найден в реестре.', 0);
      return;
    }
    global.show_alert(formatCycleOrStackInfo(cycle, 'Фоновый цикл'), 0);
  }

  function Info_showThemeImageDetail(themeId) {
    if (typeof global.show_alert !== 'function') return;
    themeId = parseInt(themeId, 10) || 0;
    if (!themeId || typeof global.SituationsTree_getThemeImage !== 'function') {
      global.show_alert('Тема #' + themeId + ' не найдена.', 0);
      return;
    }
    var tImg = global.SituationsTree_getThemeImage(themeId);
    if (!tImg) {
      global.show_alert('Тема #' + themeId + ' не найдена.', 0);
      return;
    }
    var html = '<b>Образ темы #' + tImg.id + '</b>' +
      '<br>Режим: ' + (tImg.mode === 1 ? 'целевой' : 'пассивный') +
      '<br>Компоненты (BaseID, EmotionID, ActivityID): ' +
      (tImg.componentIds && tImg.componentIds.length ? tImg.componentIds.join(', ') : 'нет') +
      '<br>Число усреднений: ' + tImg.count +
      '<br>Готовый образ: ' + (tImg.ready ? 'да' : 'нет');
    global.show_alert(html, 0);
  }

  function Info_showSituationImageDetail(situationId) {
    if (typeof global.show_alert !== 'function') return;
    situationId = parseInt(situationId, 10) || 0;
    if (!situationId || typeof global.SituationsTree_getSituationImage !== 'function') {
      global.show_alert('Ситуация #' + situationId + ' не найдена.', 0);
      return;
    }
    var sImg = global.SituationsTree_getSituationImage(situationId);
    if (!sImg) {
      global.show_alert('Ситуация #' + situationId + ' не найдена.', 0);
      return;
    }
    var comp = '';
    if (sImg.componentIds && sImg.componentIds.length) {
      var partsS = [];
      for (var si = 0; si < sImg.componentIds.length; si++) {
        var cid = sImg.componentIds[si];
        var name = (typeof global.SituationsTree_getSituationComponentName === 'function')
          ? global.SituationsTree_getSituationComponentName(cid) : '';
        partsS.push(cid + (name ? ' (' + name + ')' : ''));
      }
      comp = partsS.join(', ');
    } else {
      comp = 'нет';
    }
    var inner = '<b>Образ ситуации #' + sImg.id + '</b>' +
      '<br><b>Детекторы (состав):</b> ' + Info_formatSituationDetectorNames(sImg) +
      '<br>Режим: ' + (sImg.mode === 1 ? 'целевой' : 'пассивный') +
      '<br>Компоненты (id, ключ): ' + comp +
      '<br>Число усреднений: ' + sImg.count +
      '<br>Готовый образ: ' + (sImg.ready ? 'да' : 'нет');
    var html = sImg.ready === true
      ? ('<div style="border:2px solid #1976d2;padding:10px 12px;border-radius:8px;box-sizing:border-box">' + inner + '</div>')
      : inner;
    global.show_alert(html, 0);
  }

  /**
   * Человекочитаемое описание ОД: цепочка стимулов конечного образа (как в ветке восприятия),
   * при отсутствии — названия действий.
   */
  function Info_formatActionImageReadableBlock(od) {
    if (!od) return '<b>Образ действия:</b><br>—';
    var stim = '';
    if (od.finalImageId && typeof global.PerceptionTree_getStimulusNamesForFinalImageId === 'function') {
      stim = global.PerceptionTree_getStimulusNamesForFinalImageId(od.finalImageId) || '';
    }
    if (stim === '—') stim = '';
    var actions = '';
    if (typeof global.PerceptionTree_getActionNamesForActionImage === 'function') {
      actions = global.PerceptionTree_getActionNamesForActionImage(od.id) || '';
    }
    if (actions === '—') actions = '';
    var body = stim || actions || '—';
    var extra = '';
    if (stim && actions) {
      extra = '<br><i>Действия:</i> ' + actions;
    }
    return '<b>Образ действия:</b><br>' + body + extra;
  }

  function Info_showActionImageDetail(odId) {
    if (typeof global.show_alert !== 'function') return;
    odId = parseInt(odId, 10) || 0;
    if (!odId || typeof global.ActionImages_getActionImage !== 'function') {
      global.show_alert('ОД #' + odId + ' не найден.', 0);
      return;
    }
    var od = global.ActionImages_getActionImage(odId);
    if (!od) {
      global.show_alert('ОД #' + odId + ' не найден.', 0);
      return;
    }
    var html = '<b>ОД #' + od.id + '</b><br>' +
      Info_formatActionImageReadableBlock(od) +
      '<br><br><b>Поля ОД:</b>' +
      '<br>finalImageId: ' + (od.finalImageId || 0) +
      '<br>actionIds: ' + (Array.isArray(od.actionIds) && od.actionIds.length ? od.actionIds.join(', ') : '—') +
      '<br>odSequence: ' + (od.odSequence || '—');
    global.show_alert(html, 0);
  }

  function Info_showMentalActionImageDetail(maiId) {
    if (typeof global.show_alert !== 'function') return;
    maiId = parseInt(maiId, 10) || 0;
    if (!maiId || typeof global.MentalActionImages_get !== 'function') {
      global.show_alert('Ментальный ОД #' + maiId + ' не найден.', 0);
      return;
    }
    var m = global.MentalActionImages_get(maiId);
    if (!m) {
      global.show_alert('Ментальный ОД #' + maiId + ' не найден.', 0);
      return;
    }
    var ids = Array.isArray(m.abstractActionIds) ? m.abstractActionIds : [];
    var absParts = [];
    for (var i = 0; i < ids.length; i++) {
      if (i > 0) absParts.push(', ');
      absParts.push(infoAlertIdLink('Info_showAbstractionDetail', ids[i]));
    }
    var srcLink = (m.sourceActionImageId || 0)
      ? infoAlertIdLink('Info_showActionImageDetail', m.sourceActionImageId)
      : '0';
    var html = '<b>Ментальный образ действия #' + m.id + '</b>' +
      '<br>Исходный ОД (моторный): ' + srcLink +
      '<br>Абстрактные действия (символы Abstractions, щелчок — детализация): ' +
      (absParts.length ? absParts.join('') : '—') +
      '<br>maSequence: ' + (m.maSequence || '—');
    global.show_alert(html, 0);
  }

  function Info_findAbstractPerceptionImage(id) {
    id = parseInt(id, 10) || 0;
    if (!id) return null;
    var ap = Array.isArray(global.AbstractPerceptionImages) ? global.AbstractPerceptionImages : [];
    for (var i = 0; i < ap.length; i++) {
      if (ap[i] && ap[i].id === id) return ap[i];
    }
    return null;
  }

  function Info_findAbstractActionImage(id) {
    id = parseInt(id, 10) || 0;
    if (!id) return null;
    var aa = Array.isArray(global.AbstractActionImages) ? global.AbstractActionImages : [];
    for (var j = 0; j < aa.length; j++) {
      if (aa[j] && aa[j].id === id) return aa[j];
    }
    return null;
  }

  function Info_showAbstractPerceptionImageDetail(recId) {
    if (typeof global.show_alert !== 'function') return;
    var o = Info_findAbstractPerceptionImage(recId);
    if (!o) {
      global.show_alert('Абстрактный клон восприятия #' + (parseInt(recId, 10) || 0) + ' не найден (AbstractPerceptionImages).', 0);
      return;
    }
    var stimNames = '';
    if (o.stimulusImageId && typeof global.PerceptionTree_getStimulusNamesForFinalImageId === 'function') {
      stimNames = global.PerceptionTree_getStimulusNamesForFinalImageId(o.stimulusImageId) || '';
    }
    var html = '<b>Клон восприятия #' + o.id + '</b> <small>(эпизод → abstract_images.js)</small>' +
      '<br>Узел ветки: ' + (o.perceptionNodeId ? infoAlertIdLink('Info_showPerceptionNodeDetail', o.perceptionNodeId) : '0') +
      '<br>Жёсткий стимул (FinalImageId): ' + (o.stimulusImageId ? infoAlertIdLink('Info_showFinalImageStimulusDetail', o.stimulusImageId) : '0') +
      (stimNames && stimNames !== '—' ? '<br><i>Стимул:</i> ' + stimNames : '') +
      '<br>Средняя значимость: ' + (typeof o.meanImportance === 'number' ? o.meanImportance : 0) +
      '<br>Число усреднений: ' + (typeof o.count === 'number' ? o.count : 0) +
      '<br>Создан: ' + infoFormatAbstractionFieldValue('createdAt', o.createdAt) +
      '<br>Последнее использование: ' + infoFormatAbstractionFieldValue('lastUsedAt', o.lastUsedAt) +
      infoFormatAbstractSemanticStructureHtml(o.semanticStructure);
    global.show_alert(html, 0);
  }

  function Info_showAbstractActionImageDetail(recId) {
    if (typeof global.show_alert !== 'function') return;
    var o = Info_findAbstractActionImage(recId);
    if (!o) {
      global.show_alert('Абстрактный клон действия #' + (parseInt(recId, 10) || 0) + ' не найден (AbstractActionImages).', 0);
      return;
    }
    var odBlock = '';
    if (o.actionImageId && typeof global.ActionImages_getActionImage === 'function') {
      var od = global.ActionImages_getActionImage(o.actionImageId);
      if (od) odBlock = '<br>' + Info_formatActionImageReadableBlock(od);
    }
    var html = '<b>Клон действия #' + o.id + '</b> <small>(эпизод → abstract_images.js)</small>' +
      '<br>Узел ветки: ' + (o.perceptionNodeId ? infoAlertIdLink('Info_showPerceptionNodeDetail', o.perceptionNodeId) : '0') +
      '<br>Жёсткий ОД: ' + (o.actionImageId ? infoAlertIdLink('Info_showActionImageDetail', o.actionImageId) : '0') +
      (odBlock || '') +
      '<br>Средняя значимость: ' + (typeof o.meanImportance === 'number' ? o.meanImportance : 0) +
      '<br>Число усреднений: ' + (typeof o.count === 'number' ? o.count : 0) +
      '<br>Создан: ' + infoFormatAbstractionFieldValue('createdAt', o.createdAt) +
      '<br>Последнее использование: ' + infoFormatAbstractionFieldValue('lastUsedAt', o.lastUsedAt) +
      infoFormatAbstractSemanticStructureHtml(o.semanticStructure);
    global.show_alert(html, 0);
  }

  function Info_showMotorAutomatizmsList() {
    if (typeof global.show_alert !== 'function') return;
    var arr = Array.isArray(global.Automatizms) ? global.Automatizms : [];
    if (!arr.length) {
      global.show_alert('Моторных автоматизмов нет.', 0);
      return;
    }
    var ids = [];
    for (var i = 0; i < arr.length; i++) {
      if (arr[i] && typeof arr[i].id === 'number') ids.push(arr[i].id);
    }
    ids.sort(function (a, b) { return a - b; });
    var parts = ['<b>Моторные автоматизмы</b> (' + ids.length + '). Щелчок по ID — детализация:<br><br>'];
    for (var j = 0; j < ids.length; j++) {
      if (j > 0) parts.push(' &nbsp; ');
      parts.push(infoAlertIdLink('Info_showMotorAutomatizmDetail', ids[j]));
    }
    global.show_alert(parts.join(''), 0);
  }

  function Info_showMotorAutomatizmDetail(id) {
    if (typeof global.show_alert !== 'function') return;
    id = parseInt(id, 10) || 0;
    var arr = Array.isArray(global.Automatizms) ? global.Automatizms : [];
    var a = null;
    for (var i = 0; i < arr.length; i++) {
      if (arr[i] && arr[i].id === id) { a = arr[i]; break; }
    }
    if (!a) {
      global.show_alert('Моторный автоматизм #' + id + ' не найден.', 0);
      return;
    }
    var odBlock = '';
    if (a.actionsImageId && typeof global.ActionImages_getActionImage === 'function') {
      var od = global.ActionImages_getActionImage(a.actionsImageId);
      if (od) {
        odBlock = '<br>' + Info_formatActionImageReadableBlock(od) +
          '<br><small>ОД ' + infoAlertIdLink('Info_showActionImageDetail', a.actionsImageId) +
          ' — все поля образа действия</small>';
      }
    }
    if (!odBlock) {
      odBlock = '<br><b>Образ действия:</b><br>—<br><small>ОД отсутствует</small>';
    }
    var html = '<b>Моторный автоматизм #' + a.id + '</b>' +
      '<br>branchId: ' + (a.branchId || 0) +
      odBlock +
      '<br>usefulness: ' + (typeof a.usefulness === 'number' ? a.usefulness : 0) +
      '<br>count: ' + (typeof a.count === 'number' ? a.count : 0) +
      '<br>штатный (isDefault): ' + (a.isDefault ? 'да' : 'нет');
    global.show_alert(html, 0);
  }

  function Info_showMentalAutomatizmsList() {
    if (typeof global.show_alert !== 'function') return;
    var arr = Array.isArray(global.MentalAutomatizms) ? global.MentalAutomatizms : [];
    if (!arr.length) {
      global.show_alert('Ментальных автоматизмов нет.', 0);
      return;
    }
    var ids = [];
    for (var i = 0; i < arr.length; i++) {
      if (arr[i] && typeof arr[i].id === 'number') ids.push(arr[i].id);
    }
    ids.sort(function (a, b) { return a - b; });
    var parts = ['<b>Ментальные автоматизмы</b> (' + ids.length + '). Щелчок по ID — детализация:<br><br>'];
    for (var j = 0; j < ids.length; j++) {
      if (j > 0) parts.push(' &nbsp; ');
      parts.push(infoAlertIdLink('Info_showMentalAutomatizmDetail', ids[j]));
    }
    global.show_alert(parts.join(''), 0);
  }

  function Info_showMentalAutomatizmDetail(id) {
    if (typeof global.show_alert !== 'function') return;
    id = parseInt(id, 10) || 0;
    var arr = Array.isArray(global.MentalAutomatizms) ? global.MentalAutomatizms : [];
    var a = null;
    for (var i = 0; i < arr.length; i++) {
      if (arr[i] && arr[i].id === id) { a = arr[i]; break; }
    }
    if (!a) {
      global.show_alert('Ментальный автоматизм #' + id + ' не найден.', 0);
      return;
    }
    var maiLink = (a.mentalActionImageId || 0)
      ? infoAlertIdLink('Info_showMentalActionImageDetail', a.mentalActionImageId)
      : '0';
    var html = '<b>Ментальный автоматизм #' + a.id + '</b>' +
      '<br>themeId: ' + (a.themeId || 0) +
      '<br>situationId: ' + (a.situationId || 0) +
      '<br>branchId: ' + (a.branchId || 0) +
      '<br>Ментальный ОД: ' + maiLink +
      '<br>usefulness: ' + (typeof a.usefulness === 'number' ? a.usefulness : 0) +
      '<br>count: ' + (typeof a.count === 'number' ? a.count : 0) +
      '<br>штатный (isDefault): ' + (a.isDefault ? 'да' : 'нет');
    global.show_alert(html, 0);
  }

  function Info_showAbstractionsList() {
    if (typeof global.show_alert !== 'function') return;
    var ap = Array.isArray(global.AbstractPerceptionImages) ? global.AbstractPerceptionImages : [];
    var aa = Array.isArray(global.AbstractActionImages) ? global.AbstractActionImages : [];
    var arr = Array.isArray(global.Abstractions) ? global.Abstractions : [];
    var blocks = [];
    blocks.push('<b>Абстракции: три разных хранилища</b> — у каждого свой тип ID; щелчок открывает <i>его</i> детализацию.');
    blocks.push('<br><br><b>1. Клоны восприятия</b> (AbstractPerceptionImages, abstract_images.js), всего ' + ap.length + ':');
    if (ap.length) {
      var apIds = [];
      for (var pi = 0; pi < ap.length; pi++) {
        if (ap[pi] && typeof ap[pi].id === 'number') apIds.push(ap[pi].id);
      }
      apIds.sort(function (a, b) { return a - b; });
      blocks.push('<br>');
      for (var pj = 0; pj < apIds.length; pj++) {
        if (pj > 0) blocks.push(' &nbsp; ');
        blocks.push(infoAlertIdLink('Info_showAbstractPerceptionImageDetail', apIds[pj]));
      }
    } else {
      blocks.push('<br><i>Нет. Появляются при <b>закрытии</b> кадра ЭП.</i>');
    }
    blocks.push('<br><br><b>2. Клоны действия</b> (AbstractActionImages), всего ' + aa.length + ':');
    if (aa.length) {
      var aaIds = [];
      for (var ai = 0; ai < aa.length; ai++) {
        if (aa[ai] && typeof aa[ai].id === 'number') aaIds.push(aa[ai].id);
      }
      aaIds.sort(function (a, b) { return a - b; });
      blocks.push('<br>');
      for (var aj = 0; aj < aaIds.length; aj++) {
        if (aj > 0) blocks.push(' &nbsp; ');
        blocks.push(infoAlertIdLink('Info_showAbstractActionImageDetail', aaIds[aj]));
      }
    } else {
      blocks.push('<br><i>Нет (или только восприятие — если в кадре не было ОД).</i>');
    }
    blocks.push('<br><br><b>3. Символы верхнего уровня</b> (Abstractions, mental_automatism.js), всего ' + arr.length + ':');
    if (!arr.length) {
      blocks.push('<br><i>Пока нет.</i>');
    } else {
      blocks.push('<br>');
      var ids = [];
      for (var i = 0; i < arr.length; i++) {
        if (arr[i] && typeof arr[i].id === 'number') ids.push(arr[i].id);
      }
      ids.sort(function (a, b) { return a - b; });
      for (var j = 0; j < ids.length; j++) {
        if (j > 0) blocks.push(' &nbsp; ');
        blocks.push(infoAlertIdLink('Info_showAbstractionDetail', ids[j]));
      }
    }
    global.show_alert(blocks.join(''), 0);
  }

  /** Человекочитаемое имя статуса гештальта (gestalt.js, GESTALT_STATUS). */
  function gestaltStatusLabel(st) {
    var n = parseInt(st, 10);
    if (isNaN(n)) return String(st);
    switch (n) {
      case 0: return '0 — новый';
      case 1: return '1 — начато';
      case 2: return '2 — частично';
      case 3: return '3 — закрыт (успех)';
      case 4: return '4 — архив';
      default: return String(n);
    }
  }

  /**
   * Список записей GestaltArr; щелчок по ID — Info_showGestaltDetail.
   * Данные живут в gestalt.js (подключается после informing.js; на момент клика уже загружены).
   */
  function Info_showGestaltsList() {
    if (typeof global.show_alert !== 'function') return;
    var arr = Array.isArray(global.GestaltArr) ? global.GestaltArr : [];
    var buf = Array.isArray(global.GestaltResonanceBuffer) ? global.GestaltResonanceBuffer : [];
    var head = '<b>Гештальты (доминанты нерешённых задач)</b><br><small>Записей: ' + arr.length
      + '. Буфер резонанса (FIFO сигнатур ЭП): ' + buf.length
      + '. Статусы: 0 новый, 1 начато, 2 частично, 3 закрыт, 4 архив.</small><br><br>';
    if (!arr.length) {
      global.show_alert(head + '<i>Записей гештальтов пока нет.</i>', 0);
      return;
    }
    var ids = [];
    for (var i = 0; i < arr.length; i++) {
      if (arr[i] && typeof arr[i].id === 'number') ids.push(arr[i].id);
    }
    ids.sort(function (a, b) { return a - b; });
    var parts = [head, 'ID: '];
    for (var j = 0; j < ids.length; j++) {
      if (j > 0) parts.push(' &nbsp; ');
      parts.push(infoAlertIdLink('Info_showGestaltDetail', ids[j]));
    }
    global.show_alert(parts.join(''), 0);
  }

  function Info_showGestaltDetail(id) {
    if (typeof global.show_alert !== 'function') return;
    id = parseInt(id, 10) || 0;
    var arr = Array.isArray(global.GestaltArr) ? global.GestaltArr : [];
    var g = null;
    for (var i = 0; i < arr.length; i++) {
      if (arr[i] && arr[i].id === id) {
        g = arr[i];
        break;
      }
    }
    if (!g) {
      global.show_alert('Гештальт #' + id + ' не найден.', 0);
      return;
    }
    var activeId = typeof global.Gestalt_activeGestaltId === 'number' ? global.Gestalt_activeGestaltId : 0;
    var masterCyc = typeof global.Gestalt_activeMasterCycleId === 'number' ? global.Gestalt_activeMasterCycleId : 0;
    var goalPart = g.goalImageId
      ? ('goalImageId: ' + infoAlertIdLink('Info_showLifeGoalDetail', g.goalImageId))
      : 'goalImageId: 0';
    var branchPart = g.problemTreeId
      ? ('ветка (problemTreeId): ' + infoAlertIdLink('Info_showPerceptionNodeDetail', g.problemTreeId))
      : ('ветка (problemTreeId): 0');
    var sitPart = g.situationId
      ? ('ситуация: ' + infoAlertIdLink('Info_showSituationImageDetail', g.situationId))
      : 'ситуация: 0';
    var autoIds = Array.isArray(g.automatismIds) ? g.automatismIds : [];
    var autoLine = 'опробованные автоматизмы: ';
    if (!autoIds.length) {
      autoLine += '—';
    } else {
      var bits = [];
      for (var a = 0; a < autoIds.length; a++) {
        bits.push(infoAlertIdLink('Info_showMotorAutomatizmDetail', autoIds[a]));
      }
      autoLine += bits.join(', ');
    }
    var pool = Array.isArray(g.analogyPool) ? g.analogyPool : [];
    var poolStr = pool.length ? ('<pre style="white-space:pre-wrap;font-size:9pt;margin:6px 0">' +
      String(JSON.stringify(pool, null, 2)) + '</pre>') : '<i>пусто</i>';
    var lines = [
      '<b>Гештальт #' + id + '</b>',
      '<br>' + goalPart,
      '<br>' + branchPart,
      '<br>' + sitPart,
      '<br>status: ' + gestaltStatusLabel(g.status),
      '<br>' + autoLine,
      '<br>lastInsightKind: ' + String(g.lastInsightKind || '—') + ', lastInsightPuls: ' + String(g.lastInsightPuls || 0)
        + ', insightChainCount: ' + String(g.insightChainCount || 0),
      '<br>ведущий сейчас: ' + (activeId === id ? 'да' : 'нет') + ' (Gestalt_activeGestaltId=' + activeId + ', цикл-хозяин=' + masterCyc + ')',
      '<br><b>analogyPool</b> (хвост аналогий):',
      poolStr
    ];
    global.show_alert(lines.join(''), 0);
  }

  function Info_showLifeGoalsList() {
    if (typeof global.show_alert !== 'function') return;
    var arr = Array.isArray(global.GoalImagesLife) ? global.GoalImagesLife : [];
    if (!arr.length) {
      global.show_alert('Образов целей жизненного опыта (GoalImagesLife) пока нет.', 0);
      return;
    }
    var ids = [];
    for (var i = 0; i < arr.length; i++) {
      if (arr[i] && typeof arr[i].id === 'number') ids.push(arr[i].id);
    }
    ids.sort(function (a, b) { return a - b; });
    var parts = ['<b>Образы целей (жизненный опыт)</b> (' + ids.length + '). Щелчок по ID — детализация:<br><br>'];
    for (var j = 0; j < ids.length; j++) {
      if (j > 0) parts.push(' &nbsp; ');
      parts.push(infoAlertIdLink('Info_showLifeGoalDetail', ids[j]));
    }
    global.show_alert(parts.join(''), 0);
  }

  function Info_showSituationImagesList() {
    if (typeof global.show_alert !== 'function') return;
    if (typeof global.SituationsTree_getAllSituationImages !== 'function') {
      global.show_alert('SituationsTree недоступен.', 0);
      return;
    }
    var arr = global.SituationsTree_getAllSituationImages() || [];
    if (!arr.length) {
      global.show_alert('Образов ситуаций (SituationImages) пока нет.', 0);
      return;
    }
    var ids = [];
    for (var i = 0; i < arr.length; i++) {
      if (arr[i] && typeof arr[i].id === 'number') ids.push(arr[i].id);
    }
    ids.sort(function (a, b) { return a - b; });
    var bySitId = {};
    for (var si = 0; si < arr.length; si++) {
      if (arr[si] && typeof arr[si].id === 'number') bySitId[arr[si].id] = arr[si];
    }
    var parts = ['<b>Образы ситуаций (SituationImages)</b> (' + ids.length + '). Щелчок по ID — детализация.<br><small>Готовые (ready) — синяя рамка.</small><br><br>'];
    for (var j = 0; j < ids.length; j++) {
      if (j > 0) parts.push(' &nbsp; ');
      var rec = bySitId[ids[j]];
      parts.push(rec ? infoSituationImageLinkFromRecord(rec) : infoAlertIdLink('Info_showSituationImageDetail', ids[j]));
    }
    global.show_alert(parts.join(''), 0);
  }

  function Info_showLifeGoalDetail(id) {
    if (typeof global.show_alert !== 'function') return;
    id = parseInt(id, 10) || 0;
    var g = typeof global.Goals_getGoalById === 'function' ? global.Goals_getGoalById(id) : null;
    if (!g) {
      global.show_alert('Образ цели #' + id + ' не найден.', 0);
      return;
    }
    var odLink = (g.actionImageId || 0)
      ? infoAlertIdLink('Info_showActionImageDetail', g.actionImageId)
      : '0';
    var lines = [
      '<b>Образ цели (жизненный опыт) #' + id + '</b>',
      '<br>perceptionNodeId: ' + (g.perceptionNodeId || 0),
      '<br>situationId: ' + (g.situationId || 0),
      '<br>stimulusImageId: ' + (g.stimulusImageId || 0),
      '<br>действие (ОД): ' + odLink,
      '<br>effect (уср.): ' + (typeof g.effect === 'number' ? g.effect : 0),
      '<br>count: ' + (typeof g.count === 'number' ? g.count : 0),
      '<br>usefulness: ' + (typeof g.usefulness === 'number' ? g.usefulness : 0),
      '<br>usefulnessCount: ' + (typeof g.usefulnessCount === 'number' ? g.usefulnessCount : 0)
    ];
    global.show_alert(lines.join(''), 0);
  }

  function Info_showAbstractionDetail(id) {
    if (typeof global.show_alert !== 'function') return;
    id = parseInt(id, 10) || 0;
    var arr = Array.isArray(global.Abstractions) ? global.Abstractions : [];
    var obj = null;
    for (var i = 0; i < arr.length; i++) {
      if (arr[i] && arr[i].id === id) { obj = arr[i]; break; }
    }
    if (!obj) {
      global.show_alert('Символ Abstractions #' + id + ' не найден.', 0);
      return;
    }
    var lines = [
      '<b>Символ Abstractions #' + id + '</b>',
      '<small>mental_automatism.js — щелчок по числу ниже ведёт в детализацию связанной сущности.</small>'
    ];
    var keys = Object.keys(obj).sort();
    for (var k = 0; k < keys.length; k++) {
      var key = keys[k];
      if (key === 'id') continue;
      var val = obj[key];
      var label = INFO_ABSTRACTION_FIELD_LABEL[key] || key;
      lines.push('<br><b>' + label + '</b> (' + key + '): ' + infoFormatAbstractionFieldValue(key, val));
    }
    global.show_alert(lines.join(''), 0);
  }

  function infoConfirmThenClear(ev, onOk) {
    if (ev && ev.stopPropagation) ev.stopPropagation();
    if (ev && ev.preventDefault) ev.preventDefault();
    var msg = 'Точно удалить?';
    if (typeof global.show_confirm === 'function') {
      global.show_confirm(msg, function (result) {
        if (result === 'OK') onOk();
      });
    } else if (typeof global.confirm === 'function' && global.confirm(msg)) {
      onOk();
    }
  }

  function Info_clearMotorAutomatizms(ev) {
    infoConfirmThenClear(ev, function () {
      if (typeof global.Automatizms_clearAll === 'function') global.Automatizms_clearAll();
    });
  }

  function Info_clearMentalAutomatizms(ev) {
    infoConfirmThenClear(ev, function () {
      if (typeof global.MentalAutomatizms_clearAll === 'function') global.MentalAutomatizms_clearAll();
    });
  }

  function Info_clearAbstractions(ev) {
    infoConfirmThenClear(ev, function () {
      if (typeof global.Abstractions_clearAll === 'function') global.Abstractions_clearAll();
    });
  }

  function Info_clearLifeGoals(ev) {
    infoConfirmThenClear(ev, function () {
      if (typeof global.Goals_clearAllLifeGoals === 'function') global.Goals_clearAllLifeGoals();
    });
  }

  function Info_clearSituationImages(ev) {
    infoConfirmThenClear(ev, function () {
      if (typeof global.SituationsTree_clearAllSituationImages === 'function') {
        global.SituationsTree_clearAllSituationImages();
      }
    });
  }

  global.Info_setField = Info_setField;
  global.Info_setMode = Info_setMode;
  global.Info_updateFromPerceptionTree = Info_updateFromPerceptionTree;
  global.Info_updateFromStimulus = Info_updateFromStimulus;
  global.Info_setProblemFromUnsolved = Info_setProblemFromUnsolved;
  global.Info_incrementStep = Info_incrementStep;
  global.Info_updateView = Info_updateView;
  global.Info_showBackgroundStack = Info_showBackgroundStack;
  global.Info_showBackgroundCycleDetail = Info_showBackgroundCycleDetail;
  global.Info_showCycleDetailById = Info_showCycleDetailById;
  global.Info_showFinalImageStimulusDetail = Info_showFinalImageStimulusDetail;
  global.Info_showPerceptionNodeDetail = Info_showPerceptionNodeDetail;
  global.Info_showEmotionRecordDetail = Info_showEmotionRecordDetail;
  global.Info_showThemeImageDetail = Info_showThemeImageDetail;
  global.Info_showSituationImageDetail = Info_showSituationImageDetail;
  global.Info_showActionImageDetail = Info_showActionImageDetail;
  global.Info_showMentalActionImageDetail = Info_showMentalActionImageDetail;
  global.Info_showMotorAutomatizmsList = Info_showMotorAutomatizmsList;
  global.Info_showMotorAutomatizmDetail = Info_showMotorAutomatizmDetail;
  global.Info_showMentalAutomatizmsList = Info_showMentalAutomatizmsList;
  global.Info_showMentalAutomatizmDetail = Info_showMentalAutomatizmDetail;
  global.Info_showAbstractionsList = Info_showAbstractionsList;
  global.Info_showAbstractionDetail = Info_showAbstractionDetail;
  global.Info_showAbstractPerceptionImageDetail = Info_showAbstractPerceptionImageDetail;
  global.Info_showAbstractActionImageDetail = Info_showAbstractActionImageDetail;
  global.Info_showLifeGoalsList = Info_showLifeGoalsList;
  global.Info_showLifeGoalDetail = Info_showLifeGoalDetail;
  global.Info_showGestaltsList = Info_showGestaltsList;
  global.Info_showGestaltDetail = Info_showGestaltDetail;
  global.Info_showMainCycleDetail = Info_showMainCycleDetail;
  global.Info_showDetail = Info_showDetail;
  global.Info_thinkingFlash = Info_thinkingFlash;
  global.Info_clearMotorAutomatizms = Info_clearMotorAutomatizms;
  global.Info_clearMentalAutomatizms = Info_clearMentalAutomatizms;
  global.Info_clearAbstractions = Info_clearAbstractions;
  global.Info_clearLifeGoals = Info_clearLifeGoals;
  global.Info_showSituationImagesList = Info_showSituationImagesList;
  global.Info_clearSituationImages = Info_clearSituationImages;
  global.Info_fillFantasmPlotOnPassiveMain = Info_fillFantasmPlotOnPassiveMain;
  global.Info_fillFantasmPlotOnPassiveBackground = Info_fillFantasmPlotOnPassiveBackground;

  function Info_showFantasmPlotDetail() {
    Info_showDetail('fantasmPlot');
  }
  global.Info_showFantasmPlotDetail = Info_showFantasmPlotDetail;

})(window);


════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/mental_automatism.js
════════════════════════════════════════════════════════════
/**
 * Ментальные автоматизмы и абстракции (заготовка).
 

Ментальные автоматизмы
Теория.
Как дети учатся сначала считать в столбик (моторные автоматизмы в виде ментальной последовательности), а потом оперировать в уме с образами и умножать в уме.
В школе сначала учат предметному мышлению: как использовать предметы, чтобы получать желаемый результат.
Как использовать молоток - это быстро и наглядно, дети легко отзеркаливают.
Как считать с помощью костяшей счетов - это сложнее и требует заучивания последовательности действий, 
Как умножать и делить в столбик - тоже с помощью моторных действий получают ментальный результат.
Потом эта последовательность становится ментальным автоматизмом и выполняется быстро.
И, наконенец, учат, как не прибегая к посредничеству моторных действий в уме оперировать образами, так же как предметами, и получать результат. Это уже чистый ментальный автоматизм.
Если работа с запоминанием моторных действий сначала фиксируется в виде цепочек в эпизодической памяти в виде Правил (стимул-действие-эффект), то потом появляются ментальные правила оперирования образами в виде образа цепочки последовательных мыслительных действий оперирования с образами.
Примеры простейших оперирований с образами с получаемым эффектом.
Детализация образа
Представьте яблоко целиком, затем мысленно "увеличьте" его поверхность: видите красную кожуру с мелкими пятнышками, чёренок с листочком. Эффект — образ становится ярче и детальнее, как будто вы его изучили вблизи, что улучшает запоминание.
Вращение образа
Возьмите образ кубика: мысленно поверните его на 90 градусов по часовой стрелке. Теперь видна другая грань с иной картинкой. Эффект — доступны скрытые детали (например, все 6 граней), что помогает анализировать объект со всех сторон без реального поворота.
Видоизменение образа
Представьте "машину": сначала как маленькую игрушку, затем как грузовик, потом как гоночный болид. Эффект — один стимул порождает серию вариаций, развивая гибкость ассоциаций и понимание категории.
Сложение образов
Сложите мысленно образ дома и дерева: дом стоит слева, дерево справа, ветки касаются крыши. Эффект — новый композиционный образ, который можно "прочитать" как сцену или историю.
Разделение образа
Разбейте образ круга на 4 сектора, мысленно окрасьте каждый в разный цвет (красный, синий, зелёный, жёлтый). Эффект — круг превращается в диаграмму, показывающую пропорции или категории без рисования.
Образ: В уме удерживаются два множества точек (например, как на гранях кубика: 3 точки и 2 точки).
Ментальное правило (бывшее моторное): «Сдвинь множества в одну кучу». Раньше ребенок сдвигал фишки руками. Теперь он мысленно «накладывает» одну группу на другую или объединяет их в одном пространстве.
Эффект: Мгновенное возникновение образа «5» без вербального счета «три... четыре... пять».
Почему это автоматизм: Мозг не считает, он распознает новую конфигурацию как целостный гештальт, сформированный предыдущим опытом физических перемещений.
Образ: В уме держится буква «R» или простой геометрический контур.
Ментальное правило: «Поверни образ на 90 градусов по часовой стрелке». Это калька с физического поворота детали в руках.
Эффект: Получение нового образа (перевернутая «R») и сравнение его с эталоном (например, «подходит ли эта деталь в паз?»).
Связь с памятью: Сначала ребенок крутил деталь руками (эпизодическая память: «я крутил, и она встала»). Теперь он крутит образ детали, и правило «поворот меняет ориентацию» работает автоматически.
Образ: Два отрезка разной длины, предъявленные последовательно (первый исчез, появился второй).
Ментальное правило: «Скопируй первый отрезок и мысленно положи его поверх второго». Это имитация действия с линейкой или двумя палочками.
Эффект: Визуальное выявление разницы (один «выступает» за край другого).
Автоматизм: Не нужно измерять в сантиметрах. Операция «наложения» становится инструментом мышления, как молоток.
Образ: Визуализированные ряды костяшек (например, 5 в нижнем ряду, 2 в верхнем).
Ментальное правило: «Сдвинь одну костяшку вниз» (операция +1).
Эффект: Изменение визуальной конфигурации, которое считывается как новое число.
Суть: Руки не двигаются, но активируется та же нейронная сеть, что и при реальном движении. Правило «стимул (задача) — действие (сдвиг костяшки) — эффект (новое число)» полностью перенесено в ментальный план.
Эпизодическое правило: «Я беру молоток (стимул), бью (действие), гвоздь входит (эффект)». Запомнено как конкретный случай.
Ментальное правило: «Я беру образ молотка, совершаю имитацию удара в воображении, предвижу вхождение гвоздя».
Чистый ментальный автоматизм: При виде задачи «забить гвоздь» мозг мгновенно активирует схему «удар -> результат», минуя сознательное проговаривание действий. Образ действия сливается с образом цели.
На высшем уровне абстракции образ сам становится инструментом.
Реализация.

 * Требование: массивы должны иметь долговременную систему сохранения:
 * - IndexedDB (инкрементно по dirty)
 * - сохранение в файл по кнопке через BEAST.persistentModules (serialize/apply)
 *
 * Здесь пока нет логики ментальных действий; только структуры и сохранение.
 */
(function (global) {
  'use strict';

  // Абстракции верхнего уровня (будущая база для «символов» и композиций).
  if (!Array.isArray(global.Abstractions)) global.Abstractions = [];
  // Ментальные образы действий (аналог ActionImages, но для мышления).
  if (!Array.isArray(global.MentalActionImages)) global.MentalActionImages = [];
  // Ментальные правила (аналог «Правил» ЭП, но без ответного стимула): Stimulus → Abstraction → Effect.
  if (!Array.isArray(global.MentalEpisodeRules)) global.MentalEpisodeRules = [];
  // Ментальные автоматизмы (аналог моторных, но запускают MentalActionImages).
  if (!Array.isArray(global.MentalAutomatizms)) global.MentalAutomatizms = [];

  var nextMentalAutomatizmId = 1;
  var nextMentalActionImageId = 1;
  var nextMentalRuleId = 1;
  var nextAbstractionId = 1;

  var memDirty = false;
  var lastSavedSig = '';

  function markDirty() {
    memDirty = true;
  }

  function normalizeIds(arr) {
    if (!Array.isArray(arr)) return [];
    var out = [];
    for (var i = 0; i < arr.length; i++) {
      var v = parseInt(arr[i], 10);
      if (!isNaN(v) && v > 0 && out.indexOf(v) === -1) out.push(v);
    }
    out.sort(function (a, b) { return a - b; });
    return out;
  }

  function serializeState() {
    return {
      nextMentalAutomatizmId: nextMentalAutomatizmId,
      nextMentalActionImageId: nextMentalActionImageId,
      nextMentalRuleId: nextMentalRuleId,
      nextAbstractionId: nextAbstractionId,
      mentalAutomatizms: global.MentalAutomatizms.slice(0),
      mentalActionImages: global.MentalActionImages.slice(0),
      mentalEpisodeRules: global.MentalEpisodeRules.slice(0),
      abstractions: global.Abstractions.slice(0)
    };
  }

  function applyState(state) {
    state = state || {};
    nextMentalAutomatizmId = parseInt(state.nextMentalAutomatizmId, 10);
    if (isNaN(nextMentalAutomatizmId) || nextMentalAutomatizmId < 1) nextMentalAutomatizmId = 1;
    nextMentalActionImageId = parseInt(state.nextMentalActionImageId, 10);
    if (isNaN(nextMentalActionImageId) || nextMentalActionImageId < 1) nextMentalActionImageId = 1;
    nextMentalRuleId = parseInt(state.nextMentalRuleId, 10);
    if (isNaN(nextMentalRuleId) || nextMentalRuleId < 1) nextMentalRuleId = 1;
    nextAbstractionId = parseInt(state.nextAbstractionId, 10);
    if (isNaN(nextAbstractionId) || nextAbstractionId < 1) nextAbstractionId = 1;

    global.MentalAutomatizms = Array.isArray(state.mentalAutomatizms) ? state.mentalAutomatizms.slice(0) : [];
    global.MentalActionImages = Array.isArray(state.mentalActionImages) ? state.mentalActionImages.slice(0) : [];
    global.MentalEpisodeRules = Array.isArray(state.mentalEpisodeRules) ? state.mentalEpisodeRules.slice(0) : [];
    global.Abstractions = Array.isArray(state.abstractions) ? state.abstractions.slice(0) : [];

    // защита от конфликтов ID
    for (var i = 0; i < global.MentalAutomatizms.length; i++) {
      var a = global.MentalAutomatizms[i];
      if (a && typeof a.id === 'number' && a.id >= nextMentalAutomatizmId) nextMentalAutomatizmId = a.id + 1;
    }
    for (var mi = 0; mi < global.MentalActionImages.length; mi++) {
      var m = global.MentalActionImages[mi];
      if (m && typeof m.id === 'number' && m.id >= nextMentalActionImageId) nextMentalActionImageId = m.id + 1;
    }
    for (var ri = 0; ri < global.MentalEpisodeRules.length; ri++) {
      var r = global.MentalEpisodeRules[ri];
      if (r && typeof r.id === 'number' && r.id >= nextMentalRuleId) nextMentalRuleId = r.id + 1;
    }
    for (var j = 0; j < global.Abstractions.length; j++) {
      var b = global.Abstractions[j];
      if (b && typeof b.id === 'number' && b.id >= nextAbstractionId) nextAbstractionId = b.id + 1;
    }

    memDirty = false;
    try {
      lastSavedSig = JSON.stringify(serializeState());
    } catch (e) {
      lastSavedSig = '';
    }
  }

  // -------------------- Ментальные образы действий --------------------
  // { id, sourceActionImageId, abstractActionIds:number[], maSequence:string }
  function MentalActionImages_getOrCreate(sourceActionImageId, abstractActionIds, maSequence) {
    var src = parseInt(sourceActionImageId, 10) || 0;
    var ids = normalizeIds(abstractActionIds);
    var seq = typeof maSequence === 'string' ? maSequence.trim() : '';
    for (var i = 0; i < global.MentalActionImages.length; i++) {
      var m = global.MentalActionImages[i];
      if (!m) continue;
      if ((m.sourceActionImageId || 0) !== src) continue;
      var a1 = Array.isArray(m.abstractActionIds) ? normalizeIds(m.abstractActionIds) : [];
      if (a1.length !== ids.length) continue;
      var ok = true;
      for (var j = 0; j < ids.length; j++) {
        if (a1[j] !== ids[j]) { ok = false; break; }
      }
      if (!ok) continue;
      if ((m.maSequence || '') !== seq) continue;
      return m.id;
    }
    var id = nextMentalActionImageId++;
    global.MentalActionImages.push({
      id: id,
      sourceActionImageId: src,
      abstractActionIds: ids.slice(0),
      maSequence: seq
    });
    markDirty();
    return id;
  }

  function MentalActionImages_get(id) {
    id = parseInt(id, 10);
    if (isNaN(id) || id <= 0) return null;
    for (var i = 0; i < global.MentalActionImages.length; i++) {
      var m = global.MentalActionImages[i];
      if (m && m.id === id) return m;
    }
    return null;
  }

  // -------------------- Ментальные правила (эпизоды) --------------------
  // { id, perceptionNodeId, stimulusImageId, abstractionId, meanEffect, count, lastUsedAt, createdAt }
  function MentalRules_addSample(perceptionNodeId, stimulusImageId, abstractionId, effect) {
    if (!perceptionNodeId || !stimulusImageId || !abstractionId) return 0;
    if (typeof effect !== 'number' || isNaN(effect)) return 0;
    var key = String(perceptionNodeId) + '|' + String(stimulusImageId) + '|' + String(abstractionId);
    var existing = null;
    for (var i = 0; i < global.MentalEpisodeRules.length; i++) {
      var r = global.MentalEpisodeRules[i];
      if (!r) continue;
      if (String(r.perceptionNodeId) + '|' + String(r.stimulusImageId) + '|' + String(r.abstractionId) === key) {
        existing = r;
        break;
      }
    }
    if (!existing) {
      existing = {
        id: nextMentalRuleId++,
        perceptionNodeId: perceptionNodeId,
        stimulusImageId: stimulusImageId,
        abstractionId: abstractionId,
        meanEffect: 0,
        count: 0,
        createdAt: Date.now(),
        lastUsedAt: Date.now()
      };
      global.MentalEpisodeRules.push(existing);
    }
    var c = typeof existing.count === 'number' ? existing.count : 0;
    var cur = typeof existing.meanEffect === 'number' ? existing.meanEffect : 0;
    existing.meanEffect = (cur * c + effect) / (c + 1);
    existing.count = c + 1;
    existing.lastUsedAt = Date.now();
    markDirty();
    return existing.id;
  }

  // abstractClone = { abstractPerceptionId, abstractActionId } из abstract_images после closeLastFrame ЭП.
  // При «нулевом» ОД в кадре abstractActionId = 0 — берётся abstractPerceptionId (см. about_episodic_memory.md).
  function MentalRules_registerFromEpisodeFrame(frame, abstractClone) {
    if (!frame || !abstractClone) return 0;
    var pn = frame.perceptionNodeId;
    var stim = frame.stimulusImageId;
    var effect = frame.effect;
    var absId = abstractClone.abstractActionId || abstractClone.abstractPerceptionId || 0;
    if (!absId) return 0;
    return MentalRules_addSample(pn, stim, absId, effect);
  }

  // -------------------- Ментальные автоматизмы --------------------
  // { id, themeId, situationId, branchId, usefulness, mentalActionImageId, count, isDefault }
  function MentalAutomatizms_create(payload) {
    if (!payload || typeof payload !== 'object') payload = {};
    var obj = Object.assign({}, payload);
    obj.id = nextMentalAutomatizmId++;
    if (typeof obj.usefulness !== 'number') obj.usefulness = 0;
    if (typeof obj.count !== 'number') obj.count = 0;
    obj.isDefault = !!obj.isDefault;
    global.MentalAutomatizms.push(obj);
    markDirty();
    return obj.id;
  }

  function MentalAutomatizms_getDefaultForThemeSituation(themeId, situationId) {
    themeId = parseInt(themeId, 10) || 0;
    situationId = parseInt(situationId, 10) || 0;
    for (var i = 0; i < global.MentalAutomatizms.length; i++) {
      var a = global.MentalAutomatizms[i];
      if (!a) continue;
      if (a.isDefault && (a.themeId || 0) === themeId && (a.situationId || 0) === situationId) return a;
    }
    return null;
  }

  function MentalAutomatizms_setDefault(id) {
    id = parseInt(id, 10);
    if (isNaN(id) || id <= 0) return;
    var target = null;
    for (var i = 0; i < global.MentalAutomatizms.length; i++) {
      if (global.MentalAutomatizms[i] && global.MentalAutomatizms[i].id === id) {
        target = global.MentalAutomatizms[i];
        break;
      }
    }
    if (!target) return;
    var themeId = target.themeId || 0;
    var situationId = target.situationId || 0;
    for (var j = 0; j < global.MentalAutomatizms.length; j++) {
      var a = global.MentalAutomatizms[j];
      if (!a) continue;
      if ((a.themeId || 0) === themeId && (a.situationId || 0) === situationId) a.isDefault = false;
    }
    target.isDefault = true;
    markDirty();
  }

  function Abstractions_create(payload) {
    if (!payload || typeof payload !== 'object') payload = {};
    var obj = Object.assign({}, payload);
    obj.id = nextAbstractionId++;
    global.Abstractions.push(obj);
    markDirty();
    return obj.id;
  }

  // Вывод на пульт при активации ментального образа действия (аналог моторного, но без эффекторов).
  function setBeastActivityText(text) {
    if (!global.document) return;
    var block = global.document.getElementById('beastActivityBlock');
    if (!block) return;
    block.textContent = text || '';
  }

  function runMentalActionImage(mentalActionImageId) {
    var m = MentalActionImages_get(mentalActionImageId);
    if (!m) return;
    var ids = Array.isArray(m.abstractActionIds) ? m.abstractActionIds : [];
    var line1 = 'Ментальная операция (ментальный автоматизм)';
    var line2 = 'Абстрактные действия: ' + (ids.length ? ids.join(', ') : '—');
    setBeastActivityText(line1 + '\n' + line2);
  }

  function MentalAutomatizms_run(id) {
    id = parseInt(id, 10);
    if (isNaN(id) || id <= 0) return false;
    var target = null;
    for (var i = 0; i < global.MentalAutomatizms.length; i++) {
      var a = global.MentalAutomatizms[i];
      if (a && a.id === id) { target = a; break; }
    }
    if (!target || !target.mentalActionImageId) return false;
    runMentalActionImage(target.mentalActionImageId);
    return true;
  }

  function findAbstractPerceptionRecordId(perceptionNodeId, stimulusImageId) {
    perceptionNodeId = parseInt(perceptionNodeId, 10) || 0;
    stimulusImageId = parseInt(stimulusImageId, 10) || 0;
    if (!perceptionNodeId || !stimulusImageId) return 0;
    var arr = global.AbstractPerceptionImages;
    if (!Array.isArray(arr)) return 0;
    for (var pi = 0; pi < arr.length; pi++) {
      var o = arr[pi];
      if (!o) continue;
      if ((o.perceptionNodeId || 0) !== perceptionNodeId) continue;
      if ((o.stimulusImageId || 0) !== stimulusImageId) continue;
      return typeof o.id === 'number' ? o.id : 0;
    }
    return 0;
  }

  function findAbstractActionRecordId(perceptionNodeId, actionImageId) {
    perceptionNodeId = parseInt(perceptionNodeId, 10) || 0;
    actionImageId = parseInt(actionImageId, 10) || 0;
    if (!perceptionNodeId || !actionImageId) return 0;
    var arr = global.AbstractActionImages;
    if (!Array.isArray(arr)) return 0;
    for (var ai = 0; ai < arr.length; ai++) {
      var o = arr[ai];
      if (!o) continue;
      if ((o.perceptionNodeId || 0) !== perceptionNodeId) continue;
      if ((o.actionImageId || 0) !== actionImageId) continue;
      return typeof o.id === 'number' ? o.id : 0;
    }
    return 0;
  }

  function findMentalAutomatizmByThemeSituationMai(themeId, situationId, mentalActionImageId) {
    themeId = parseInt(themeId, 10) || 0;
    situationId = parseInt(situationId, 10) || 0;
    mentalActionImageId = parseInt(mentalActionImageId, 10) || 0;
    for (var fi = 0; fi < global.MentalAutomatizms.length; fi++) {
      var a = global.MentalAutomatizms[fi];
      if (!a) continue;
      if ((a.themeId || 0) !== themeId) continue;
      if ((a.situationId || 0) !== situationId) continue;
      if ((a.mentalActionImageId || 0) !== mentalActionImageId) continue;
      return a;
    }
    return null;
  }

  /**
   * После ответа оператора: при позитивном effect и наличии кадра semanticStructure (abstract_images) —
   * ментальный образ действия + ментальный автоматизм по умолчанию для пары themeId=ветка, situationId.
   *
   * @param {Object} params — perceptionNodeId, situationId, actualStimulusImageId, episodeEffect
   * @returns {boolean}
   */
  function MentalAutomatizms_tryCreateFromSemanticStructureRule(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 episodeEffect = params.episodeEffect;
    if (typeof episodeEffect !== 'number' || isNaN(episodeEffect) || episodeEffect <= 0) return false;
    if (!branchId || !actualStim) return false;
    if (typeof global.AbstractImages_findRuleFrameMatchingContext !== 'function') return false;

    var fr = global.AbstractImages_findRuleFrameMatchingContext({
      perceptionNodeId: branchId,
      situationId: situationId,
      actualStimulusImageId: actualStim
    });
    if (!fr || fr.actionImageId == null) return false;
    var odId = parseInt(fr.actionImageId, 10) || 0;
    if (!odId) return false;

    var aaRecId = findAbstractActionRecordId(branchId, odId);
    var apRecId = findAbstractPerceptionRecordId(branchId, actualStim);
    var absIds = [];
    if (aaRecId) absIds.push(aaRecId);
    else if (apRecId) absIds.push(apRecId);
    if (!absIds.length) return false;

    var maiId = MentalActionImages_getOrCreate(odId, absIds, '');
    if (!maiId) return false;

    var existing = findMentalAutomatizmByThemeSituationMai(branchId, situationId, maiId);
    var mid;
    if (existing) {
      var u = typeof existing.usefulness === 'number' ? existing.usefulness : 0;
      var c = typeof existing.count === 'number' ? existing.count : 0;
      existing.usefulness = (u * c + episodeEffect) / (c + 1);
      existing.count = c + 1;
      mid = existing.id;
      markDirty();
    } else {
      mid = MentalAutomatizms_create({
        themeId: branchId,
        situationId: situationId,
        branchId: branchId,
        mentalActionImageId: maiId,
        usefulness: episodeEffect,
        count: 1,
        isDefault: false
      });
    }
    MentalAutomatizms_setDefault(mid);
    return true;
  }

  // --- IndexedDB ---
  var DB_NAME = 'BEAST_MentalAutomatizms_DB';
  var DB_VERSION = 1;
  var STORE = 'mental_automatizms_state';
  var KEY = 'mental_automatizms_singleton';

  function openDb() {
    if (!global.IndexedDBModule || typeof global.IndexedDBModule.openDB !== 'function') {
      return Promise.reject(new Error('IndexedDBModule not available'));
    }
    return global.IndexedDBModule.openDB({
      dbName: DB_NAME,
      version: DB_VERSION,
      storeName: STORE,
      keyPath: 'id'
    });
  }

  function loadFromIndexedDB() {
    openDb()
      .then(function (d) {
        return global.IndexedDBModule.get({ db: d, storeName: STORE, key: KEY });
      })
      .then(function (record) {
        if (record && record.state) {
          applyState(record.state);
          if (typeof global.Info_updateView === 'function') global.Info_updateView();
        }
      })
      .catch(function () {});
  }

  function clearAllMentalAutomatizms() {
    global.MentalAutomatizms = [];
    nextMentalAutomatizmId = 1;
    markDirty();
    if (typeof global.Info_updateView === 'function') global.Info_updateView();
  }

  function clearAllAbstractions() {
    global.Abstractions = [];
    nextAbstractionId = 1;
    markDirty();
    if (typeof global.Info_updateView === 'function') global.Info_updateView();
  }

  loadFromIndexedDB();

  if (typeof global.registerIndexedDBSaver === 'function') {
    global.registerIndexedDBSaver(function () {
      if (!memDirty) return;
      memDirty = false;
      var state = serializeState();
      var sig = '';
      try { sig = JSON.stringify(state); } catch (e) { sig = ''; }
      if (sig && sig === lastSavedSig) return;
      lastSavedSig = sig;
      openDb()
        .then(function (db) {
          return global.IndexedDBModule.put({
            db: db,
            storeName: STORE,
            value: { id: KEY, state: state, ts: Date.now() }
          });
        })
        .catch(function () {});
    });
  }

  // --- Экспорт ---
  global.MentalActionImages_getOrCreate = MentalActionImages_getOrCreate;
  global.MentalActionImages_get = MentalActionImages_get;
  global.MentalRules_registerFromEpisodeFrame = MentalRules_registerFromEpisodeFrame;
  global.MentalAutomatizms_create = MentalAutomatizms_create;
  global.MentalAutomatizms_setDefault = MentalAutomatizms_setDefault;
  global.MentalAutomatizms_getDefaultForThemeSituation = MentalAutomatizms_getDefaultForThemeSituation;
  global.MentalAutomatizms_run = MentalAutomatizms_run;
  global.MentalAutomatizms_tryCreateFromSemanticStructureRule = MentalAutomatizms_tryCreateFromSemanticStructureRule;
  global.Abstractions_create = Abstractions_create;
  global.MentalAutomatizms_serializeState = serializeState;
  global.MentalAutomatizms_applyState = applyState;
  global.MentalAutomatizms_clearAll = clearAllMentalAutomatizms;
  global.Abstractions_clearAll = clearAllAbstractions;
})(window);




════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/situatioms_tree.js
════════════════════════════════════════════════════════════
(function (global) {
  'use strict';

  /**
   * Дерево ситуаций: трёхуровневая структура
   *  Level 1: режим осмысления (mode: 0 — пассивный, 1 — целевой)
   *  Level 2: образ Темы (ThemeImage: id, mode, componentIds[], count, ready)
   *  Level 3: образ Ситуации (SituationImage: id, mode, componentIds[], count, ready)
   *
   * Образы Тем и Ситуаций формируются по состоянию полей инфокартины:
   *  - Тема: сначала только по уровням дерева восприятия (BaseID, EmotionID, ActivityID)
   *  - Ситуация: по набору детекторных переменных инфокартины (criticalVital, badState, ...).
   *
   * Новый образ становится «готовым» (ready = true), когда count > 3.
   * Попытка активации дерева — по каждой активации дерева восприятия:
   *  - из текущей инфокартины извлекается снимок (mode, themeComponents, situationComponents),
   *  - счётчики образов увеличиваются,
   *  - если count > 3 — образ помечается ready,
   *  - активируется та Тема/Ситуация, чьи готовые образы наиболее соответствуют текущим компонентам.
   *
   * Результат активации:
   *  - InfoDet_updateCurrentTheme(themeId)
   *  - InfoDet_updateCurrentSituation(situationId)
   *
   * Детекторы полей инфокартины (виталы, эффекты, ответы и т.д.) — в info_detectors.js.
   */

  var ThemeImages = [];      // { id, mode, componentIds, count, ready }
  var SituationImages = [];  // { id, mode, componentIds, count, ready }
  var nextThemeId = 1;
  var nextSituationId = 1;
  var treeDirty = false;
  var lastSavedSig = '';

  // Фиксированное отображение детекторных полей инфокартины в компонентные ID для ситуаций.
  // Это даёт стабильные числовые компоненты для образов ситуаций.
  var DETECTOR_COMPONENT_IDS = {
    // Соответствие полям InfoPicture: при ненулевом значении детектора в компоненты ситуации попадает этот числовой id.
    criticalVital: 1,              // активен критический витал (>0 — id витала)
    shockState: 2,                 // шоковое состояние
    suddenChange: 3,               // зафиксировано резкое изменение состояния
    playMode: 4,                   // игровой режим
    learningMode: 5,               // режим обучения
    negativeMotorEffect: 6,        // негативный эффект моторного автоматизма
    negativeMentalEffect: 7,       // негативный эффект ментального автоматизма
    badState: 8,                   // состояние «Плохо»
    consoleStimulus: 9,            // есть активный стимул с пульта
    searchInterest: 10,            // поисковый интерес
    teacherLearning: 11,           // идёт обучение с учителем
    operatorIgnoring: 12,          // оператор игнорирует
    dissatisfaction: 13,           // неудовлетворённость существующим
    misunderstanding: 14,          // непонимание
    doubtInDefaultAutomatism: 16,  // сомнение в штатном автоматизме
    defense: 17,                   // режим защиты
    fear: 18,                      // страх
    aggression: 19,                // агрессия
    highImportanceObject: 20,      // есть объект высокой значимости
    moodImproving: 21,             // зафиксировано улучшение настроения
    imageNovelty: 22,              // высокая новизна образа
    highSemanticImportance: 23,    // высокая семантическая значимость
    conflictingSemantics: 24,        // противоречивая семантика
    hardProblem: 25,               // трудная проблема
    positiveActionEffect: 26,      // положительный эффект последнего действия
    negativeActionEffect: 27,      // отрицательный эффект последнего действия
    noReactionToStimulus: 28,      // нет реакции на стимул
    operatorAnswerReceived: 29,    // получен ответ оператора
    activeWaitingAnswer: 30,       // активное ожидание ответа
    seriesWithoutEffect: 31,       // серия эпизодов без эффекта
    lowAutomatismUsefulness: 32,   // низкая полезность автоматизма
    foundSuitableAutomatism: 33,    // найден подходящий автоматизм
    notFoundSuitableAutomatism: 34, // НЕ найден подходящий автоматизм
    usedRule: 35,                   // реагирование по правилу
    /**
     * Синтетический маркер «всё спокойно»: не дублирует поле InfoPicture — подставляется в
     * extractSituationComponentsFromInfo, когда нет ни одного ненулевого детектора, но
     * BadNormWell — Норма/Хорошо, активен базовый контекст «Покой» (id 14), нет крит. витала.
     * Иначе ячейка «Ситуация» и образы ситуаций не накапливались при типичной спокойной фазе.
     */
    peaceNormBaseline: 36
  };

  function cloneSortedComponents(arr) {
    if (!Array.isArray(arr)) return [];
    var filtered = [];
    for (var i = 0; i < arr.length; i++) {
      var v = parseInt(arr[i], 10);
      if (!isNaN(v) && v !== 0 && filtered.indexOf(v) === -1) {
        filtered.push(v);
      }
    }
    filtered.sort(function (a, b) { return a - b; });
    return filtered;
  }

  function componentsEqual(a, b) {
    if (!Array.isArray(a) || !Array.isArray(b)) return false;
    if (a.length !== b.length) return false;
    for (var i = 0; i < a.length; i++) {
      if (a[i] !== b[i]) return false;
    }
    return true;
  }

  // Простейшая мера соответствия: число общих компонент.
  function overlapScore(a, b) {
    if (!Array.isArray(a) || !Array.isArray(b) || !a.length || !b.length) return 0;
    var score = 0;
    for (var i = 0; i < a.length; i++) {
      if (b.indexOf(a[i]) !== -1) score++;
    }
    return score;
  }

  function findOrCreateThemeImage(mode, componentIds) {
    var comps = cloneSortedComponents(componentIds);
    if (!comps.length) return null;
    for (var i = 0; i < ThemeImages.length; i++) {
      var t = ThemeImages[i];
      if (t.mode === mode && componentsEqual(t.componentIds, comps)) {
        t.count++;
        if (!t.ready && t.count > 3) {
          t.ready = true;
        }
        return t;
      }
    }
    // Новый образ темы
    var theme = {
      id: nextThemeId++,
      mode: mode,
      componentIds: comps,
      count: 1,
      ready: false
    };
    ThemeImages.push(theme);
    treeDirty = true;
    return theme;
  }

  function findOrCreateSituationImage(mode, componentIds) {
    var comps = cloneSortedComponents(componentIds);
    if (!comps.length) return null;
    for (var i = 0; i < SituationImages.length; i++) {
      var s = SituationImages[i];
      if (s.mode === mode && componentsEqual(s.componentIds, comps)) {
        s.count++;
        if (!s.ready && s.count > 3) {
          s.ready = true;
        }
        return s;
      }
    }
    // Новый образ ситуации
    var sit = {
      id: nextSituationId++,
      mode: mode,
      componentIds: comps,
      count: 1,
      ready: false
    };
    SituationImages.push(sit);
    treeDirty = true;
    return sit;
  }

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

  function serializeState() {
    return {
      nextThemeId: nextThemeId,
      nextSituationId: nextSituationId,
      themeImages: ThemeImages.slice(0),
      situationImages: SituationImages.slice(0)
    };
  }

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

    ThemeImages = Array.isArray(state.themeImages) ? state.themeImages.slice(0) : [];
    SituationImages = Array.isArray(state.situationImages) ? state.situationImages.slice(0) : [];

    // пересчитать nextId на всякий случай (защита от конфликтов при загрузке)
    for (var i = 0; i < ThemeImages.length; i++) {
      var t = ThemeImages[i];
      if (t && typeof t.id === 'number' && t.id >= nextThemeId) nextThemeId = t.id + 1;
    }
    for (var j = 0; j < SituationImages.length; j++) {
      var s = SituationImages[j];
      if (s && typeof s.id === 'number' && s.id >= nextSituationId) nextSituationId = s.id + 1;
    }

    treeDirty = false;
    try {
      lastSavedSig = JSON.stringify(serializeState());
    } catch (e) {
      lastSavedSig = '';
    }
  }

  var DB_NAME = 'BEAST_SituationsTree_DB';
  // Важно: увеличение версии принудительно запускает onupgradeneeded и создаёт store,
  // если раньше БД была создана без него (из-за изменений в обёртке IndexedDB).
  var DB_VERSION = 2;
  var STORE = 'situations_tree_state';
  var KEY = 'situations_tree_singleton';

  function openDb() {
    if (!global.IndexedDBModule || typeof global.IndexedDBModule.openDB !== 'function') {
      return Promise.reject(new Error('IndexedDBModule not available'));
    }
    return global.IndexedDBModule.openDB({
      dbName: DB_NAME,
      version: DB_VERSION,
      storeName: STORE,
      keyPath: 'id'
    });
  }

  function loadFromIndexedDB() {
    openDb()
      .then(function (d) {
        return global.IndexedDBModule.get({ db: d, storeName: STORE, key: KEY });
      })
      .then(function (record) {
        if (record && record.state) {
          applyState(record.state);
        }
      })
      .catch(function (e) {
        if (global.console && typeof global.console.error === 'function') {
          global.console.error('SituationsTree IndexedDB load error:', e);
        }
      });
  }

  loadFromIndexedDB();

  if (typeof global.registerIndexedDBSaver === 'function') {
    global.registerIndexedDBSaver(function () {
      if (!treeDirty) return;
      treeDirty = false;
      var state = serializeState();
      var sig = '';
      try { sig = JSON.stringify(state); } catch (e) { sig = ''; }
      if (sig && sig === lastSavedSig) return;
      lastSavedSig = sig;
      openDb()
        .then(function (db) {
          return global.IndexedDBModule.put({
            db: db,
            storeName: STORE,
            value: { id: KEY, state: state, ts: Date.now() }
          });
        })
        .catch(function (e) {
          if (global.console && typeof global.console.error === 'function') {
            global.console.error('SituationsTree IndexedDB save error:', e);
          }
        });
    });
  }

  function findBestReadyTheme(mode, componentIds) {
    var comps = cloneSortedComponents(componentIds);
    if (!comps.length) return 0;
    var bestId = 0;
    var bestScore = 0;
    for (var i = 0; i < ThemeImages.length; i++) {
      var t = ThemeImages[i];
      if (!t.ready || t.mode !== mode) continue;
      var s = overlapScore(comps, t.componentIds);
      if (s > bestScore) {
        bestScore = s;
        bestId = t.id;
      }
    }
    return bestId;
  }

  function findBestReadySituation(mode, componentIds) {
    var comps = cloneSortedComponents(componentIds);
    if (!comps.length) return 0;
    var bestId = 0;
    var bestScore = 0;
    for (var i = 0; i < SituationImages.length; i++) {
      var sIt = SituationImages[i];
      if (!sIt.ready || sIt.mode !== mode) continue;
      var s = overlapScore(comps, sIt.componentIds);
      if (s > bestScore) {
        bestScore = s;
        bestId = sIt.id;
      }
    }
    return bestId;
  }

  // ---------- Публичные помощники для детализации ----------

  function SituationsTree_getThemeImage(id) {
    for (var i = 0; i < ThemeImages.length; i++) {
      if (ThemeImages[i].id === id) return ThemeImages[i];
    }
    return null;
  }

  function SituationsTree_getAllThemeImages() {
    return ThemeImages.slice(0);
  }

  function SituationsTree_getSituationImage(id) {
    for (var i = 0; i < SituationImages.length; i++) {
      if (SituationImages[i].id === id) return SituationImages[i];
    }
    return null;
  }

  function SituationsTree_getAllSituationImages() {
    return SituationImages.slice(0);
  }

  function SituationsTree_getSituationComponentName(componentId) {
    for (var key in DETECTOR_COMPONENT_IDS) {
      if (!Object.prototype.hasOwnProperty.call(DETECTOR_COMPONENT_IDS, key)) continue;
      if (DETECTOR_COMPONENT_IDS[key] === componentId) {
        return key;
      }
    }
    return '';
  }

  /**
   * Извлекает компоненты образа Темы из текущей инфокартины:
   * пока: [BaseID, EmotionID, ActivityID] (ненулевые).
   */
  function extractThemeComponentsFromInfo(info) {
    var comps = [];
    if (!info || !info.tree) return comps;
    if (info.tree.baseID) comps.push(info.tree.baseID);
    if (info.tree.emotionID) comps.push(info.tree.emotionID);
    if (info.tree.activityID) comps.push(info.tree.activityID);
    return comps;
  }

  /**
   * Извлекает компоненты образа Ситуации из текущей инфокартины:
   * по детекторным переменным (каждый ненулевой флаг даёт компонент).
   * Если набор пуст (типично при Норме виталов и «Покой», без тревожных детекторов),
   * подставляется компонент peaceNormBaseline (36), чтобы накапливался и активировался
   * образ ситуации «спокойная норма» — см. DETECTOR_COMPONENT_IDS.peaceNormBaseline.
   */
  function extractSituationComponentsFromInfo(info) {
    var comps = [];
    if (!info) return comps;
    var k;
    for (k in DETECTOR_COMPONENT_IDS) {
      if (!Object.prototype.hasOwnProperty.call(DETECTOR_COMPONENT_IDS, k)) continue;
      if (k === 'peaceNormBaseline') continue;
      var cid = DETECTOR_COMPONENT_IDS[k];
      var v = info[k];
      if (typeof v === 'number' && v !== 0) {
        if (comps.indexOf(cid) === -1) comps.push(cid);
      }
    }
    if (!comps.length) {
      var bnw = typeof global.BadNormWell === 'number' ? global.BadNormWell : 0;
      var wellBenign = (bnw === 2 || bnw === 3);
      var act = global.BasicContextsActived;
      var peace = Array.isArray(act) && act.indexOf(14) !== -1;
      var crit = typeof info.criticalVital === 'number' ? info.criticalVital : 0;
      if (wellBenign && peace && !crit) {
        var baselineCid = DETECTOR_COMPONENT_IDS.peaceNormBaseline;
        if (baselineCid != null) comps.push(baselineCid);
      }
    }
    return comps;
  }

  /**
   * Основная точка входа: вызывается по каждой активации дерева восприятия
   * (после обновления инфокартины по PerceptionTree).
   *
   * 1) Извлекает mode (по режиму мышления главного цикла ФО), themeComponents, situationComponents из InfoPicture.
   * 2) Обновляет/создаёт образы Темы и Ситуации (счётчики, ready при count>3).
   * 3) Находит лучшую готовую Тему/Ситуацию для данного режима и обновляет
   *    текущие themeId / situationId через инфо‑детекторы.
   */
  function SituationsTree_onPerceptionTreeActivated() {
    if (typeof global.Info_initIfNeeded !== 'function') return;
    var info = global.Info_initIfNeeded();
    if (!info) return;

    var mainTm = (typeof global.Consciousness_getMainCycleThinkingMode === 'function')
      ? global.Consciousness_getMainCycleThinkingMode()
      : 'target';
    var mode = (mainTm === 'target') ? 1 : 0;
    var themeComps = extractThemeComponentsFromInfo(info);
    var situationComps = extractSituationComponentsFromInfo(info);

    // Регистрируем / усиливаем образы (обновление счётчиков и ready).
    if (themeComps.length) {
      findOrCreateThemeImage(mode, themeComps);
    }
    if (situationComps.length) {
      findOrCreateSituationImage(mode, situationComps);
    }

    // Активация готовых образов — поиск наиболее соответствующей ТЕМЫ и СИТУАЦИИ.
    var activeThemeId = findBestReadyTheme(mode, themeComps);
    var activeSituationId = findBestReadySituation(mode, situationComps);

    if (typeof global.InfoDet_updateCurrentTheme === 'function') {
      global.InfoDet_updateCurrentTheme(activeThemeId);
    }
    if (typeof global.InfoDet_updateCurrentSituation === 'function') {
      global.InfoDet_updateCurrentSituation(activeSituationId);
    }
  }

  /**
   * Заготовка: рассмотрение новых образов ситуаций на будущее в ходе семантической обработки моделей понимания.
   * Вызывается из PassiveMode_processUnderstandingModels (goals.js), когда в пассивном режиме идёт
   * семантическая обработка моделей понимания; в ходе этой работы возникают новые образы ситуаций на будущее.
   * Полная реализация: по данным моделей понимания (семантика, образы) создавать или усиливать
   * SituationImages с соответствующими componentIds (режим 0 — пассивный).
   */
  function SituationsTree_considerNewSituationFromUnderstanding() {
    // Заглушка: при семантической обработке — формирование/обновление образов ситуаций на будущее.
  }

  /** Очистить все образы ситуаций (SituationImages); темы не трогаем. Состояние сохранится по treeDirty. */
  function SituationsTree_clearAllSituationImages() {
    SituationImages = [];
    nextSituationId = 1;
    treeDirty = true;
    if (typeof global.InfoDet_updateCurrentSituation === 'function') {
      global.InfoDet_updateCurrentSituation(0);
    } else if (global.InfoPicture) {
      global.InfoPicture.situationId = 0;
    }
    if (typeof global.Info_updateView === 'function') global.Info_updateView();
  }

  global.SituationsTree_onPerceptionTreeActivated = SituationsTree_onPerceptionTreeActivated;
  global.SituationsTree_getThemeImage = SituationsTree_getThemeImage;
  global.SituationsTree_getAllThemeImages = SituationsTree_getAllThemeImages;
  global.SituationsTree_getSituationImage = SituationsTree_getSituationImage;
  global.SituationsTree_getAllSituationImages = SituationsTree_getAllSituationImages;
  global.SituationsTree_getSituationComponentName = SituationsTree_getSituationComponentName;
  global.SituationsTree_considerNewSituationFromUnderstanding = SituationsTree_considerNewSituationFromUnderstanding;
  global.SituationsTree_serializeState = serializeState;
  global.SituationsTree_applyState = applyState;
  global.SituationsTree_clearAllSituationImages = SituationsTree_clearAllSituationImages;

})(window);


════════════════════════════════════════════════════════════

/BEAST/beast/_9_Conscience/target_rule_stack.js
════════════════════════════════════════════════════════════
(function (global) {
  'use strict';

  /**
   * Стек целей для правил (LIFO), не глубже 3 ячеек.
   * Хранит FinalImageId образов цели; индексы массива: [0] — дно, [length-1] — вершина.
   */
  var MAX_DEPTH = 3;

  if (!Array.isArray(global.Target_rule_stack)) {
    global.Target_rule_stack = [];
  }

  function normalize() {
    while (global.Target_rule_stack.length > MAX_DEPTH) {
      global.Target_rule_stack.shift();
    }
  }

  /**
   * Положить ID образа цели на вершину стека (0 игнорируется).
   */
  function TargetRuleStack_push(finalImageId) {
    var id = parseInt(finalImageId, 10) || 0;
    if (!id) return;
    global.Target_rule_stack.push(id);
    normalize();
  }

  /** Снять вершину стека, вернуть ID или 0. */
  function TargetRuleStack_pop() {
    if (!global.Target_rule_stack.length) return 0;
    return global.Target_rule_stack.pop() || 0;
  }

  /** Верх стека без изъятия. */
  function TargetRuleStack_peek() {
    var a = global.Target_rule_stack;
    return a.length ? (a[a.length - 1] || 0) : 0;
  }

  function TargetRuleStack_clear() {
    global.Target_rule_stack.length = 0;
  }

  function TargetRuleStack_depth() {
    return global.Target_rule_stack.length;
  }

  global.TargetRuleStack_push = TargetRuleStack_push;
  global.TargetRuleStack_pop = TargetRuleStack_pop;
  global.TargetRuleStack_peek = TargetRuleStack_peek;
  global.TargetRuleStack_clear = TargetRuleStack_clear;
  global.TargetRuleStack_depth = TargetRuleStack_depth;
})(typeof window !== 'undefined' ? window : globalThis);

════════════════════════════════════════════════════════════

/BEAST/beast/about_global_variables.md
════════════════════════════════════════════════════════════
# Справочник глобальных переменных Beast

Переменные и объекты, объявляемые в коде как `window.*` или `global.*` (в браузере `window` обычно и есть `global`). Группировка по назначению и модулям.

---

## Пульс и время

| Переменная | Тип | Описание | Где задаётся |
|------------|-----|----------|--------------|
| **Cur_puls_val** | number | Счётчик пульсов. Инкрементируется в sep_of_puls(); сохраняется в localStorage. | puls.js, saving_ui.js |
| **PrevDiffForEpisode** | number | Значение getDiffImportance() в начале пульса (до активации стимулов). Для расчёта effect в кадре ЭП. | puls.js |
| **update_beast_pulse_view** | function | Обновление отображения пульса в UI. | puls.js |

---

## Виталы и состояние организма (_1_Genetic_vitals, _2_Genetic_basic_styles)

| Переменная | Тип | Описание | Где задаётся |
|------------|-----|----------|--------------|
| **VitalsArr** | Array | Массив виталов: { Id, value, importance, shift, name, title }. value 0..100. | vitals.js |
| **VitalsArrDefaults** | Array | Копия исходных значений виталов для сброса. | vitals.js |
| **BadVitalsValue** | Object | Степень выхода виталов из нормы (id → значение). | vitals.js |
| **BadNormWell** | number | Базовое состояние: 1=Плохо, 2=Норма, 3=Хорошо. | vitals.js, BasicContexts.js, prepare.js |
| **IntergalConditionImportance** | number | Интегральная значимость состояния организма. | vitals.js, BasicContexts.js, prepare.js |
| **IntergalConditionImportanceOld** | number | Предыдущее значение интегральной значимости. | vitals.js, BasicContexts.js |
| **DiffZnacher** | number | Дифференциатор значимости. | vitals.js, BasicContexts.js, prepare.js |
| **isShockState** | boolean | Режим шока. | vitals.js, vitals_ui.js, vitals_prepare.js |
| **DeathSequenceStarted** | boolean | Запущена ли последовательность «смерти». | vitals_ui.js, vitals_prepare.js |
| **IsSpeeping** | boolean | Режим сна. | vitals.js |
| **isGameMode** | boolean | Режим «Играть» (vitalID 11). | vitals.js, stimuls_ui.js |
| **isAuthoritarianEducation** | boolean | Режим «Учить» (vitalID 12). | vitals.js, stimuls_ui.js |
| **WellHoldRemainingPulses** | number | Оставшиеся пульсы удержания «Хорошо». | prepare.js |
| **BadHoldRemainingPulses** | number | Оставшиеся пульсы удержания «Плохо». | prepare.js |
| **NormHoldRemainingPulses** | number | Оставшиеся пульсы удержания «Норма» (явное удержание, SetNormForHolding / info_setBadNormWellForHolding(2)). | prepare.js |
| **ContextHoldRemainingPulses** | number | Оставшиеся пульсы удержания контекста. | prepare.js |
| **WellBadEffectManual** | number | Ручная настройка влияния Хорошо/Плохо на diff (по умолчанию 5). | prepare.js |
| **saveVitalsToStorage** | function | Сохранение виталов (localStorage/IndexedDB). | vitals_prepare.js |
| **performSensorOperations** | function | Операции «сенсоров» по виталам. | vitals_prepare.js |
| **update_vitals_view** | function | Обновление отображения виталов. | vitals_ui.js |
| **resetVitalsToDefaults** | function | Сброс виталов к умолчаниям. | vitals_ui.js |
| **startDeathSequence** | function | Запуск последовательности смерти. | vitals_ui.js |

---

## Базовые контексты (_2_Genetic_basic_styles)

| Переменная | Тип | Описание | Где задаётся |
|------------|-----|----------|--------------|
| **BasicContexts** | Array | Базовые контексты (Вентиляция, Водопой, Страх, Агрессия, Покой и т.д.). | BasicContexts.js |
| **VitalsContextsActivingArr** | Object | Соответствие «выход витала из нормы» → массив ID активируемых контекстов. | BasicContexts.js |
| **BasicContextsActived** | Array | ID активных в данный момент базовых контекстов. | BasicContexts.js |
| **getActiveBasicContexts** | function | Получение/пересчёт активных контекстов. | basic_contexts_ui.js / prepare |
| **update_basic_contexts_view** | function | Обновление отображения контекстов. | basic_contexts_ui.js |
| **ActivateBasicContextsForHolding** | function | Принудительный набор базовых контекстов на период удержания (столько пульсов, сколько `state_retention_period` в prepare.js). | prepare.js |
| **SetWellForHolding** / **SetBadForHolding** / **SetNormForHolding** | function | Установка удержания Хорошо (3) / Плохо (1) / Норма (2); взаимно сбрасывают счётчики друг друга. | prepare.js |

---

## Стимулы (_3_perception)

| Переменная | Тип | Описание | Где задаётся |
|------------|-----|----------|--------------|
| **StimulMotivarion** | Array | Мотивирующие стимулы (Еда, Вода, Поощрение, Приятно, Играть…). | stimuls.js |
| **ActiveMotivationStimuls** | Array | ID активных мотивирующих стимулов. | stimuls.js, stimuls_ui.js |
| **StimulPositiv** | Array | Позитивные стимулы. | stimuls.js |
| **StimulNegative** | Array | Негативные стимулы. | stimuls.js |
| **StimulNeutral** | Array | Нейтральные стимулы. | stimuls.js |
| **ActivePositivStimuls** | Array | ID активных позитивных стимулов. | stimuls_ui.js (init) |
| **ActiveNegativeStimuls** | Array | ID активных негативных стимулов. | stimuls_ui.js (init) |
| **ActiveNeutralStimuls** | Array | ID активных нейтральных стимулов. | stimuls_ui.js (init) |
| **OrientationReflex_runOnStimulusActivation** | function | Запуск ОР при изменении набора стимулов (вызывается из UI после клика). | orientation_reflex.js |

---

## Действия (_4_actions)

| Переменная | Тип | Описание | Где задаётся |
|------------|-----|----------|--------------|
| **BasicActions** | Array | Базовые действия (Плачет, Ест, Улыбается…). | effectors.js |
| **BasicVerbal** | Array | Базовые «звуки» (а, о, слоги…). | effectors.js |
| **BasicMirrorActions** | Array | Зеркальные действия оператора для ЭП: { Id, mirrorParentId "n_m", name }. | effectors.js |

---

## Генетические рефлексы (_5_Genetic__reflexes)

| Переменная | Тип | Описание | Где задаётся |
|------------|-----|----------|--------------|
| **StimulMotivarion_EffectsToBContexts** | Object | Влияние мотивирующих стимулов на базовые контексты. | genetic_reflexes.js |
| **StimulPositiv_EffectsToBContexts** | Object | Влияние позитивных стимулов на контексты. | genetic_reflexes.js |
| **StimulNegative_EffectsToBContexts** | Object | Влияние негативных стимулов на контексты. | genetic_reflexes.js |
| **StimulMotivarion_GeneticReflexes** | Object | Стимул (id) → массив actionId. | genetic_reflexes.js |
| **StimulPositiv_GeneticReflexes** | Object | Аналогично для позитивных. | genetic_reflexes.js |
| **StimulNegative_GeneticReflexes** | Object | Аналогично для негативных. | genetic_reflexes.js |
| **ActionProlongator** | Array | Пролонгированные действия (еда, питьё и т.д.): vitalID, shift. | genetic_reflexes.js |
| **processGeneticReflexes** | function | Обработка рефлексов на каждом пульсе. | genetic_reflexes_engine.js |
| **runGeneticActionFromSynonymReflex** | function | Запуск действия из условного рефлекса. | genetic_reflexes_engine.js |
| **setBeastActivityText** | function | Установка текста в блоке «Активность Beast». | genetic_reflexes_engine.js |

---

## Условные рефлексы (_5_2_synonyms_reflexes)

| Переменная | Тип | Описание | Где задаётся |
|------------|-----|----------|--------------|
| **SynonymReflexes** | Array | Массив условных (синонимических) рефлексов. | synonyms_reflexes.js |
| **NextSynonymReflexId** | number | Счётчик ID для новых рефлексов. | synonyms_reflexes.js |
| **synonyms_onGeneticActionFired** | function | Вызов при срабатывании генетического рефлекса (формирование/усиление условного). | synonyms_reflexes.js |
| **synonyms_tryRunReflexes** | function | Попытка запуска условных рефлексов по текущим стимулам и контексту. | synonyms_reflexes.js |
| **serializeConditionalReflexesState** / **applyConditionalReflexesState** | function | Сериализация/восстановление состояния УР. | synonyms_reflexes.js |
| **clearAllConditionalReflexes** | function | Сброс всех условных рефлексов. | synonyms_reflexes.js |
| **get_cur_context_for_synonyms** | function | Текущий контекст для условных рефлексов. | synonyms_reflexes.js |

---

## Семантическая память и образы (_6_semantic_memory)

| Переменная | Тип | Описание | Где задаётся |
|------------|-----|----------|--------------|
| **FinalImages** | Array | Образы восприятия: { id, stimulIds }. | FinalImage.js |
| **KnownFinalImageIds** | Array | Известные ID образов (для семантики). | semantic_memory.js |
| **FinalImages_findFinalImageId** | function | Поиск id по массиву stimulIds. | FinalImage.js |
| **FinalImages_getOrCreateFinalImageId** | function | Получить или создать ID образа по stimulIds. | FinalImage.js |
| **Emotions** | Array | Эмоции (сочетания контекстов). | Emotion.js |
| **KnownEmotionIds** | Array | Известные ID эмоций. | semantic_memory.js |
| **Emotions_findEmotionId** / **Emotions_getOrCreateEmotionId** | function | Поиск/создание ID эмоции по контекстам. | Emotion.js |
| **SemanticMemory** | Array | Массив записей семантической значимости. | semantic_memory.js |
| **SemanticMemoryIndex** | Object | Индекс [well][emotionId][finalImageId] → { meanImportance, samplesCount }. | semantic_memory.js |
| **registerSemanticSample** | function | Регистрация опыта (z, условия, finalImageId). | semantic_memory.js |
| **getSemanticObjectFast** | function | Быстрый поиск по индексу (well, emotionId, finalImageId) → объект или null. | semantic_memory.js |
| **getSemanticFramesForFinalImageId** | function | Все кадры по finalImageId через индекс (без линейного скана). | semantic_memory.js |
| **getDiffImportance** | function | Интегральная значимость состояния (для семантики и ЭП). | vitals_prepare / prepare.js |
| **LastDiffImportanceRaw** | number \| null | Сырой diff до `normalizeValue` в последнем `getDiffImportance()`; для «лупы» в `registerSemanticSample`. | prepare.js |
| **serializeSemanticState** / **applySemanticState** | function | Сериализация/восстановление семантики. | semantic_memory.js |
| **ImageUnderstandingModelArr** | Object | Модели понимания образов. | Understanding_model.js |
| **getImageUnderstandingModel** / **buildImageUnderstandingModel** | function | Получение/построение модели понимания. | Understanding_model.js |

---

## Дерево восприятия (_7_perception_tree)

| Переменная | Тип | Описание | Где задаётся |
|------------|-----|----------|--------------|
| **PerceptionTree_getOrCreateTerminalNode** | function | Получить или создать терминальный узел по пути [baseID, emotionID, activityID, 0,0,0]. | perception_tree.js |
| **PerceptionTree_updateActiveBranchView** | function | Обновление блока «Активная ветка дерева» и кадров ЭП в UI. | perception_tree.js |
| **PerceptionTree_getStimulusNamesForFinalImageId** | function | Имена стимулов по FinalImageId. | perception_tree.js |
| **PerceptionTree_getActionNamesForActionImage** | function | Имена действий по ID образа действия. | perception_tree.js |
| **PerceptionTree_serializeState** / **PerceptionTree_applyState** | function | Сохранение/загрузка дерева. | perception_tree.js |
| **PerceptionTree_*** | (прочие) | addNode, findActiveBranch, getPathByNodeId, serializeCompact и т.д. | perception_tree.js |

---

## Гиппокамп и ОР (_8_Hippocampus)

| Переменная | Тип | Описание | Где задаётся |
|------------|-----|----------|--------------|
| **active_stimulsArr_ID** | Array | ID образов (FinalImageId), вызвавших ОР. | hippocampus.js |
| **actual_stimul_ID** | number | Наиболее актуальный стимул (ID образа). Заполняется при ОР. | hippocampus.js, orientation_reflex.js |
| **Hippocampus_holdStimulusFromOR** | function | Удержать стимул в памяти после ОР. | hippocampus.js |
| **OR_well_known** | number | Порог samplesCount для допуска стимула к ОР и ЭП (по умолчанию 10). | orientation_reflex.js |
| **OrientationReflex_runOnStimulusActivation** | function | Запуск конкуренции и ОР при активации стимулов. | orientation_reflex.js |
| **OrientationReflex_isReflexBlockedByOR** | function | Проверка, заблокирован ли рефлекс ОР на 2 пульса. | orientation_reflex.js |
| **OrientationReflex_setBlockedReflex** | function | Заблокировать рефлекс по ключу. | orientation_reflex.js |

---

## Осознание (_9_Conscience)

| Переменная | Тип | Описание | Где задаётся |
|------------|-----|----------|--------------|
| **ConsciousnessCycles** | Object | Реестр циклов осмысления: id → cycleInfo. Поля: … **createdThemeId**, **createdSituationId** — ID темы и ситуации инфокартины на момент создания цикла; **expired**, **events[]**, `noInfoChangeStreak`; **fantasizmArr** (снимки правил ЭП для фонового цикла), **fantasizmPassiveFillStarted** и др. | consciousness_dispatcher.js |
| **ConsciousnessProcessNextId** | number | Счётчик для присвоения уникального ID новому циклу. | consciousness_dispatcher.js |
| **ConsciousnessCurrentProcessId** | number | ID текущего главного (осознаваемого) цикла. Отображается в UI «Главный цикл ID: nn». | consciousness_dispatcher.js, conscious_attention_channel.js |
| **ConsciousnessBackgroundStack** | Array | Очередь прерываний (до 10): при прерывании в стек пушится только ID текущего главного цикла (LIFO). Цикл остаётся в реестре как фоновый. При завершении главного promoteFromBackgroundCycle() достаёт ID из стека и делает этот цикл снова главным; если цикл уже завершён — берётся следующий ID или выбор по weight. | conscious_attention_channel.js, consciousness_dispatcher.js |
| **Consciousness_createCycle** | function | Создать новый цикл и зарегистрировать в ConsciousnessCycles. | consciousness_dispatcher.js |
| **Consciousness_setMainCycle** | function | Установить цикл с заданным ID главным (остальные isMainCycle = false). | consciousness_dispatcher.js |
| **Consciousness_getMainCycle** | function | Получить текущий главный цикл (status === 'running'). | consciousness_dispatcher.js |
| **Consciousness_cancelProcess** | function | Отменить цикл (status = 'cancelled'). | consciousness_dispatcher.js |
| **Consciousness_runElementary** | function | Элементарный проход осознания (уровни 1 и 2) по контексту цикла. Вызывается при создании цикла и при подъёме из стека. | conscious_attention_channel.js |
| **Consciousness_runOneStep** | function | Один шаг цикла мышления (для диспетчера). При `cycle.thinkingMode === 'passive'` — `info_fantasizming` и выход (без ур. 3–4); иначе ур. 3–4 в `conscience_level_3_runFromChannel` / `conscience_level_4_runFromChannel`. | conscious_attention_channel.js |
| **Consciousness_getMainCycleThinkingMode** | function | `'target'` \| `'passive'` — режим мышления **главного** цикла (не поле инфокартины). | consciousness_dispatcher.js |
| **Consciousness_getInfoPictureSignature** | function | Строка-сигнатура инфокартины для антистагнации пассивного фантазирования **главного** цикла (диспетчер и `runOneStep`). | consciousness_dispatcher.js |
| **dispetchConsciousnessThinking** | function | Диспетчер: шаг главного и фоновых (раз в 5 пульсов); при завершении главного или expired — decay фоновых weight×0.5; promote из стека. | consciousness_dispatcher.js, puls.js |
| **Consciousness_decayBackgroundOperativeMemory** | function | Умножает weight всех фоновых running-циклов на 0.5 (оперативная память). Вызывается при решении проблемы (главный finished), исчерпании главного (expired), вручную из пассива. | consciousness_dispatcher.js |
| **PassiveMode_notifyScenariosExhausted** | function | Исчерпание сценариев пассивного режима — вызвать для decay фоновых циклов. | goals.js |
| **stimuls_consciousness** | function | Вход ФО. Порог прерывания: эффективная значимость стимула ≥ 150% от thinkingImportance главного; при несовпадении темы/ситуации главного с инфокартиной эффективная значимость ×0.5. Новый цикл получает weight = значимости стимула. | conscious_attention_channel.js |
| **automatismRanForBranchIds** | Object | Флаг по ветке: branchId → true, если по этой ветке уже выполнен автоматизм. Ставится в stimuls_consciousness при запуске автоматизма; в processGeneticReflexes генрефлекс по этой ветке не запускается (исключает появление «сначала автоматизм, потом рефлекс»). Сбрасывается при смене ветки. | conscious_attention_channel.js, genetic_reflexes_engine.js |
| **unsolved_problem** | number \| null | Маркер нерешённой проблемы первого уровня осознания: FinalImageId актуального стимула, по которому не найдено приемлемого автоматического ответа (ни по ЭП, ни по штатному автоматизму). Устанавливается в stimuls_consciousness, может очищаться при успешном запуске автоматизма по тому же стимулу. | conscious_attention_channel.js |
| **conscience_level_2** | function | Реализация второго уровня осознания. Вызывается из Consciousness_runElementary при наличии нерешённой проблемы (unsolved_problem) для актуального стимула. Принимает расширенный контекст (actualId, branchId, well, emotionId, activityID, sourceContext). | conscience_level_2.js |
| **info_automatismWithEpisodeFeedbackAndExpireCycle** | function | Запуск автоматизма и ОД, привязка к ожиданию ЭП, expired+не главный цикл. Ур.1–3. Ответ оператора: `closeLastFrame` → `conscience_level_2`. | infofunctions.js |
| **info_insight** | function | Инсайт: цель «что для меня означает это?» (`meaning`), режим target; фоновый цикл — в главный (`Consciousness_setMainCycle`). Вызывается из `registerInsight` (gestalt.js), при \|effect\| ≥ `FANTASMI_INSITE_COMPARE` из `info_fantasizming`. | infofunctions.js |
| **info_new_abstract_semantic** | function | `(cycle, opts?)` — перенос `fantasizmArr` в `semanticStructure` клонов с `ruleKind` fantasy (`AbstractImages_upsertSemanticRuleFromFantasyFrame`), очистка сюжета на цикле; для сна. Учительские слоты не трогает. | infofunctions.js, abstract_images.js |
| **info_fantasizming** | function | Один шаг пассивного фантазирования: цепочка по кадрам ЭП с `responseStimulusImageId === currentStimulusId`, лучший по \|effect\|; сюжет в `fantasmPlotRules` / `fantasizmArr`; опционально инсайт без `runActionImage`. | fantasizming.js |
| **FANTASMI_INSITE_COMPARE** | number | Порог \|effect\| для инсайта из фантазирования (по умолчанию 5). | fantasizming.js |
| **info_bestEpisodicRule** | function | `(perceptionNodeId, situationId, options)` → `{ frame, exactRule }`; опции: `filterByStimulus`, `stimulusImageId`, `allowStimulusFallback` (ур.2 выживание / ур.3). | infofunctions.js |
| **info_bestEpisodicRuleForStimulus** | function | Обёртка: `filterByStimulus: true`. | infofunctions.js |
| **info_teacherEpisodicRuleForAction** | function | Учительский кадр ЭП: `(perceptionNodeId, situationId, actionImageId)` → `{ frame, exactRule }`; закрытый кадр с тем же ОД и ненулевым `responseStimulusImageId`, без ранжирования по effect. | infofunctions.js |
| **AbstractImages_findRuleFrameMatchingContext** | function | Кадр `semanticStructure` клона восприятия при точном совпадении ветки, `situationId`, `actualStimulusImageId` (ур.3 shortcut). | abstract_images.js |
| **AbstractImages_applyEpisodeEffectToSemanticContextAbstractions** | function | Ответ оператора: усреднить `meanImportance` клонов восприятия/действия по `effect`, если есть соответствующий кадр `semanticStructure`. | abstract_images.js |
| **AbstractImages_boostSemanticRulesForAction** | function | Ур.2, цель «значение»: усреднить `meanImportance` абстракций, в `semanticStructure.frames` есть правило с данным `actionImageId`. | abstract_images.js |
| **info_activateBasicContextForHolding** | function | `(basicContextId)` — удержание одного базового контекста на период как в UI (`ActivateBasicContextsForHolding([id])`). | infofunctions.js |
| **info_setBadNormWellForHolding** | function | `(1\|2\|3)` — удержание Плохо / Норма / Хорошо (`SetBadForHolding` / `SetNormForHolding` / `SetWellForHolding`). | infofunctions.js |
| **activateStimulusForBasicMirrorActionId** | function | Включить стимул по `BasicMirrorActions[].Id` (поле `mirrorParentId` → тип строки стимулов и локальный id). | stimuls_ui.js |
| **info_activateBasicMirrorAction** | function | Обёртка над `activateStimulusForBasicMirrorActionId`. | infofunctions.js |
| **conscience_level_3** | function | Одно событие 3-го уровня: `{ sourceCycle, reason? }`. Пайплайн ментальный → semanticStructure → ЭП → учитель; гейт ожидания ЭП; гейты значимости кадра/семантики; лог через `Consciousness_cycleAppendLog`. Вызывается из `conscience_level_3_runFromChannel`. | conscience_level_3.js |
| **conscience_level_3_runFromChannel** | function | Тик ур. 3 из `Consciousness_runOneStep`: отложенное планирование ур.2, «плач», `isDissatisfaction`; вложенный `runLevel3` не вызывает повторный полный проход, если уже ожидание ЭП по автоматизму. | conscience_level_3.js |
| **conscience_level_4** | function | Одно событие 4-го уровня (расширения; гештальт — через `Gestalt_*`). | conscience_level_4.js |
| **conscience_level_4_runFromChannel** | function | Тик ур. 4 из канала: `Gestalt_onConsciousnessTick` (резонанс, контекст). | conscience_level_4.js |
| **GestaltArr** | Array | Записи гештальтов (нерешённые задачи по образу цели). | gestalt.js |
| **GestaltResonanceBuffer** | Array | FIFO сигнатур закрытых кадров ЭП для кросс-цикловой аналогии. | gestalt.js |
| **GestaltContextForLevel3** | Object \| null | Контекст открытого гештальта для ур.3 после `Gestalt_prepareContextForCycle`. | gestalt.js |
| **Gestalt_prepareContextForCycle** | function | Вызывается в начале `conscience_level_3_runFromChannel`. | gestalt.js |
| **Gestalt_onConsciousnessTick** | function | Тик ур.4: резонанс, обновление контекста. | gestalt.js |
| **Gestalt_onEpisodeFeedbackClosed** | function | Вызывается из `EpisodicMemory_closeLastFrame` после расчёта effect. | gestalt.js |
| **Gestalt_bumpFromMeaningForecast** | function | Ур.2, цель «значение»: повысить статус открытого гештальта при совпадении цели/автоматизма с прогнозируемым ОД. | gestalt.js |
| **GESTALT_STATUS** | Object | Константы статусов 0–4. | gestalt.js |
| **Info_showGestaltsList** | function | Диалог со списком ID гештальтов; щелчок по ID → `Info_showGestaltDetail`. | informing.js |
| **Info_showGestaltDetail** | function | Детализация одной записи `GestaltArr` (ссылки на цель, ветку, ситуацию, моторные автоматизмы). | informing.js |
| **InfoPicture.fantasmPlotRules** | Array | Сюжет фантазма: снимки кадров ЭП; при переходе target→passive (`Info_fillFantasmPlotOnPassiveMain`) и на каждом шаге `info_fantasizming`. | informing.js |
| **InfoPicture.thinkingSource** | string | `'external'` \| `'imagination'` \| `'fantasy'` — откуда оператору виден текущий ход мысли (стимул / воображение / пассивная фантазия). | informing.js, fantasizming.js |
| **Info_showFantasmPlotDetail** / **Info_showDetail('fantasmPlot')** | function | Детализация сюжета фантазма. | informing.js |
| **Info_fillFantasmPlotOnPassiveMain** / **Info_fillFantasmPlotOnPassiveBackground** | function | Заполнение сюжета при переходе в пассивный режим (заглушка: последние кадры `Episodes`). Развитие сюжета в фоне — через `info_fantasizming`, не через отдельный вызов background-fill. | informing.js |
| **addBlockedReflexToQueue** | function | Поставить заблокированный ОР генетический рефлекс в очередь delayedReflexes (вызов из ОР после setBlockedReflex). Рефлекс привязан к branchId. | genetic_reflexes_engine.js |
| **removeDelayedReflexesForBranch** | function | Удалить из очереди отложенных рефлексов все записи по ветке branchId. Вызывается из stimuls_consciousness при запуске автоматизма — без сравнения действий. | genetic_reflexes_engine.js |
| **runActionImage** | function | Выполнить образ действия (ОД) по ID: actionIds и/или odSequence. Вызывается при срабатывании автоматизма. | genetic_reflexes_engine.js |

---

## Образы действия (_10_actions_image)

| Переменная | Тип | Описание | Где задаётся |
|------------|-----|----------|--------------|
| **ActionImages** | Array | Образы действия: { id, finalImageId, actionIds, odSequence }. | actions_image.js |
| **ActionImages_registerReaction** | function | Зарегистрировать реакцию (finalImageId, actionId) → id ОД. | actions_image.js |
| **ActionImages_getOrCreateActionImageId** | function | Получить или создать ID ОД по finalImageId. | actions_image.js |
| **ActionImages_getActionImage** | function | Получить объект ОД по id. | actions_image.js |
| **ActionImages_serializeState** / **ActionImages_applyState** | function | Сериализация/восстановление. | actions_image.js |

---

## Эпизодическая память (_10_Episodic_memory)

| Переменная | Тип | Описание | Где задаётся |
|------------|-----|----------|--------------|
| **Episodes** | Array | Массив кадров эпизодической памяти. | episodic_memory.js |
| **EpisodicMemory_waitingStartedAt** | number \| null | Время начала периода ожидания в этой сессии (timestamp) или null. Используется для видимости прогресс-бара и для isInWaitingPeriod(); не восстанавливается из IndexedDB. | episodic_memory.js |
| **EpisodicMemory_pulsesWaiting** | number | Текущее число пульсов ожидания (обновляется в EpisodicMemory_onPulse). | episodic_memory.js |
| **EpisodicMemory_barShowCompletePulsesLeft** | number | Оставшиеся пульсы показа 100% прогресс-бара после закрытия кадра с ответом. | episodic_memory.js |
| **EpisodicMemory_testMode** | boolean | Режим тестирования (пошаговые сообщения). | episodic_memory.js |
| **EpisodicMemory_skipConditionalReflexFormation** | boolean | Ставится в true в ОР при закрытии кадра ответным стимулом; в startAction блокирует образование УР; сбрасывается в puls.js после processGeneticReflexes. | orientation_reflex.js, genetic_reflexes_engine.js, puls.js |
| **save_episodic_memory** | function | Создание/закрытие кадра (вызывается из ОР). | episodic_memory.js |
| **EpisodicMemory_setLastFrameActionFromActionId** | function | Записать ответное действие в последний кадр по actionId. | episodic_memory.js |
| **EpisodicMemory_isInWaitingPeriod** | function | Идёт ли период ожидания ответа. | episodic_memory.js |
| **EpisodicMemory_closeLastFrameWithResponse** | function | Закрыть последний кадр с ответным стимулом. | episodic_memory.js |
| **EpisodicMemory_onPulse** | function | Вызывается из puls.js каждый пульс; считает пульсы ожидания и закрывает кадр по таймауту. | episodic_memory.js |
| **EpisodicMemory_response_waiting_period** | number | Длительность периода ожидания в пульсах (не таймер). | episodic_memory.js |
| **EpisodicMemory_serializeState** / **EpisodicMemory_applyState** | function | Сериализация/восстановление ЭП. | episodic_memory.js |
| **EpisodicMemory_clearAll** | function | Очистка всей ЭП. | episodic_memory.js |
| **EpisodicMemory_pickBestPositiveFrameForStimulus** | function | Лучший кадр с effect > 0 и заданным `stimulusImageId` (ур.3 ФО). | episodic_memory.js |
| **EpisodicMemory_getFramesByResponseStimulus** | function | Все кадры, где `responseStimulusImageId` совпадает с аргументом (учительский ответ на стимул). | episodic_memory.js |
| **EpisodicMemory_pickBestFrameByAbsEffect** | function | Лучший кадр по максимуму \|effect\| среди кандидатов с ненулевым `actionImageId`. | episodic_memory.js |

---

## Автоматизмы (_11_Automatizms)

| Переменная | Тип | Описание | Где задаётся |
|------------|-----|----------|--------------|
| **Automatizms** | Array | Массив автоматизмов (id, branchId, usefulness, actionsImageId, count, isDefault). | automatizms.js |
| **Automatizms_getAutomatizmsByBranchId** | function | Все автоматизмы по ID ветки дерева. | automatizms.js |
| **Automatizms_getDefaultAutomatizmForBranch** | function | Штатный автоматизм для ветки (isDefault=true). | automatizms.js |
| **Automatizms_createAutomatizm** | function | Создать автоматизм (без дубля по branchId+actionsImageId); возвращает id или 0. | automatizms.js |
| **Automatizms_setDefaultAutomatizm** | function | Назначить автоматизм штатным для ветки (остальные той же ветки — isDefault=false). | automatizms.js |
| **Automatizms_addUsefulnessSample** | function | Обновить usefulness и count по эффекту. | automatizms.js |
| **Automatizms_serializeState** / **Automatizms_applyState** | function | Сериализация/восстановление для файла и IndexedDB. | automatizms.js |
| **GoalImagesLife** | Array | Образы цели жизненного опыта: { id, perceptionNodeId, stimulusImageId, actionImageId, effect, count, lastUsedAt }. Формируются на втором уровне при работе с эпизодами памяти. | goals.js |
| **Goals_determineGoalInSituation** | function | Определение цели (первый шаг ур.2): база → жизненный опыт по ветке → при значимости стимула >3 и отсутствии цели по ветке — умолчательная цель «значение» (`meaning`). Устанавливает currentGoalType/currentGoalId/currentGoalVitalId. | goals.js |
| **Goals_ensureDefaultMeaningGoal** / **Goals_isDefaultMeaningGoalId** / **Goals_getDefaultMeaningGoalId** | function | Умолчательная цель «что для меня означает это?» (perceptionNodeId=0, situationId=0, actionImageId=-1); проверка id. | goals.js |
| **Goals_getBestLifeGoalForBranch** | function | Лучший образ цели жизненного опыта для ветки (по точному совпадению и родителям), с учётом minCount. | goals.js |
| **Goals_considerNewGoalFromEpisode** | function | Рассмотрение кадра ЭП: при effect > 0 — добавление/обновление записи в GoalImagesLife. | goals.js |
| **getActiveBaseGoalVitalId** | function | Базовая гомеостатическая цель: ID витала для возврата в норму (только при BadNormWell === 1). | goals.js |
| **Goals_serializeState** / **Goals_applyState** | function | Сериализация/восстановление образов целей для файла и IndexedDB. | goals.js |
| **PerceptionTree_getActiveTerminalNodeId** | function | Текущий активный терминальный узел дерева по состоянию виталов и активных стимулов. Используется для привязки автоматизмов и передачи branchId в stimuls_consciousness. | perception_tree.js |

---

## UI и сохранение

| Переменная | Тип | Описание | Где задаётся |
|------------|-----|----------|--------------|
| **show_alert** / **close_alert** | function | Показ/закрытие кастомного алерта. | sys/alerts/alerts.js |
| **show_confirm** / **show_prompt** | function | Подтверждение и ввод. | sys/alerts/alerts.js |
| **showSaveIndicator** / **hideSaveIndicator** | function | Показ/скрытие индикатора записи (синий круг). | index.html (inline) |
| **registerIndexedDBSaver** | function | Регистрация функции сохранения на пульс. Сохранение по мере изменения: каждый модуль использует dirty + подпись (lastSavedSig), чтобы писать в IndexedDB только при реальном изменении данных (ЭП — новый/удалённый кадр, дерево — новый узел, семантика/УР/ОД — изменение массивов). Список модулей — в комментарии в indexeddb.js. | sys/IndexedDB/indexeddb.js |
| **runIndexedDBSavesForPulse** | function | Запуск всех зарегистрированных сохранений раз в пульс (вызывается из sep_of_puls). | sys/IndexedDB/indexeddb.js |
| **IndexedDBModule** | Object | Обёртка openDatabase, get, put, remove. | sys/IndexedDB/indexeddb.js |
| **BEAST** | Object | Объект для общих структур проекта (например, реестр сохраняемых модулей). | date/saving_ui.js |
| **BEAST.persistentModules** | Array | Реестр сохраняемых модулей: массив { key, serialize, apply }. Добавление нового хранилища в файл = одна запись здесь. | date/saving_ui.js |
| **handle_save_memory_click** / **load_all_memory** | function | Сохранение/загрузка памяти в файл. | date/saving_ui.js |
| **was_emergency_shutdown** | boolean | Был ли аварийный выход (до закрытия страницы). | saving_before_closing.js |
| **init_saving_before_closing** / **saving_before_closing** | function | Инициализация и сохранение при закрытии. | saving_before_closing.js |
| **updateStimulusButtonsReadinessView** | function | Обновить UI-бордеры «готовности к ОР» у всех кнопок (без hover). Вызывается из `puls.js` только на 2-м пульсе после старта. | _3_perception/stimuls_ui.js |
| **updateStimulusButtonsReadinessViewForFinalImageId** | function | Точечное обновление «готовности к ОР» для кнопок, относящихся к конкретному `finalImageId`. Вызывается из `semantic_memory.js` после записи нового семантического образца. | _3_perception/stimuls_ui.js, _6_semantic_memory/semantic_memory.js |

---

При добавлении новых глобальных переменных или функций их стоит дописывать в этот файл с указанием модуля и кратким назначением.

════════════════════════════════════════════════════════════

/BEAST/beast/date/saving_all/saving_all.js
════════════════════════════════════════════════════════════
/**
 * Модуль полной выгрузки и загрузки памяти существа в локальный файл
 * по выбору пользователя (File System Access API).
 *
 * Назначение:
 * - По команде из index.html (кнопка «Сохранить память») сохраняет данные
 *   в файл, который пользователь выбирает через диалог (window.showSaveFilePicker).
 * - Позволяет загрузить ранее сохранённые данные из файла, выбранного
 *   пользователем (window.showOpenFilePicker).
 *
 * Требования:
 * - File System Access API полностью поддерживается в Chrome.
 * - В браузерах без поддержки API при попытке сохранения/загрузки
 *   выводится сообщение (show_alert): данный браузер не поддерживает
 *   сохранение памяти, необходимо использовать Chrome.
 *
 * Основные функции:
 * - save_all_memory(data)     — сохранение данных в файл (диалог выбора места).
 * - load_all_memory()         — загрузка данных из выбранного пользователем файла.
 * - handle_save_memory_click(collectDataFn) — обработчик кнопки «Сохранить память».
 *
 * Примеры использования (в index.html):
 *
 *   // Кнопка «Сохранить память»:
 *   document.getElementById('btnSaveMemory').addEventListener('click', function () {
 *     handle_save_memory_click(function collectData() {
 *       return { timestamp: Date.now(), version: 1 };
 *     });
 *   });
 *
 *   // Загрузка сохранённых данных (например, кнопка «Загрузить память»):
 *   load_all_memory()
 *     .then(function (loaded) {
 *       // здесь применить загруженные данные
 *     })
 *     .catch(function (error) {
 *       console.error(error);
 *     });
 */

(function (global) {
  'use strict';

  // Рекомендуемое расширение и описание для диалога сохранения
  var SAVE_DEFAULT_NAME = 'beast_memory.json';
  var SAVE_ACCEPT = { 'application/json': ['.json'] };

  /**
   * Проверка поддержки File System Access API (showSaveFilePicker).
   * @returns {boolean}
   */
  function is_file_system_access_supported() {
    return typeof global.showSaveFilePicker === 'function';
  }

  /**
   * Сообщение пользователю о неподдерживаемом браузере.
   */
  function alert_unsupported_browser() {
    if (typeof global.show_alert === 'function') {
      global.show_alert(
        'Данный браузер не поддерживает сохранение памяти. Используйте Chrome.',
        0
      );
    }
  }

  /**
   * Сохранение данных в файл по выбору пользователя (File System Access API).
   * Использует последнюю выбранную папку (startIn), если она сохранена в IndexedDB.
   *
   * @param {Object} data - объект с памятью существа (будет сериализован в JSON).
   * @returns {Promise<void>} - выполняется после успешной записи.
   */
  function save_all_memory(data) {
    if (!is_file_system_access_supported()) {
      alert_unsupported_browser();
      return Promise.reject(new Error('File System Access API не поддерживается.'));
    }
    if (typeof global.showSaveIndicator === 'function') global.showSaveIndicator();

    var payloadString;
    try {
      payloadString = JSON.stringify(data || {}, null, 2);
    } catch (e) {
      if (typeof global.hideSaveIndicator === 'function') global.hideSaveIndicator();
      return Promise.reject(new Error('Не удалось сериализовать данные: ' + e.message));
    }

    var pickerOptions = {
      suggestedName: SAVE_DEFAULT_NAME,
      types: [{ description: 'JSON', accept: SAVE_ACCEPT }]
    };

    var startInPromise = typeof global.loadLastMemoryDirectory === 'function'
      ? global.loadLastMemoryDirectory()
      : Promise.resolve(undefined);

    return startInPromise
      .then(function (startIn) {
        if (startIn) pickerOptions.startIn = startIn;
        return global.showSaveFilePicker(pickerOptions);
      })
      .then(function (handle) {
        return handle.createWritable().then(function (writable) {
          return writable.write(payloadString)
            .then(function () {
              return writable.close();
            })
            .then(function () {
              // Сохраняем сам файл-хэндл как "последнее место".
              if (typeof global.saveLastMemoryDirectory === 'function') {
                return global.saveLastMemoryDirectory(handle);
              }
            });
        });
      })
      .then(function () {
        if (typeof global.hideSaveIndicator === 'function') global.hideSaveIndicator();
      }, function (err) {
        if (typeof global.hideSaveIndicator === 'function') global.hideSaveIndicator();
        throw err;
      });
  }

  /**
   * Загрузка данных из файла, выбранного пользователем (File System Access API).
   * Открывает диалог в последней выбранной папке (startIn), если она сохранена.
   *
   * @returns {Promise<Object>} - объект с загруженными данными (распарсенный JSON).
   */
  function load_all_memory() {
    if (!is_file_system_access_supported()) {
      alert_unsupported_browser();
      return Promise.reject(new Error('File System Access API не поддерживается.'));
    }

    var pickerOptions = {
      types: [{ description: 'JSON', accept: SAVE_ACCEPT }],
      multiple: false
    };

    var startInPromise = typeof global.loadLastMemoryDirectory === 'function'
      ? global.loadLastMemoryDirectory()
      : Promise.resolve(undefined);

    return startInPromise
      .then(function (startIn) {
        if (startIn) pickerOptions.startIn = startIn;
        return global.showOpenFilePicker(pickerOptions);
      })
      .then(function (handles) {
        if (!handles || handles.length === 0) {
          return Promise.reject(new Error('Файл не выбран.'));
        }
        var fileHandle = handles[0];
        if (typeof global.saveLastMemoryDirectory === 'function') {
          global.saveLastMemoryDirectory(fileHandle).catch(function () {});
        }
        return fileHandle.getFile();
      })
      .then(function (file) {
        return file.text();
      })
      .then(function (text) {
        try {
          return JSON.parse(text);
        } catch (e) {
          throw new Error('Файл не является корректным JSON: ' + e.message);
        }
      });
  }

  /**
   * Обработчик нажатия на кнопку «Сохранить память».
   *
   * @param {function(): Object} collectDataFn - функция, возвращающая объект для сохранения.
   * @returns {Promise<void>}
   */
  function handle_save_memory_click(collectDataFn) {
    if (!is_file_system_access_supported()) {
      alert_unsupported_browser();
      return Promise.reject(new Error('File System Access API не поддерживается.'));
    }

    var data;
    try {
      data = collectDataFn ? collectDataFn() : {};
    } catch (e) {
      if (typeof global.show_alert === 'function') {
        global.show_alert('Ошибка при сборе данных: ' + e.message, 0);
      }
      return Promise.reject(e);
    }

    if (typeof global.show_alert === 'function') {
      global.show_alert('Выберите место для сохранения файла памяти…', 2000);
    }

    return save_all_memory(data)
      .then(function () {
        if (typeof global.show_alert === 'function') {
          global.show_alert('Память успешно сохранена в файл.', 2000);
        }
      })
      .catch(function (error) {
        // Пользователь отменил выбор файла — не показывать как ошибку
        if (error && error.name === 'AbortError') {
          return;
        }
        if (typeof global.show_alert === 'function') {
          global.show_alert('Ошибка при сохранении памяти: ' + (error && error.message ? error.message : String(error)), 0);
        }
        throw error;
      });
  }

  // Экспорт в глобальное пространство имён
  global.save_all_memory = save_all_memory;
  global.load_all_memory = load_all_memory;
  global.handle_save_memory_click = handle_save_memory_click;
  global.is_file_system_access_supported = is_file_system_access_supported;

})(window);

════════════════════════════════════════════════════════════

/BEAST/beast/date/saving_before_closing/saving_before_closing.js
════════════════════════════════════════════════════════════
/**
 * Модуль сохранения активных данных перед закрытием окна index.html.
 *
 * Назначение:
 * - Устанавливает и снимает флаг аварийного завершения работы was_emergency_shutdown.
 * - Предоставляет заготовку функции saving_before_closing(), в которую будут
 *   добавляться конкретные операции сохранения данных.
 * - Может использоваться из index.html и других модулей для проверки факта
 *   предыдущего аварийного завершения работы.
 *
 * Основные понятия:
 * - was_emergency_shutdown == true  — предыдущее завершение было аварийным
 *   (браузер/компьютер завис, вкладку "убили", saving_before_closing() не успела отработать).
 * - was_emergency_shutdown == false — предыдущее завершение было штатным
 *   с выполнением saving_before_closing() и сбросом флага.
 *
 * Хранение флага:
 * - Флаг представлен глобальной переменной проекта was_emergency_shutdown.
 * - Для сохранения значения между перезапусками вкладки он дополнительно
 *   дублируется в localStorage (НЕ в IndexedDB).
 *
 * Примеры использования:
 *
 *   // Инициализация обработки закрытия окна:
 *   init_saving_before_closing();
 *
 *   // Проверка на старте index.html:
 *   if (get_was_emergency_shutdown()) {
 *     // Выполнить восстановление / диагностику
 *   }
 *
 * ВНИМАНИЕ:
 * - Функция saving_before_closing() сейчас пустая и предназначена для
 *   дальнейшего наполнения логикой сохранения активных данных.
 */

(function (global) {
  'use strict';

  // Ключ для хранения флага в localStorage (для восстановления после перезапуска)
  var EMERGENCY_FLAG_KEY = 'was_emergency_shutdown';

  // Глобальная переменная проекта, отражающая текущее значение флага.
  // Инициализируется из localStorage, если там есть сохранённое значение.
  var was_emergency_shutdown = false;
  try {
    was_emergency_shutdown = global.localStorage.getItem(EMERGENCY_FLAG_KEY) === 'true';
  } catch (e) {
    was_emergency_shutdown = false;
  }
  global.was_emergency_shutdown = was_emergency_shutdown;

  /**
   * Установить флаг аварийного завершения в true.
   * Вызывается при инициализации работы системы.
   */
  function set_emergency_flag_true() {
    was_emergency_shutdown = true;
    global.was_emergency_shutdown = true;
    try {
      global.localStorage.setItem(EMERGENCY_FLAG_KEY, 'true');
    } catch (e) {
      // Игнорируем ошибки доступа к localStorage
    }
  }

  /**
   * Установить флаг аварийного завершения в false (штатное завершение).
   * Вызывается при успешном выполнении saving_before_closing().
   */
  function set_emergency_flag_false() {
    was_emergency_shutdown = false;
    global.was_emergency_shutdown = false;
    try {
      global.localStorage.setItem(EMERGENCY_FLAG_KEY, 'false');
    } catch (e) {
      // Игнорируем ошибки доступа к localStorage
    }
  }

  /**
   * Получить текущее значение флага was_emergency_shutdown.
   * @returns {boolean}
   */
  function get_was_emergency_shutdown() {
    return !!was_emergency_shutdown;
  }

  /**
   * Заготовка функции сохранения активных данных перед закрытием окна.
   *
   * ВАЖНО:
   * - В эту функцию будут добавляться конкретные вызовы сохранения
   *   (запись в IndexedDB, отправка логов и т.п.).
   * - После успешного сохранения функция должна вызвать
   *   set_emergency_flag_false(), чтобы отметить штатное завершение.
   */
  function saving_before_closing() {
    // TODO: здесь будут размещены вызовы функций сохранения активных данных.
    // Пример будущей логики:
    //   save_all_active_data();
    //   set_emergency_flag_false();
  }

  /**
   * Инициализация логики сохранения перед закрытием окна.
   *
   * Рекомендуется вызывать один раз при старте index.html.
   * Логика:
   * - При инициализации сразу выставляем флаг was_emergency_shutdown = true.
   * - При штатном закрытии (beforeunload) вызываем saving_before_closing()
   *   и сбрасываем флаг в false.
   * - Если браузер/система "падает" до вызова обработчика, то флаг
   *   остаётся true и при следующем запуске можно обнаружить аварийное
   *   завершение.
   */
  function init_saving_before_closing() {
    // Отмечаем, что по умолчанию будем считать завершение аварийным,
    // пока не сработает saving_before_closing().
    set_emergency_flag_true();

    // Обработчик закрытия/перезагрузки вкладки/окна
    global.addEventListener('beforeunload', function () {
      try {
        saving_before_closing();
        set_emergency_flag_false();
      } catch (e) {
        // В случае ошибки сохранения флаг аварийного завершения
        // останется true, что соответствует постановке задачи.
      }
      // Возвращать текст из обработчика не нужно (чтобы не показывать
      // стандартное системное предупреждение браузера).
    });
  }

  // Экспортируем функции в глобальное пространство имён
  global.init_saving_before_closing = init_saving_before_closing;
  global.saving_before_closing = saving_before_closing;
  global.get_was_emergency_shutdown = get_was_emergency_shutdown;

})(window);


════════════════════════════════════════════════════════════

/BEAST/beast/date/saving_ui.js
════════════════════════════════════════════════════════════
/**
 * Привязка кнопок сохранения/загрузки памяти на главной странице index.html.
 *
 * Назначение:
 * - Подключает обработчики к кнопкам "Сохранить память" и "Загрузить память".
 * - Сбор и применение данных идут через единый реестр BEAST.persistentModules:
 *   один массив пар «ключ в JSON ↔ serialize/apply», добавление нового модуля = одна запись.
 *
 * Порядок и DOM: скрипты подключаются в конце body; при переносе в head нужно
 * вызывать init только после готовности DOM (DOMContentLoaded или аналог).
 *
 * Зависимости (должны быть подключены до этого скрипта):
 * - beast/date/saving_all/saving_all.js (handle_save_memory_click, load_all_memory)
 * - beast/puls.js (Cur_puls_val, update_beast_pulse_view)
 *
 * Ожидаемые id в index.html: btnSaveMemory, btnLoadMemory.
 */

(function (global) {
  'use strict';

  var PULSE_KEY = 'Cur_puls_val';

  if (!global.BEAST) global.BEAST = {};
  /**
   * Реестр сохраняемых модулей. Каждый элемент: { key, serialize, apply }.
   * key — поле в JSON при сохранении в файл.
   * serialize() — возвращает значение для сохранения (или undefined, тогда ключ не пишется).
   * apply(data) — применяет загруженное значение; вызывается только если в loaded есть этот ключ.
   * Добавление нового хранилища = одна запись здесь, без правок collect_memory_data/apply_loaded_memory.
   */
  var persistentModules = [];
  global.BEAST.persistentModules = persistentModules;

  persistentModules.push({
    key: 'Cur_puls_val',
    serialize: function () {
      return global.Cur_puls_val;
    },
    apply: function (value) {
      var n = parseInt(value, 10);
      if (isNaN(n) || n < 0) return;
      global.Cur_puls_val = n;
      try {
        global.localStorage.setItem(PULSE_KEY, String(global.Cur_puls_val));
      } catch (e) {}
      if (typeof global.update_beast_pulse_view === 'function') {
        global.update_beast_pulse_view();
      }
    }
  });

  persistentModules.push({
    key: 'VitalsValues',
    serialize: function () {
      if (!global.VitalsArr || !global.VitalsArr.length) return undefined;
      var values = [];
      for (var i = 0; i < global.VitalsArr.length; i++) {
        values.push(global.VitalsArr[i].value);
      }
      return values;
    },
    apply: function (values) {
      if (!global.VitalsArr || !Array.isArray(values)) return;
      for (var i = 0; i < global.VitalsArr.length && i < values.length; i++) {
        var v = parseFloat(values[i]);
        if (isNaN(v)) continue;
        if (v < 0) v = 0;
        if (v > 100) v = 100;
        global.VitalsArr[i].value = v;
      }
      if (typeof global.saveVitalsToStorage === 'function') {
        global.saveVitalsToStorage();
      }
      if (typeof global.update_vitals_view === 'function') {
        global.update_vitals_view();
      }
    }
  });

  persistentModules.push({
    key: 'SemanticState',
    serialize: function () {
      if (typeof global.serializeSemanticState !== 'function') return undefined;
      return global.serializeSemanticState();
    },
    apply: function (data) {
      if (typeof global.applySemanticState !== 'function') return;
      global.applySemanticState(data);
    }
  });

  persistentModules.push({
    key: 'ConditionalReflexesState',
    serialize: function () {
      if (typeof global.serializeConditionalReflexesState !== 'function') return undefined;
      return global.serializeConditionalReflexesState();
    },
    apply: function (data) {
      if (typeof global.applyConditionalReflexesState !== 'function') return;
      global.applyConditionalReflexesState(data);
    }
  });

  persistentModules.push({
    key: 'PerceptionTreeState',
    serialize: function () {
      if (typeof global.PerceptionTree_serializeState !== 'function') return undefined;
      return global.PerceptionTree_serializeState();
    },
    apply: function (data) {
      if (typeof global.PerceptionTree_applyState !== 'function') return;
      global.PerceptionTree_applyState(data);
    }
  });

  persistentModules.push({
    key: 'ActionImagesState',
    serialize: function () {
      if (typeof global.ActionImages_serializeState !== 'function') return undefined;
      return global.ActionImages_serializeState();
    },
    apply: function (data) {
      if (typeof global.ActionImages_applyState !== 'function') return;
      global.ActionImages_applyState(data);
    }
  });

  persistentModules.push({
    key: 'EpisodesState',
    serialize: function () {
      if (typeof global.EpisodicMemory_serializeState !== 'function') return undefined;
      return global.EpisodicMemory_serializeState();
    },
    apply: function (data) {
      if (typeof global.EpisodicMemory_applyState !== 'function') return;
      global.EpisodicMemory_applyState(data);
    }
  });

  persistentModules.push({
    key: 'AutomatizmsState',
    serialize: function () {
      if (typeof global.Automatizms_serializeState !== 'function') return undefined;
      return global.Automatizms_serializeState();
    },
    apply: function (data) {
      if (typeof global.Automatizms_applyState !== 'function') return;
      global.Automatizms_applyState(data);
    }
  });

  persistentModules.push({
    key: 'GoalImagesState',
    serialize: function () {
      if (typeof global.Goals_serializeState !== 'function') return undefined;
      return global.Goals_serializeState();
    },
    apply: function (data) {
      if (typeof global.Goals_applyState !== 'function') return;
      global.Goals_applyState(data);
    }
  });

  persistentModules.push({
    key: 'SituationsTreeState',
    serialize: function () {
      if (typeof global.SituationsTree_serializeState !== 'function') return undefined;
      return global.SituationsTree_serializeState();
    },
    apply: function (data) {
      if (typeof global.SituationsTree_applyState !== 'function') return;
      global.SituationsTree_applyState(data);
    }
  });

  persistentModules.push({
    key: 'MentalAutomatizmsState',
    serialize: function () {
      if (typeof global.MentalAutomatizms_serializeState !== 'function') return undefined;
      return global.MentalAutomatizms_serializeState();
    },
    apply: function (data) {
      if (typeof global.MentalAutomatizms_applyState !== 'function') return;
      global.MentalAutomatizms_applyState(data);
    }
  });

  persistentModules.push({
    key: 'AbstractImagesState',
    serialize: function () {
      if (typeof global.AbstractImages_serializeState !== 'function') return undefined;
      return global.AbstractImages_serializeState();
    },
    apply: function (data) {
      if (typeof global.AbstractImages_applyState !== 'function') return;
      global.AbstractImages_applyState(data);
    }
  });

  /**
   * Собрать объект данных для сохранения в файл.
   * Проход по реестру persistentModules, вызов serialize() у каждого модуля.
   */
  function collect_memory_data() {
    var data = {};
    var list = global.BEAST.persistentModules;
    if (!Array.isArray(list)) return data;
    for (var i = 0; i < list.length; i++) {
      var mod = list[i];
      if (!mod || !mod.key) continue;
      try {
        var value = mod.serialize();
        if (value !== undefined) {
          data[mod.key] = value;
        }
      } catch (e) {
        if (typeof global.console !== 'undefined' && global.console.error) {
          global.console.error('collect_memory_data:', mod.key, e);
        }
      }
    }
    return data;
  }

  /**
   * Применить загруженные из файла данные.
   * Проход по реестру persistentModules, вызов apply(loaded[key]) при наличии ключа.
   * Ошибки не скрываются: каждая исключение логируется в консоль, ключ попадает в массив ошибок.
   * Возвращает массив ключей, по которым apply выбросил ошибку (для сообщения пользователю).
   */
  function apply_loaded_memory(loaded) {
    var errors = [];
    if (!loaded || typeof loaded !== 'object') return errors;
    var list = global.BEAST.persistentModules;
    if (!Array.isArray(list)) return errors;
    for (var i = 0; i < list.length; i++) {
      var mod = list[i];
      if (!mod || !mod.key || loaded[mod.key] === undefined) continue;
      if (typeof mod.apply !== 'function') continue;
      try {
        mod.apply(loaded[mod.key]);
      } catch (e) {
        if (typeof global.console !== 'undefined' && global.console.error) {
          global.console.error('apply_loaded_memory:', mod.key, e);
        }
        errors.push(mod.key);
      }
    }
    return errors;
  }

  function init() {
    var btnSave = global.document.getElementById('btnSaveMemory');
    var btnLoad = global.document.getElementById('btnLoadMemory');

    if (btnSave && typeof global.handle_save_memory_click === 'function') {
      btnSave.addEventListener('click', function () {
        global.handle_save_memory_click(collect_memory_data);
      });
    }

    if (btnLoad && typeof global.load_all_memory === 'function') {
      btnLoad.addEventListener('click', function () {
        global.load_all_memory()
          .then(function (loaded) {
            var errors = apply_loaded_memory(loaded);
            if (typeof global.show_alert === 'function') {
              if (errors.length) {
                global.show_alert('Память загружена с ошибками в части данных: ' + errors.join(', ') + '. Проверьте консоль.', 0);
              } else {
                global.show_alert('Память успешно загружена из файла.', 2000);
              }
            }
          })
          .catch(function (error) {
            if (error && error.name === 'AbortError') return;
            if (typeof global.show_alert === 'function') {
              global.show_alert('Ошибка при загрузке памяти: ' + (error && error.message ? error.message : String(error)), 0);
            }
          });
      });
    }
  }

  if (global.document.readyState === 'loading') {
    global.document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})(window);

════════════════════════════════════════════════════════════

/BEAST/beast/docs/analysis_adaptivity_system.md
════════════════════════════════════════════════════════════
# Анализ системы адаптивности: УР — ОР — СЗ — ЭП

Оценка взаимной функциональности, непротиворечивости, логичности и эффективности цепочки: **условные рефлексы (УР)** — **ориентировочный рефлекс (ОР)** — **семантическая память (СЗ)** — **эпизодическая память (ЭП)**.

---

## 1. Взаимная функциональность

### 1.1 Поток данных

- **Стимул (клик)** → обновление `Active*Stimuls` → `setTimeout(ОР, 0)` → в следующем тике вызывается ОР.
- **ОР** использует: дерево восприятия (путь), СЗ (значимость/новизна), порог `OR_well_known`; при прохождении условий — блокировка рефлекса на 2 пульса, гиппокамп, осознание, **регистрация в СЗ** (один раз за активацию), **создание/закрытие кадра ЭП**.
- **Пульс** → `processGeneticReflexes`: сначала `tryRunSynonymReflexes` (УР), при совпадении — `runGeneticActionFromSynonymReflex` (с проверкой блокировки ОР); иначе генетические карты; при блокировке ОР рефлекс уходит в `delayedReflexes` с `noConditionalReflex: true`.
- **СЗ** ключи: `(BadNormWell, emotionId, finalImageId)`. Регистрация только из ОР — один вызов `registerSemanticSample` на активацию, двойной учёт исключён.
- **ЭП**: кадр создаётся в ОР при каждом срабатывании ОР (после прохождения OR_well_known); ответный стимул в период ожидания не проверяется на порог известности; период ожидания только при `waitingStartedAt != null` (загруженный незакрытый кадр не даёт «в ожидании»); прогресс-бар виден только при активном ожидании в этой сессии; деактивация стимулов — без сообщения. Действие в кадр пишется из `startAction` через `EpisodicMemory_setLastFrameActionFromActionId`; закрытие по ответному стимулу — при активации в период ожидания (проверка isInWaitingPeriod() до проверки OR_well_known); по таймауту — в `EpisodicMemory_onPulse` по пульсам.

**Итог:** Роли разделены: ОР решает «что сейчас важнее» и запускает СЗ/ЭП; УР и генетические рефлексы исполняются на пульсе с учётом блокировки ОР; СЗ и ЭП не дублируют друг друга (СЗ — значимость образа, ЭП — последовательность правил).

### 1.2 Согласование контекста

- **УР:** контекст = `getCurrentContextKey()` → строка `"well:id1,id2,..."` по `BadNormWell` и ID из `BasicContextsActived`.
- **ОР, СЗ, ЭП:** контекст = `BadNormWell` + `Emotions_getOrCreateEmotionId(BasicContextsActived)` (тот же набор контекстов, другой вид — числовой id).
- Источник один и тот же (`BadNormWell`, `BasicContextsActived`), представления согласованы.

---

## 2. Непротиворечивость

### 2.1 Что согласовано

- **Блокировка по FinalImageId:** в ОР блок ставится по `winnerId` (FinalImageId); в `processGeneticReflexes` проверка по `fullFinalImageId`; в `runGeneticActionFromSynonymReflex` передаётся `finalImageId` — ключ один и тот же, блокировка для генетического и условного рефлекса согласована.
- **Один учёт семантики:** регистрация только в ОР; в `startAction` вызов `registerSemanticExperience` убран (заглушка), повторного увеличения `samplesCount` нет.
- **УР не создаётся при ОР:** при отложенном рефлексе в `delayedReflexes` передаётся `noConditionalReflex: true`, в `startAction` не вызывается `synonyms_onGeneticActionFired` — условный рефлекс по стимулу, вызвавшему ОР, не формируется.
- **Период ожидания ЭП:** во время него новый стимул только закрывает текущий кадр с ответом, новый кадр и новый ОР не создаются; счётчик пульсов и один обработчик `EpisodicMemory_onPulse` исключают гонки таймеров.

### 2.2 Возможные расхождения

- **Порог OR_well_known (10):** при `samplesCount < 10` ОР и кадр ЭП не создаются, но семантика уже регистрируется (вызов `registerSemanticSample` до проверки). Логика «сначала учёт, потом решение» выдержана, но сам порог 10 — эвристика; при ином значении поведение изменится (больше/меньше кадров ЭП и срабатываний ОР).
- **ЭП:** кадр и прогресс-бар создаются при каждом срабатывании ОР (после прохождения порога известности). Ответный стимул в период ожидания не проверяется на OR_well_known. Подробности — в about_episodic_memory.md и about_episodic_memory_rules.md.

---

## 3. Логичность

### 3.1 Последовательность решений

1. Активация стимула → ОР решает, есть ли «достаточно новое/значимое» и не идёт ли уже период ожидания ЭП.
2. Если период ожидания — закрытие кадра с ответным стимулом, без нового кадра и без нового ОР.
3. Если нет — регистрация в СЗ, при выполнении порога новизны/значимости — блокировка рефлекса, гиппокамп, осознание, при необходимости — создание кадра ЭП.
4. На пульсе: при блокировке ОР рефлекс откладывается без формирования УР; при срабатывании (сразу или после задержки) действие попадает в ОД и в последний кадр ЭП.

Цепочка «стимул → внимание (ОР) → память (СЗ/ЭП) → действие (ген/УР)» выдержана.

### 3.2 Смысловое разделение

- **СЗ:** «насколько значим этот образ в этих условиях» (well, emotionId, finalImageId) — усреднённая оценка, растёт с числом экспозиций.
- **ЭП:** «что за чем следовало и какой эффект» — правила вида стимул → действие → эффект/ответ, порядок важен.
- **УР:** «в этом контексте этот образ ведёт к этому действию» — быстрое срабатывание без пересчёта значимости.
- **ОР:** выбор «главного» стимула по новизне и значимости, порог «достаточно известного» отсекает шум.

Роли не пересекаются по смыслу.

---

## 4. Эффективность

### 4.1 Что сделано хорошо

- Поиск в СЗ по индексу `SemanticMemoryIndex[well][emotionId][finalImageId]` — O(1).
- Сбор кадров СЗ для одного `finalImageId` через обход индекса, без полного скана `SemanticMemory`.
- Один обработчик ожидания ЭП по пульсам, без нескольких таймеров.
- Регистрация семантики в одном месте (ОР), без дублирования.

### 4.2 Узкие места и затраты

- **ОР при каждом клике:** полный пересчёт актуальности по всем кандидатам и путям дерева. При большом числе активных стимулов и глубоком дереве стоимость растёт, но для текущего масштаба приемлемо.
- **УР:** линейный перебор `SynonymReflexes` при каждом пульсе. При росте числа условных рефлексов можно ввести индекс по контексту и/или по множеству стимулов (например, по ключу `activeIdsKey`).
- **ЭП:** массив кадров, добавление/закрытие в конец O(1); при очень длинной истории возможна пагинация или ограничение длины при сериализации.

---

## 5. Замечания и предложения

### 5.1 Документация и явность

- **Рекомендация:** В одном месте (например, `about_global_variables.md` или отдельный `docs/adaptivity_flow.md`) кратко описать порядок: клик → ОР (семантика, блокировка, ЭП) → пульс (УР, генрефлексы, отложенные с `noConditionalReflex`). Это упростит онбординг и отладку.
- **Рекомендация:** В коде ОР явно пометить комментарием, что `registerSemanticSample` вызывается осознанно один раз на активацию и не должен дублироваться в других модулях.

### 5.2 Пороги и константы

- **OR_well_known = 10**, **OR_HYSTERESIS**, **OR_BLOCK_PULSES = 2**, **response_waiting_period = 15** (пульсов) — вынесены, но разбросаны по файлам. **Предложение:** вынести в один конфиг (например, `beast/config/adaptivity.js` или секция в начале `orientation_reflex.js` и `episodic_memory.js`) с короткими комментариями, чтобы проще подбирать при калибровке.
- **Предложение:** Если позже появятся «режимы обучения» (агрессивный/экономный ЭП), условие создания кадра ЭП (`!hasSignificance || testMode`) оформить как функцию или флаг, чтобы не размазывать логику по условиям.

### 5.3 Контекст УР и эмоция

- УР использует строковый ключ контекста, СЗ/ОР — числовой `emotionId`. При смене реализации эмоций (объединение, нормализация) нужно синхронно проверить `getCurrentContextKey` и `Emotions_getOrCreateEmotionId`, чтобы одно и то же состояние окружения давало согласованные ключи. **Рекомендация:** при изменении формата эмоций пройтись по УР, ОР и СЗ в одном ревью.

### 5.4 ЭП и «известные» стимулы

- Сейчас кадр ЭП не создаётся при наличии значимости в СЗ (в не‑тестовом режиме). Если потребуется строить эпизоды и для известных стимулов (например, для анализа последовательностей), нужно будет расширить условие `runEpisodic` без нарушения однократной регистрации в СЗ.

### 5.5 Масштабирование УР

- При большом числе условных рефлексов поиск по массиву при каждом пульсе может стать заметным. **Предложение:** индекс по `contextKey` и/или по хэшу/ключу множества стимулов (`activeIdsKey`) для быстрого отбора кандидатов, с сохранением текущей семантики (контекст + множество стимулов).

### 5.6 Тестовый режим ЭП

- Режим `EpisodicMemory_testMode` даёт пошаговые сообщения и создаёт кадры даже при наличии значимости — полезно для отладки. Имеет смысл в документации явно указать, что в тестовом режиме поведение ЭП намеренно «расширено» (больше кадров и сообщений), чтобы не путать с продакшеном.

---

## 6. Краткий вывод

- **Взаимная функциональность:** ОР, СЗ, ЭП и УР согласованы по потоку (клик → ОР → семантика/блокировка/эпизод; пульс → УР/ген с учётом блокировки и без двойного учёта СЗ).
- **Непротиворечивость:** блокировка по одному ключу (FinalImageId), один источник регистрации СЗ, период ожидания ЭП не создаёт новых кадров/ОР; мелкие нюансы — порог OR_well_known и политика «ЭП только при отсутствии значимости».
- **Логичность:** разделение ролей (внимание, значимость, последовательность, условное действие) и порядок принятия решений выдержаны.
- **Эффективность:** СЗ и ожидание ЭП оптимизированы; при росте числа УР имеет смысл ввести индекс по контексту/стимулам.

Система адаптивности в текущем виде согласована и пригодна для развития; предложения выше направлены на упрощение настройки, документирования и масштабирования.

════════════════════════════════════════════════════════════

/BEAST/beast/docs/design_conscious_automatisms.html
════════════════════════════════════════════════════════════
<!DOCTYPE html>
                <html lang="en">
                <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>Markdown Export</title>
                <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.3.0/github-markdown.min.css">
                <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
                <style>
                    body {
                        background-color: #ffffff;
                        color: #24292e;
                    }
                    .markdown-body {
                        box-sizing: border-box;
                        min-width: 200px;
                        max-width: 980px;
                        margin: 0 auto;
                        padding: 45px;
                        background-color: #ffffff;
                        color: #24292e;
                    }
                    @media (max-width: 767px) {
                        .markdown-body {
                            padding: 15px;
                        }
                    }
                </style>
                </head>
                <body>
                <article class="markdown-body">
                    <h1>Осознанные автоматизмы и процесс осознания — стратегия реализации</h1>
<p>Документ для подготовки следующего этапа: система осознанных автоматизмов и процесс осознания. Старая реализация была на языке ГО (OLD_CODE); в новой JS-реализации уже есть качественно более сильные адаптивные системы — важно не тащить в новую реализацию устаревшие паттерны, но сохранить понятийный каркас.</p>
<hr>
<h2>0. Назначение проекта и границы</h2>
<p><strong>Назначение:</strong> проект — не «работающее существо и его воспитание», а <strong>наиболее адекватный способ реализации теории</strong> индивидуальной адаптивности. Как теории формализуются математически, так и теория адаптивности лучше всего описывается программной реализацией — это аналог математики в физике, но более эффективный для описания <strong>схемотехники систем адаптивности</strong>.</p>
<p><strong>Вне рамок новой реализации:</strong></p>
<ul>
<li><strong>Ввод слов и фраз оператором</strong> и <strong>вербальные деревья</strong> (распознавание фраз, слова, символы, тон и настроение сообщения). Их добавление сильно усложняет проект и его понимание; для формализации теории достаточно слоёв: гомеостаз, контексты, стимулы (кнопки), рефлексы, дерево условий восприятия (по активности стимулов), эпизодическая и семантическая память, осознание и автоматизмы.</li>
<li>Дерево восприятия сохраняет <strong>6 уровней</strong> структуры (BaseID, EmotionID, ActivityID, ToneMoodID, SimbolID, PhraseID) для совместимости с теорией и сопоставлением с OLD_CODE, но <strong>уровни 4–6 не заполняются</strong> вербальным вводом — остаются 0 или могут использоваться позже для иных невербальных измерений.</li>
</ul>
<hr>
<h2>1. Где мы сейчас (JS)</h2>
<p>Уже есть зацепки под осознание и автоматизмы:</p>
<table>
<thead>
<tr>
<th>Компонент</th>
<th>Назначение</th>
</tr>
</thead>
<tbody><tr>
<td><strong>ОР</strong></td>
<td>Выбор наиболее актуального стимула, блокировка рефлекса на 2 пульса, вызов гиппокампа, <strong>stimuls_consciousness(actual_stimul_ID)</strong>, сохранение ЭП.</td>
</tr>
<tr>
<td><strong>Гиппокамп</strong></td>
<td><code>active_stimulsArr_ID</code>, <code>actual_stimul_ID</code> — удержание стимула, вызвавшего ОР.</td>
</tr>
<tr>
<td><strong>_9_Conscience</strong></td>
<td><strong>stimuls_consciousness(actual_stimul_ID)</strong> — заглушка; вызывается из ОР при новом победителе и при ответном стимуле в период ожидания ЭП. Сюда естественно подвешивать «процесс осознания».</td>
</tr>
<tr>
<td><strong>Дерево восприятия</strong></td>
<td>6 уровней, терминальный узел = условия; привязка рефлексов/автоматизмов к ветке уже заложена в описании.</td>
</tr>
<tr>
<td><strong>ЭП</strong></td>
<td>Кадр: условия (perceptionNodeId), стимул, действие, эффект, ответный стимул — готовый контекст для осмысления.</td>
</tr>
<tr>
<td><strong>ОД</strong></td>
<td>Образы действия; связь стимул → действие уже есть.</td>
</tr>
<tr>
<td><strong>УР</strong></td>
<td>Условные рефлексы по контексту (BadNormWell + эмоция) и FinalImageId.</td>
</tr>
</tbody></table>
<p>То есть: <strong>канал в «кору»</strong> уже есть (вызов <code>stimuls_consciousness</code>), <strong>контекст</strong> можно собирать из дерева, ЭП и ОД. Не хватает: явной модели «осознанный автоматизм» и цикла «процесс осознания» (что именно обрабатывается в этом канале).</p>
<hr>
<h2>2. Рекомендация: опора на новый стек, старый код — только концепт</h2>
<ul>
<li><strong>Не переносить старый код из OLD_CODE в JS целиком.</strong> Язык, структуры данных и потоки там другие; уже реализованные вещи (пульс, ОР, ЭП по пульсам, дерево, СЗ, УР с глобальными ID и т.д.) в старой версии могут отсутствовать или работать иначе. Перенос «как было» рискован и создаст трение.</li>
<li><strong>Использовать OLD_CODE как справочник понятий и сценариев:</strong> что там называлось «осознанный автоматизм», что входило в «процесс осознания», в каком порядке что вызывалось. Термины и последовательность шагов можно выписать и спроецировать на текущую архитектуру.</li>
<li><strong>Собирать новую логику поверх уже сделанного:</strong> дерево восприятия (терминальный узел = условия), ЭП (кадр с условиями, стимулом, действием, эффектом, ответом), ОД, гиппокамп, ОР. Осознанные автоматизмы тогда — следующий слой над условными рефлексами и деревом (например, привязка к узлу дерева + учёт осознания). Процесс осознания — то, что выполняется внутри или вокруг <code>stimuls_consciousness</code> (что именно «проходит» по каналу внимания и как используется).</li>
</ul>
<p>Если папка OLD_CODE доступна (рядом с проектом или отдельный репозиторий), полезно:</p>
<ul>
<li>выписать оттуда ключевые сущности (структуры автоматизма, шаги осознания);</li>
<li>сопоставить их с текущими: perceptionNodeId, stimulusImageId, actionImageId, Episodes, FinalImage, дерево;</li>
<li>зафиксировать расхождения и принять решения уже в рамках JS-архитектуры.</li>
</ul>
<hr>
<h2>3. Возможная форма «осознанного автоматизма» в новом стеке</h2>
<p>Без опоры на старый код можно задать черновой формат:</p>
<ul>
<li><strong>Осознанный автоматизм</strong> — правило вида: при условиях <strong>C</strong> (ветка дерева восприятия, т.е. perceptionNodeId или путь) и образе стимула <strong>S</strong> (FinalImageId) выполнять образ действия <strong>A</strong> (ОД), с возможностью «прогона» через осознание (т.е. решение может быть подтверждено/скорректировано процессом осознания).</li>
<li>Отличие от условного рефлекса: привязка к <strong>узлу дерева</strong> (условия развёрнуты по уровням), возможно участие <strong>эпизодического контекста</strong> (последние кадры ЭП) и явный шаг <strong>осознания</strong> перед/после действия.</li>
</ul>
<p>Тогда:</p>
<ul>
<li>входы: текущий терминальный узел дерева, actual_stimul_ID (и при необходимости последние кадры ЭП);</li>
<li>выходы: выбор/подтверждение действия (ОД), возможно обновление памяти (ЭП/СЗ) по результату осознания.</li>
</ul>
<hr>
<h2>4. Возможная форма «процесса осознания»</h2>
<ul>
<li><strong>Процесс осознания</strong> — то, что происходит при вызове <code>stimuls_consciousness(actual_stimul_ID)</code> и, при необходимости, в ближайшие пульсы: что именно «осознаётся» и как это влияет на поведение и память.</li>
<li>В текущем коде в <code>stimuls_consciousness</code> уже можно передавать не только <code>actual_stimul_ID</code>, но и пакет контекста: терминальный узел дерева, последний/текущий кадр ЭП (если есть), флаг «ответный стимул в период ожидания» и т.д. Тогда «процесс осознания» — это функция, которая принимает этот пакет и решает, что делать дальше (например, запуск осознанного автоматизма, обновление приоритетов, запись в память).</li>
</ul>
<p>Следующий шаг реализации можно разбить на этапы, например:</p>
<ol>
<li>Расширить вызов <code>stimuls_consciousness</code> контекстом (дерево, ЭП, ОР/ЭП-флаги).</li>
<li>Описать структуру данных «осознанный автоматизм» и место хранения (массив + персистенция, по аналогии с УР/ЭП).</li>
<li>Ввести цикл осознания: по контексту выбрать кандидатов-автоматизмов, при необходимости прогнать через «осознание» и выполнить выбранное действие (через существующие эффекторы/ОД).</li>
</ol>
<hr>
<h2>5. Итог</h2>
<ul>
<li><strong>Старый код (OLD_CODE):</strong> использовать только как источник терминов и идей; не копировать реализацию.</li>
<li><strong>Новая реализация:</strong> строить осознанные автоматизмы и процесс осознания поверх текущего JS-стека (пульс, ОР, дерево, ЭП, ОД, СЗ, УР, гиппокамп, <code>stimuls_consciousness</code>).</li>
<li><strong>Следующий шаг:</strong> при наличии доступа к OLD_CODE — выписать оттуда понятия и потоки и сопоставить с этим документом; затем зафиксировать контракт <code>stimuls_consciousness</code> (входной контекст) и формат осознанного автоматизма и приступить к реализации по этапам выше.</li>
</ul>
<hr>
<h2>6. Сопоставление с OLD_CODE (GO) — после загрузки старой реализации</h2>
<p>Папка <strong>OLD_CODE</strong> содержит старую реализацию (GO + PULT). Ниже — выжимка понятий и потоков для переноса идей в JS, без копирования кода.</p>
<h3>6.1 Дерево автоматизмов (GO)</h3>
<ul>
<li><strong>Файлы:</strong> <code>OLD_CODE/GO/brain/psychic/automatism_tree.go</code>, <code>automatizm_functions.go</code>, <code>automatism_tree_func.go</code>.</li>
<li><strong>Структура узла:</strong> 6 уровней — BaseID, EmotionID, ActivityID, ToneMoodID, SimbolID, PhraseID (формат записи совпадает с нашим деревом восприятия). В новой реализации уровни 4–6 (ToneMoodID, SimbolID, PhraseID) <strong>не используются</strong> — вербальный ввод и вербальные деревья в проект не входят (см. §0).</li>
<li><strong>Идея:</strong> К ветке дерева привязаны автоматизмы; при активации ветки выбирается «штатный» автоматизм (Usefulness, Belief). Отсутствие подходящей ветки/автоматизма вызывает ОР и осмысление. Автоматизм с успешностью == 1 блокирует рефлексы (уже «произвольность преодолена»).</li>
<li><strong>В JS:</strong> Дерево восприятия уже есть (<code>_7_perception_tree</code>), формат узла совместим; активная ветка определяется по уровням 1–3 (BaseID, EmotionID, ActivityID). Нет пока привязки «автоматизм к узлу» и выбора действия по узлу — это слой поверх дерева и УР.</li>
</ul>
<h3>6.2 Процесс осознания (GO)</h3>
<ul>
<li><strong>Файлы:</strong> <code>understanding.go</code>, <code>0_main_psychic.go</code>, <code>mental_automatizm_INFO_functions.go</code>, <code>dreaming.go</code>.</li>
<li><strong>Цепочка:</strong> По каждому стимулу — <strong>consciousnessElementary()</strong> (1–2 уровни осознания). Если остаётся проблема — <strong>consciousnessThinking(cID)</strong> в циклах мышления. Циклы типа <strong>cycleInfo</strong>, диспетчер <strong>dispetchConsciousnessThinking()</strong> по пульсу.</li>
<li><strong>consciousnessElementary():</strong><ul>
<li>Определяет «рвущийся» автоматизм по узлу дерева: <code>atmtzmActualTreeNodeID = getAutomatizmFromNodeID(detectedActiveLastNodID)</code>.</li>
<li>Игнорирующий автоматизм приравнивается к отсутствию реакции.</li>
<li>Проверки: опасность, соответствие теме/цели (isStimulToForce), прогноз эффекта, Правила (эпизодические). Решение: запустить штатный автоматизм, альтернативный, или заблокировать и передать в циклы мышления.</li>
</ul>
</li>
<li><strong>consciousnessThinking():</strong> циклы мышления (инфо-функции infoFunc*), доминанта проблемы, пассивное размышление/сновидения. Запуск ментальных и моторных автоматизмов из цикла.</li>
<li><strong>В JS:</strong> Аналог «элементарного» осознания — расширенный <strong>stimuls_consciousness(actual_stimul_ID)</strong>: на вход — actual_stimul_ID + текущий терминальный узел дерева + последний кадр ЭП (и флаг «ответный стимул»). Внутри — выбор: есть ли автоматизм для узла; если да — подтвердить/запустить; если нет — передать в «цикл мышления» (который в JS пока можно заменить простой очередью задач или одним шагом эвристик по ЭП/Правилам).</li>
</ul>
<h3>6.3 Автоматизм в GO</h3>
<ul>
<li><strong>Структура:</strong> привязка к узлу дерева (TreeNodeID), образ действия (ActionsImageID), Usefulness, Belief (штатный/предположительный/заблокированный). Есть моторные и ментальные автоматизмы.</li>
<li><strong>В JS:</strong> Образ действия уже есть (ОД, action_image). «Осознанный автоматизм» в JS можно задать как: perceptionNodeId + (опционально stimulusImageId) → actionImageId, с полями вроде usefulness/belief и приоритетом относительно УР (автоматизм блокирует рефлекс при успешности).</li>
</ul>
<h3>6.4 Что брать из OLD_CODE только как идеи</h3>
<ul>
<li>Последовательность: <strong>пульс → активация дерева → выбор автоматизма по узлу → consciousnessElementary → при необходимости consciousnessThinking</strong>.</li>
<li>Понятия: штатный/игнорирующий/альтернативный автоматизм, период ожидания ответа после действия, «Правила» (эпизодические кадры) для прогноза эффекта и выбора альтернативы.</li>
<li>Не переносить буквально: EvolushnStage, сон/сновидения, полный набор инфо-функций и циклов — реализовать по мере необходимости, опираясь на ЭП, дерево и ОД в JS.</li>
</ul>
<h3>6.5 Рекомендуемый порядок в JS</h3>
<ol>
<li>Расширить <strong>stimuls_consciousness</strong>: передавать контекст (terminalNodeId, lastEpisodeFrame, isResponseStimulus).</li>
<li>Ввести <strong>хранилище осознанных автоматизмов</strong>: привязка к perceptionNodeId (и опционально к stimulusImageId), actionImageId, флаги usefulness/belief.</li>
<li>В <strong>stimuls_consciousness</strong> по контексту: искать автоматизм по узлу; если найден и «штатный» — запустить действие (через ОД/эффекторы); если нет — оставить заглушку под будущий «цикл мышления» или простой выбор по Правилам (ЭП).</li>
<li>Связь с рефлексами: при наличии успешного автоматизма для узла не запускать условный/генетический рефлекс (по аналогии с GO: блокировка рефлекса автоматизмом).</li>
<li>При необходимости добавить по пульсу вызов «диспетчера мышления» (упрощённый аналог dispetchConsciousnessThinking), обрабатывающего очередь нерешённых ситуаций.</li>
</ol>
<p>Детали старых структур (Automatizm, cycleInfo, infoFunc*) при необходимости можно уточнять по файлам в <code>OLD_CODE/GO/brain/psychic/</code>.</p>

                </article>
                </body>
                </html>
════════════════════════════════════════════════════════════

/BEAST/beast/docs/design_conscious_automatisms.md
════════════════════════════════════════════════════════════
# Осознанные автоматизмы и процесс осознания — стратегия реализации

Документ для подготовки следующего этапа: система осознанных автоматизмов и процесс осознания. Старая реализация была на языке ГО (OLD_CODE); в новой JS-реализации уже есть качественно более сильные адаптивные системы — важно не тащить в новую реализацию устаревшие паттерны, но сохранить понятийный каркас.

---

## 0. Назначение проекта и границы

**Назначение:** проект — не «работающее существо и его воспитание», а **наиболее адекватный способ реализации теории** индивидуальной адаптивности. Как теории формализуются математически, так и теория адаптивности лучше всего описывается программной реализацией — это аналог математики в физике, но более эффективный для описания **схемотехники систем адаптивности**.

**Вне рамок новой реализации:**
- **Ввод слов и фраз оператором** и **вербальные деревья** (распознавание фраз, слова, символы, тон и настроение сообщения). Их добавление сильно усложняет проект и его понимание; для формализации теории достаточно слоёв: гомеостаз, контексты, стимулы (кнопки), рефлексы, дерево условий восприятия (по активности стимулов), эпизодическая и семантическая память, осознание и автоматизмы.
- Дерево восприятия сохраняет **6 уровней** структуры (BaseID, EmotionID, ActivityID, ToneMoodID, SimbolID, PhraseID) для совместимости с теорией и сопоставлением с OLD_CODE, но **уровни 4–6 не заполняются** вербальным вводом — остаются 0 или могут использоваться позже для иных невербальных измерений.

---

## 1. Где мы сейчас (JS)

Уже есть зацепки под осознание и автоматизмы:

| Компонент | Назначение |
|-----------|------------|
| **ОР** | Выбор наиболее актуального стимула, блокировка рефлекса на 2 пульса, вызов гиппокампа, **stimuls_consciousness(actual_stimul_ID)**, сохранение ЭП. |
| **Гиппокамп** | `active_stimulsArr_ID`, `actual_stimul_ID` — удержание стимула, вызвавшего ОР. |
| **_9_Conscience** | **stimuls_consciousness(actual_stimul_ID)** — заглушка; вызывается из ОР при новом победителе и при ответном стимуле в период ожидания ЭП. Сюда естественно подвешивать «процесс осознания». |
| **Дерево восприятия** | 6 уровней, терминальный узел = условия; привязка рефлексов/автоматизмов к ветке уже заложена в описании. |
| **ЭП** | Кадр: условия (perceptionNodeId), стимул, действие, эффект, ответный стимул — готовый контекст для осмысления. |
| **ОД** | Образы действия; связь стимул → действие уже есть. |
| **УР** | Условные рефлексы по контексту (BadNormWell + эмоция) и FinalImageId. |

То есть: **канал в «кору»** уже есть (вызов `stimuls_consciousness`), **контекст** можно собирать из дерева, ЭП и ОД. Не хватает: явной модели «осознанный автоматизм» и цикла «процесс осознания» (что именно обрабатывается в этом канале).

---

## 2. Рекомендация: опора на новый стек, старый код — только концепт

- **Не переносить старый код из OLD_CODE в JS целиком.** Язык, структуры данных и потоки там другие; уже реализованные вещи (пульс, ОР, ЭП по пульсам, дерево, СЗ, УР с глобальными ID и т.д.) в старой версии могут отсутствовать или работать иначе. Перенос «как было» рискован и создаст трение.
- **Использовать OLD_CODE как справочник понятий и сценариев:** что там называлось «осознанный автоматизм», что входило в «процесс осознания», в каком порядке что вызывалось. Термины и последовательность шагов можно выписать и спроецировать на текущую архитектуру.
- **Собирать новую логику поверх уже сделанного:** дерево восприятия (терминальный узел = условия), ЭП (кадр с условиями, стимулом, действием, эффектом, ответом), ОД, гиппокамп, ОР. Осознанные автоматизмы тогда — следующий слой над условными рефлексами и деревом (например, привязка к узлу дерева + учёт осознания). Процесс осознания — то, что выполняется внутри или вокруг `stimuls_consciousness` (что именно «проходит» по каналу внимания и как используется).

Если папка OLD_CODE доступна (рядом с проектом или отдельный репозиторий), полезно:
- выписать оттуда ключевые сущности (структуры автоматизма, шаги осознания);
- сопоставить их с текущими: perceptionNodeId, stimulusImageId, actionImageId, Episodes, FinalImage, дерево;
- зафиксировать расхождения и принять решения уже в рамках JS-архитектуры.

---

## 3. Возможная форма «осознанного автоматизма» в новом стеке

Без опоры на старый код можно задать черновой формат:

- **Осознанный автоматизм** — правило вида: при условиях **C** (ветка дерева восприятия, т.е. perceptionNodeId или путь) и образе стимула **S** (FinalImageId) выполнять образ действия **A** (ОД), с возможностью «прогона» через осознание (т.е. решение может быть подтверждено/скорректировано процессом осознания).
- Отличие от условного рефлекса: привязка к **узлу дерева** (условия развёрнуты по уровням), возможно участие **эпизодического контекста** (последние кадры ЭП) и явный шаг **осознания** перед/после действия.

Тогда:
- входы: текущий терминальный узел дерева, actual_stimul_ID (и при необходимости последние кадры ЭП);
- выходы: выбор/подтверждение действия (ОД), возможно обновление памяти (ЭП/СЗ) по результату осознания.

---

## 4. Возможная форма «процесса осознания»

- **Процесс осознания** — то, что происходит при вызове `stimuls_consciousness(actual_stimul_ID)` и, при необходимости, в ближайшие пульсы: что именно «осознаётся» и как это влияет на поведение и память.
- В текущем коде в `stimuls_consciousness` уже можно передавать не только `actual_stimul_ID`, но и пакет контекста: терминальный узел дерева, последний/текущий кадр ЭП (если есть), флаг «ответный стимул в период ожидания» и т.д. Тогда «процесс осознания» — это функция, которая принимает этот пакет и решает, что делать дальше (например, запуск осознанного автоматизма, обновление приоритетов, запись в память).

Следующий шаг реализации можно разбить на этапы, например:
1. Расширить вызов `stimuls_consciousness` контекстом (дерево, ЭП, ОР/ЭП-флаги).
2. Описать структуру данных «осознанный автоматизм» и место хранения (массив + персистенция, по аналогии с УР/ЭП).
3. Ввести цикл осознания: по контексту выбрать кандидатов-автоматизмов, при необходимости прогнать через «осознание» и выполнить выбранное действие (через существующие эффекторы/ОД).

---

## 5. Итог

- **Старый код (OLD_CODE):** использовать только как источник терминов и идей; не копировать реализацию.
- **Новая реализация:** строить осознанные автоматизмы и процесс осознания поверх текущего JS-стека (пульс, ОР, дерево, ЭП, ОД, СЗ, УР, гиппокамп, `stimuls_consciousness`).
- **Следующий шаг:** при наличии доступа к OLD_CODE — выписать оттуда понятия и потоки и сопоставить с этим документом; затем зафиксировать контракт `stimuls_consciousness` (входной контекст) и формат осознанного автоматизма и приступить к реализации по этапам выше.

---

## 6. Сопоставление с OLD_CODE (GO) — после загрузки старой реализации

Папка **OLD_CODE** содержит старую реализацию (GO + PULT). Ниже — выжимка понятий и потоков для переноса идей в JS, без копирования кода.

### 6.1 Дерево автоматизмов (GO)

- **Файлы:** `OLD_CODE/GO/brain/psychic/automatism_tree.go`, `automatizm_functions.go`, `automatism_tree_func.go`.
- **Структура узла:** 6 уровней — BaseID, EmotionID, ActivityID, ToneMoodID, SimbolID, PhraseID (формат записи совпадает с нашим деревом восприятия). В новой реализации уровни 4–6 (ToneMoodID, SimbolID, PhraseID) **не используются** — вербальный ввод и вербальные деревья в проект не входят (см. §0).
- **Идея:** К ветке дерева привязаны автоматизмы; при активации ветки выбирается «штатный» автоматизм (Usefulness, Belief). Отсутствие подходящей ветки/автоматизма вызывает ОР и осмысление. Автоматизм с успешностью == 1 блокирует рефлексы (уже «произвольность преодолена»).
- **В JS:** Дерево восприятия уже есть (`_7_perception_tree`), формат узла совместим; активная ветка определяется по уровням 1–3 (BaseID, EmotionID, ActivityID). Нет пока привязки «автоматизм к узлу» и выбора действия по узлу — это слой поверх дерева и УР.

### 6.2 Процесс осознания (GO)

- **Файлы:** `understanding.go`, `0_main_psychic.go`, `mental_automatizm_INFO_functions.go`, `dreaming.go`.
- **Цепочка:** По каждому стимулу — **consciousnessElementary()** (1–2 уровни осознания). Если остаётся проблема — **consciousnessThinking(cID)** в циклах мышления. Циклы типа **cycleInfo**, диспетчер **dispetchConsciousnessThinking()** по пульсу.
- **consciousnessElementary():**
  - Определяет «рвущийся» автоматизм по узлу дерева: `atmtzmActualTreeNodeID = getAutomatizmFromNodeID(detectedActiveLastNodID)`.
  - Игнорирующий автоматизм приравнивается к отсутствию реакции.
  - Проверки: опасность, соответствие теме/цели (isStimulToForce), прогноз эффекта, Правила (эпизодические). Решение: запустить штатный автоматизм, альтернативный, или заблокировать и передать в циклы мышления.
- **consciousnessThinking():** циклы мышления (инфо-функции infoFunc*), доминанта проблемы, пассивное размышление/сновидения. Запуск ментальных и моторных автоматизмов из цикла.
- **В JS:** Аналог «элементарного» осознания — расширенный **stimuls_consciousness(actual_stimul_ID)**: на вход — actual_stimul_ID + текущий терминальный узел дерева + последний кадр ЭП (и флаг «ответный стимул»). Внутри — выбор: есть ли автоматизм для узла; если да — подтвердить/запустить; если нет — передать в «цикл мышления» (который в JS пока можно заменить простой очередью задач или одним шагом эвристик по ЭП/Правилам).

### 6.3 Автоматизм в GO

- **Структура:** привязка к узлу дерева (TreeNodeID), образ действия (ActionsImageID), Usefulness, Belief (штатный/предположительный/заблокированный). Есть моторные и ментальные автоматизмы.
- **В JS:** Образ действия уже есть (ОД, action_image). «Осознанный автоматизм» в JS можно задать как: perceptionNodeId + (опционально stimulusImageId) → actionImageId, с полями вроде usefulness/belief и приоритетом относительно УР (автоматизм блокирует рефлекс при успешности).

### 6.4 Что брать из OLD_CODE только как идеи

- Последовательность: **пульс → активация дерева → выбор автоматизма по узлу → consciousnessElementary → при необходимости consciousnessThinking**.
- Понятия: штатный/игнорирующий/альтернативный автоматизм, период ожидания ответа после действия, «Правила» (эпизодические кадры) для прогноза эффекта и выбора альтернативы.
- Не переносить буквально: EvolushnStage, сон/сновидения, полный набор инфо-функций и циклов — реализовать по мере необходимости, опираясь на ЭП, дерево и ОД в JS.

### 6.5 Рекомендуемый порядок в JS

1. Расширить **stimuls_consciousness**: передавать контекст (terminalNodeId, lastEpisodeFrame, isResponseStimulus).
2. Ввести **хранилище осознанных автоматизмов**: привязка к perceptionNodeId (и опционально к stimulusImageId), actionImageId, флаги usefulness/belief.
3. В **stimuls_consciousness** по контексту: искать автоматизм по узлу; если найден и «штатный» — запустить действие (через ОД/эффекторы); если нет — оставить заглушку под будущий «цикл мышления» или простой выбор по Правилам (ЭП).
4. Связь с рефлексами: при наличии успешного автоматизма для узла не запускать условный/генетический рефлекс (по аналогии с GO: блокировка рефлекса автоматизмом).
5. При необходимости добавить по пульсу вызов «диспетчера мышления» (упрощённый аналог dispetchConsciousnessThinking), обрабатывающего очередь нерешённых ситуаций.

Детали старых структур (Automatizm, cycleInfo, infoFunc*) при необходимости можно уточнять по файлам в `OLD_CODE/GO/brain/psychic/`.

════════════════════════════════════════════════════════════

/BEAST/beast/puls.js
════════════════════════════════════════════════════════════
/**
 * Центральный пульс системы существа Beast.
 *
 * Назначение:
 * - Обеспечивает базовый "сердечный" ритм существа с периодом 1 секунда.
 * - Поддерживает глобальную переменную window.Cur_puls_val — количество
 *   пульсов с начала жизни существа (а не запуска index.html).
 * - Обновляет визуальный индикатор пульса на главной странице index.html:
 *   мигающий зелёный кружок и числовой счётчик пульсов.
 *
 * Взаимодействие с интерфейсом:
 * - В index.html должны присутствовать элементы с id "pulseCircle" и "pulseCounter".
 * - Функция update_beast_pulse_view() может вызываться другими модулями,
 *   если им нужно принудительно обновить отображение (например, после
 *   загрузки сохранённого значения Cur_puls_val).
 *
 * Хранение пульса между запусками:
 * - Значение Cur_puls_val периодически сохраняется в localStorage под ключом 'Cur_puls_val'.
 */
(function (global) {
  'use strict';

  var PULSE_KEY = 'Cur_puls_val';

  // Инициализация глобальной переменной Cur_puls_val из localStorage
  var saved = 0;
  try {
    var raw = global.localStorage.getItem(PULSE_KEY);
    if (raw !== null) {
      var n = parseInt(raw, 10);
      if (!isNaN(n) && n >= 0) {
        saved = n;
      }
    }
  } catch (e) {
    saved = 0;
  }

  global.Cur_puls_val = saved;

  /** Локальный счётчик пульсов (нарастает с каждым пульсом), передаётся в getActiveBasicContexts. */
  global.cpuls = 0;

  var pulseCircle = null;
  var pulseCounterEl = null;

  /**
   * Обновление ссылок на DOM-элементы индикатора пульса.
   * Вызывается при первом обновлении отображения.
   */
  function ensure_dom_refs() {
    if (!pulseCircle) {
      pulseCircle = global.document.getElementById('pulseCircle');
    }
    if (!pulseCounterEl) {
      pulseCounterEl = global.document.getElementById('pulseCounter');
    }
  }

  /**
   * Обновить отображение текущего значения пульса на странице.
   * Экспортируется как глобальная функция update_beast_pulse_view.
   */
  function update_beast_pulse_view() {
    ensure_dom_refs();
    if (pulseCounterEl) {
      pulseCounterEl.textContent = String(global.Cur_puls_val);
    }
  }

  // Глобальный пульс: используется sep_of_puls() — один цикл с учётом времени записи в IndexedDB и голубой индикацией.
  var isOn = true;
  update_beast_pulse_view();

  global.update_beast_pulse_view = update_beast_pulse_view;

  function sep_of_puls() {
    var startTime = Date.now();
    try {
      if (typeof global.getDiffImportance === 'function') {
        global.PrevDiffForEpisode = global.getDiffImportance();
      }
      if (typeof global.runIndexedDBSavesForPulse === 'function') {
        global.runIndexedDBSavesForPulse();
      }

      if (global.Cur_puls_val === 0) {
        // начальная загрузка данных из IndexedDB
      }

      global.Cur_puls_val += 1;
      update_beast_pulse_view();
      try {
        global.localStorage.setItem(PULSE_KEY, String(global.Cur_puls_val));
      } catch (e) {}

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

      if (typeof global.performSensorOperations === 'function') {
        global.performSensorOperations();
      }
      global.cpuls++;
      if (typeof global.getActiveBasicContexts === 'function') {
        global.getActiveBasicContexts(global.cpuls);
      }
      if (typeof global.update_basic_contexts_view === 'function') {
        global.update_basic_contexts_view();
      }
      if (typeof global.PerceptionTree_updateActiveBranchView === 'function') {
        global.PerceptionTree_updateActiveBranchView();
      }
      // Синхронизируем Base/Emotion/Activity инфокартины с текущей веткой дерева.
      if (typeof global.Info_updateFromPerceptionTree === 'function') {
        global.Info_updateFromPerceptionTree();
      }
      // Бордеры «готовности к ОР»: полный пересчёт только при смене BadNormWell / эмоции / ID ветки
      // (см. stimuls_ui.scheduleStimulusReadinessRefreshIfNeeded). Иначе точечно — в semantic_memory.js.
      if (typeof global.scheduleStimulusReadinessRefreshIfNeeded === 'function') {
        global.scheduleStimulusReadinessRefreshIfNeeded();
      }
      // В пассивном режиме: семантическая обработка моделей понимания; в ходе неё возникают новые образы ситуаций на будущее.
      var passiveMain = typeof global.Consciousness_getMainCycleThinkingMode === 'function' &&
        global.Consciousness_getMainCycleThinkingMode() === 'passive';
      if (passiveMain && typeof global.PassiveMode_processUnderstandingModels === 'function') {
        global.PassiveMode_processUnderstandingModels();
      }
      // Третий пульс — ориентировочный рефлекс начала осмысления: создаём первый цикл осознания.
      if (global.cpuls === 3) {
        if (typeof global.Info_updateFromPerceptionTree === 'function') {
          global.Info_updateFromPerceptionTree();
        }
        if (typeof global.Info_updateFromStimulus === 'function') {
          global.Info_updateFromStimulus(0, 'external');
        }
        if (typeof global.Info_incrementStep === 'function') {
          global.Info_incrementStep();
        }
        if (typeof global.Info_thinkingFlash === 'function') {
          global.Info_thinkingFlash();
        }
        if (typeof global.stimuls_consciousness === 'function') {
          var branchId = typeof global.PerceptionTree_getActiveTerminalNodeId === 'function'
            ? global.PerceptionTree_getActiveTerminalNodeId()
            : 0;
          global.stimuls_consciousness({
            actual_stimul_ID: 0,
            branchId: branchId,
            well: global.BadNormWell || 0,
            emotionId: 0,
            activityID: 0,
            significance: 1
          });
        }
      }
      // Диспетчер циклов мышления: один шаг главному циклу, фоновым — раз в 3 пульса.
      if (typeof global.dispetchConsciousnessThinking === 'function') {
        global.dispetchConsciousnessThinking(global.cpuls);
      }
      if (typeof global.processGeneticReflexes === 'function') {
        global.processGeneticReflexes();
      }
      global.EpisodicMemory_skipConditionalReflexFormation = false;

      ensure_dom_refs();
      if (pulseCircle) {
        isOn = !isOn;
        if (isOn) pulseCircle.classList.remove('off');
        else pulseCircle.classList.add('off');
      }
    } catch (e) {
      if (typeof global.console !== 'undefined' && global.console.error) {
        global.console.error('sep_of_puls:', e);
      }
    } finally {
      var elapsedTime = Date.now() - startTime;
      var delayTime = elapsedTime >= 1000 ? 0 : Math.max(0, 1000 - elapsedTime);
      global.setTimeout(sep_of_puls, delayTime);
    }
  }

  global.setTimeout(sep_of_puls, 1000);
})(window);
════════════════════════════════════════════════════════════

/BEAST/beast/sys/IndexedDB/indexeddb.js
════════════════════════════════════════════════════════════
/**
 * Модуль работы с IndexedDB для проекта BEAST.
 *
 * Назначение:
 * - Предоставляет простую обёртку над IndexedDB.
 * - Скрывает детали открытия базы, создания хранилищ и выполнения операций.
 * - Обеспечивает единообразный интерфейс для чтения и записи данных по ID.
 *
 * Основные функции:
 * - openDatabase(config)  — открыть/создать базу и объектные хранилища.
 * - put(params)           — сохранить или обновить запись.
 * - get(params)           — получить одну запись по ключу.
 * - getAll(params)        — получить все записи из хранилища.
 * - remove(params)        — удалить запись по ключу.
 * - clearStore(params)    — очистить хранилище.
 *
 * Пример использования:
 *
 *   // Открываем базу и создаём хранилище 'samples' с ключом 'id'
 *   IndexedDBModule.openDatabase({
 *     dbName: 'BEAST_DB',
 *     version: 1,
 *     stores: [
 *       { name: 'samples', options: { keyPath: 'id' } }
 *     ]
 *   }).then(function (db) {
 *     return IndexedDBModule.put({
 *       db: db,
 *       storeName: 'samples',
 *       value: { id: 1, text: 'Привет, IndexedDB!' }
 *     });
 *   }).then(function () {
 *     return IndexedDBModule.getAll({
 *       db: db,
 *       storeName: 'samples'
 *     });
 *   }).then(function (allItems) {
 *     console.log(allItems);
 *   }).catch(function (error) {
 *     console.error('Ошибка IndexedDB:', error);
 *   });
 *
 * Все комментарии даны на русском языке в соответствии с правилами проекта.
 */

(function (global) {
  'use strict';

  /**
   * Открыть (или создать) базу данных IndexedDB.
   *
   * @param {Object} config - конфигурация базы данных.
   * @param {string} config.dbName - имя базы данных.
   * @param {number} [config.version] - версия базы данных.
   * @param {Array<Object>} [config.stores] - описание создаваемых хранилищ.
   *        Каждый элемент: { name: string, options?: IDBObjectStoreParameters, indexes?: Array<{name, keyPath, options}> }
   * @returns {Promise<IDBDatabase>} - промис с открытой базой данных.
   */
  function openDatabase(config) {
    if (!config || !config.dbName) {
      return Promise.reject(new Error('Не указано имя базы данных (config.dbName).'));
    }

    var dbName = config.dbName;
    var version = config.version || 1;
    var stores = Array.isArray(config.stores) ? config.stores : null;

    return new Promise(function (resolve, reject) {
      var request;
      try {
        request = global.indexedDB.open(dbName, version);
      } catch (e) {
        reject(e);
        return;
      }

      // Вызывается один раз при создании новой БД или повышении версии.
      request.onupgradeneeded = function (event) {
        var db = event.target.result;

        if (!stores) {
          return;
        }

        stores.forEach(function (storeConfig) {
          if (!storeConfig || !storeConfig.name) {
            return;
          }

          // Если хранилище уже есть — пропускаем.
          if (db.objectStoreNames.contains(storeConfig.name)) {
            return;
          }

          var options = storeConfig.options || { keyPath: 'id' };
          var objectStore = db.createObjectStore(storeConfig.name, options);

          // Создаём индексы, если описаны.
          if (Array.isArray(storeConfig.indexes)) {
            storeConfig.indexes.forEach(function (idx) {
              if (!idx || !idx.name || !idx.keyPath) {
                return;
              }
              objectStore.createIndex(idx.name, idx.keyPath, idx.options || {});
            });
          }
        });
      };

      request.onsuccess = function (event) {
        var db = event.target.result;

        // Если версия БД меньше требуемой — закрываем и пробуем ещё раз.
        if (db.version !== version && version > db.version) {
          db.close();
          // Рекурсивный вызов для повышения версии.
          openDatabase({
            dbName: dbName,
            version: version,
            stores: stores
          }).then(resolve).catch(reject);
          return;
        }

        resolve(db);
      };

      request.onerror = function (event) {
        reject(event.target.error || new Error('Неизвестная ошибка при открытии IndexedDB.'));
      };

      request.onblocked = function () {
        reject(new Error('Операция обновления IndexedDB заблокирована. Закройте другие вкладки с этим приложением.'));
      };
    });
  }

  /**
   * Совместимость со старым интерфейсом модулей BEAST.
   *
   * Старые модули вызывали:
   *   IndexedDBModule.openDB({ dbName, version, storeName, keyPath })
   *
   * Новый openDatabase ожидает:
   *   { dbName, version, stores: [{ name, options: { keyPath } }] }
   *
   * Эта обёртка принимает оба формата.
   */
  function openDB(config) {
    config = config || {};
    // Если уже передан новый формат — просто проксируем.
    if (Array.isArray(config.stores)) {
      return openDatabase(config);
    }
    // Старый формат: storeName/keyPath.
    var storeName = config.storeName;
    var keyPath = config.keyPath || 'id';
    // Важно: если БД уже была создана ранее без нужного store (из-за старого API),
    // то onupgradeneeded не сработает при той же версии. В этом случае повышаем версию и переоткрываем.
    var desired = {
      dbName: config.dbName,
      version: config.version,
      stores: storeName ? [{ name: storeName, options: { keyPath: keyPath } }] : undefined
    };
    if (!storeName) {
      return openDatabase(desired);
    }
    return openDatabase(desired).then(function (db) {
      try {
        if (db && db.objectStoreNames && db.objectStoreNames.contains(storeName)) {
          return db;
        }
      } catch (e) {}
      // Store не найден — повышаем версию и пробуем создать.
      var nextVersion = Math.max(parseInt(desired.version, 10) || 1, db && db.version ? db.version : 1) + 1;
      try { if (db) db.close(); } catch (e2) {}
      return openDatabase({
        dbName: desired.dbName,
        version: nextVersion,
        stores: desired.stores
      });
    });
  }

  /**
   * Вспомогательная функция для выполнения транзакции.
   *
   * @param {IDBDatabase} db - открытая база данных.
   * @param {string} storeName - имя хранилища объектов.
   * @param {string} mode - режим транзакции ('readonly' или 'readwrite').
   * @param {function(IDBObjectStore): IDBRequest} operation - операция над хранилищем.
   * @returns {Promise<any>} - промис с результатом операции.
   */
  function runTransaction(db, storeName, mode, operation) {
    if (!db) {
      return Promise.reject(new Error('База данных не открыта.'));
    }
    if (!storeName) {
      return Promise.reject(new Error('Не указано имя хранилища (storeName).'));
    }

    return new Promise(function (resolve, reject) {
      var tx;
      var store;
      try {
        tx = db.transaction(storeName, mode);
        store = tx.objectStore(storeName);
      } catch (e) {
        reject(e);
        return;
      }

      var request;
      try {
        request = operation(store);
      } catch (e) {
        reject(e);
        return;
      }

      request.onsuccess = function (event) {
        resolve(event.target.result);
      };

      request.onerror = function (event) {
        reject(event.target.error || new Error('Ошибка операции с IndexedDB.'));
      };
    });
  }

  /**
   * Сохранить или обновить запись в хранилище.
   *
   * @param {Object} params
   * @param {IDBDatabase} params.db - база данных.
   * @param {string} params.storeName - имя хранилища.
   * @param {any} params.value - сохраняемое значение.
   * @returns {Promise<IDBValidKey>} - промис с ключом записи.
   */
  function put(params) {
    if (typeof global.showSaveIndicator === 'function') global.showSaveIndicator();
    var p = runTransaction(params.db, params.storeName, 'readwrite', function (store) {
      return store.put(params.value);
    });
    return p.then(function (r) {
      if (typeof global.hideSaveIndicator === 'function') global.hideSaveIndicator();
      return r;
    }, function (err) {
      if (typeof global.hideSaveIndicator === 'function') global.hideSaveIndicator();
      throw err;
    });
  }

  /**
   * Получить одну запись по ключу.
   *
   * @param {Object} params
   * @param {IDBDatabase} params.db - база данных.
   * @param {string} params.storeName - имя хранилища.
   * @param {IDBValidKey | IDBKeyRange} params.key - ключ поиска.
   * @returns {Promise<any>} - промис с найденной записью или undefined.
   */
  function get(params) {
    return runTransaction(params.db, params.storeName, 'readonly', function (store) {
      return store.get(params.key);
    });
  }

  /**
   * Получить все записи из хранилища.
   *
   * @param {Object} params
   * @param {IDBDatabase} params.db - база данных.
   * @param {string} params.storeName - имя хранилища.
   * @returns {Promise<Array<any>>} - промис с массивом записей.
   */
  function getAll(params) {
    return runTransaction(params.db, params.storeName, 'readonly', function (store) {
      if (typeof store.getAll === 'function') {
        return store.getAll();
      }

      // Для старых браузеров без getAll — вручную обходим курсор.
      var dummyRequest = {};
      setTimeout(function () {
        var tx = params.db.transaction(params.storeName, 'readonly');
        var st = tx.objectStore(params.storeName);
        var cursorRequest = st.openCursor();
        var items = [];

        cursorRequest.onsuccess = function (event) {
          var cursor = event.target.result;
          if (cursor) {
            items.push(cursor.value);
            cursor.continue();
          } else {
            // Эмулируем интерфейс request и вызываем onsuccess у оригинального промиса.
            if (typeof dummyRequest.onsuccess === 'function') {
              dummyRequest.onsuccess({ target: { result: items } });
            }
          }
        };

        cursorRequest.onerror = function (event) {
          if (typeof dummyRequest.onerror === 'function') {
            dummyRequest.onerror(event);
          }
        };
      }, 0);

      return dummyRequest;
    });
  }

  /**
   * Удалить запись по ключу.
   *
   * @param {Object} params
   * @param {IDBDatabase} params.db - база данных.
   * @param {string} params.storeName - имя хранилища.
   * @param {IDBValidKey | IDBKeyRange} params.key - ключ.
   * @returns {Promise<void>} - промис без значения.
   */
  function remove(params) {
    if (typeof global.showSaveIndicator === 'function') global.showSaveIndicator();
    var p = runTransaction(params.db, params.storeName, 'readwrite', function (store) {
      return store.delete(params.key);
    });
    return p.then(function (r) {
      if (typeof global.hideSaveIndicator === 'function') global.hideSaveIndicator();
      return r;
    }, function (err) {
      if (typeof global.hideSaveIndicator === 'function') global.hideSaveIndicator();
      throw err;
    });
  }

  /**
   * Очистить всё хранилище.
   *
   * @param {Object} params
   * @param {IDBDatabase} params.db - база данных.
   * @param {string} params.storeName - имя хранилища.
   * @returns {Promise<void>} - промис без значения.
   */
  function clearStore(params) {
    if (typeof global.showSaveIndicator === 'function') global.showSaveIndicator();
    var p = runTransaction(params.db, params.storeName, 'readwrite', function (store) {
      return store.clear();
    });
    return p.then(function (r) {
      if (typeof global.hideSaveIndicator === 'function') global.hideSaveIndicator();
      return r;
    }, function (err) {
      if (typeof global.hideSaveIndicator === 'function') global.hideSaveIndicator();
      throw err;
    });
  }

  /**
   * Регистрация отложенного сохранения по пульсу.
   * Модули регистрируют функцию fn(); она вызывается раз в пульс (из puls.js).
   *
   * Сохранение по мере изменения данных (не полный дамп каждый пульс):
   * - Модуль ведёт флаг dirty (например, epDirty, treeDirty): устанавливать true только при реальном
   *   изменении данных (добавление/удаление/редактирование кадров, узлов, записей).
   * - В зарегистрированной функции: если !dirty — сразу return (сериализация и put не вызываются).
   * - Если dirty: сбросить dirty, сериализовать состояние, сравнить подпись (sig) с lastSavedSig;
   *   если подпись не изменилась — return без put; иначе обновить lastSavedSig и выполнить put(state).
   * Так в IndexedDB пишется только при изменении данного вида данных (ЭП — при новом/удалённом кадре,
   * дерево — при добавлении узла, семантика — при новой/изменённой записи, УР/ОД — при изменении массивов).
   *
   * Модули, которые должны вызывать registerIndexedDBSaver (чеклист при добавлении хранилища):
   * - semantic_memory.js  (семантическая память)
   * - perception_tree.js  (дерево восприятия; dirty только в addNode, не в updateActiveBranchView)
   * - synonyms_reflexes.js (условные рефлексы)
   * - episodic_memory.js  (эпизодическая память)
   * - actions_image.js    (образы действия)
   * - automatizms.js      (автоматизмы)
   * - goals.js            (образы цели)
   * - situatioms_tree.js  (образы тем и ситуаций)
   * - mental_automatism.js (абстракции и ментальные автоматизмы — заготовка)
   * - abstract_images.js  (абстрактные образы восприятия/действия — заготовка)
   * - gestalt.js            (гештальты и буфер резонанса)
   */
  var indexedDBSavers = [];
  global.registerIndexedDBSaver = function (fn) {
    if (typeof fn === 'function') indexedDBSavers.push(fn);
  };
  global.runIndexedDBSavesForPulse = function () {
    for (var i = 0; i < indexedDBSavers.length; i++) {
      try {
        indexedDBSavers[i]();
      } catch (e) {
        if (typeof global.console !== 'undefined' && global.console.error) {
          global.console.error('IndexedDB saver error:', e);
        }
      }
    }
  };

  // Экспортируем модуль в глобальное пространство имён.
  global.IndexedDBModule = {
    openDatabase: openDatabase,
    openDB: openDB,
    put: put,
    get: get,
    getAll: getAll,
    remove: remove,
    clearStore: clearStore
  };
})(window);


════════════════════════════════════════════════════════════

/BEAST/beast/sys/IndexedDB/samples.htm
════════════════════════════════════════════════════════════
<!--
  Пример использования модуля IndexedDBModule для проекта BEAST.

  Назначение файла:
  - Показать простейшие операции записи и чтения данных из IndexedDB.
  - Продемонстрировать работу обёртки IndexedDBModule, расположенной в файле indexeddb.js.
  - Отобразить результаты операций прямо на странице.

  Основные действия:
  - При загрузке страницы открывается база данных 'BEAST_DB' и создаётся хранилище 'samples'.
  - При нажатии на кнопку "Сохранить тестовую запись" в хранилище добавляется объект с уникальным ID.
  - При нажатии на кнопку "Прочитать все записи" загружается всё содержимое хранилища и выводится в область логов.

  Весь JavaScript-код примера (кроме общего модуля indexeddb.js) находится в этом файле, внутри тега <script>.
-->
<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <title>Пример работы с IndexedDB (BEAST)</title>
  <style>
    body {
      font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      margin: 20px;
      background-color: #f5f5f5;
      color: #222;
    }

    h1 {
      font-size: 20px;
      margin-bottom: 10px;
    }

    .panel {
      background: #ffffff;
      border-radius: 6px;
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
      padding: 16px;
      max-width: 720px;
    }

    .controls {
      margin-bottom: 12px;
    }

    button {
      padding: 8px 14px;
      margin-right: 8px;
      border-radius: 4px;
      border: 1px solid #0078d4;
      background-color: #0078d4;
      color: #ffffff;
      cursor: pointer;
      font-size: 14px;
    }

    button:disabled {
      opacity: 0.6;
      cursor: default;
    }

    button.secondary {
      background-color: #ffffff;
      color: #0078d4;
    }

    #status {
      margin-top: 4px;
      font-size: 13px;
      color: #555;
    }

    #log {
      margin-top: 12px;
      background: #111827;
      color: #e5e7eb;
      padding: 10px;
      border-radius: 4px;
      max-height: 260px;
      overflow-y: auto;
      font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
      font-size: 13px;
      white-space: pre-wrap;
    }
  </style>
</head>
<body>
  <div class="panel">
    <h1>Пример работы с IndexedDB (BEAST)</h1>

    <div class="controls">
      <button id="btnSave" disabled>Сохранить семантическое состояние (эмуляция)</button>
      <button id="btnRead" class="secondary" disabled>Прочитать семантическое состояние</button>
    </div>

    <div id="status">Ожидание инициализации базы данных…</div>
    <div id="log"></div>
  </div>

  <!-- Подключаем общий модуль обёртки для IndexedDB -->
  <script src="indexeddb.js"></script>

  <!-- Основная логика примера. Весь JS для samples.htm находится здесь. -->
  <script>
    (function () {
      'use strict';

      // Элементы интерфейса
      var btnSave = document.getElementById('btnSave');
      var btnRead = document.getElementById('btnRead');
      var statusEl = document.getElementById('status');
      var logEl = document.getElementById('log');

      // Имя базы и хранилища по умолчанию для примера.
      // Используем отдельную лабораторную БД, чтобы не конфликтовать с боевой BEAST_DB.
      var DB_NAME = 'BEAST_DB_SEMANTIC_LAB';
      var DB_VERSION = 1;
      // В этом примере мы используем то же хранилище, что и Beast для семантики,
      // чтобы поведение полностью совпадало с боевым кодом.
      var STORE_NAME = 'semantic_state';

      // Ссылка на открытую базу данных
      var dbInstance = null;

      /**
       * Добавить строку в лог.
       * @param {string} message - текст сообщения.
       */
      function log(message) {
        var time = new Date().toLocaleTimeString();
        logEl.textContent += '[' + time + '] ' + message + '\n';
        logEl.scrollTop = logEl.scrollHeight;
      }

      /**
       * Установить текст статуса над логом.
       * @param {string} text - новый статус.
       */
      function setStatus(text) {
        statusEl.textContent = text;
      }

      /**
       * Инициализация базы данных:
       * - открываем/создаём базу DB_NAME,
       * - при первой инициализации создаём хранилище STORE_NAME.
       */
      function initDatabase() {
        if (!window.IndexedDBModule) {
          setStatus('Ошибка: модуль IndexedDBModule не найден.');
          log('Модуль IndexedDBModule не найден. Проверьте подключение файла indexeddb.js.');
          return;
        }

        setStatus('Инициализация базы данных…');

        IndexedDBModule.openDatabase({
          dbName: DB_NAME,
          version: DB_VERSION,
          stores: [
            {
              name: STORE_NAME,
              options: { keyPath: 'id', autoIncrement: false }
            }
          ]
        })
          .then(function (db) {
            dbInstance = db;
            setStatus('База данных инициализирована. Можно работать.');
            log('База данных "' + DB_NAME + '" и хранилище "' + STORE_NAME + '" готовы к работе.');
            btnSave.disabled = false;
            btnRead.disabled = false;
          })
          .catch(function (error) {
            setStatus('Ошибка инициализации базы данных (см. лог).');
            log('Ошибка при инициализации базы: ' + (error && error.message ? error.message : String(error)));
          });
      }

      /**
       * Сохранить эмуляцию семантического состояния в хранилище.
       * Структура полностью повторяет формат serializeSemanticState() из semantic_memory.js,
       * чтобы можно было отлаживать сохранение/воспроизведение семантики в изолированном режиме.
       */
      function saveSampleRecord() {
        if (!dbInstance) {
          log('База данных ещё не инициализирована.');
          return;
        }

        // --- Эмуляция трёх массивов: Emotions, FinalImages и SemanticMemory ---
        // Эмоции: соответствие emotionId -> набор базовых контекстов
        var emotions = [
          { id: 1, contextIds: [1] },          // Например: "Спокойствие"
          { id: 2, contextIds: [2, 3] }        // Например: "Радость + Интерес"
        ];

        // Конечные образы: соответствие finalImageId -> набор активных стимулов
        var finalImages = [
          { id: 1, stimulIds: [101] },         // Например: только стимул "Приятно"
          { id: 2, stimulIds: [101, 202] }     // Например: "Приятно" + ещё один стимул
        ];

        // Кадры семантической памяти: формат, который использует SemanticMemory
        var semanticMemory = [
          {
            wellNormaBad: 3,        // Хорошо
            emotionId: 1,
            finalImageId: 1,
            meanImportance: 7.5,
            samplesCount: 3,
            lastImportance: 8.1
          },
          {
            wellNormaBad: 2,        // Норма
            emotionId: 2,
            finalImageId: 2,
            meanImportance: 4.2,
            samplesCount: 5,
            lastImportance: 3.9
          }
        ];

        var knownFinalImageIds = finalImages.map(function (fi) { return fi.id; });
        var knownEmotionIds = emotions.map(function (e) { return e.id; });

        var state = {
          semanticMemory: semanticMemory,
          knownFinalImageIds: knownFinalImageIds,
          knownEmotionIds: knownEmotionIds,
          finalImages: finalImages,
          emotions: emotions
        };

        // В боевом коде ключ всегда один — 'semantic_state_singleton'.
        var value = {
          id: 'semantic_state_singleton',
          state: state,
          ts: Date.now()
        };

        btnSave.disabled = true;
        setStatus('Сохранение записи…');

        IndexedDBModule.put({
          db: dbInstance,
          storeName: STORE_NAME,
          value: value
        })
          .then(function (key) {
            log('Запись сохранена. Ключ: ' + key);
            setStatus('Запись сохранена.');
          })
          .catch(function (error) {
            log('Ошибка при сохранении записи: ' + (error && error.message ? error.message : String(error)));
            setStatus('Ошибка при сохранении записи (см. лог).');
          })
          .finally(function () {
            btnSave.disabled = false;
          });
      }

      /**
       * Прочитать текущее семантическое состояние из хранилища и вывести его в лог.
       */
      function readAllRecords() {
        if (!dbInstance) {
          log('База данных ещё не инициализирована.');
          return;
        }

        btnRead.disabled = true;
        setStatus('Чтение семантического состояния…');

        // В боевом коде всегда используется один ключ — 'semantic_state_singleton'.
        IndexedDBModule.get({
          db: dbInstance,
          storeName: STORE_NAME,
          key: 'semantic_state_singleton'
        })
          .then(function (record) {
            if (!record) {
              log('Запись с ключом \"semantic_state_singleton\" не найдена.');
              setStatus('Семантическое состояние отсутствует в БД.');
              return;
            }
            log('Прочитана запись semantic_state_singleton:\n' + JSON.stringify(record, null, 2));
            setStatus('Чтение семантического состояния завершено.');
          })
          .catch(function (error) {
            log('Ошибка при чтении записей: ' + (error && error.message ? error.message : String(error)));
            setStatus('Ошибка при чтении записей (см. лог).');
          })
          .finally(function () {
            btnRead.disabled = false;
          });
      }

      // Обработчики событий для кнопок
      btnSave.addEventListener('click', saveSampleRecord);
      btnRead.addEventListener('click', readAllRecords);

      // Старт инициализации после загрузки страницы
      window.addEventListener('load', initDatabase);
    })();
  </script>
</body>
</html>


════════════════════════════════════════════════════════════

/BEAST/beast/sys/alerts/alerts.js
════════════════════════════════════════════════════════════
/*
  Модуль модальных диалогов для оператора (проект BEAST).

  Назначение:
  - Заменяет стандартные alert/confirm/prompt собственными модальными окнами.
  - Обеспечивает единый стиль и управление диалогами для интерфейса оператора.
  - Используется для интерактивного общения системы с оператором.

  Доступные функции (глобальные):
  - show_alert(message, timeShow = 0)
      Показать информационное сообщение.
      message  – HTML‑строка с текстом сообщения.
      timeShow – время автозакрытия в миллисекундах (0 – без автозакрытия).

  - show_confirm(message, callBack)
      Показать диалог подтверждения с кнопками "Да" / "Нет".
      message  – HTML‑строка с текстом вопроса.
      callBack – функция(result), куда передаётся 'OK' или 'NO'.

  - show_prompt(message, callBack)
      Показать диалог ввода текста.
      message  – HTML‑строка с поясняющим текстом.
      callBack – функция(value), вызывается только при подтверждении ввода.

  - close_alert()
      Принудительно закрыть активный диалог.

  Примеры использования см. в файле beast/sys/alerts/samples.htm.
*/

(function (global) {
  'use strict';

  // Глобальные переменные для отслеживания активного диалога и таймера автозакрытия
  var activeDialog = null;
  var closeTimeout = null;
  var activeOnClose = null;
  var lastCloseUserInitiated = false;

  /**
   * Создать (или вернуть уже существующий) затемняющий слой под диалогом.
   * @returns {HTMLElement}
   */
  function createOverlay() {
    var overlay = document.getElementById('alert-overlay');
    if (!overlay) {
      overlay = document.createElement('div');
      overlay.id = 'alert-overlay';
      overlay.style.cssText =
        'position:fixed;top:0;left:0;width:100%;height:100%;' +
        'background-color:rgba(0,0,0,0.5);z-index:9998;display:none;' +
        'cursor:pointer;margin:0;padding:0;';
      document.body.appendChild(overlay);

      // Закрытие при щелчке по фону (кроме простого alert)
      overlay.addEventListener('click', function () {
        if (activeDialog && activeDialog.dataset.type !== 'alert') {
          closeDialog();
        }
      });
    }
    return overlay;
  }

  /**
   * Создать структуру диалога.
   * @param {string} type - тип диалога: 'alert' | 'confirm' | 'prompt'
   * @returns {HTMLElement}
   */
  function createDialog(type) {
    // Закрываем существующий диалог, если он есть
    if (activeDialog) {
      closeDialog();
    }

    var dialog = document.createElement('div');
    dialog.className = 'alert-dialog';
    dialog.dataset.type = type;
    dialog.style.cssText =
      'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);' +
      'z-index:9999;background-color:#ffffff;border-radius:8px;' +
      'box-shadow:0 4px 12px rgba(0,0,0,0.25);padding:20px;padding-top:40px;' +
      'max-width:90%;max-height:90vh;overflow:auto;' +
      'font-family:Arial,sans-serif;font-size:12pt;text-align:left;' +
      'box-sizing:border-box;margin:0;';

    // Кнопка закрытия в правом верхнем углу
    var closeButton = document.createElement('button');
    closeButton.className = 'alert-close';
    closeButton.innerHTML = '&times;';
    closeButton.setAttribute('aria-label', 'Закрыть');
    closeButton.style.cssText =
      'position:absolute;top:0;right:0;background-color:#f5f5f5;' +
      'border:1px solid #ddd;border-radius:0 8px 0 0;font-size:20px;' +
      'color:#666;cursor:pointer;padding:0;margin:0;width:32px;height:32px;' +
      'display:flex;align-items:center;justify-content:center;z-index:10000;' +
      'box-sizing:border-box;';
    closeButton.addEventListener('mouseenter', function () {
      closeButton.style.backgroundColor = '#e0e0e0';
      closeButton.style.color = '#000';
      closeButton.style.borderColor = '#ccc';
    });
    closeButton.addEventListener('mouseleave', function () {
      closeButton.style.backgroundColor = '#f5f5f5';
      closeButton.style.color = '#666';
      closeButton.style.borderColor = '#ddd';
    });
    closeButton.addEventListener('click', function (e) {
      e.stopPropagation();
      lastCloseUserInitiated = true;
      closeDialog();
    });
    dialog.appendChild(closeButton);

    // Контейнер для текста сообщения
    var messageContainer = document.createElement('div');
    messageContainer.className = 'alert-message';
    messageContainer.style.cssText =
      'margin-bottom:15px;line-height:1.4;word-wrap:break-word;margin-top:0;';
    dialog.appendChild(messageContainer);

    document.body.appendChild(dialog);
    activeDialog = dialog;

    return dialog;
  }

  /**
   * Закрыть текущий диалог и скрыть фон.
   */
  function closeDialog() {
    if (closeTimeout) {
      clearTimeout(closeTimeout);
      closeTimeout = null;
    }

    if (activeDialog) {
      if (activeDialog.parentNode) {
        activeDialog.parentNode.removeChild(activeDialog);
      }
      activeDialog = null;
    }

    if (typeof activeOnClose === 'function') {
      try { activeOnClose({ userInitiated: !!lastCloseUserInitiated }); } catch (e) {}
    }
    activeOnClose = null;
    lastCloseUserInitiated = false;

    var overlay = document.getElementById('alert-overlay');
    if (overlay) {
      overlay.style.display = 'none';
    }
  }

  /**
   * Публичная функция: показать информационный диалог.
   * @param {string} message - HTML‑сообщение.
   * @param {number} [timeShow=0] - время автозакрытия в миллисекундах (0 – без автозакрытия).
   */
  function show_alert(message, timeShow) {
    if (typeof timeShow === 'undefined') {
      timeShow = 0;
    }
    var opts = arguments.length >= 3 ? arguments[2] : null;
    if (typeof opts === 'function') {
      opts = { onClose: opts };
    }
    if (!opts || typeof opts !== 'object') opts = {};
    var dialogType = opts.dialogType || 'alert';

    // Не перетираем "чужие" модалки (confirm/prompt/alert), если пытаемся показать cyclelog.
    if (dialogType === 'cyclelog' && activeDialog && activeDialog.dataset && activeDialog.dataset.type !== 'cyclelog') {
      return;
    }

    var dialog = createDialog(dialogType);
    activeOnClose = (typeof opts.onClose === 'function') ? opts.onClose : null;
    dialog.querySelector('.alert-message').innerHTML = message;

    // Если нет автозакрытия – добавляем кнопку "Закрыть"
    if (timeShow === 0) {
      var buttonsContainer = document.createElement('div');
      buttonsContainer.className = 'alert-buttons';
      buttonsContainer.style.cssText =
        'display:flex;justify-content:flex-end;gap:10px;margin-top:15px;';

      var closeButton = document.createElement('button');
      closeButton.className = 'alert-button alert-button-close';
      closeButton.textContent = 'Закрыть';
      closeButton.style.cssText =
        'padding:8px 16px;border:none;border-radius:4px;font-size:12pt;' +
        'cursor:pointer;min-width:80px;background-color:#2196F3;color:white;';
      closeButton.addEventListener('mouseenter', function () {
        closeButton.style.backgroundColor = '#0b7dda';
      });
      closeButton.addEventListener('mouseleave', function () {
        closeButton.style.backgroundColor = '#2196F3';
      });
      closeButton.addEventListener('click', function () {
        lastCloseUserInitiated = true;
        closeDialog();
      });

      buttonsContainer.appendChild(closeButton);
      dialog.appendChild(buttonsContainer);
    }

    // Автозакрытие, если указано положительное время
    if (timeShow > 0) {
      closeTimeout = global.setTimeout(closeDialog, timeShow);
    }
  }

  // Служебные экспортируемые хелперы: чтобы лог цикла не перетирал другие диалоги.
  function getActiveDialogType() {
    return activeDialog && activeDialog.dataset ? (activeDialog.dataset.type || null) : null;
  }
  global.Alert_getActiveDialogType = getActiveDialogType;

  /**
   * Публичная функция: показать диалог подтверждения.
   * @param {string} message - HTML‑сообщение.
   * @param {function(string)} callBack - обработчик результата ('OK' или 'NO').
   */
  function show_confirm(message, callBack) {
    var overlay = createOverlay();
    overlay.style.display = 'block';

    var dialog = createDialog('confirm');
    dialog.querySelector('.alert-message').innerHTML = message;

    var buttonsContainer = document.createElement('div');
    buttonsContainer.className = 'alert-buttons';
    buttonsContainer.style.cssText =
      'display:flex;justify-content:flex-end;gap:10px;margin-top:15px;';

    // Кнопка "Нет"
    var noButton = document.createElement('button');
    noButton.className = 'alert-button alert-button-no';
    noButton.textContent = 'Нет';
    noButton.style.cssText =
      'padding:8px 16px;border:none;border-radius:4px;font-size:12pt;' +
      'cursor:pointer;min-width:80px;background-color:#f44336;color:white;';
    noButton.addEventListener('mouseenter', function () {
      noButton.style.backgroundColor = '#da190b';
    });
    noButton.addEventListener('mouseleave', function () {
      noButton.style.backgroundColor = '#f44336';
    });
    noButton.addEventListener('click', function () {
      closeDialog();
      if (typeof callBack === 'function') {
        callBack('NO');
      }
    });

    // Кнопка "Да"
    var okButton = document.createElement('button');
    okButton.className = 'alert-button alert-button-ok';
    okButton.textContent = 'Да';
    okButton.style.cssText =
      'padding:8px 16px;border:none;border-radius:4px;font-size:12pt;' +
      'cursor:pointer;min-width:80px;background-color:#4CAF50;color:white;';
    okButton.addEventListener('mouseenter', function () {
      okButton.style.backgroundColor = '#45a049';
    });
    okButton.addEventListener('mouseleave', function () {
      okButton.style.backgroundColor = '#4CAF50';
    });
    okButton.addEventListener('click', function () {
      closeDialog();
      if (typeof callBack === 'function') {
        callBack('OK');
      }
    });

    buttonsContainer.appendChild(noButton);
    buttonsContainer.appendChild(okButton);
    dialog.appendChild(buttonsContainer);

    // Поддержка клавиши Escape для отмены
    var escHandler = function (e) {
      if (e.key === 'Escape' && activeDialog && activeDialog.dataset.type === 'confirm') {
        closeDialog();
        global.document.removeEventListener('keydown', escHandler);
      }
    };
    global.document.addEventListener('keydown', escHandler);
  }

  /**
   * Публичная функция: показать диалог ввода текста.
   * @param {string} message - HTML‑сообщение.
   * @param {function(string)} callBack - обработчик введённого значения.
   */
  function show_prompt(message, callBack) {
    var overlay = createOverlay();
    overlay.style.display = 'block';

    var dialog = createDialog('prompt');
    dialog.querySelector('.alert-message').innerHTML = message;

    // Поле ввода
    var input = document.createElement('input');
    input.className = 'alert-prompt-input';
    input.type = 'text';
    input.autofocus = true;
    input.style.cssText =
      'width:100%;padding:8px;border:1px solid #ccc;border-radius:4px;' +
      'font-size:12pt;box-sizing:border-box;margin-bottom:15px;';
    dialog.appendChild(input);

    var buttonsContainer = document.createElement('div');
    buttonsContainer.className = 'alert-buttons';
    buttonsContainer.style.cssText =
      'display:flex;justify-content:flex-end;gap:10px;margin-top:15px;';

    // Кнопка "Отмена"
    var cancelButton = document.createElement('button');
    cancelButton.className = 'alert-button alert-button-cancel';
    cancelButton.textContent = 'Отмена';
    cancelButton.style.cssText =
      'padding:8px 16px;border:none;border-radius:4px;font-size:12pt;' +
      'cursor:pointer;min-width:80px;background-color:#9e9e9e;color:white;';
    cancelButton.addEventListener('mouseenter', function () {
      cancelButton.style.backgroundColor = '#888888';
    });
    cancelButton.addEventListener('mouseleave', function () {
      cancelButton.style.backgroundColor = '#9e9e9e';
    });
    cancelButton.addEventListener('click', function () {
      closeDialog();
      // При отмене callback не вызывается
    });

    // Кнопка "OK"
    var okButton = document.createElement('button');
    okButton.className = 'alert-button alert-button-ok';
    okButton.textContent = 'OK';
    okButton.style.cssText =
      'padding:8px 16px;border:none;border-radius:4px;font-size:12pt;' +
      'cursor:pointer;min-width:80px;background-color:#4CAF50;color:white;';
    okButton.addEventListener('mouseenter', function () {
      okButton.style.backgroundColor = '#45a049';
    });
    okButton.addEventListener('mouseleave', function () {
      okButton.style.backgroundColor = '#4CAF50';
    });
    okButton.addEventListener('click', function () {
      var value = input.value;
      closeDialog();
      if (typeof callBack === 'function' && value !== '') {
        callBack(value);
      }
    });

    // Поддержка Enter в поле ввода
    input.addEventListener('keypress', function (e) {
      if (e.key === 'Enter') {
        okButton.click();
      }
    });

    buttonsContainer.appendChild(cancelButton);
    buttonsContainer.appendChild(okButton);
    dialog.appendChild(buttonsContainer);

    // Поддержка Escape для отмены
    var escHandler = function (e) {
      if (e.key === 'Escape' && activeDialog && activeDialog.dataset.type === 'prompt') {
        closeDialog();
        global.document.removeEventListener('keydown', escHandler);
      }
    };
    global.document.addEventListener('keydown', escHandler);
  }

  /**
   * Публичная функция: принудительно закрыть активный диалог.
   */
  function close_alert() {
    closeDialog();
  }

  // Экспорт функций в глобальное пространство имён
  global.show_alert = show_alert;
  global.show_confirm = show_confirm;
  global.show_prompt = show_prompt;
  global.close_alert = close_alert;
})(window);


════════════════════════════════════════════════════════════

/BEAST/beast/sys/alerts/samples.htm
════════════════════════════════════════════════════════════
<!--
  Пример использования модальных диалогов show_alert(), show_confirm(), show_prompt()
  из модуля beast/sys/alerts/alerts.js.

  Назначение файла:
  - Показать, как система живого существа может выводить информирующие,
    уточняющие и запрашивающие ввод сообщения оператору.
  - Демонстрация всех трёх типов диалогов с отображением результатов на странице.

  Весь JavaScript-код примера находится в этом файле (в теге <script>),
  кроме общего модуля alerts.js, подключаемого отдельным тегом <script src="alerts.js">.
-->
<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Примеры модальных диалогов (BEAST)</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      padding: 20px;
      max-width: 1200px;
      margin: 0 auto;
      background-color: #f5f5f5;
    }
    h1 {
      font-size: 22pt;
      margin-bottom: 20px;
    }
    .test-section {
      background: white;
      padding: 25px;
      margin-bottom: 25px;
      border-radius: 8px;
      box-shadow: 0 2px 6px rgba(0,0,0,0.1);
    }
    .test-title {
      font-size: 18pt;
      margin-bottom: 20px;
      color: #333;
    }
    .test-buttons {
      display: flex;
      flex-wrap: wrap;
      gap: 15px;
      margin-top: 15px;
    }
    .test-button {
      padding: 12px 24px;
      background-color: #2196F3;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 14px;
    }
    .test-button:hover {
      background-color: #0b7dda;
    }
    .test-button.warning {
      background-color: #ff9800;
    }
    .test-button.warning:hover {
      background-color: #fb8c00;
    }
    .result-area {
      margin-top: 20px;
      padding: 15px;
      background-color: #e8f5e9;
      border-radius: 4px;
      min-height: 40px;
      display: none;
    }
    .result-title {
      font-weight: bold;
      margin-bottom: 8px;
      color: #2e7d32;
    }
    .result-content {
      word-break: break-word;
    }
    @media (max-width: 768px) {
      .test-buttons {
        flex-direction: column;
      }
      .test-button {
        width: 100%;
      }
    }
  </style>
</head>
<body>
  <h1>Тестирование модальных диалогов show_alert / show_confirm / show_prompt</h1>

  <!-- Alert Tests -->
  <div class="test-section">
    <div class="test-title">Тесты show_alert()</div>
    <div class="test-buttons">
      <button class="test-button"
              onclick="show_alert('Простое текстовое сообщение для оператора', 0)">
        Alert без автозакрытия
      </button>
      <button class="test-button"
              onclick="show_alert('Сообщение закроется через 3 секунды', 3000)">
        Alert с автозакрытием (3с)
      </button>
      <button class="test-button"
              onclick="show_alert('<strong>HTML</strong> форматирование<br><span style=&quot;color:red;&quot;>Красный текст</span><br><em>Курсив</em>', 0)">
        Alert с HTML
      </button>
      <button class="test-button"
              onclick="show_alert('Очень длинное сообщение для проверки адаптивности на мобильных устройствах. Текст должен корректно переноситься и не выходить за границы диалогового окна даже на узких экранах. Это важно для интерфейса оператора, работающего на разных типах устройств.', 0)">
        Длинное сообщение
      </button>
    </div>
  </div>

  <!-- Confirm Tests -->
  <div class="test-section">
    <div class="test-title">Тесты show_confirm()</div>
    <div class="test-buttons">
      <button class="test-button" onclick="testConfirm()">
        Подтверждение с обработкой результата
      </button>
      <button class="test-button"
              onclick="show_confirm('<h3>Важный вопрос системы</h3><p>Вы уверены, что хотите продолжить текущее действие?</p><ul><li>Пункт 1</li><li>Пункт 2</li></ul>', function(result){ console.log('Результат:', result); })">
        Confirm с HTML
      </button>
    </div>
    <div id="confirm-result" class="result-area">
      <div class="result-title">Результат подтверждения:</div>
      <div class="result-content" id="confirm-content"></div>
    </div>
  </div>

  <!-- Prompt Tests -->
  <div class="test-section">
    <div class="test-title">Тесты show_prompt()</div>
    <div class="test-buttons">
      <button class="test-button" onclick="testPrompt()">
        Prompt с обработкой ввода
      </button>
      <button class="test-button"
              onclick="show_prompt('<strong>Введите ваше имя:</strong><br><small>(можно использовать пробелы)</small>', function(value){ alert('Введено: ' + value); })">
        Prompt с HTML в сообщении
      </button>
    </div>
    <div id="prompt-result" class="result-area">
      <div class="test-title">Результат ввода:</div>
      <div class="result-content" id="prompt-content"></div>
    </div>
  </div>

  <!-- Close Alert Test -->
  <div class="test-section">
    <div class="test-title">Тест закрытия алертов</div>
    <div class="test-buttons">
      <button class="test-button"
              onclick="show_alert('Этот алерт можно закрыть через 5 секунд кнопкой ниже', 0)">
        Показать алерт
      </button>
      <button class="test-button warning" onclick="close_alert()">
        Принудительно закрыть алерт
      </button>
    </div>
  </div>

  <!-- Подключаем модуль с реализацией диалогов -->
  <script src="alerts.js"></script>

  <!-- Основная логика примера (весь JS этого файла находится здесь) -->
  <script>
    /**
     * Пример использования show_confirm() с отображением результата на странице.
     */
    function testConfirm() {
      show_confirm('Вы действительно хотите выполнить это действие?', function (result) {
        var resultArea = document.getElementById('confirm-result');
        var content = document.getElementById('confirm-content');
        content.innerHTML = '<strong>Выбрано:</strong> ' + result;
        resultArea.style.display = 'block';
        resultArea.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
      });
    }

    /**
     * Пример использования show_prompt() с обработкой введённого текста.
     */
    function testPrompt() {
      show_prompt('Введите ваш комментарий:', function (value) {
        var resultArea = document.getElementById('prompt-result');
        var content = document.getElementById('prompt-content');
        // Экранируем угловые скобки, чтобы не ломать HTML
        var safeValue = String(value).replace(/</g, '&lt;').replace(/>/g, '&gt;');
        content.innerHTML = '<strong>Введено:</strong> ' + safeValue;
        resultArea.style.display = 'block';
        resultArea.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
      });
    }
  </script>
</body>
</html>


════════════════════════════════════════════════════════════

/BEAST/index.html
════════════════════════════════════════════════════════════
<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <title>Существо Beast, пульт связи</title>
  <style>
    html, body {
      height: 100%;
      margin: 0;
      padding: 0;
    }

    body {
      font-family: Arial, sans-serif;
      font-size: 12pt;
      background-color: #e5e5e5; /* светло-серый фон по краям */
      display: flex;
      justify-content: center;
      align-items: stretch;
    }

    .beast-root {
      margin: 0 30px; /* по 30px слева и справа */
      background-color: #ffffff; /* белый рабочий экран */
      flex: 1 1 auto;
      display: flex;
      flex-direction: column;
      box-sizing: border-box;
      min-width: 0;
    }

    .beast-top-bar {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 5px 10px;
      box-sizing: border-box;
      height: 30px;
    }

    .beast-left-indicator {
      display: flex;
      align-items: center;
      gap: 6px;
    }

    .beast-pulse-circle {
      width: 15px;
      height: 15px;
      border-radius: 50%;
      background-color: #00aa00;
      box-shadow: 0 0 4px rgba(0, 170, 0, 0.8);
      transition: background-color 0.15s ease, box-shadow 0.15s ease;
    }

    .beast-pulse-circle.off {
      background-color: #006600;
      box-shadow: none;
    }

    .beast-pulse-counter {
      font-size: 11pt;
    }

    .beast-header-title {
      text-align: center;
      font-weight: bold;
      flex: 1 1 auto;
    }

    .beast-right-buttons {
      display: flex;
      flex-direction: column;
      gap: 2px;
      align-items: flex-end;
    }

    .beast-right-buttons button {
      height: 15px;
      line-height: 13px;
      padding: 0 8px;
      font-size: 10pt;
      border-radius: 3px;
      border: 1px solid #888;
      background-color: #f5f5f5;
      cursor: pointer;
    }

    .beast-right-buttons button:hover {
      background-color: #e0e0e0;
    }

    .beast-save-row {
      display: flex;
      flex-direction: row;
      align-items: center;
      gap: 2px;
    }
    .beast-save-indicator {
      width: 10px;
      height: 10px;
      border-radius: 50%;
      background-color: #4A9EFF;
      opacity: 0;
      transition: opacity 0.15s ease;
      flex-shrink: 0;
    }
    .beast-save-indicator.is-saving {
      opacity: 1;
    }

    .beast-content {
      flex: 1 1 auto;
      box-sizing: border-box;
      padding: 10px;
    }

    /* Индикаторы базовых контекстов: неактивный — серый, активный — светло-зелёный + зелёное свечение */
    .basic-context-indicator {
      background-color: #d8d8d8;
      box-shadow: none;
    }
    .basic-context-indicator.active {
      background-color: #b8e8b8;
      box-shadow: 0 0 12px rgba(0, 200, 80, 0.7);
    }

    .ep-waiting-progress-wrap {
      width: 100%;
      height: 2px;
      background: #e0e0e0;
      margin-bottom: 2px;
      overflow: hidden;
      border: none;
      box-sizing: border-box;
    }
    .ep-waiting-progress-fill {
      height: 100%;
      width: 0%;
      background: #1976D2;
      transition: width 0.2s linear;
      border: none;
      box-sizing: border-box;
    }
    .pt-episodes-total-label {
      margin-left: 6px;
      font-size: 9pt;
      color: #444;
      white-space: nowrap;
    }
    .perception-tree-active-branch {
      border: 1px solid #DDD700;
      background-color: #FFFFE1;
      padding: 4px 10px;
      font-size: 10pt;
      margin-bottom: 4px;
      box-sizing: border-box;
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 8px;
    }
    .pt-branch-content {
      flex: 1 1 auto;
      min-width: 0;
      display: flex;
      flex-direction: column;
      gap: 2px;
    }
    .pt-branch-first-row {
      display: flex;
      align-items: center;
      gap: 6px;
      min-width: 0;
    }
    .pt-branch-text {
      min-width: 0;
      flex: 1 1 auto;
    }
    .pt-test-mode-btn {
      flex: 0 0 auto;
      padding: 0 5px;
      height: 18px;
      min-width: 20px;
      border: 1px solid #888;
      border-radius: 2px;
      background: #f5f5f5;
      font-size: 10pt;
      cursor: pointer;
      line-height: 16px;
    }
    .pt-test-mode-btn:hover {
      background: #e8e8e8;
    }
    .pt-test-mode-btn.active {
      background: #ffcdd2;
      border-color: #c62828;
      color: #b71c1c;
    }
    .pt-episodes-row {
      display: flex;
      align-items: center;
      flex-wrap: wrap;
      gap: 4px;
      min-width: 0;
    }
    .pt-episodes-line {
      font-size: 9pt;
      color: #555;
      min-width: 0;
      flex: 0 1 auto;
    }
    .pt-episodes-clear-btn {
      flex: 0 0 auto;
      padding: 0 4px;
      height: 16px;
      min-width: 16px;
      border: 1px solid #c62828;
      border-radius: 2px;
      background: #ffebee;
      color: #c62828;
      font-size: 9pt;
      line-height: 14px;
      cursor: pointer;
      font-weight: bold;
      vertical-align: middle;
    }
    .pt-episodes-clear-btn:hover {
      background: #ffcdd2;
    }
    .pt-episodes-line .pt-ep-frame {
      cursor: pointer;
      text-decoration: underline;
      color: #1565C0;
      margin-right: 6px;
    }
    .pt-episodes-line .pt-ep-frame:hover {
      color: #0D47A1;
    }
    .pt-branch-copy-btn {
      flex: 0 0 auto;
      width: 22px;
      height: 22px;
      padding: 0;
      border-radius: 50%;
      border: 1px solid #aaa;
      background-color: #f0f0f0;
      color: #555;
      font-size: 12px;
      line-height: 20px;
      text-align: center;
      cursor: pointer;
    }
    .pt-branch-copy-btn:hover {
      background-color: #e0e0e0;
    }
    .perception-tree-active-branch a {
      color: #1565C0;
      cursor: pointer;
      text-decoration: underline;
    }
    .perception-tree-active-branch a:hover {
      color: #0D47A1;
    }

    .beast-activity-wrapper {
      margin-top: 10px;
    }

    .beast-activity-title-row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 2px;
    }

    .beast-activity-title {
      font-size: 10pt;
    }
    .beast-activity-title-buttons {
      display: flex;
      align-items: center;
    }
    .beast-activity-copy-icon {
      width: 22px;
      height: 22px;
      padding: 0;
      border-radius: 50%;
      border: 1px solid #aaa;
      background-color: #f0f0f0;
      color: #555;
      font-size: 12px;
      line-height: 20px;
      text-align: center;
      cursor: pointer;
      margin-right: 4px;
    }
    .beast-activity-copy-icon:hover {
      background-color: #e0e0e0;
    }
    .beast-activity-reset-ur {
      font-size: 10pt;
      padding: 2px 8px;
      border-radius: 3px;
      border: 1px solid #888;
      background-color: #f5f5f5;
      cursor: pointer;
    }
    .beast-activity-reset-ur:hover {
      background-color: #e0e0e0;
    }

    .beast-activity-block {
      height: 50px;
      width: 100%;
      background-color: #EEEEEE;
      box-sizing: border-box;
      display: flex;
      align-items: flex-start;
      justify-content: flex-start;
      padding: 4px 10px 0 10px;
      font-size: 11pt;
      white-space: pre-line;
    }

    .info-picture {
      margin-top: 8px;
      border: 1px solid #ccc;
      background-color: #f9f9f9;
      padding: 4px 6px;
      font-size: 9pt;
      box-sizing: border-box;
    }
    .info-picture-title {
      margin-top: 8px;
      font-size: 9pt;
      font-weight: bold;
    }
    .info-thinking-indicator {
      display: inline-block;
      margin-left: 6px;
      width: 10px;
      height: 10px;
      border-radius: 50%;
      background-color: #4A9EFF;
      opacity: 0;
      transition: opacity 0.15s ease;
      vertical-align: middle;
    }
    .info-thinking-indicator.on {
      opacity: 1;
    }
    .info-bg-count-label {
      margin-left: 6px;
      font-size: 9pt;
    }
    .info-bg-stack-indicator {
      display: inline-block;
      margin-left: 0;
      padding: 1px 4px;
      font-size: 8pt;
      border-radius: 4px;
      border: 1px solid #4A9EFF;
      background-color: #eef6ff;
      cursor: pointer;
    }
    .info-indicator-wrap {
      position: relative;
      display: inline-block;
      margin-left: 6px;
      vertical-align: middle;
    }
    .info-indicator-clear {
      position: absolute;
      top: -5px;
      right: -5px;
      z-index: 2;
      width: 14px;
      height: 14px;
      margin: 0;
      padding: 0;
      line-height: 12px;
      font-size: 11px;
      font-weight: bold;
      color: #c62828;
      background: #fff;
      border: 1px solid #c62828;
      border-radius: 50%;
      cursor: pointer;
      box-sizing: border-box;
    }
    .info-indicator-clear:hover {
      background: #ffebee;
    }
    .info-picture-row {
      display: flex;
      flex-direction: row;
      gap: 4px;
      margin-bottom: 2px;
    }
    .info-cell {
      flex: 1 1 0;
      min-width: 0;
      padding: 2px 4px;
      border: 1px solid #ddd;
      background-color: #ffffff;
      cursor: pointer;
      text-align: center;
      box-sizing: border-box;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .info-cell-active {
      background-color: #FFF9C4; /* светло-жёлтый */
    }
  </style>
</head>
<body>
  <div class="beast-root">
    <div class="beast-top-bar">
      <div class="beast-left-indicator">
        <div id="pulseCircle"
             class="beast-pulse-circle"
             title="Жизнь существа измеряется числом пульсов."></div>
        <span class="beast-pulse-counter" id="pulseCounter">0</span>
      </div>
      <div class="beast-header-title">
        Существо Beast, пульт связи
      </div>
      <div class="beast-right-buttons">
        <div class="beast-save-row">
          <div class="beast-save-indicator" id="saveIndicator" title="Идёт запись в IndexedDB или на диск"></div>
          <button id="btnSaveMemory" type="button" title="Сохранить память Beast на компьютере в выбранной папке.">Сохранить</button>
        </div>
        <button id="btnLoadMemory" type="button"  title="загрузить память Beast из выбранной папки.">Загрузить</button>
      </div>
    </div>
    <div class="beast-content">
      <div id="basicContextsRow"></div>
      <div class="ep-waiting-progress-wrap" id="epWaitingProgressWrap" title="Ожидание ответа (response_waiting_period)">
        <div class="ep-waiting-progress-fill" id="epWaitingProgressFill"></div>
      </div>
      <div id="perceptionTreeActiveBranch" class="perception-tree-active-branch">
        <div class="pt-branch-content">
          <div class="pt-branch-first-row">
            <div id="perceptionTreeActiveBranchText" class="pt-branch-text"></div>
            <button type="button" id="btnEpTestMode" class="pt-test-mode-btn" title="Режим тестирования формирования эпизодической памяти">т</button>
          </div>
          <div class="pt-episodes-row">
            <div id="perceptionTreeEpisodesLine" class="pt-episodes-line"></div>
            <button type="button" id="btnClearEpisodicMemory" class="pt-episodes-clear-btn" title="Удалить всю эпизодическую память" style="display: none;">✕</button>
            <span id="epTotalCountLabel" class="pt-episodes-total-label"></span>
          </div>
        </div>
        <button id="btnCopyActiveBranch" type="button" class="pt-branch-copy-btn" title="Скопировать содержимое блока активной ветки">📋</button>
      </div>
      <div class="beast-activity-wrapper">
        <div class="beast-activity-title-row">
          <div class="beast-activity-title">Активность Beast</div>
          <div class="beast-activity-title-buttons">
            <button id="btnCopyActivityBlock" type="button" class="beast-activity-copy-icon" title="Скопировать содержимое блока Активность Beast">📋</button>
            <button id="btnResetConditionalReflexes" type="button" class="beast-activity-reset-ur" title="Сбросить все условные рефлексы">Ух</button>
            <button id="btnResetSemanticMemory" type="button" class="beast-activity-reset-ur" title="Сброс всей семантической памяти">Sx</button>
          </div>
        </div>
        <div id="beastActivityBlock" class="beast-activity-block"></div>
      </div>
      <div class="info-picture-title" title="Картина информированности о текущем состоянии организма и ситуациях">
        Информационная картина:
        <span id="infoThinkingIndicator" class="info-thinking-indicator"></span>
        <span id="infoMainProcessLabel" class="info-bg-count-label">Главный цикл ID: 0</span>
        <span id="infoBackgroundCountLabel" class="info-bg-count-label">Чисто фоновых процессов: 0</span>
        <span id="infoInterruptQueueLabel" class="info-bg-count-label" title="Число прерванных главных циклов в очереди на возврат (LIFO)">очередь прерываний: 0</span>
        <span id="infoBackgroundStackIndicator" class="info-bg-stack-indicator" onclick="Info_showBackgroundStack()">фон: 0</span>
        <span class="info-indicator-wrap">
          <span id="infoMotorAutomatizmsIndicator" class="info-bg-stack-indicator" onclick="Info_showMotorAutomatizmsList()">авто: 0</span>
          <button type="button" class="info-indicator-clear" title="Удалить все моторные автоматизмы" aria-label="Удалить все моторные автоматизмы" onclick="event.stopPropagation(); Info_clearMotorAutomatizms(event);">×</button>
        </span>
        <span class="info-indicator-wrap">
          <span id="infoMentalAutomatizmsIndicator" class="info-bg-stack-indicator" onclick="Info_showMentalAutomatizmsList()">м-авто: 0</span>
          <button type="button" class="info-indicator-clear" title="Удалить все ментальные автоматизмы" aria-label="Удалить все ментальные автоматизмы" onclick="event.stopPropagation(); Info_clearMentalAutomatizms(event);">×</button>
        </span>
        <span class="info-indicator-wrap">
          <span id="infoAbstractionsIndicator" class="info-bg-stack-indicator" onclick="Info_showAbstractionsList()">абстр: 0</span>
          <button type="button" class="info-indicator-clear" title="Удалить все абстракции" aria-label="Удалить все абстракции" onclick="event.stopPropagation(); Info_clearAbstractions(event);">×</button>
        </span>
        <span class="info-indicator-wrap">
          <span id="infoLifeGoalsIndicator" class="info-bg-stack-indicator" onclick="Info_showLifeGoalsList()">цели: 0</span>
          <button type="button" class="info-indicator-clear" title="Удалить все образы целей жизненного опыта" aria-label="Удалить все образы целей жизненного опыта" onclick="event.stopPropagation(); Info_clearLifeGoals(event);">×</button>
        </span>
        <span class="info-indicator-wrap">
          <span id="infoSituationImagesIndicator" class="info-bg-stack-indicator" onclick="Info_showSituationImagesList()">ситуации: 0</span>
          <button type="button" class="info-indicator-clear" title="Удалить все образы ситуаций (SituationImages)" aria-label="Удалить все образы ситуаций" onclick="event.stopPropagation(); Info_clearSituationImages(event);">×</button>
        </span>
        <span class="info-indicator-wrap">
          <span id="infoGestaltsIndicator" class="info-bg-stack-indicator" onclick="Info_showGestaltsList()">гешт: 0</span>
        </span>
        <span class="info-indicator-wrap">
          <span id="infoFantasmPlotIndicator" class="info-bg-stack-indicator" onclick="Info_showFantasmPlotDetail()">фант: 0</span>
        </span>
      </div>
      <div id="infoPictureBlock" class="info-picture">
        <div class="info-picture-row">
          <div id="infoModeCell" class="info-cell" onclick="Info_showDetail('mode')">Режим</div>
          <div id="infoThemeCell" class="info-cell" onclick="Info_showDetail('theme')">Тема</div>
          <div id="infoSituationCell" class="info-cell" onclick="Info_showDetail('situation')">Ситуация</div>
          <div id="infoFantasmPlotCell" class="info-cell" onclick="Info_showDetail('fantasmPlot')">Сюжет фантазма</div>
        </div>
        <div class="info-picture-row">
          <div id="infoProblemCell" class="info-cell" onclick="Info_showDetail('problem')">Проблема</div>
          <div id="infoStepCell" class="info-cell" onclick="Info_showDetail('step')">Шаг</div>
          <div id="infoStimulusCell" class="info-cell" onclick="Info_showDetail('stimulus')">Стимул</div>
          <div id="infoBaseCell" class="info-cell" onclick="Info_showDetail('tree')">Base</div>
          <div id="infoEmotionCell" class="info-cell" onclick="Info_showDetail('emotion')">Эмоция</div>
          <div id="infoActivityCell" class="info-cell" onclick="Info_showDetail('activity')">Активность</div>
        </div>
        <div class="info-picture-row">
          <div id="infoDetCriticalVital" class="info-cell" onclick="Info_showDetail('criticalVital')">Крит. витал</div>
          <div id="infoDetImportantProblem" class="info-cell" onclick="Info_showDetail('ImportantProblem')">Важная проблема</div>
          <div id="infoDetUsedRule" class="info-cell" onclick="Info_showDetail('usedRule')">Реагирование по правилу</div>
          <div id="infoDetShockState" class="info-cell" onclick="Info_showDetail('shockState')">Шок</div>
          <div id="infoDetSuddenChange" class="info-cell" onclick="Info_showDetail('suddenChange')">Резко хуже</div>
          <div id="infoDetPlayMode" class="info-cell" onclick="Info_showDetail('playMode')">Игра</div>
          <div id="infoDetLearningMode" class="info-cell" onclick="Info_showDetail('learningMode')">Обучение</div>
          <div id="infoDetNegativeMotor" class="info-cell" onclick="Info_showDetail('negativeMotorEffect')">Нег. мотор</div>
        </div>
        <div class="info-picture-row">
          <div id="infoDetNegativeMental" class="info-cell" onclick="Info_showDetail('negativeMentalEffect')">Нег. ментал</div>
          <div id="infoDetBadState" class="info-cell" onclick="Info_showDetail('badState')">Плохо</div>
          <div id="infoDetConsoleStimulus" class="info-cell" onclick="Info_showDetail('consoleStimulus')">С пульта</div>
          <div id="infoDetSearchInterest" class="info-cell" onclick="Info_showDetail('searchInterest')">Поиск</div>
          <div id="infoDetTeacherLearning" class="info-cell" onclick="Info_showDetail('teacherLearning')">Учитель</div>
          <div id="infoDetOperatorIgnoring" class="info-cell" onclick="Info_showDetail('operatorIgnoring')">Игнор</div>
        </div>
        <div class="info-picture-row">
          <div id="infoDetDissatisfaction" class="info-cell" onclick="Info_showDetail('dissatisfaction')">Неудовл.</div>
          <div id="infoDetMisunderstanding" class="info-cell" onclick="Info_showDetail('misunderstanding')">Непоним.</div>
          <div id="infoDetDoubtAuto" class="info-cell" onclick="Info_showDetail('doubtInDefaultAutomatism')">Сомнение</div>
          <div id="infoDetDefense" class="info-cell" onclick="Info_showDetail('defense')">Защита</div>
          <div id="infoDetFear" class="info-cell" onclick="Info_showDetail('fear')">Страх</div>
        </div>
        <div class="info-picture-row">
          <div id="infoDetAggression" class="info-cell" onclick="Info_showDetail('aggression')">Агрессия</div>
          <div id="infoDetHighObj" class="info-cell" onclick="Info_showDetail('highImportanceObject')">Важный объект</div>
          <div id="infoDetMoodImproving" class="info-cell" onclick="Info_showDetail('moodImproving')">Настр.↑</div>
          <div id="infoDetImageNovelty" class="info-cell" onclick="Info_showDetail('imageNovelty')">Новизна</div>
          <div id="infoDetHighSem" class="info-cell" onclick="Info_showDetail('highSemanticImportance')">Выс. семантика</div>
          <div id="infoDetConfSem" class="info-cell" onclick="Info_showDetail('conflictingSemantics')">Противор.</div>
        </div>
        <div class="info-picture-row">
          <div id="infoDetHardProblem" class="info-cell" onclick="Info_showDetail('hardProblem')">Трудн. проблема</div>
          <div id="infoDetPosEffect" class="info-cell" onclick="Info_showDetail('positiveActionEffect')">Эффект +</div>
          <div id="infoDetNegEffect" class="info-cell" onclick="Info_showDetail('negativeActionEffect')">Эффект −</div>
          <div id="infoDetNoReaction" class="info-cell" onclick="Info_showDetail('noReactionToStimulus')">Нет реакции</div>
          <div id="infoDetAnswer" class="info-cell" onclick="Info_showDetail('operatorAnswerReceived')">Ответ</div>
          <div id="infoDetActiveWait" class="info-cell" onclick="Info_showDetail('activeWaitingAnswer')">Ожидание</div>
        </div>
        <div class="info-picture-row">
          <div id="infoDetSeriesNoEffect" class="info-cell" onclick="Info_showDetail('seriesWithoutEffect')">Серия без эфф.</div>
          <div id="infoDetLowAutoUse" class="info-cell" onclick="Info_showDetail('lowAutomatismUsefulness')">Низ. пол. авт.</div>
          <div id="infoDetFoundAuto" class="info-cell" onclick="Info_showDetail('foundSuitableAutomatism')">Нашёл авт.</div>
        </div>
      </div>
    </div>
  </div>

  <script>
  (function () {
    var MIN_DISPLAY_MS = 300;
    var showTime = 0;
    var saveCount = 0;
    var hideTimer = null;
    function el() { return document.getElementById('saveIndicator'); }
    window.showSaveIndicator = function () {
      saveCount++;
      var e = el();
      if (e) e.classList.add('is-saving');
      if (saveCount === 1) showTime = Date.now();
      if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; }
    };
    window.hideSaveIndicator = function () {
      saveCount = saveCount > 0 ? saveCount - 1 : 0;
      if (saveCount > 0) return;
      var e = el();
      if (!e) return;
      var delay = Math.max(0, MIN_DISPLAY_MS - (Date.now() - showTime));
      function hide() {
        e.classList.remove('is-saving');
        hideTimer = null;
      }
      if (delay > 0) hideTimer = setTimeout(hide, delay);
      else hide();
    };
  })();
  </script>
  <!-- Порядок скриптов обязателен. Подключение — в конце body (DOM уже готов).
       При добавлении нового модуля: вставить в нужное место и обновить этот список.
       Порядок: sys/alerts → IndexedDB → vitals (vitals, vitals_prepare, vitals_ui) →
       BasicContexts, prepare, basic_contexts_ui → effectors → genetic_reflexes, genetic_reflexes_engine →
       synonyms_reflexes → stimuls, stimulus_global_id, stimuls_ui → Emotion, FinalImage, semantic_memory, Understanding_model →
       perception_tree → hippocampus, orientation_reflex → consciousness_dispatcher → conscious_attention_channel →
       target_rule_stack → episodic_memory → actions_image → saving_all → puls → saving_ui -->
  <script src="beast/sys/alerts/alerts.js"></script>
  <script src="beast/sys/IndexedDB/indexeddb.js"></script>
  <script src="beast/_1_Genetic_vitals/vitals.js"></script>
  <script src="beast/_1_Genetic_vitals/vitals_prepare.js"></script>
  <script src="beast/_1_Genetic_vitals/vitals_ui.js"></script>
  <script src="beast/_2_Genetic_basic_styles/BasicContexts.js"></script>
  <script src="beast/_2_Genetic_basic_styles/prepare.js"></script>
  <script src="beast/_2_Genetic_basic_styles/basic_contexts_ui.js"></script>
  <script src="beast/_4_actions/effectors.js"></script>
  <script src="beast/_5_Genetic__reflexes/genetic_reflexes.js"></script>
  <script src="beast/_5_Genetic__reflexes/genetic_reflexes_engine.js"></script>
  <script src="beast/_5_2_synonyms_reflexes/synonyms_reflexes.js"></script>
  <script src="beast/_3_perception/stimuls.js"></script>
  <script src="beast/_3_perception/stimulus_global_id.js"></script>
  <script src="beast/_3_perception/stimuls_ui.js"></script>
  <script src="beast/_6_semantic_memory/Emotion.js"></script>
  <script src="beast/_6_semantic_memory/FinalImage.js"></script>
  <script src="beast/_6_semantic_memory/semantic_memory.js"></script>
  <script src="beast/_6_semantic_memory/Understanding_model.js"></script>
  <script src="beast/_7_perception_tree/perception_tree.js"></script>
  <script src="beast/_8_Hippocampus/hippocampus.js"></script>
  <script src="beast/_8_Hippocampus/orientation_reflex.js"></script>
  <script src="beast/_9_Conscience/consciousness_dispatcher.js"></script>
  <script src="beast/_9_Conscience/infofunctions.js"></script>
  <script src="beast/_9_Conscience/conscious_attention_channel.js"></script>
  <script src="beast/_9_Conscience/goals.js"></script>
  <script src="beast/_9_Conscience/mental_automatism.js"></script>
  <script src="beast/_9_Conscience/conscience_level_2.js"></script>
  <script src="beast/_9_Conscience/conscience_level_3.js"></script>
  <script src="beast/_9_Conscience/conscience_level_4.js"></script>
  <script src="beast/_9_Conscience/gestalt.js"></script>
  <script src="beast/_9_Conscience/target_rule_stack.js"></script>
  <script src="beast/_9_Conscience/informing.js"></script>
  <script src="beast/_9_Conscience/info_detectors.js"></script>
  <script src="beast/_9_Conscience/situatioms_tree.js"></script>
  <script src="beast/_9_Conscience/abstract_images.js"></script>
  <script src="beast/_10_Episodic_memory/episodic_memory.js"></script>
  <script src="beast/_9_Conscience/fantasizming.js"></script>
  <script src="beast/_10_actions_image/actions_image.js"></script>
  <script src="beast/_11_Automatizms/automatizms.js"></script>
  <script src="beast/date/saving_all/saving_all.js"></script>
  <script src="beast/puls.js"></script>
  <script src="beast/date/saving_ui.js"></script>
</body>
</html>


════════════════════════════════════════════════════════════

/BEAST/manuals/Mad_programmer_manual.htm
════════════════════════════════════════════════════════════
<!DOCTYPE html>
                <html lang="en">
                <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>Markdown Export</title>
                <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.3.0/github-markdown.min.css">
                <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
                <style>
                    body {
                        background-color: #ffffff;
                        color: #24292e;
                    }
                    .markdown-body {
                        box-sizing: border-box;
                        min-width: 200px;
                        max-width: 980px;
                        margin: 0 auto;
                        padding: 45px;
                        background-color: #ffffff;
                        color: #24292e;
                    }
                    @media (max-width: 767px) {
                        .markdown-body {
                            padding: 15px;
                        }
                    }
                </style>
                </head>
                <body>
                <article class="markdown-body">
                    <h1>Mad Programmer Manual — Beast</h1>
<p>Руководство для того, кто <strong>всерьёз</strong> решил разобраться в коде <strong>Beast</strong> («Существо Beast, пульт связи») и продолжить его развитие. Это не замена исходников и не дублирование каждого модуля — это <strong>карта мышления</strong>, принципы и <strong>где искать правду</strong> (в коде и в <code>about_*.md</code>).</p>
<hr>
<h2>1. Зачем этот документ</h2>
<ul>
<li>Проект <strong>большой</strong>, связи <strong>неочевидны</strong>, порядок загрузки скриптов <strong>критичен</strong>.</li>
<li>Многое живёт в <strong><code>window</code> / глобальных переменных</strong> — без ментальной модели легко сломать то, что «случайно работало».</li>
</ul>
<p>Если правите архитектуру — обновляйте <strong><code>PROJECT_STATE_FOR_NEW_CHAT.md</code></strong> и соответствующие <strong><code>beast/**/about_*.md</code></strong>.</p>
<hr>
<h2>2. Что такое Beast концептуально</h2>
<p><strong>Beast</strong> — браузерное приложение, моделирующее связку <strong>организм ↔ стимулы ↔ ориентировочный рефлекс (ОР) ↔ процесс осмысления (ФО) ↔ эпизодическая память (ЭП)</strong>.</p>
<p>Принципы:</p>
<ol>
<li><strong>Время дискретно</strong> — «пульс» (<code>global.cpuls</code>, <code>Cur_puls_val</code>). Почти всё важное привязано к пульсу или к событию UI между пульсами.</li>
<li><strong>Состояние тела</strong> — виталы, выход из нормы, интегральная значимость, базовое состояние <strong>Плохо / Норма / Хорошо</strong> (<code>BadNormWell</code>).</li>
<li><strong>Контексты</strong> — сокращённая эмоционально-поведенческая разметка (базовые контексты), из них строится «эмоция» и ветка дерева.</li>
<li><strong>Стимулы</strong> — типизированы (мотивация, позитив, негатив, нейтрал); у глобальных id <strong>нет коллизий</strong> между массивами (см. <code>StimulusGlobalId_*</code>).</li>
<li><strong>ОР</strong> — конкуренция стимулов по актуальности, порог «зрелости» семантики (<code>OR_well_known</code>), исключения для отдельных сценариев (см. код ОР).</li>
<li><strong>ФО</strong> — не один вызов, а <strong>реестр циклов</strong> (<code>ConsciousnessCycles</code>), диспетчер шагов, главный vs фоновые циклы, <code>expired</code>, лог цикла.</li>
<li><strong>Память</strong> — семантика (значимость по условиям), эпизоды (кадры с <code>effect</code>, стимул, действие, ответ оператора), абстракции и правила для высоких уровней.</li>
</ol>
<p>Реализация этой схемы <strong>распределена</strong> по папкам <code>beast/_1_</code> … <code>beast/_11_</code> — номер — ориентир по слою (от «тела» к «сознанию и памяти»).</p>
<hr>
<h2>3. Карта каталогов <code>beast/</code> (слои)</h2>
<table>
<thead>
<tr>
<th>Папка</th>
<th>Роль</th>
</tr>
</thead>
<tbody><tr>
<td><code>_1_Genetic_vitals</code></td>
<td>Виталы, шкалы, шок, смерть, подготовка к расчётам</td>
</tr>
<tr>
<td><code>_2_Genetic_basic_styles</code></td>
<td>Базовые контексты, стили, <strong><code>prepare.js</code></strong>: <code>BadNormWell</code>, <code>GetBadNormWell</code>, <code>SetWellForHolding</code>, удержания, <code>getDiffImportance</code></td>
</tr>
<tr>
<td><code>_3_perception</code></td>
<td>Списки стимулов, UI стимулов, активация, связь с ОР</td>
</tr>
<tr>
<td><code>_4_actions</code></td>
<td><code>BasicActions</code>, эффекторы, зеркальные действия</td>
</tr>
<tr>
<td><code>_5_Genetic__reflexes</code></td>
<td>Таблицы рефлексов, <strong><code>genetic_reflexes_engine.js</code></strong>: <code>processGeneticReflexes</code>, <code>runActionImage</code>, <code>startAction</code></td>
</tr>
<tr>
<td><code>_5_2_synonyms_reflexes</code></td>
<td>Условные (синонимические) рефлексы</td>
</tr>
<tr>
<td><code>_6_semantic_memory</code></td>
<td>FinalImage, семантический индекс, регистрация опыта</td>
</tr>
<tr>
<td><code>_7_perception_tree</code></td>
<td>Дерево восприятия, терминальный узел = «ветка» для ЭП и автоматизмов</td>
</tr>
<tr>
<td><code>_8_Hippocampus</code></td>
<td>Гиппокамп, <strong><code>orientation_reflex.js</code></strong> — ОР</td>
</tr>
<tr>
<td><code>_9_Conscience</code></td>
<td>Инфокартина, детекторы, <strong>уровни сознания 2–4</strong>, гештальт, абстракции, канал внимания</td>
</tr>
<tr>
<td><code>_10_Episodic_memory</code></td>
<td>Кадры ЭП, ожидание ответа, закрытие кадра, <code>effect</code></td>
</tr>
<tr>
<td><code>_10_actions_image</code></td>
<td>Образы действия (ОД), связь с целями</td>
</tr>
<tr>
<td><code>_11_Automatizms</code></td>
<td>Автоматизмы по веткам, привязка к ОД</td>
</tr>
</tbody></table>
<p>Плюс: <strong><code>beast/puls.js</code></strong> — сердце тика; <strong><code>beast/docs/</code></strong> — проектные заметки и анализ.</p>
<hr>
<h2>4. Точка входа и священный порядок скриптов</h2>
<ul>
<li>Вход: <strong><code>index.html</code></strong> в корне репозитория.</li>
<li>В конце <strong><code>body</code></strong> порядок подключения <strong><code>index.html</code></strong> — <strong>контракт</strong>. Если переставить файлы, часть глобалов будет <code>undefined</code> в момент инициализации.</li>
</ul>
<p>Правило: <strong>кто от кого зависит, тот ниже</strong>. Примеры:</p>
<ul>
<li><code>consciousness_dispatcher.js</code> задаёт реестр циклов и диспетчеризацию.</li>
<li><code>gestalt.js</code> должен быть <strong>до</strong> <code>episodic_memory.js</code> (хук <code>Gestalt_onEpisodeFeedbackClosed</code> в <code>closeLastFrame</code>).</li>
<li>Модули «уровней» сознания зависят от инфокартины и ЭП.</li>
</ul>
<p><strong>Практика:</strong> перед крупным рефакторингом — экспорт списка <code>&lt;script src=...&gt;</code> в <code>index.html</code> и проверка цепочки вручную.</p>
<hr>
<h2>5. Пульс: что происходит «каждый такт»</h2>
<p>Файл <strong><code>beast/puls.js</code></strong> (и связанные вызовы) задаёт:</p>
<ul>
<li>инкремент счётчика пульса;</li>
<li>обновление виталов/контекстов (<code>getActiveBasicContexts</code> и др.);</li>
<li>генетические и условные рефлексы;</li>
<li><strong>диспетчер сознания</strong> (<code>Consciousness_runOneStep</code> и т.п.);</li>
<li>эпизодическую память (тики ожидания, закрытие кадров по сценарию).</li>
</ul>
<p><strong>Принцип:</strong> если логика «должна жить в пульсе», не дублируйте её только в UI — UI и пульс должны согласоваться, иначе гонки (например, инфокартина vs рефлекс).</p>
<hr>
<h2>6. Глобальное состояние: дисциплина</h2>
<ol>
<li><strong><code>beast/about_global_variables.md</code></strong> — справочник по ключевым <code>window.*</code>. Перед добавлением нового глобала подумайте: <strong>нужен ли он в <code>window</code></strong>, или достаточно замыкания в модуле.</li>
<li><strong>Именование</strong> — в проекте уже есть смесь русских комментариев и английских идентификаторов; новый код лучше <strong>вписывать в существующий стиль</strong> файла, который вы правите.</li>
<li><strong>Сериализация</strong> — виталы, дерево, семантика, ЭП, цели, ОД, автоматизмы частично уходят в <strong>IndexedDB</strong> / файлы. Любая новая сущность в <code>window</code> должна либо <strong>попадать в save/load</strong>, либо явно быть «чисто сессионной» (и это задокументировать).</li>
</ol>
<hr>
<h2>7. Поток данных «как в учебнике»</h2>
<p>Упрощённо (реальность всегда с ветвлениями):</p>
<pre><code class="hljs plaintext">Виталы → BadVitalsValue / контексты → BasicContextsActived
       → BadNormWell, эмоция (Emotions_*)
       → путь дерева восприятия → terminal node (branchId)
Стимулы (UI) → глобальные id стимулов → FinalImage
       → ОР (конкуренция, пороги) → actual_stimul_ID
       → stimuls_consciousness → циклы ФО / автоматизмы
       → кадр ЭП, effect, закрытие кадра → семантика / абстракции / гештальт</code></pre><p><strong>Ориентировочный рефлекс</strong> (<code>orientation_reflex.js</code>) — не «украшение», а <strong>центральный узел</strong>: от него зависят запуск ФО, блокировка рефлекса на пару пульсов, очередь отложенных рефлексов, второй проход после ответа в ожидании (см. <code>about_episodic_memory_rules.md</code>).</p>
<hr>
<h2>8. Инфокартина и детекторы</h2>
<ul>
<li><strong><code>informing.js</code></strong> — структура <code>InfoPicture</code>, обновление представления.</li>
<li><strong><code>info_detectors.js</code></strong> — вычисляемые поля: <code>criticalVital</code>, <code>badState</code>, режимы игры/обучения и т.д. Ситуации в <strong><code>situatioms_tree.js</code></strong> собираются из <strong>компонентов детекторов</strong>.</li>
</ul>
<p><strong>Принцип:</strong> детекторы должны быть <strong>идемпотентны относительно своих входов</strong> (виталы, <code>BadNormWell</code>, флаги). Если вводите «перекрытие» смысла (например, еда гасит индикатор критичности при Хорошо), документируйте в коде <strong>кратко</strong>, почему так.</p>
<hr>
<h2>9. Уровни сознания (ур.2–4) — куда смотреть</h2>
<table>
<thead>
<tr>
<th>Уровень</th>
<th>Файл</th>
<th>Заметка</th>
</tr>
</thead>
<tbody><tr>
<td>Диспетчер, реестр циклов, лог</td>
<td><code>consciousness_dispatcher.js</code></td>
<td><code>Consciousness_cycleAppendLog</code>, <code>expired</code>, <code>force</code></td>
</tr>
<tr>
<td>Канал внимания, вход ФО</td>
<td><code>conscious_attention_channel.js</code></td>
<td>прерывания, «тишина мысли»</td>
</tr>
<tr>
<td>Ур.2</td>
<td><code>conscience_level_2.js</code></td>
<td>выживание, цели, ответ оператора, связка с ЭП</td>
</tr>
<tr>
<td>Ур.3</td>
<td><code>conscience_level_3.js</code></td>
<td>пайплайн правил, ожидание ЭП, гейты мотора</td>
</tr>
<tr>
<td>Ур.4 / гештальт</td>
<td><code>conscience_level_4.js</code>, <code>gestalt.js</code></td>
<td>см. <code>about_gestalt.md</code></td>
</tr>
<tr>
<td>Общие хелперы</td>
<td><code>infofunctions.js</code></td>
<td><code>info_automatismWithEpisodeFeedbackAndExpireCycle</code>, поиск правил ЭП</td>
</tr>
</tbody></table>
<p>Подробности и актуальные соглашения — <strong><code>beast/_9_Conscience/about_conscience.md</code></strong>, <strong><code>about_adaptivity_levels_2_4.md</code></strong>.</p>
<hr>
<h2>10. Эпизодическая память и образы действия</h2>
<ul>
<li><strong><code>episodic_memory.js</code></strong> — кадры, ожидание, <code>effect</code>, связь с оператором.</li>
<li><strong><code>actions_image.js</code></strong> — ОД (массив <code>actionIds</code>, <code>finalImageId</code>). Цели жизненного опыта в <strong><code>goals.js</code></strong> ссылаются на ОД; при загрузке состояния из IndexedDB <strong>нельзя</strong> терять согласованность id.</li>
</ul>
<p><strong>Принцип:</strong> если добавляете новую «точку правды», определите её <strong>один раз</strong> (как в случае умолчательной цели «Плачь» и ОД с <code>actionIds: [1]</code>).</p>
<hr>
<h2>11. Как отлаживать «без сумасшествия»</h2>
<ol>
<li><strong><code>global.cpuls</code> / <code>Cur_puls_val</code></strong> — номер пульса в логах.</li>
<li><strong><code>Consciousness_cycleAppendLog</code></strong> — человекочитаемый лог главного цикла; помните про <strong><code>expired</code></strong> и <strong><code>{ force: true }</code></strong>.</li>
<li><strong>Тестовый режим ЭП</strong> (<code>EpisodicMemory_testMode</code>) — алерты ОР (см. <code>orientation_reflex.js</code>).</li>
<li>Блок <strong>«Активность Beast»</strong> — текст из генетических/условных рефлексов и автоматизмов (<code>setBeastActivityText</code>).</li>
</ol>
<hr>
<h2>12. Типичные ловушки (Madness Checklist)</h2>
<table>
<thead>
<tr>
<th>Симптом</th>
<th>Возможная причина</th>
</tr>
</thead>
<tbody><tr>
<td>«Ничего не происходит» при стимуле</td>
<td>ОР не прошёл порог <code>OR_well_known</code>, нет терминального узла дерева, пустые кандидаты</td>
</tr>
<tr>
<td>Дублирующиеся объекты в <code>window</code></td>
<td>второй <code>window.StimulMotivarion_GeneticReflexes = {...}</code> перезаписал первый</td>
</tr>
<tr>
<td>Цель «Плачь», а действие другое</td>
<td><strong>рассинхрон ОД</strong> после загрузки IndexedDB и <code>actionImageId</code> цели</td>
</tr>
<tr>
<td>Генетический рефлекс не срабатывает</td>
<td><code>automatismRanForBranchIds</code>, блокировка ОР, очередь отложенных рефлексов</td>
</tr>
<tr>
<td>Лог цикла пустой в начале</td>
<td><code>OrientationReflex_anyStimulusReadyForOr</code> и условия авто-лога в диспетчере</td>
</tr>
</tbody></table>
<hr>
<h2>13. Как безопасно развивать систему</h2>
<h3>Новый стимул</h3>
<ol>
<li>Добавить в <code>stimuls.js</code> в нужный массив.</li>
<li>Прописать эффекты в <strong><code>genetic_reflexes.js</code></strong> (внутренние и/или моторные таблицы).</li>
<li>Убедиться, что <strong>глобальный id</strong> строится через <code>StimulusGlobalId_toGlobal</code>.</li>
<li>При необходимости — правила в UI (<code>stimuls_ui.js</code>).</li>
</ol>
<h3>Новое действие</h3>
<ol>
<li><code>BasicActions</code> в <strong><code>effectors.js</code></strong> (если не зеркальное).</li>
<li>Связь с ОД — через <code>ActionImages_registerReaction</code> / <code>getOrCreateActionImageId</code>.</li>
</ol>
<h3>Новая ветка логики ФО</h3>
<ol>
<li>Сначала — <strong>где в пайплайне</strong> (ур.2 vs 3, до или после ожидания ЭП).</li>
<li>Не ломать <strong>гейт</strong> <code>awaitingAutomatismEpisodeFeedback</code> без чтения <code>conscience_level_3.js</code> и <code>about_conscience.md</code>.</li>
</ol>
<h3>Персистентность</h3>
<ol>
<li>Найти аналогичный модуль с <code>serialize/apply</code> и IndexedDB.</li>
<li>Не забыть <strong>порядок загрузки</strong> — после восстановления ОД обновить зависимые сущности (пример: цели и ОД).</li>
</ol>
<hr>
<h2>14. Что читать дальше (порядок)</h2>
<ol>
<li><strong><code>PROJECT_STATE_FOR_NEW_CHAT.md</code></strong> — снимок архитектуры.</li>
<li><strong><code>beast/about_global_variables.md</code></strong> — словарь глобалов.</li>
<li><strong><code>beast/_10_Episodic_memory/about_episodic_memory_rules.md</code></strong> — ОР, ожидание, цепочки.</li>
<li><strong><code>beast/_8_Hippocampus/about_hippocampus.md</code></strong></li>
<li><strong><code>beast/_9_Conscience/about_conscience.md</code></strong></li>
<li><strong><code>beast/_6_semantic_memory/about_semantic_memory.md</code></strong></li>
<li><strong><code>beast/_11_Automatizms/about_automatizms.md</code></strong></li>
<li>Остальные <code>about_*.md</code> по задаче.</li>
</ol>
<hr>
<h2>15. Этика «сумасшедшего программиста»</h2>
<ul>
<li><strong>Code is truth</strong> — комментарий и <code>about_*</code> могут отстать; при противоречии <strong>верьте коду</strong> и исправляйте документацию.</li>
<li><strong>Маленькие шаги</strong> — один сценарий, один тест в голове, один коммит в душе.</li>
<li><strong>Не размножайте глобалы</strong> без нужды.</li>
<li><strong>Уважайте пульс</strong> — если поведение «должно быть сразу после клика», проверьте порядок: UI → виталы/контексты → ОР → пульс.</li>
</ul>
<hr>
<p><em>Версия мануала: 2026-03-21. Проект Beast — живой; при существенных изменениях архитектуры обновите этот файл и <code>PROJECT_STATE_FOR_NEW_CHAT.md</code>.</em></p>

                </article>
                </body>
                </html>
════════════════════════════════════════════════════════════

/BEAST/manuals/Mad_programmer_manual.md
════════════════════════════════════════════════════════════
# Mad Programmer Manual — Beast

Руководство для того, кто **всерьёз** решил разобраться в коде **Beast** («Существо Beast, пульт связи») и продолжить его развитие. Это не замена исходников и не дублирование каждого модуля — это **карта мышления**, принципы и **где искать правду** (в коде и в `about_*.md`).

---

## 1. Зачем этот документ

- Проект **большой**, связи **неочевидны**, порядок загрузки скриптов **критичен**.
- Много состояния живёт в **`window` / глобальных переменных** — без ментальной модели легко сломать то, что «случайно работало».
- Документ **`PROJECT_STATE_FOR_NEW_CHAT.md`** в корне — **сжатый снимок** для ИИ и людей; **этот мануал** шире: философия, слои, типичные ловушки, как растить фичи.

Если правите архитектуру — обновляйте **`PROJECT_STATE_FOR_NEW_CHAT.md`** и соответствующие **`beast/**/about_*.md`**.

---

## 2. Что такое Beast концептуально

**Beast** — браузерное приложение, моделирующее связку **организм ↔ стимулы ↔ ориентировочный рефлекс (ОР) ↔ процесс осмысления (ФО) ↔ эпизодическая память (ЭП)**.

Принципы:

1. **Время дискретно** — «пульс» (`global.cpuls`, `Cur_puls_val`). Почти всё важное привязано к пульсу или к событию UI между пульсами.
2. **Состояние тела** — виталы, выход из нормы, интегральная значимость, базовое состояние **Плохо / Норма / Хорошо** (`BadNormWell`).
3. **Контексты** — сокращённая эмоционально-поведенческая разметка (базовые контексты), из них строится «эмоция» и ветка дерева.
4. **Стимулы** — типизированы (мотивация, позитив, негатив, нейтрал); у глобальных id **нет коллизий** между массивами (см. `StimulusGlobalId_*`).
5. **ОР** — конкуренция стимулов по актуальности, порог «зрелости» семантики (`OR_well_known`), исключения для отдельных сценариев (см. код ОР).
6. **ФО** — не один вызов, а **реестр циклов** (`ConsciousnessCycles`), диспетчер шагов, главный vs фоновые циклы, `expired`, лог цикла.
7. **Память** — семантика (значимость по условиям), эпизоды (кадры с `effect`, стимул, действие, ответ оператора), абстракции и правила для высоких уровней.

Реализация этой схемы **распределена** по папкам `beast/_1_` … `beast/_11_` — номер — ориентир по слою (от «тела» к «сознанию и памяти»).

---

## 3. Карта каталогов `beast/` (слои)

| Папка | Роль |
|--------|------|
| `_1_Genetic_vitals` | Виталы, шкалы, шок, смерть, подготовка к расчётам |
| `_2_Genetic_basic_styles` | Базовые контексты, стили, **`prepare.js`**: `BadNormWell`, `GetBadNormWell`, `SetWellForHolding`, удержания, `getDiffImportance` |
| `_3_perception` | Списки стимулов, UI стимулов, активация, связь с ОР |
| `_4_actions` | `BasicActions`, эффекторы, зеркальные действия |
| `_5_Genetic__reflexes` | Таблицы рефлексов, **`genetic_reflexes_engine.js`**: `processGeneticReflexes`, `runActionImage`, `startAction` |
| `_5_2_synonyms_reflexes` | Условные (синонимические) рефлексы |
| `_6_semantic_memory` | FinalImage, семантический индекс, регистрация опыта |
| `_7_perception_tree` | Дерево восприятия, терминальный узел = «ветка» для ЭП и автоматизмов |
| `_8_Hippocampus` | Гиппокамп, **`orientation_reflex.js`** — ОР |
| `_9_Conscience` | Инфокартина, детекторы, **уровни сознания 2–4**, гештальт, абстракции, канал внимания |
| `_10_Episodic_memory` | Кадры ЭП, ожидание ответа, закрытие кадра, `effect` |
| `_10_actions_image` | Образы действия (ОД), связь с целями |
| `_11_Automatizms` | Автоматизмы по веткам, привязка к ОД |

Плюс: **`beast/puls.js`** — сердце тика; **`beast/docs/`** — проектные заметки и анализ.

---

## 4. Точка входа и священный порядок скриптов

- Вход: **`index.html`** в корне репозитория.
- В конце **`body`** порядок подключения **`index.html`** — **контракт**. Если переставить файлы, часть глобалов будет `undefined` в момент инициализации.

Правило: **кто от кого зависит, тот ниже**. Примеры:

- `consciousness_dispatcher.js` задаёт реестр циклов и диспетчеризацию.
- `gestalt.js` должен быть **до** `episodic_memory.js` (хук `Gestalt_onEpisodeFeedbackClosed` в `closeLastFrame`).
- Модули «уровней» сознания зависят от инфокартины и ЭП.

**Практика:** перед крупным рефакторингом — экспорт списка `<script src=...>` в `index.html` и проверка цепочки вручную.

---

## 5. Пульс: что происходит «каждый такт»

Файл **`beast/puls.js`** (и связанные вызовы) задаёт:

- инкремент счётчика пульса;
- обновление виталов/контекстов (`getActiveBasicContexts` и др.);
- генетические и условные рефлексы;
- **диспетчер сознания** (`Consciousness_runOneStep` и т.п.);
- эпизодическую память (тики ожидания, закрытие кадров по сценарию).

**Принцип:** если логика «должна жить в пульсе», не дублируйте её только в UI — UI и пульс должны согласоваться, иначе гонки (например, инфокартина vs рефлекс).

---

## 6. Глобальное состояние: дисциплина

1. **`beast/about_global_variables.md`** — справочник по ключевым `window.*`. Перед добавлением нового глобала подумайте: **нужен ли он в `window`**, или достаточно замыкания в модуле.
2. **Именование** — в проекте уже есть смесь русских комментариев и английских идентификаторов; новый код лучше **вписывать в существующий стиль** файла, который вы правите.
3. **Сериализация** — виталы, дерево, семантика, ЭП, цели, ОД, автоматизмы частично уходят в **IndexedDB** / файлы. Любая новая сущность в `window` должна либо **попадать в save/load**, либо явно быть «чисто сессионной» (и это задокументировать).

---

## 7. Поток данных «как в учебнике»

Упрощённо (реальность всегда с ветвлениями):

```
Виталы → BadVitalsValue / контексты → BasicContextsActived
       → BadNormWell, эмоция (Emotions_*)
       → путь дерева восприятия → terminal node (branchId)
Стимулы (UI) → глобальные id стимулов → FinalImage
       → ОР (конкуренция, пороги) → actual_stimul_ID
       → stimuls_consciousness → циклы ФО / автоматизмы
       → кадр ЭП, effect, закрытие кадра → семантика / абстракции / гештальт
```

**Ориентировочный рефлекс** (`orientation_reflex.js`) — не «украшение», а **центральный узел**: от него зависят запуск ФО, блокировка рефлекса на пару пульсов, очередь отложенных рефлексов, второй проход после ответа в ожидании (см. `about_episodic_memory_rules.md`).

---

## 8. Инфокартина и детекторы

- **`informing.js`** — структура `InfoPicture`, обновление представления.
- **`info_detectors.js`** — вычисляемые поля: `criticalVital`, `badState`, режимы игры/обучения и т.д. Ситуации в **`situatioms_tree.js`** собираются из **компонентов детекторов**.

**Принцип:** детекторы должны быть **идемпотентны относительно своих входов** (виталы, `BadNormWell`, флаги). Если вводите «перекрытие» смысла (например, еда гасит индикатор критичности при Хорошо), документируйте в коде **кратко**, почему так.

---

## 9. Уровни сознания (ур.2–4) — куда смотреть

| Уровень | Файл | Заметка |
|---------|------|---------|
| Диспетчер, реестр циклов, лог | `consciousness_dispatcher.js` | `Consciousness_cycleAppendLog`, `expired`, `force` |
| Канал внимания, вход ФО | `conscious_attention_channel.js` | прерывания, «тишина мысли» |
| Ур.2 | `conscience_level_2.js` | выживание, цели, ответ оператора, связка с ЭП |
| Ур.3 | `conscience_level_3.js` | пайплайн правил, ожидание ЭП, гейты мотора |
| Ур.4 / гештальт | `conscience_level_4.js`, `gestalt.js` | см. `about_gestalt.md` |
| Общие хелперы | `infofunctions.js` | `info_automatismWithEpisodeFeedbackAndExpireCycle`, поиск правил ЭП |

Подробности и актуальные соглашения — **`beast/_9_Conscience/about_conscience.md`**, **`about_adaptivity_levels_2_4.md`**.

---

## 10. Эпизодическая память и образы действия

- **`episodic_memory.js`** — кадры, ожидание, `effect`, связь с оператором.
- **`actions_image.js`** — ОД (массив `actionIds`, `finalImageId`). Цели жизненного опыта в **`goals.js`** ссылаются на ОД; при загрузке состояния из IndexedDB **нельзя** терять согласованность id.

**Принцип:** если добавляете новую «точку правды», определите её **один раз** (как в случае умолчательной цели «Плачь» и ОД с `actionIds: [1]`).

---

## 11. Как отлаживать «без сумасшествия»

1. **`global.cpuls` / `Cur_puls_val`** — номер пульса в логах.
2. **`Consciousness_cycleAppendLog`** — человекочитаемый лог главного цикла; помните про **`expired`** и **`{ force: true }`**.
3. **Тестовый режим ЭП** (`EpisodicMemory_testMode`) — алерты ОР (см. `orientation_reflex.js`).
4. Блок **«Активность Beast»** — текст из генетических/условных рефлексов и автоматизмов (`setBeastActivityText`).

---

## 12. Типичные ловушки (Madness Checklist)

| Симптом | Возможная причина |
|---------|-------------------|
| «Ничего не происходит» при стимуле | ОР не прошёл порог `OR_well_known`, нет терминального узла дерева, пустые кандидаты |
| Дублирующиеся объекты в `window` | второй `window.StimulMotivarion_GeneticReflexes = {...}` перезаписал первый |
| Цель «Плачь», а действие другое | **рассинхрон ОД** после загрузки IndexedDB и `actionImageId` цели |
| Генетический рефлекс не срабатывает | `automatismRanForBranchIds`, блокировка ОР, очередь отложенных рефлексов |
| Лог цикла пустой в начале | `OrientationReflex_anyStimulusReadyForOr` и условия авто-лога в диспетчере |

---

## 13. Как безопасно развивать систему

### Новый стимул

1. Добавить в `stimuls.js` в нужный массив.
2. Прописать эффекты в **`genetic_reflexes.js`** (внутренние и/или моторные таблицы).
3. Убедиться, что **глобальный id** строится через `StimulusGlobalId_toGlobal`.
4. При необходимости — правила в UI (`stimuls_ui.js`).

### Новое действие

1. `BasicActions` в **`effectors.js`** (если не зеркальное).
2. Связь с ОД — через `ActionImages_registerReaction` / `getOrCreateActionImageId`.

### Новая ветка логики ФО

1. Сначала — **где в пайплайне** (ур.2 vs 3, до или после ожидания ЭП).
2. Не ломать **гейт** `awaitingAutomatismEpisodeFeedback` без чтения `conscience_level_3.js` и `about_conscience.md`.

### Персистентность

1. Найти аналогичный модуль с `serialize/apply` и IndexedDB.
2. Не забыть **порядок загрузки** — после восстановления ОД обновить зависимые сущности (пример: цели и ОД).

---

## 14. Что читать дальше (порядок)

1. **`PROJECT_STATE_FOR_NEW_CHAT.md`** — снимок архитектуры.
2. **`beast/about_global_variables.md`** — словарь глобалов.
3. **`beast/_10_Episodic_memory/about_episodic_memory_rules.md`** — ОР, ожидание, цепочки.
4. **`beast/_8_Hippocampus/about_hippocampus.md`**
5. **`beast/_9_Conscience/about_conscience.md`**
6. **`beast/_6_semantic_memory/about_semantic_memory.md`**
7. **`beast/_11_Automatizms/about_automatizms.md`**
8. Остальные `about_*.md` по задаче.

---

## 15. Этика «сумасшедшего программиста»

- **Code is truth** — комментарий и `about_*` могут отстать; при противоречии **верьте коду** и исправляйте документацию.
- **Маленькие шаги** — один сценарий, один тест в голове, один коммит в душе.
- **Не размножайте глобалы** без нужды.
- **Уважайте пульс** — если поведение «должно быть сразу после клика», проверьте порядок: UI → виталы/контексты → ОР → пульс.

---

*Версия мануала: 2026-03-21. Проект Beast — живой; при существенных изменениях архитектуры обновите этот файл и `PROJECT_STATE_FOR_NEW_CHAT.md`.*

════════════════════════════════════════════════════════════

