# SINGULYAR ADAPTER OS v0.1 — интеграция в платформу

## Что это

Инженерный слой поверх ·19 ЧЕЛОВЕК → ·21 ОБЩЕНИЕ → ·22 СВЯЗЬ, собранный из
присланного пакета `singulyar-adapter-os-v0.1.zip` (SHA-256
`6ab03a4f…74af`, проверен байт-в-байт). Ядра страниц **не тронуты** — мост
только регистрирует их проверенные механизмы как адаптеры, ведёт шину
смыслов и даёт честные статусы.

```text
ЧЕЛОВЕК → ПРЕДПОЧТЕНИЯ → ВОЗМОЖНОСТИ → СМЫСЛ → АДАПТЕР → ПРЕДСТАВЛЕНИЕ
```

## Состав в репо

`БИБЛИОТЕКИ/adapter-os/` — пакет из архива байт-в-байт + один новый файл:

| Файл | Статус | Что это |
|---|---|---|
| `src/core.mjs` | из архива; срез 6 — минимальная правка политики выбора в `MeaningRouter` | CapabilityModel · AdapterRegistry · MeaningRouter |
| `src/store.mjs` | из архива | IndexedDB + явный JSON-экспорт |
| `adapters/basic.mjs` | из архива | Text/TTS/Vibration/Speech/Camera/Mic (READY-датчики) |
| `adapters/contracts.mjs` | из архива | OCR/Whisper/Gaze/Braille/Demucs — **EXPERIMENTAL, моделей нет** |
| `prototype/`, `tests/`, `docs/` | из архива | рабочий прототип, их тесты 4/4, ledger |
| `bridge.mjs` | **новый**; срез 6 — добавочное `os.routeMixed` | мост: загрузка OS, шина смыслов, окно `window.SingulyarOS` |

## Как подключён (v0.1, срез №1)

Каждая страница получает маленький `<script type="module">` перед
UX-движком. На file:// module-import браузер блокирует → блок ловит ошибку
и честно пишет `window.SingulyarOS = {state:'unavailable'}`; страница
работает как раньше.

| Страница | Что регистрируется мостом | Источник правды |
|---|---|---|
| ·19 ЧЕЛОВЕК | предпочтения человека (`singulyar.adaptation.v1`) → capability-модель + адаптер-источник `source.19-адаптация` | localStorage ·19 |
| ·21 ОБЩЕНИЕ | `output.21-брайль` (реальная функция S21.braille), `tool.21-маршрутизация` (S21.route) | ядро S21 |
| ·22 СВЯЗЬ | `channel.22-зал` — транспорт зала как адаптер-канал (проверка BroadcastChannel) | ядро ·22 |

Шина смыслов (`singular-meaning-bus-v1`, конверты
`{id,type,payload,createdAt,author}`) — отдельный транспорт моста; живой
мост ·21⇄·22 (`singular-most-21-22-v1`) работает как работал.

## Доступ из консоли/тестов

```js
SingulyarOS.version            // '0.1'
SingulyarOS.statuses()         // реестр с честными состояниями
SingulyarOS.routeOut({text:'привет'}, ['output.text'])
SingulyarOS.bus.publish('note', {text:'…'}); SingulyarOS.bus.log()
await SingulyarOS.store.save('k', v)   // IndexedDB + exportJson
```

## Запуск прототипа — ВАЖНОЕ ИСПРАВЛЕНИЕ

В README архива команда указана неверно: `python -m http.server 8080 -d
prototype` обрезает импорты `../src/*.mjs` (404, прототип мёртв при живом
HTTP 200). Правильно — из корня пакета:

```bash
cd БИБЛИОТЕКИ/adapter-os
python -m http.server 8080
# открыть http://localhost:8080/prototype/
```

## Честные границы (UNKNOWN / EXPERIMENTAL)

- OCR, локальный Whisper, gaze-трекинг, Брайль-дисплей, Demucs — контракты
  без реализаций: нужны pinned-модели и бенчмарки (ledger архива).
- OCR, локальный Whisper, gaze-трекинг, Брайль-дисплей, Demucs — контракты
  без реализаций: нужны pinned-модели и бенчмарки (ledger архива).
- Дефект README архива исправлен документом выше; сам архив не менялся.

## Срез 2 — production-проводка (v1.13.3)

Правило №10 плана пакета выполнено буквально: сначала тесты против живых
файлов (`ИНСТРУМЕНТЫ/test_adapter_os_wire.py`, RED 14/19 на прежних
файлах), затем проводка. Итог GREEN 19/19 + вся регрессия
(54/54, 49/49, 17/17, 36/36).

**·21 ОБЩЕНИЕ.** В реестре — по одному адаптеру на каждый канал человека
(`output.21-канал-text/speech/braille/haptic/aac/sign`); `render` строит
ровно те же объекты `{ch,label,view[,voice]}`, что и ветки ядра.
`S21.route` переведён на реестр: реестр выбирает адаптер по каналу
получателя, адаптер строит представление средствами ядра. Реестр ответил
не полностью, статус не READY или упал → исходная функция ядра без
изменений (фолбэк). Контракт среза 1 (output.21-брайль,
tool.21-маршрутизация) не тронут. Блок ядра между маркерами
`S21:CORE-BEGIN/END` — байт-в-байт прежний (node-тест 54/54 это
подтверждает).

