﻿OKHOTSK FC — BPE И POOL-REPACKER v0.6
====================================

1. ЗАЧЕМ ЭТО ПОЯВИЛОСЬ
----------------------
Русский текст значительно длиннее японского. Старый v0.4/v0.5 repacker мог двигать запись
только внутри непрерывной группы уже переведённых строк. Для WIP из 266 записей этого уже
не хватало.

v0.6 решает две отдельные задачи:
  A) сильнее сжимает русский текст через родной словарный механизм игры;
  B) переиспользует только те оригинальные текстовые байты, которые доказанно перестали
     быть достижимыми из ещё непереведённых pointer roots.

2. ПОЧЕМУ ВЛОЖЕННЫЙ СЛОВАРЬ ВООБЩЕ РАБОТАЕТ
-------------------------------------------
Статический разбор fixed bank подтверждает, что обработчик токенов 60-CF не копирует
"готовое слово" напрямую. Он переключает текущий источник байтов на запись словаря:

  CPU $C758...  INC $5A
                 ...
                 записывает новый source pointer в стек $5B/$5C + 2*$5A

Следовательно, если внутри словарной записи встречается ещё один токен 60-CF, он проходит
через тот же dispatcher и может открыть вложенную запись. Это настоящий стек источников.

Следующие отдельные переменные начинаются с $6B/$6C. Между $5B и $6A помещается восемь
пар source-pointer, то есть индексы источника 0..7. BPE v0.6 строится так, что текущий
словарь для 266 строк имеет максимальную зависимость 5. Переведённый WIP не содержит F2
в своих новых payload, поэтому запас по source stack остаётся.

3. СОХРАНЁННЫЕ ОРИГИНАЛЬНЫЕ ТОКЕНЫ
----------------------------------
60 = F8 F1 02
62 = F8 F1 0A FE F9

Это не слова, а родные управляющие макросы. Их НЕ переписываем. Encoder автоматически
сворачивает совпадающие последовательности управления обратно в 60/62.

Остальные 61,63- CF могут стать RU BPE-токенами.

4. КАК СТРОИТСЯ BPE
-------------------
build_ru_bpe.py по умолчанию:
- первые 79 токенов тратит на частые пары уже закодированных видимых русских байтов;
- оставшийся бюджет тратит на повторяющиеся n-gram до 20 байтов;
- n-gram может состоять из ранее созданных токенов, поэтому словарь вложенный;
- F0-FF markup и его аргументы НЕ подвергаются BPE. Токен никогда не окажется вместо
  сырого аргумента F1/F2/F5 и т.п.;
- каждый visible-run после сжатия проверяется обратным раскрытием до исходных байтов.

Для текущих 266 строк:
  исходные физические JP records: 2454 bytes
  RU payload после BPE + FF:       2593 bytes
  dictionary:                       516 / 516 bytes
  BPE rules:                         106
  максимальная BPE depth:              5

5. КАК POOL-REPACKER ДОКАЗЫВАЕТ, ЧТО БАЙТЫ МОЖНО ПЕРЕПИСАТЬ
-----------------------------------------------------------
repack_pool.py строит два графа:

ALL = все потоки, достижимые от исходных pointer roots через F2.
LIVE = потоки, достижимые от НЕПЕРЕВЕДЁННЫХ roots.

Но если LIVE встречает F2 прямо на начало уже переведённой физической записи, traversal
останавливается перед ней: этот F2 позднее будет переписан на новый адрес перевода.

Pool = байты ALL, которые не принадлежат LIVE.

Таким образом pool не включает неизвестные таблицы/код/неиндексированные области и не
строится по принципу "похоже на свободное место".

Для 266 строк:
  reclaimable stream bytes: 2513

6. KNOWN ZERO PADDING
---------------------
Для изученной ROM дополнительно разрешён диапазон:
  text-region $3FBA-$3FD7 = 30 bytes

Проверки:
- все 30 байт в чистой ROM равны 00;
- диапазон не входит ни в один pointer/F2 stream;
- во всём PRG отсутствует literal little-endian $BFBA и offset $3FBA;
- сразу с $3FD8 начинается непустой блок, адрес $BFD8 действительно встречается в PRG.

Поэтому $3FBA-$3FD7 трактуется как padding. repack_pool.py всё равно перед использованием
проверяет, что эти байты по-прежнему нулевые. При несовпадении сборка аварийно остановится.
Отключить использование можно опцией build_project.py --no-known-padding.