**·22 СВЯЗЬ.** Когда ·22 пересылает смысл зала на представление в ·21,
зеркальный инстанс моста (только читает, формат `singular-most-21-22-v1`
не изменён) публикует конверт `meaning.22.зал` в шину. Представление
по-прежнему строит получатель. Вся цепочка смысла теперь имеет след в
шине: зал → `meaning.22.зал` (·22) → `meaning.21.route` (·21, через
новую проводку).

**Честные границы среза 2:** канальные адаптеры сообщат `READY` как
объявленные страницы каналы (это статусы канала, не железа); роутер
`MeaningRouter` (выбор одного лучшего) в проводке не участвует — выбор
делает реестр по каналам получателя, потому что получатель уже назвал
свои каналы; при смешении нескольких кандидатов на один смысл роутер
подключится следующим срезом.

## Срез 3 — инструкция без автора

Принцип, сформулированный автором проекта: он не строит — он делает так,
чтобы его инструкция была понятна любому без него. Машинальная проверка
этого принципа — `ИНСТРУМЕНТЫ/test_instruction_alone.py`: любой, кто
склонировал репо, может понять и проверить всё, не спрашивая автора.

Что проверяется (18 проверок против живых файлов): контракт ядра ·21 по
маркерам-комментариям; честные строки index.html (карточки ·19/·21/·22
говорят про Adapter OS v0.2); документы ссылаются только на существующие
файлы репо; в ИНСТРУМЕНТАХ ноль путей машины автора; у каждого инструмента
шапка «что это», у каждого теста строка «Запуск»; синтакс-чистота всего
python/js; ключевые зависимости в репо; wire-тесты среза 2 лежат в репо.

Что было исправлено в этом срезе (найдено самим тестом):

- wire-тесты среза 2 (`test_adapter_os_wire.py`, `test_live_wire.py`)
  лежали только на машине автора — перенесены в `ИНСТРУМЕНТЫ/`, пути
  считаются от файла;
- 11 инструментов были привязаны к путям машины автора — отвязаны
  (корень репо от `__file__`/`__dirname`, временные файлы — в системный
  temp);
- `ДОКУМЕНТЫ/ADAPTER_OS.md` ссылался на несуществующий уровень путей
  (вне репо) — путь исправлен на `ИНСТРУМЕНТЫ/`;
- эталонная сверка QR (`test_qr_vs_python.py`) зависела от внешнего
  файла `qr_dev.js`, утраченного даже у автора — JS-класс теперь
  извлекается в рантайме из живой страницы ·17, эталон — python qrcode;
- `test_s16_logic.js` читал `/tmp/s16.js`, собранный вручную — теперь
  извлекает код из живой страницы ·16 сам;
- QR-сверка в `test_s17_fusion.js` сравнивала страницу с dev-копией —
  заменена на проверку детерминизма и полноты масок средствами самого
  класса (эталонная сверка живёт в `test_qr_vs_python.py`);
- `assemble_s18.py` — исторический инструмент одноразовой сборки: честная
  шапка и честный отказ, если parts отсутствуют (пересборка не канонична —
  живая страница эволюционировала после сборки);
- `build_s18_catalog.py` пишет каталог в `ИНСТРУМЕНТЫ/s18_catalog.json`
  (в репо), а не на машину автора.

**Честные границы среза 3:** «понятно без автора» здесь проверено
машинально для структуры (пути, шапки, зависимости, синтаксис) и
фактически — для тестов (свежий клон, полный прогон без машины автора:
логика ·16/·17/·18/·21/·22, мост, adapter-os 4/4, проводка 19/19).
Понимание СМЫСЛА страниц человеком-читателем тестом не измеряется —
это остаётся на совести формулировок.

## Срез 4 — МОЗГ: локальная модель и голос (v1.14.0)

Запрос РУВСОН: «залить ЛЛМ-модель из репы… важно как ты её обучишь… любой
может голосом давать инструкции, а она шустрит». Разрезан на два слоя,
и это не украшение, а честная архитектура:

**Веса — в соседней репе `rtynbirf/singulyar-llm`** (отдельно, потому что
веса не влезают в лимиты git по-честному и не должны: они чужие,
Apache-2.0, у них своя история происхождения). Qwen2.5-0.5B-Instruct
GGUF (q4_k_m), 469 МБ, разрезан на 6 частей ≤100 МБ — обычные бинарные
куски без LFS, склейка 01→06 байт-в-байт даёт оригинал (SHA-256 в
MANIFEST.txt той репы, проверка — `СБОРКА/ПРОВЕРКА_ЦЕЛОСТНОСТИ.py`).

**«Обучение» — инструкционный слой, и он как раз наш:** файл
`ОБУЧЕНИЕ/ПРАВИЛА_СИНГУЛЯРА.txt` (системный промпт: язык, честность,
границы, формат, запрет подхалимажа) и `ОБУЧЕНИЕ/ПРИМЕРЫ_КОМАНД.jsonl`
(словарь голосовых команд, v0.3 — 41 фраза). Поправил правило или добавил
фразу — модель стала умнее для всех без GPU и новых весов (пункт 9
правил). Веса не дообучались — для этого нужен GPU и корпус; здесь этого
нет, и мы не притворяемся.

**Проводка (БИБЛИОТЕКИ/adapter-os/llm.mjs):**
- `llm.local` (kind `llm`): грузит 6 частей (Cache API — один раз,
  дальше из кеша браузера), склеивает в Blob, отдаёт wllama
  (llama.cpp → WebAssembly; WebGPU включается сам, где поддержан).
  Статусы честные: `UNAVAILABLE` с причиной до загрузки → `READY`.
- `input.speech` (kind `input`): Web Speech API браузера. Честно:
  в Chrome звук распознавания уходит на серверы Google
  (`privacy: cloud-input`) — единственное облако в цепочке, названо
  прямо; без поддержки — `UNAVAILABLE`, текстовый ввод работает всегда.
- **Правило-слой** исполняет команды словаря БЕЗ модели: «статус»,
  «отправь: …», «прочитай поле сообщения», «прочитай журнал», «очисти
  журнал», «очисти поле сообщения», «стоп», «помоги», «загрузи модель».
  Платформа управляема голосом даже на слабой машине.
- Свободная речь: модель не загружена → честное «модель не загружена,
  скажи „загрузи модель"», никакого выдуманного ответа.
- Панель «МОЗГ» на ·21 собрана DOM-фабрикой (0 innerHTML), каждая
  инструкция оставляет конверт `meaning.21.инструкция {фраза,
  намерение, via: rule-layer|llm.local|none}` в шине смыслов.

**Тесты (правило №10: сначала RED против живых файлов, потом проводка):**
- `ИНСТРУМЕНТЫ/test_llm_logic.mjs` — 35/35: словарь, нормализация живой
  речи (регистр, ё/е, пунктуация), префиксы, честный null, статусы до
  загрузки; файл llm.mjs чистый ESM — проверяется node-ом;
- `ИНСТРУМЕНТЫ/test_llm_wire.py` — 22/22: панель, статусы реестра,
  исполнение команд без модели, конверты шины, маршрут «отправь» через
  реестр, 0 ошибок консоли;
- `ИНСТРУМЕНТЫ/test_llm_real.py` — НАСТОЯЩИЙ e2e: wllama реально
  запускает модель в браузере (веса с локального сервера или из репы
  singulyar-llm), загрузка и живой ответ; на машине срезу 4 модель
  загрузилась за 7,5 с и ответила: «Я — локальный голос СИНГУЛЯРА
  RUVSON… работаю по правилам РУВСОН» (правило 7 сработало);
- регресс: логика ·21 54/54, ·22 49/49, мост 33/33, adapter-os 4/4,
  проводка среза 2 19/19, «инструкция без автора» 17/17.

**Честные границы среза 4:** первый запуск качает ~469 МБ из репы
singulyar-llm (дальше кеш браузера); скорость генерации зависит от
устройства; 0.5B — маленькая модель: знания ограничены, markdown в
ответах иногда пролезает вопреки правилам — граница модели, а не правил.
Ядра ·21/·22 не тронуты (байт-в-байт, тесты подтверждают).

## Срез 5 — ДОУЧКА v0.4: голос ходит по СИНГУЛЯРУ (v1.14.1)

Продолжение срез 4 по прямому приказу «доуставляй, доучивай». Словарь
инструкционного слоя вырос с 41 до 76 фраз, и главное — из МОЗГа можно
голосом ходить по всей платформе, без модели, на любой машине:

- **Навигация (open):** «открой зал», «пойди в суфлёр», «открой,
  пожалуйста, связь», «вернись на главную» — `решитьСтраницу()`
  резолвит живую речь (стемы, запятые, падежи) в реальные файлы
  страниц репы: ·01 «когда теряем», ·02 фабрика/резонанс, ·15–·22,
  главная. Незнакомая цель — честный список того, что знает, без
  выдумок; пустое «открой» — встречный вопрос.
- **Эхо (repeat):** «повтори: …» / «скажи: …» — произнести текст вслух
  (префикс «скажи получателю:» стоит раньше и не перехватывается).
- **Связь (ping):** «пинг», «ты здесь», «откликнись» — подтверждение
  без модели с версией словаря и честным состоянием модели.
- **Счёт (count):** «сколько команд ты знаешь» — число фраз из живого
  словаря, не из головы.
- **Само-ответ (identity):** «кто ты» — правило-слой отвечает без
  модели; свободный «привет, кто ты» остаётся примером llm-речи.
- Расширены старые намерения: «прекрати», «перестань», «состояние
  системы», «стереть журнал», «что в поле сообщения», «зачитай
  сообщение», «покажи журнал», «включи мозг», «подними модель»,
  «скачай мозг».

**Технически:** запятые больше не режутся глобально — Chrome вкладывает
пунктуацию в транскрипт, и payload «отправь: …» должен доходить без
потерь; сверка с префиксами/картой идёт в отдельной командной форме
(запятые → пробелы), payload берётся из строки с сохранёнными запятыми.

**Тесты:** logic 77/77 (включая решитьСтраницу: 13 страниц, честный
null), wire 27/27 (включая НАСТОЯЩУЮ навигацию: «открой зал» переводит
браузер на СИНГУЛЯР_17_ЗАЛ.html и возвращение на ·21 без ошибок
консоли); тестовый сервер получил SO_REUSEADDR — прогоны не зависят от
TIME_WAIT прошлых запусков. ОБУЧЕНИЕ/ репы singulyar-llm синхронно
поднято до v0.4 (правила + jsonl). Ядра ·21/·22 не тронуты.

## Срез 6 — MeaningRouter на шину (v10.74 «СВОД»)

Закрыта открытая граница среза 2: «при смешении нескольких кандидатов на
один смысл роутер подключится следующим срезом». Правило №10 соблюдено
буквально: сначала тест против живых файлов — RED 12 OK / 5 FAIL на
непроводных файлах (`ИНСТРУМЕНТЫ/test_meaning_router.mjs`), потом
проводка. Итог GREEN 38/38; регресс: adapter-os 4/4, llm logic —
111 OK / 1 FAIL, ровно как на HEAD до среза (тот единственный FAIL —
давний дефект самого теста: строковое сравнение `СЛОВАРЬ_ВЕРСИЯ >=
'v0.6'` ложно при живой версии 'v0.10'; файлы срезов 4/5 срезом 6 не
трогались).