Итого pool v0.6:
  2513 reclaimable text
  + 30 verified padding
  = 2543 bytes

7. DEDUP И SUFFIX SHARING
-------------------------
Перед размещением:
- полностью одинаковые payload хранятся один раз;
- если payload A является точным байтовым суффиксом payload B, pointer A может указывать
  прямо внутрь B. NES-движок и оригинальная ROM уже используют interior starts.

Для текущих 266 строк:
  encoded before sharing: 2593 bytes
  exact duplicate saving:   15 bytes
  suffix sharing saving:     52 bytes
  physically stored:       2526 bytes
  pool capacity:           2543 bytes
  остаток:                    17 bytes

8. F2 ПРИ ПЕРЕМЕЩЕНИИ
---------------------
Если непереведённый live-stream вызывает через F2 запись, которая была переведена и
перемещена, repack_pool.py исправляет little-endian target. В текущем WIP исправляется
один такой live F2 call.

Внутри переведённых payload F2 тоже может быть исправлен по mapping. Для suffix sharing
потоки с явным F2 намеренно не объединяются суффиксом, чтобы patch адреса не создал
неоднозначность точки входа.

9. ОГРАНИЧЕНИЕ WIP-СБОРКИ
-------------------------
RU BPE заменяет глобальный японский словарь 60-CF. Пока переведены не все строки,
оставшийся JP-текст читать нельзя. Это нормально для тестовой ветки. Перед финальным
релизом весь реально достижимый сценарий должен быть переведён/проверен.


v0.7 — SHARED STREAM OBJECTS
----------------------------
Новый параметр repack_pool.py:
  --shared-translations shared_ru.json

JSON ключ = исходный virtual text-region address в HEX, значение = русский markup.
Пример:
{
  "1535": "Куроки: Опросим людей вокруг.[F0:$A2]\n[F1:$0A][FE][F9]",
  "153A": "Куроки: Ничего полезного\n[F1:$02]они не рассказали, Шеф."
}

Shared stream участвует в allocation/dedup/suffix sharing наравне с root, но pointer table для
него не существует. Его новый адрес попадает в old->new mapping, после чего F2 callers
исправляются автоматически.

Это предпочтительный способ для оригинальных F2-деревьев. Flattening expanded текста в root
допустим только когда он короткий или уникальный; повторяемые куски надо переводить как shared.

10. v0.8 — РАСШИРЕНИЕ СЛОВАРЯ ДО D0-EF
--------------------------------------
После полного перевода японская функция D0-DF (дополнительные глифы) и E0-EF
(katakana/lower shift) больше не нужны текстовому bytecode.

Чтобы освободить их безопасно:
- цифры 0-9 зеркалируются из font tiles 74-7D в прямые tiles/codes 22-2B;
- строчные а-э выводятся напрямую через 41-5F, ю=$2C, я=$2D; UI arrows $40/$3B сохранены;
- dispatcher high-nibble D и E перенаправляется на secondary dictionary handler.

Original main handler 60-CF использует код token-5F как номер FF-delimited entry.
Secondary handler преобразует D0-EF -> synthetic 60-7F, поэтому тот же scanner
адресует ровно 32 entries.

КРИТИЧЕСКИ ВАЖНО: secondary handler обязан повторить prologue основного dictionary handler:
  LDA #$0A / JSR $FEF4    выбрать bank A
  JSR $C69A               сдвинуть caller source за token byte
  INC $5A                 push нового source level
после чего устанавливает base $994A и JMP $C76E (LDY #0 + scanner).

Secondary dictionary физически занимает 171 bytes в текущем BPE и лежит по
text-region $194A-$19F4. Это старый переводимый text-stream range, поэтому build order:
  1) анализ/repack исходного stream graph;
  2) область $194A.. резервируется и не используется payload allocator;
  3) ПОСЛЕ repack туда записывается secondary dictionary.

11. v0.8 — PRUNING SHARED STREAMS
---------------------------------
Полная база теперь содержит 208 переведённых shared F2 streams. Для текущей сборки
`shared_translations_full.json` подаёт только 125 live/QA-relevant потоков; многие прочие
исходные F2 были намеренно flattened и не нужны после редактирования root translations.

repack_pool начинает с новых ROOT payload, читает только их реальные F2 refs и рекурсивно
добавляет нужные shared translations. Текущая финальная графика достижимости:
  supplied shared: 125
  selected shared: 110
  pruned shared:     15

Это экономит почти 2 KB и при этом сохраняет полную переводческую базу в
shared_translations_all.json.