**Что сделано.**

- `src/core.mjs` — минимальная правка `MeaningRouter.route`: тотальная
  детерминированная сортировка кандидатов; canUse зовётся ровно один
  раз на адаптера; форма ответа прежняя (message/selected/candidates),
  поле `match` в candidates — добавочное (0 — generic, 1 — полное
  совпадение capability). Остальное ядро пакета — байт-в-байт.
- `bridge.mjs` — только добавочное: `os.routeMixed(meaning,
  {preferred, target})` — смешение кандидатов на один смысл. Роутер
  выбирает ровно одного; доставка — render выбранного адаптера, ТОЛЬКО
  если передан target; след — конверт `meaning.router`
  `{via:'router', selected, candidates, policy, delivered, reason}` в
  шину (и в возврате, и в bus.log()); отказ честный: никого готового →
  selected:null, delivered:false, render не зовётся ни разу.
  `os.routeOut` (срезы 1–5) не изменился и конвертов не публикует.

**Политика выбора (одной формулой):**

```text
выбор = preferred человека (правило ·19) > capability-full (canUse объявлен и вернул истину) > priority (больше) > id (лексикографически, по возрастанию)
```

Сортировка тотальная → при одинаковых входах выбор один и тот же,
порядок регистрации адаптеров на результат не влияет. Раньше при равных
приоритетах побеждал порядок регистрации (недетерминизм при смешении) —
теперь побеждает меньший id, и это задокументированный контракт.

**Как проверено (38 проверок, node, против живых файлов):** (a) два
кандидата на один смысл → ровно один победитель, одинаковый при любом
порядке регистрации и при повторе; (b) каждая ступень политики
проверена по отдельности + preferred остаётся выше автоматики; (c)
одиночный кандидат и `routeOut` идут как в срезах 1–5 (прежняя форма
ответа, без конвертов в шину); (d) конверт `meaning.router` —
настоящий конверт шины (id/type/payload/createdAt/author), несёт
via:'router', выбранного, ПОЛНЫЙ список кандидатов со статусами и
название политики; лежит в bus.log(); доставка уходит ровно одному
адаптеру, проигравший не доставлял, смысл доходит без искажений; (e)
честный отказ: selected:null, reason:'no-usable-adapter',
delivered:false, render 0 раз, цель не тронута; после восстановления
статусов реестр снова обслуживает (не застревает в отказе).

**Честные границы среза 6:** роутер участвует только в СМЕШЕНИИ
кандидатов на один смысл (`os.routeMixed`) — production-проводка ·21 по
каналам получателя (срез 2, via=registry/kernel) на роутер не
переключалась и не должна: получатель уже назвал свои каналы; `canUse`
в стоковых адаптерах пакета не объявлен — поле контрактное, для будущих
адаптеров (политика проверена на адаптерах по полному контракту);
node-прогон честно ставит in-memory заглушку localStorage (в браузере
мост пишет в настоящий localStorage — это уже проверяют wire-тесты
среза 2 против живых страниц); py-wire (проводка 19/19, llm wire 27/27)
и мост 33/33 требуют живых страниц/браузера — их прогоняет главный
агент в vm после залива; доставка через render — синхронный контракт
текущих адаптеров, асинхронные каналы (например, TTS-очередь) остаются
делом самого адаптера.
## Срез 7 — ГОЛОС: локальный Whisper по контракту (v10.94 «ГОЛОС», С70)

Приоритет владельца (2026-10-13): «1) локальный Whisper (речь — ядро), 2) OCR,
3) gaze, 4) Demucs, 5) Брайль — последним». Шаг 1 из пяти: контракт
`input.whisper-local` из contracts.mjs получает реализацию-дверь. Веса —
у владельца (закон честности, дом не тащит и не качает веса); статус до пина —
EXPERIMENTAL, честно (критерий А3: «модель пинится и проходит контрактные
тесты» — тесты зелёные, пин ожидает люка).

**Проводка (`adapters/speech.mjs`):**
- ledger `adapter-os.speech.v1` — пуст на импорте (моделей в доме нет, и это
  честно); записи возвращаются копиями, снаружи ledger не подменить;
- контракт runner — синхронный, чистый, 0 сети (родня ladom.open-model.v1):
  `runner(задание) → {сегменты, язык} | {отказ}`; асинхронные рантаймы
  (whisper.cpp→wasm, transformers.js, локальный сервер вывода) гоняются за
  пределами синхронного цикла и приносят «замороженный ответ»;
- строгий разбор сегментов: наложение таймингов, пустой текст, нечитаемый
  язык, граница 4096 — честные отказы, ничего не чинится молча; доказано
  тестом: при `конец > начало` наложение покрывает нарушение порядка —
  мёртвой ветки в парсере нет;
- пин весов all-or-nothing (полупиннинг запрещён): `склеитьЧасти()` +
  `сверитьПин()` через crypto.subtle (SHA-256); расхождение — склейка не
  выдаётся даже на байт; проверено известным вектором SHA-256("abc");
- честный мок `mockRunner()` — всегда отказ «МОК whisper: весов нет…», мок
  не притворяется распознаванием (родня MOCK_NO_TOKEN адаптера Reve);
- дверь `распознать()` никогда не бросает наружу: беда — честный отказ в
  конверте; одна модель в ledger — она, несколько — id обязателен (дом не
  выбирает за человека);
- `createSpeechAdapter()` — статус по ledger: EXPERIMENTAL (пусто/без пина,
  причина названа) → READY (есть пин), privacy local; реестр дома принимает.

**Бенчмарк (`adapters/speech-bench.mjs`) — требование карты «бенчмарки (ledger трека)»:**
WER/CER против размеченного корпуса; нормализация названа честно (регистр,
ё→е, пунктуация прочь); протокол `adapter-os.speech.bench.v1` детерминирован
байт-в-байт — 0 часов (тайминги устройств вписывает запускающий снаружи);
отказы runner'а не превращаются в метрики, прогон мока честно помечен
«не мера качества».

**Люк весов (`tools/раскрой_весов.py` + `docs/ЛЮК_ВЕСОВ_WHISPER.md`):**
локальный ggml → части ≤100 МБ (правда платформы: лимит git) → SHA-256
целого/частей → фрагмент манифеста → выкладка в репу весов
`rtynbirf/singulyar-llm → WHISPER/` (по образцу Qwen-частей) → пин в ledger
→ бенчмарк корпуса владельца. Скрипт проверен фикстурами: 3-частный и
одночастный раскрой, склейка байт-в-байт = оригинал.

**Тесты (правило №10: RED против живых файлов, честный PASS-счёт):**
- `tests/test-speech.mjs` — **16/16 PASS**: ledger/мок/регистрация
  (дубликат, полупин, плохой хэш — false)/статусы до-после пина
  (EXPERIMENTAL→READY)/валидация задания/копия входа (не мутируется)/
  9 честных отказов разбора/дверь (пусто·одна·несколько·чужой id·бросок)/
  детерминизм/вектор SHA-256/все ветки пина/метрики WER·CER/протокол;
- их регресс не тронут: `tests/run-tests.mjs` — **4/4 PASS** (файл байт-в-байт).

**Честные границы среза 7:** весов в доме нет — `input.whisper-local`
честно EXPERIMENTAL; двигатель (whisper.cpp→wasm) не выбран гвоздями — runner
принимает любой рантайм с замороженным ответом, двигатель фиксируется при
пине (поле `движок`); OCR/gaze/Demucs/Брайль — контракты не тронуты,
очередь по приоритету владельца. Отчёт такта: `docs/ОТЧЁТ_ГОЛОС.md`.
## Срез 8 — ЧТЕНИЕ: локальный OCR по контракту (v10.95 «ЧТЕНИЕ», С71)

Очередь владельца: шаг 1 (v10.94 «ГОЛОС») — Whisper; **шаг 2 — OCR, этот срез**;
далее gaze (3), Demucs (4), Брайль (5 — последним: аудитория узкая, а работы
много). Контракт `input.ocr` получает реализацию-дверь. Модель (tesseract
traineddata) — у владельца; статус до пина — EXPERIMENTAL, честно.

**Проводка (`adapters/ocr.mjs`):**
- ledger `adapter-os.ocr.v1` — пуст на импорте (моделей в доме нет, и это
  честно); записи возвращаются копиями;
- контракт runner — синхронный, чистый, 0 сети: `runner(задание) → {текст,
  строки?, язык} | {отказ}`; Tesseract.js-воркер и прочие асинхронные
  рантаймы приносят «замороженный ответ»;
- ПОДПИСЬ ФАЙЛА — отличие от шага 1: адаптер не верит названному формату,
  а сверяет магические байты (PNG/JPEG/BMP/GIF/WEBP/TIFF — 6 форматов);
  «названо png, лежит jpeg» — честный отказ, картинка не подменяется молча;
- строгий разбор: ПУСТОЙ текст — валидный ответ (чистый лист честно
  распознаётся пустотой — в отличие от речи, где пустая реплика — отказ);
  отсутствие строки «текст» — отказ; границы МАКС_СТРОК 4096 и
  МАКС_СИМВОЛОВ 1 000 000 названы;
- пин весов all-or-nothing + `склеитьЧасти`/`сверитьПин` через crypto.subtle
  (SHA-256; вектор "abc" держат обе копии — speech и ocr; копия механики
  сознательна: самодостаточность адаптера, расхождение ловят тесты);
- честный мок — всегда отказ «МОК ocr: модели нет в доме…»;
- дверь `распознать` никогда не бросает; одна модель — она, несколько — id
  обязателен (дом не выбирает за человека);
- `createOcrAdapter` — статус по ledger: EXPERIMENTAL → READY, privacy local.

**Бенчмарк (`adapters/ocr-bench.mjs`) — на ОБЩИХ мерилах дома:** нормализация,
Левенштейн, WER/CER импортированы из `speech-bench.mjs` (мерила не разъезжаются
между адаптерами — одна нормализация на дом); добавлена доля точных строк
(line accuracy); протокол `adapter-os.ocr.bench.v1` детерминирован байт-в-байт,
0 часов; отказы не превращаются в метрики.

**Люк весов (`docs/ЛЮК_ВЕСОВ_OCR.md`):** tessdata (fast/best) по Закону о
языках — rus первым, сербский вторым ВСЕГДА, казахский третьим, немецкий
четвёртым; SHA-256 по факту скачки (дом по памяти не называет);
`singulyar-llm → OCR/` → пин → бенчмарк корпуса владельца; оффлайн-кэш по
образцу llm.mjs (Cache API, один раз; ключ `singular-ocr-v1` при пине).

**Тесты:** `tests/test-ocr.mjs` — **14/14 PASS** (пустой ledger, мок,
регистрация, статусы до/после пина, 6 форматов по подписи байтов, язык,
копия входа, 9 провалов разбора, дверь, детерминизм, вектор SHA-256, ветки
пина, метрики, форматы); регресс не тронут: их run-tests **4/4**, шаг 1 —
**16/16** (файлы байт-в-байт).

**Честные границы среза 8:** модели OCR в доме нет — `input.ocr` честно
EXPERIMENTAL; двигатель не выбран гвоздями (runner принимает любой рантайм с
замороженным ответом, фиксируется при пине); предобработка картинки — не
молчаливая забота адаптера. Отчёт такта: `docs/ОТЧЁТ_ЧТЕНИЕ.md` (в adapter-os
docs/).


## Срез 9 — ВЗГЛЯД: локальный gaze по контракту (v10.98 «ВЗГЛЯД», С74)

Очередь владельца: шаг 1 (v10.94 «ГОЛОС») — Whisper; шаг 2 (v10.95 «ЧТЕНИЕ»)
— OCR; **шаг 3 — gaze, этот срез**; далее Demucs (4), Брайль (5 — последним:
аудитория узкая, а работы много). Контракт `input.gaze` получает
реализацию-дверь. Модель (Face Landmarker / iris-рантайм) — у владельца;
статус до пина — EXPERIMENTAL, честно.

**Честное разделение труда** (контракт требует четыре вещи): LANDMARKS —
рантайм владельца, дом не тащит и не запускает веса; CALIBRATION / MAPPING /
DWELL-SELECTION — три детерминированных ядра дома, покрытых тестами
байт-в-байт.

**Проводка (`adapters/gaze.mjs`):**
- ledger `adapter-os.gaze.v1` — пуст на импорте (моделей в доме нет, и это
  честно); записи возвращаются копиями;
- контракт runner — синхронный, чистый, 0 сети: `runner(задание) → { черта:
  { x, y, уверенность } } | { отказ }`; wasm-воркер и прочие асинхронные
  рантаймы приносят «замороженный ответ»;
- ПОДПИСЬ ФАЙЛА — кадр камеры та же картинка, что у OCR: магические байты
  (6 форматов) сверяются с названным форматом; расхождение — честный отказ,
  кадр не подменяется молча;
- калибровка `gaze-calib-affine/0.1` — пары «точка кадра → точка экрана»,
  минимум 3, предел МАКС_ТОЧЕК 4096; аффинное соответствие наименьшими
  квадратами (нормальные уравнения 3×3, правило Крамера — без библиотек, без
  часов); вырожденность (коллинеарные/совпадающие точки) — честный отказ
  «плоскость экрана не натянуть»; остаток сеанса назван числами
  (`ошибка_средняя`, `ошибка_макс`) — плохой сеанс виден, а не обещан;
- проекция — выход за экран НЕ поджимается молча: честный флаг `в_границах`
  (границы 0 и 1 включены); диапазона на черте кадра нет — границы ставит
  только проекция;
- dwell-selection (`событияDwell`) — чистый редюсер выдержки с ВНЕШНИМИ
  часами: радиус 0.04, выдержка 600 мс, гистерезис 0.08 (≥ радиуса, иначе
  мёртв), порог_уверенности (точки ниже пропущены — названо); немонотонное
  время — честный отказ; событие «выбор» запирает точку до честного выхода
  из её радиуса — пулемёта кликов нет;
- БЕЗ КАЛИБРОВКИ ДВЕРЬ ОТКАЗЫВАЕТ (причина контракта: «landmarks are not
  calibrated screen gaze») — молча проецировать «на глаз» — подмена данных;
- пин весов all-or-nothing + `склеитьЧасти`/`сверитьПин` (SHA-256; вектор
  "abc" держат все три копии — speech/ocr/gaze); расхождение — данные не
  выдаются;
- честный мок — всегда отказ «МОК gaze: модели нет в доме…»;
- дверь `взгляд` — никогда не бросает; порядок честный: валидация →
  калибровка → соответствие (вырожденность ловится ДО вызова двигателя) →
  ledger → runner → разбор → проекция; одна модель — она, несколько — id
  обязателен;
- `createGazeAdapter` — статус по ledger: EXPERIMENTAL → READY по пину,
  privacy local — камера не покидает устройство.

**Бенчмарк (`adapters/gaze-bench.mjs`):** взгляду — свои честные мерила,
геометрические: евклидова ошибка черты против эталона + доля точек в
названном допуске (по умолчанию 0.05). Текстовые мерила speech-bench сюда не
тянутся. Протокол `adapter-os.gaze.bench.v1` без часов, детерминирован
байт-в-байт; отказы не метрики; качество калибровки меряется отдельно
(остаток соответствия).

**Люк весов (`docs/ЛЮК_ВЕСОВ_GAZE.md`):** модель → раскрой
(`tools/раскрой_весов.py`) → `singulyar-llm → GAZE/` → пин → калибровочный
сеанс владельца → бенчмарк корпуса. Веса взгляда нейтральны к языку (Закон о
языках действует на словарь подсказок сеанса); оффлайн-кэш по образцу llm.mjs
(ключ `singular-gaze-v1` при пине).

**Тесты:** `tests/test-gaze.mjs` — **16/16 PASS** (пустой ledger, мок,
регистрация, статусы до/после пина, 6 форматов по подписи байтов, копия
входа, 13 провалов разбора, дверь, детерминизм, вектор SHA-256, ветки пина,
аффинное соответствие до 1e-9 + вырожденность, проекция без поджатия, dwell
— 5 веток, бенчмарк, схемы); регресс не тронут: их run-tests **4/4**, шаг 1 —
**16/16**, шаг 2 — **14/14**.

**Честные границы среза 9:** модели взгляда в доме нет — `input.gaze` честно
EXPERIMENTAL; качество калибровки дом не проверяет — считает остаток и
называет числами; двигатель не выбран гвоздями (runner принимает любой
рантайм с замороженным ответом, фиксируется при пине); камера/разрешения —
вне контракта адаптера (дверь получает кадр, а не поток; явное действие
человека — дело вызывающего слоя). Отчёт такта: `docs/ОТЧЁТ_ВЗГЛЯД.md` (в
adapter-os docs/).


## Срез 10 — МИНУС: локальный Demucs по контракту (v10.99 «МИНУС», С75)

Очередь владельца: шаг 1 (v10.94 «ГОЛОС») — Whisper; шаг 2 (v10.95 «ЧТЕНИЕ»)
— OCR; шаг 3 (v10.98 «ВЗГЛЯД») — gaze; **шаг 4 — Demucs, этот срез**; далее
Брайль (5 — последним: аудитория узкая, а работы много). Контракт
`tool.demucs` получает реализацию-дверь. Модель (htdemucs и родня) — у
владельца; статус до пина — EXPERIMENTAL, честно.

**Честное разделение труда** (контракт: audio → четыре стема): ДВИЖОК —
рантайм владельца, дом не тащит и не запускает веса; ПОДПИСЬ АУДИО / СТРОГИЙ
РАЗБОР / ЧЕСТНЫЕ ЧИСЛА — три детерминированных ядра дома, покрытых тестами
байт-в-байт.

**Проводка (`adapters/demucs.mjs`):**
- ledger `adapter-os.demucs.v1` — пуст на импорте (моделей в доме нет, и это
  честно); записи возвращаются копиями;
- контракт runner — синхронный, чистый, 0 сети: `runner(задание) → { стемы:
  { vocals, drums, bass, other }, смесь?, частота_дискретизации? } |
  { отказ }`; wasm-воркер и прочие асинхронные рантаймы приносят
  «замороженный ответ»;
- ПОДПИСЬ АУДИО — магические байты (5 форматов: wav RIFF/WAVE, mp3 ID3 или
  frame-sync, ogg OggS, flac fLaC, m4a ftyp) сверяются с названным форматом;
  расхождение — честный отказ, звук не подменяется молча;
- строгий разбор — ровно четыре канонических стема (лишний ключ — мусор,
  недостающий — отказ), равные длины (каналы одного микса), конечные
  отсчёты; диапазона на отсчётах НЕТ — конечность требует разбор, поджатие
  не делаем; смесь (если названа) — той же длины; частота (если названа) —
  конечное число > 0;
- честные числа стемов (`числаСтемов`) — рмс/пик/громкость_дБ/доля мощности
  по каждому стему: кто главнее в миксе видно числами; тишина названа честно
  (рмс 0 → громкость null, «тишина: true») — выдуманный −∞ в JSON — грех
  подмены; сумма долей — 1 математически, у полной тишины доли не
  выдумываются;
- остаток микса (`остатокМикса`) — смесь − сумма стемов: расхождение названо
  числами (`рмс_остатка`, `против_смеси_дБ`), полная сходимость названа
  словами; f32-остаток тождественного микса (~−153 дБ, за порогом слуха)
  назван числом — не обещание «идеального разделения»;
- пин весов all-or-nothing + `склеитьЧасти`/`сверитьПин` (SHA-256; вектор
  "abc" держат все четыре копии — speech/ocr/gaze/demucs); расхождение —
  данные не выдаются;
- честный мок — всегда отказ «МОК demucs: модели нет в доме…»;
- дверь `разделить` — никогда не бросает; порядок честный: валидация →
  ledger → runner → строгий разбор → честные числа → остаток; одна модель —
  она, несколько — id обязателен;
- `createDemucsAdapter` — статус по ledger: EXPERIMENTAL → READY по пину,
  privacy local — звук не покидает устройство.

**Бенчмарк (`adapters/demucs-bench.mjs`):** разделению — своё честное
мерило: SDR без выравнивания (10·log10(||эталон||² / ||эталон − оценка||²));
граница названа в протоколе («bss_eval не притворяется»). Идеальная
сходимость пишется словом, не числом (JSON.stringify(Infinity) молча дал бы
null — грех подмены); тишина в эталоне — «не мера»; энергопрофиль оценок
(рмс) назван даже там, где SDR не мера. Протокол
`adapter-os.demucs.bench.v1` без часов, детерминирован байт-в-байт.

**Люк весов (`docs/ЛЮК_ВЕСОВ_DEMUCS.md`):** модель → раскрой
(`tools/раскрой_весов.py`; htdemucs крупнее 100 МБ — части) →
`singulyar-llm → DEMUCS/` → пин → контрактные ожидания движка → бенчмарк
корпуса. Веса Demucs нейтральны к языку (Закон о языках действует на словарь
UI стемов: «голос / ударные / бас / остальное»); оффлайн-кэш по образцу
llm.mjs (ключ `singular-demucs-v1` при пине); склейка стемов в караоке-минус
— дело вызывающего слоя, адаптер честен и не обещает минус раньше времени.

**Тесты:** `tests/test-demucs.mjs` — **16/16 PASS** (пустой ledger, мок,
регистрация, статусы до/после пина, 5 форматов по подписи байтов, копия
входа, 12 провалов разбора, дверь, детерминизм, вектор SHA-256, ветки пина,
READY в реестре дома, честные числа до 1e-6 с f32-запасом + тишина + доли,
остаток микса на дайадиках — точный ноль, бенчмарк — SDR ×0.9 → 20 дБ,
схемы); регресс не тронут: их run-tests **4/4**, шаг 1 — **16/16**, шаг 2 —
**14/14**, шаг 3 — **16/16**.

**Честные границы среза 10:** модели Demucs в доме нет — `tool.demucs`
честно EXPERIMENTAL; качество разделения дом не проверяет — называет числа
(рмс, пик, доли, остаток), против эталонных стемов мерит бенчмарк по корпусу
владельца; двигатель не выбран гвоздями (runner принимает любой рантайм с
замороженным ответом, фиксируется при пине); минусовка для караоке — дело
вызывающего слоя. Отчёт такта: `docs/ОТЧЁТ_МИНУС.md` (в adapter-os docs/).


## Срез 11 — БРАЙЛЬ: детерминированный Unicode-Брайль по контракту (v11.00 «БРАЙЛЬ», С76)

Очередь владельца: шаг 1 (v10.94 «ГОЛОС») — Whisper; шаг 2 (v10.95 «ЧТЕНИЕ»)
— OCR; шаг 3 (v10.98 «ВЗГЛЯД») — gaze; шаг 4 (v10.99 «МИНУС») — Demucs;
**шаг 5 — Брайль, этот срез, ПОСЛЕДНИЙ: строка А3 закрыта**. Контракт
`output.braille` получает реализацию-дверь — и здесь честная готовность
(READY) возможна без пина: контракт требует output `unicode-braille`, дом
делает ровно это.

**Почему без весов** (честное отличие от четырёх предыдущих дверей):
перевод текста в Unicode-Брайль — детерминированная математика ячеек
U+2800–U+28FF, а не нейросеть. Склейка частей, SHA-256-пин и честный мок
сюда не тянутся — вместо них таблицы, закреплённые тестами байт-в-байт.

**Проводка (`adapters/braille.mjs`):**
- встроенная таблица `latin-g1` — канон Луи Брайля/UEB grade 1: 26 букв
  (a–z), 4 знака пунктуации (запятая 2, точка 2-5-6, апостроф 3, дефис
  3-6); знак заглавной — точка 6 перед каждой заглавной; знак цифр —
  точки 3-4-5-6 перед каждой группой цифр, цифры 1–0 через a–j;
- пробел и перевод строки — служебные дома: проходят как есть, структурой
  текста не торгуют; таблицей не подменяются;
- ledger `adapter-os.braille.v1` — на импорте ровно одна встроенная
  таблица; таблицы владельца регистрируются структурно: ключ — ровно один
  символ, точки — целые 1..8 без дубликатов, дубликат id — честный false,
  вход защищён копией;
- два названных режима: `строгий` (по умолчанию) — незнакомый символ →
  честный отказ с позицией и кодом, ячейки не выдумываются; `с_заглушками`
  — пустая ячейка U+2800 и посимвольный список потерь в отчёте — ничего
  не теряется молча; цифра без буквы a–j в таблице владельца — тот же
  честный отказ/заглушка;
- пустой текст — честная пустота (как чистый лист у OCR);
- дверь `брайль` — никогда не бросает; одна таблица — она, после
  регистрации второй id обязателен — дом не выбирает язык за человека;
- `createBrailleAdapter` — READY честно: причина называет границы
  (Unicode Braille only, hardware optional — физический дисплей вне
  контракта); privacy local.

**Бенчмарка НЕТ — и это честно:** у четырёх предыдущих дверей было ЧТО
мерить (качество движка против корпуса: WER/CER, CER, евклид, SDR). У
Брайля движка нет; таблицы закреплены независимой копией канона в тесте
(сравнение байт-в-байт), соответствие стандарту языка — за владельцем.

**Люк таблиц (`docs/ЛЮК_ТАБЛИЦ_БРАЙЛЬ.md`):** форма таблицы → структурная
проверка домом → контрактный тест с независимой копией канона → дверь с id.
Закон о языках действует прямо: rus → srp → kaz → deu, далее языки СССР и
народные. Grade 2 — не в доме; дисплей — вне контракта.

**Тесты:** `tests/test-braille.mjs` — **16/16 PASS** (встроенная таблица,
алфавит a–z против независимой копии канона, знак заглавной, цифры и
группы — 31 → ⠼⠉⠁, 10.88 → две группы, пунктуация, строка целиком с
отчётом, валидация, пустая строка — честная пустота, строгий режим —
позиция и код, с_заглушками — потери названы, 13 провалов регистрации,
свойства после регистраций, границы точек, пробел/перевод строки +
детерминизм двух режимов, READY честно в реестре дома, схемы и нетронутая
контрактная заглушка); регресс не тронут: их run-tests **4/4**, шаг 1 —
**16/16**, шаг 2 — **14/14**, шаг 3 — **16/16**, шаг 4 — **16/16**.

**Честные границы среза 11:** Unicode Braille — только представление;
физический дисплей (WebHID/OS accessibility) — вне контракта, названо;
grade 2 — не в доме; соответствие таблиц владельца стандартам языков — за
владельцем (дом гарантирует структуру, детерминизм и честные отказы).
Отчёт такта: `docs/ОТЧЁТ_БРАЙЛЬ.md` (в adapter-os docs/).
