# Phase 1: Matrix QA & Polish — Research

**Researched:** 2026-04-02
**Domain:** matrix-nio AsyncClient — Space+rooms architecture, OutgoingUI text rendering, !yes/!no confirmation flow
**Confidence:** HIGH (all critical APIs verified against the installed library)

---

<user_constraints>
## User Constraints (from CONTEXT.md)

### Locked Decisions

- **D-01:** Space+rooms — единственная поддерживаемая модель. DM-first убрать.
- **D-02:** При первом invite бот создаёт Space `Lambda — {display_name}`, внутри — первую комнату `Чат 1`. Приглашает пользователя.
- **D-03:** `!new [name]` создаёт новую комнату внутри Space пользователя, приглашает его туда.
- **D-04:** `!archive` выводит комнату из Space (не удаляет).
- **D-05:** Маппинг `room_id → space_id` + `room_id → chat_id` хранится в SQLite через `adapter/matrix/store.py`.
- **D-06:** Реакции (`👍`/`❌`) — убрать полностью. `OutgoingUI` с кнопками рендерится как текст + `!yes` / `!no`.
- **D-07:** Когда агент запрашивает подтверждение, бот пишет текстовое сообщение с описанием и подсказкой: `Ответьте !yes для подтверждения или !no для отмены.`
- **D-08:** `!yes` / `!no` работают в текущей комнате. Состояние ожидания подтверждения хранится per (user_id, room_id).
- **D-09:** Все команды работают из любой комнаты Space — нет выделенной комнаты «Настройки».
- **D-10:** Команды: `!new [name]`, `!chats`, `!rename <name>`, `!archive`, `!skills`, `!soul`, `!safety`, `!settings`, `!yes`, `!no`.
- **D-11:** `!start` — не нужен, онбординг через invite flow.
- **D-12:** `!settings` — read-only дашборд: одно сообщение со статусом всего (скиллы, soul, safety, активные чаты). Ничего не меняет.
- **D-13:** Изменения через субкоманды: `!skills`, `!skill on/off <name>`, `!soul`, `!soul name/style/priority/reset <value>`, `!safety`, `!safety on/off <action>`.
- **D-14:** Каждая команда без аргументов (`!skills`, `!soul`, `!safety`) выводит текущее состояние + подсказку как менять.

### Claude's Discretion

- Формат текста в Matrix (plain text vs markdown) — на усмотрение, Matrix клиенты рендерят markdown
- Структура invite-сообщения в новом Space — на усмотрение, главное: приветствие + список команд
- Обработка ошибок matrix-nio (room_create fail, join fail) — логировать + ответить пользователю

### Deferred Ideas (OUT OF SCOPE)

- Комната «Настройки» как отдельная закреплённая комната — решили не делать в Phase 1
- E2EE / python-olm — инфраструктурный трек, вне scope
- Space discovery (бот находит существующий Space при повторном invite) — Phase 2+
- Attachment handling (m.file, m.image, m.audio) — Phase 2+
</user_constraints>

---

## Summary

Phase 1 переписывает Matrix адаптер с DM-first на Space+rooms модель, убирает реакции в пользу `!yes`/`!no`, и реализует все команды управления. Большая часть бизнес-логики уже работает через `core/handlers/` и `adapter/matrix/handlers/settings.py`. Главная работа — в трёх точках: `handle_invite` (создание Space + двух комнат), `make_handle_new_chat` (добавление комнаты в Space), и `send_outgoing` (убрать реакции, добавить pending-state для `!yes`/`!no`).

Текущее состояние: 97 тестов зелёные. Для "96+ зелёных" после рефакторинга нужно обновить 3 существующих теста (они проверяют DM-поведение и реакции) и добавить ~12 новых тестов на Space-сценарии. Итого целевой range — 106–110 тестов.

Критическая деталь: `AsyncClient.room_create` принимает `space=True` (булевый параметр, не `room_type="m.space"`) для создания Space. Добавление дочерней комнаты — через `room_put_state` на Space с event_type `m.space.child` и state_key = child room_id. Это проверено против установленной версии matrix-nio.

**Primary recommendation:** Реализовать в трёх независимых задачах Codex: (1) invite flow — Space+rooms creation, (2) send_outgoing — убрать реакции, добавить pending-confirm store, (3) обновить тесты под новое поведение.

---

## Standard Stack

### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| matrix-nio | установлена (проверено: `space=True` параметр присутствует) | Matrix async клиент — room_create, room_put_state, room_invite, join | Единственный maintained async Python Matrix клиент |
| structlog | уже используется | Логирование | Уже в проекте |
| pytest-asyncio | уже используется | Async тесты | Уже в проекте |

**Версию matrix-nio не нужно менять.** Установленная версия поддерживает `space=True` в `room_create` и `room_put_state` для state events.

---

## Architecture Patterns

### Паттерн 1: Создание Space + первой комнаты (invite flow)

**Что:** При первом invite бот делает 5 последовательных API вызовов — создание Space, создание chat-комнаты, линковка child→Space, приглашение пользователя в обе, запись в store.

**Verified API** (из installed matrix-nio):

```python
# 1. Создать Space
space_resp = await client.room_create(
    name=f"Lambda — {display_name}",
    space=True,               # <-- булевый флаг, не room_type
    visibility="private",
    is_direct=False,
)
# space_resp.room_id — строка

# 2. Создать первую chat-комнату
chat_resp = await client.room_create(
    name="Чат 1",
    visibility="private",
    is_direct=False,
)
# chat_resp.room_id — строка

# 3. Добавить комнату в Space как child
# state_key = room_id дочерней комнаты
await client.room_put_state(
    room_id=space_resp.room_id,
    event_type="m.space.child",
    content={
        "via": [homeserver_domain],  # например "matrix.org"
    },
    state_key=chat_resp.room_id,
)

# 4. Пригласить пользователя в Space и в chat-комнату
await client.room_invite(space_resp.room_id, matrix_user_id)
await client.room_invite(chat_resp.room_id, matrix_user_id)

# 5. Записать в store
await set_user_meta(store, matrix_user_id, {
    "space_id": space_resp.room_id,
    "next_chat_index": 2,  # C1 уже занят
})
await set_room_meta(store, chat_resp.room_id, {
    "room_type": "chat",
    "chat_id": "C1",
    "display_name": "Чат 1",
    "matrix_user_id": matrix_user_id,
    "space_id": space_resp.room_id,
})
```

**Важный gotcha:** Бот сам не вступает в Space (join). Он создаёт Space как владелец, поэтому уже является членом. `join` нужен только для входящей DM-комнаты (invite в существующую комнату). В новом flow: бот создаёт комнаты сам, поэтому `join` для Space и chat-комнаты не нужен.

### Паттерн 2: Добавление новой комнаты (!new)

```python
async def handle_new_chat(...):
    user_meta = await get_user_meta(store, event.user_id) or {}
    space_id = user_meta.get("space_id")
    if not space_id:
        # Пользователь не прошёл invite flow — не должно случиться, но guard нужен
        return [OutgoingMessage(chat_id=event.chat_id, text="Ошибка: Space не найден.")]
    
    chat_id = await next_chat_id(store, event.user_id)
    room_name = " ".join(event.args).strip() or f"Чат {chat_id}"
    
    resp = await client.room_create(name=room_name, visibility="private", is_direct=False)
    room_id = resp.room_id
    
    homeserver = event.user_id.split(":")[1]  # "@user:matrix.org" → "matrix.org"
    await client.room_put_state(
        room_id=space_id,
        event_type="m.space.child",
        content={"via": [homeserver]},
        state_key=room_id,
    )
    await client.room_invite(room_id, event.user_id)
    await set_room_meta(store, room_id, {
        "room_type": "chat",
        "chat_id": chat_id,
        "display_name": room_name,
        "matrix_user_id": event.user_id,
        "space_id": space_id,
    })
```

### Паттерн 3: Archive (!archive) — убрать из Space

```python
# Убрать child: поставить пустой content (или content без 'via')
# Matrix spec: отправить m.space.child с пустым {} или без 'via' удаляет связь
await client.room_put_state(
    room_id=space_id,
    event_type="m.space.child",
    content={},          # пустой content = удалить child relationship
    state_key=room_id,   # room_id архивируемой комнаты
)
```

Confidence: MEDIUM — Matrix spec говорит что пустой content убирает child, но поведение Element может варьироваться. Альтернатива: оставить room_put_state с `{"via": []}` (пустой массив).

### Паттерн 4: OutgoingUI → текст + !yes/!no (без реакций)

**Что убрать:**
- `_button_action_to_reaction` в `bot.py` — удалить целиком
- Блок `for button in event.buttons: reaction = _button_action_to_reaction(...)` — удалить
- `ReactionEvent` callback (`on_reaction` + `client.add_event_callback`) — удалить
- `from_reaction` в converter — оставить (используется для skill-reactions), но skill-reaction инфраструктура тоже под вопросом (D-06 убирает реакции полностью)

**Что добавить в `send_outgoing` для `OutgoingUI`:**
```python
if isinstance(event, OutgoingUI):
    lines = [event.text, ""]
    for button in event.buttons:
        lines.append(f"• {button.label}")
    lines += ["", "Ответьте !yes для подтверждения или !no для отмены."]
    body = "\n".join(lines)
    await client.room_send(room_id, "m.room.message", {"msgtype": "m.text", "body": body})
    # Сохранить pending state per (user_id, room_id)
    await set_pending_confirm(store, user_id=???, room_id=room_id, action_id=???)
```

**Проблема:** `send_outgoing` сейчас не знает `user_id` — только `room_id`. Для сохранения pending state нужен либо рефакторинг сигнатуры, либо хранение pending по `room_id` (без user_id — достаточно, т.к. room_id уникален для конкретного пользователя в Space модели).

### Паттерн 5: Pending confirm state

```python
# Новые helpers в adapter/matrix/store.py
PENDING_CONFIRM_PREFIX = "matrix_pending_confirm:"

async def get_pending_confirm(store, room_id: str) -> dict | None:
    return await store.get(f"{PENDING_CONFIRM_PREFIX}{room_id}")

async def set_pending_confirm(store, room_id: str, meta: dict) -> None:
    await store.set(f"{PENDING_CONFIRM_PREFIX}{room_id}", meta)

async def clear_pending_confirm(store, room_id: str) -> None:
    await store.delete(f"{PENDING_CONFIRM_PREFIX}{room_id}")
```

`!yes`/`!no` уже конвертируются в `IncomingCallback(action="confirm"/"cancel")` в `converter.py`. Нужно обновить `handle_confirm`/`handle_cancel` в `adapter/matrix/handlers/confirm.py` чтобы читать pending state и возвращать осмысленный ответ.

### Паттерн 6: Hardcoded "C1" bug fix

```python
# auth.py:27 — СЕЙЧАС (баг):
"chat_id": "C1"

# ДОЛЖНО БЫТЬ:
chat_id = await next_chat_id(store, matrix_user_id)  # возвращает "C1" для первого пользователя
```

`next_chat_id` уже существует в `store.py` и правильно инкрементирует per-user. Нужно просто использовать его в `handle_invite` вместо хардкода.

### Рекомендуемая структура store после рефакторинга

Текущие ключи в store:
- `matrix_room:{room_id}` → `{room_type, chat_id, display_name, matrix_user_id}` — **добавить `space_id`**
- `matrix_user:{user_id}` → `{next_chat_index, ...}` — **добавить `space_id`**
- `matrix_state:{room_id}` → `{state}` — оставить как есть
- `matrix_skills_msg:{room_id}` → `{event_id}` — оставить (или убрать если реакции полностью уходят)

Новые ключи:
- `matrix_pending_confirm:{room_id}` → `{action_id, description, expires_at}` — для !yes/!no

---

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Space creation | Кастомный HTTP запрос к Matrix API | `AsyncClient.room_create(space=True)` | Встроено в matrix-nio, управляет session state |
| Adding child room to Space | Кастомный state event builder | `AsyncClient.room_put_state(room_id, "m.space.child", ...)` | Правильный Content-Type, auth headers автоматически |
| User invite | Прямой HTTP PUT | `AsyncClient.room_invite(room_id, user_id)` | Обрабатывает ошибки M_FORBIDDEN, already-joined |
| Error detection | Проверка статус-кодов | `isinstance(resp, RoomCreateError)` / `isinstance(resp, RoomPutStateError)` | matrix-nio возвращает типизированные error-объекты |

---

## Common Pitfalls

### Pitfall 1: `room_create(space=True)` vs `room_type="m.space"`

**What goes wrong:** Передача `room_type="m.space"` как отдельный параметр — работает, но `space=True` — это удобный shortcut в matrix-nio, который внутри устанавливает тот же `room_type`. Оба варианта корректны, но `space=True` проще читается.

**Проверено:** `room_create` signature в installed matrix-nio имеет `space: bool = False`. Нет отдельного `is_space` параметра.

**How to avoid:** Использовать `space=True`, не `room_type="m.space"`.

### Pitfall 2: `room_id` из RoomCreateResponse — не `getattr`

**What goes wrong:** Текущий код в `handlers/chat.py:55`: `room_id = getattr(response, "room_id", None)`. Это работает для RoomCreateResponse, но молча возвращает None если пришёл RoomCreateError (у которого нет `room_id`).

**How to avoid:**
```python
from nio.responses import RoomCreateError
resp = await client.room_create(...)
if isinstance(resp, RoomCreateError):
    logger.error("room_create failed", status_code=resp.status_code)
    return [OutgoingMessage(..., text="Не удалось создать комнату.")]
room_id = resp.room_id  # прямой доступ, не getattr
```

### Pitfall 3: `m.space.child` — state_key это room_id дочерней комнаты, не пустая строка

**What goes wrong:** `room_put_state` по умолчанию `state_key=""`. Для `m.space.child` state_key ДОЛЖЕН быть room_id дочерней комнаты — иначе Space создастся некорректно.

**How to avoid:** Всегда передавать `state_key=child_room_id` явно.

### Pitfall 4: Бот должен быть в Space чтобы добавлять children

**What goes wrong:** Бот создаёт Space (становится владельцем), потом пытается сделать `room_put_state` на Space. Это работает т.к. создатель автоматически имеет power level 100. Но если бот потерял membership (kicked out), `room_put_state` вернёт `M_FORBIDDEN`.

**How to avoid:** Логировать ошибку и сообщать пользователю. Не ретраить молча.

### Pitfall 5: Дублирование invite flow (идемпотентность)

**What goes wrong:** Текущий `handle_invite` проверяет `get_room_meta(store, room.room_id)` чтобы не запускать flow дважды. После рефакторинга на Space+rooms нужно проверять `get_user_meta(store, matrix_user_id)` — потому что invite может прийти повторно в разные комнаты Space, а Space создаётся один раз per user.

**How to avoid:** Idempotency check переносится на уровень user_meta: `if user_meta.get("space_id"): return`.

### Pitfall 6: `skills_message` реакции — остаток от старого UX

**What goes wrong:** `adapter/matrix/reactions.py` и `build_skills_text` до сих пор рендерят "Реакции 1️⃣-9️⃣ переключают навыки." По D-06 реакции убраны полностью. `build_skills_text` нужно обновить чтобы убрать эту строку и заменить инструкцией `!skill on/off <name>`.

**How to avoid:** Обновить `build_skills_text` + тест `test_reactions.py::test_build_skills_text`.

### Pitfall 7: `on_reaction` callback остаётся зарегистрированным

**What goes wrong:** В `main()` есть `client.add_event_callback(bot.on_reaction, ReactionEvent)`. Если убрать реакции но оставить этот callback — matrix-nio будет продолжать обрабатывать реакции и вызывать `on_reaction`. Нужно удалить и callback-регистрацию, и импорт `ReactionEvent`.

---

## Gaps between Current Implementation and Target

| File | Current State | Target State | Action |
|------|--------------|-------------|--------|
| `adapter/matrix/handlers/auth.py` | DM join + hardcoded C1 | Space creation + C1 from next_chat_id | Переписать `handle_invite` |
| `adapter/matrix/handlers/chat.py` | room_create без Space | room_create + room_put_state в Space | Обновить `make_handle_new_chat` |
| `adapter/matrix/bot.py` | `on_reaction` + `_button_action_to_reaction` | Без реакций, pending-state для !yes/!no | Убрать reaction code; обновить `send_outgoing` |
| `adapter/matrix/store.py` | Нет `space_id`, нет pending_confirm | `space_id` в room_meta + user_meta; `pending_confirm` helpers | Добавить поля и helpers |
| `adapter/matrix/reactions.py` | `build_skills_text` упоминает реакции | `build_skills_text` без реакций, с `!skill on/off` | Обновить текст |
| `adapter/matrix/handlers/confirm.py` | Заглушка без state | Читает pending_confirm, даёт реальный ответ | Обновить handlers |
| `adapter/matrix/handlers/settings.py` | `handle_settings` — список команд | `handle_settings` — read-only дашборд (D-12) | Обновить до дашборда со статусом |
| `adapter/matrix/converter.py` | `from_reaction` используется для skill toggle | Skill toggle через реакции убирается | `from_reaction` можно оставить или удалить |

---

## Code Examples

### Создание Space + child room (verified API)

```python
# Source: matrix-nio installed version — inspect.signature(AsyncClient.room_create)
from nio.responses import RoomCreateError, RoomPutStateError

async def create_user_space(client, display_name: str, matrix_user_id: str, store):
    homeserver = matrix_user_id.split(":")[-1]  # "@user:matrix.org" → "matrix.org"

    # Step 1: Create Space
    space_resp = await client.room_create(
        name=f"Lambda — {display_name}",
        space=True,
        visibility="private",
    )
    if isinstance(space_resp, RoomCreateError):
        return None, None
    space_id = space_resp.room_id

    # Step 2: Create first chat room
    chat_resp = await client.room_create(
        name="Чат 1",
        visibility="private",
        is_direct=False,
    )
    if isinstance(chat_resp, RoomCreateError):
        return space_id, None
    chat_room_id = chat_resp.room_id

    # Step 3: Link child room into Space (state_key = child's room_id)
    await client.room_put_state(
        room_id=space_id,
        event_type="m.space.child",
        content={"via": [homeserver]},
        state_key=chat_room_id,
    )

    # Step 4: Invite user to Space and to chat room
    await client.room_invite(space_id, matrix_user_id)
    await client.room_invite(chat_room_id, matrix_user_id)

    return space_id, chat_room_id
```

### send_outgoing для OutgoingUI (без реакций)

```python
if isinstance(event, OutgoingUI):
    lines = [event.text]
    if event.buttons:
        lines.append("")
        for btn in event.buttons:
            lines.append(f"• {btn.label}")
        lines.append("")
        lines.append("Ответьте !yes для подтверждения или !no для отмены.")
    body = "\n".join(lines)
    await client.room_send(room_id, "m.room.message", {"msgtype": "m.text", "body": body})
```

### Проверка ошибок matrix-nio

```python
from nio.responses import RoomCreateError, RoomPutStateError, RoomInviteError

resp = await client.room_create(...)
if isinstance(resp, RoomCreateError):
    logger.error("room_create failed", status_code=resp.status_code)
    # resp не имеет room_id — безопасный ранний возврат
    return [OutgoingMessage(chat_id=event.chat_id, text="Не удалось создать комнату.")]
room_id = resp.room_id  # str, гарантированно присутствует
```

---

## Validation Architecture

nyquist_validation = true в config.json — раздел обязателен.

### Test Framework
| Property | Value |
|----------|-------|
| Framework | pytest + pytest-asyncio |
| Config file | pytest.ini или pyproject.toml (проверить наличие) |
| Quick run command | `pytest tests/adapter/matrix/ -q` |
| Full suite command | `pytest tests/ -q` |
| Current count | 97 passed |

### Существующие тесты Matrix, требующие обновления

Эти тесты написаны под DM/reaction-based поведение и сломаются после рефакторинга:

| Test | Текущее поведение | После рефакторинга | Действие |
|------|------------------|-------------------|---------|
| `test_dispatcher.py::test_invite_event_creates_dm_room_and_sends_welcome` | Проверяет `chat_id == "C1"` через hardcode, join DM | Должен проверять Space creation + chat room creation | Переписать |
| `test_dispatcher.py::test_new_chat_creates_real_matrix_room_when_client_available` | Проверяет `room_create` без Space | Должен проверять `room_create` + `room_put_state` | Обновить mock + assertions |
| `test_reactions.py::test_build_skills_text` | Ожидает "Реакции 1️⃣-9️⃣" в тексте | После удаления реакций эта строка исчезнет | Обновить assertion |
| `test_reactions.py::test_build_confirmation_text` | Проверяет `CONFIRM_REACTION` + "подтвердить" | Если `build_confirmation_text` обновится под D-07 | Обновить |

### Новые тесты, необходимые для покрытия Space+rooms

| ID | Behavior | Test Type | File | Command |
|----|----------|-----------|------|---------|
| MAT-01 | handle_invite создаёт Space + Чат 1, сохраняет space_id в user_meta | unit | `tests/adapter/matrix/test_invite_space.py` | `pytest tests/adapter/matrix/test_invite_space.py -x` |
| MAT-02 | handle_invite идемпотентен: повторный вызов не создаёт второй Space | unit | `tests/adapter/matrix/test_invite_space.py` | `pytest tests/adapter/matrix/test_invite_space.py -x` |
| MAT-03 | handle_invite использует next_chat_id, не хардкод "C1" | unit | `tests/adapter/matrix/test_invite_space.py` | `pytest tests/adapter/matrix/test_invite_space.py -x` |
| MAT-04 | make_handle_new_chat вызывает room_put_state с space_id из user_meta | unit | `tests/adapter/matrix/test_chat_space.py` | `pytest tests/adapter/matrix/test_chat_space.py -x` |
| MAT-05 | make_handle_new_chat без space_id возвращает error message | unit | `tests/adapter/matrix/test_chat_space.py` | `pytest tests/adapter/matrix/test_chat_space.py -x` |
| MAT-06 | send_outgoing для OutgoingUI рендерит текст + "!yes / !no", без реакций | unit | `tests/adapter/matrix/test_send_outgoing.py` | `pytest tests/adapter/matrix/test_send_outgoing.py -x` |
| MAT-07 | send_outgoing для OutgoingUI НЕ отправляет m.reaction event | unit | `tests/adapter/matrix/test_send_outgoing.py` | `pytest tests/adapter/matrix/test_send_outgoing.py -x` |
| MAT-08 | get/set/clear_pending_confirm roundtrip в store | unit | `tests/adapter/matrix/test_store.py` (extend) | `pytest tests/adapter/matrix/test_store.py -x` |
| MAT-09 | handle_confirm читает pending_confirm и возвращает описание действия | unit | `tests/adapter/matrix/test_confirm.py` | `pytest tests/adapter/matrix/test_confirm.py -x` |
| MAT-10 | handle_archive вызывает room_put_state с пустым content | unit | `tests/adapter/matrix/test_chat_space.py` | `pytest tests/adapter/matrix/test_chat_space.py -x` |
| MAT-11 | !settings возвращает дашборд со статусом (не список команд) | unit | `tests/adapter/matrix/test_dispatcher.py` (extend) | `pytest tests/adapter/matrix/test_dispatcher.py -x` |
| MAT-12 | RoomCreateError обрабатывается корректно (нет crash, есть user message) | unit | `tests/adapter/matrix/test_chat_space.py` | `pytest tests/adapter/matrix/test_chat_space.py -x` |

### Wave 0 Gaps (новые файлы)

- [ ] `tests/adapter/matrix/test_invite_space.py` — покрывает MAT-01, MAT-02, MAT-03
- [ ] `tests/adapter/matrix/test_chat_space.py` — покрывает MAT-04, MAT-05, MAT-10, MAT-12
- [ ] `tests/adapter/matrix/test_send_outgoing.py` — покрывает MAT-06, MAT-07
- [ ] `tests/adapter/matrix/test_confirm.py` — покрывает MAT-09

### Sampling Rate

- **Per task commit:** `pytest tests/adapter/matrix/ -q`
- **Per wave merge:** `pytest tests/ -q`
- **Phase gate:** All 97+ tests green (целевой диапазон 106–110 после добавления новых)

### Численный ориентир для "96+ зелёных"

- Сейчас: 97 тестов, все зелёные
- После рефакторинга без добавления тестов: 4 теста сломаются (3 dispatcher + 1 reactions) → ~93 зелёных
- После обновления сломанных: 97 зелёных
- После добавления 12 новых: ~109 зелёных
- **Итого: требование "96+" выполнено с запасом**

---

## Environment Availability

| Dependency | Required By | Available | Version | Fallback |
|------------|------------|-----------|---------|----------|
| matrix-nio | All Matrix API calls | ✓ | установлена, space=True присутствует | — |
| pytest + pytest-asyncio | Test suite | ✓ | работает (97 passed) | — |
| SQLite | SQLiteStore | ✓ | встроен в Python | — |
| Matrix homeserver | Manual QA только | не проверялось | — | Без homeserver — только unit тесты |

**Missing dependencies with no fallback:** Нет (homeserver нужен только для ручного QA, не для автотестов).

---

## Project Constraints (from CLAUDE.md)

| Directive | Impact on Phase |
|-----------|----------------|
| `core/protocol.py` — типы не менять | `IncomingCommand`, `OutgoingUI`, `UIButton` используем as-is |
| Все вызовы платформы через `platform/interface.py` | MockPlatformClient остаётся, SDK не трогать |
| Хотфиксы < 20 строк → Claude Code напрямую | Небольшие правки реакций-в-текст могут идти напрямую |
| Реализацию делает Codex | Три задачи — три параллельных Codex запуска |
| Blueprint перед реализацией | Плану нужны blueprint-документы для каждой задачи |
| Порядок зависимостей: core/ → platform/ → adapters/ | Все изменения только в adapter/matrix/, core/ не трогаем |

---

## Open Questions

1. **Стоит ли полностью убирать `from_reaction` и `reactions.py`?**
   - D-06 говорит "убрать реакции полностью"
   - `reactions.py` содержит `build_confirmation_text` и `build_skills_text` — они нужны после рефакторинга
   - Рекомендация: оставить `reactions.py`, удалить `CONFIRM_REACTION`/`CANCEL_REACTION`/`add_reaction`/`remove_reaction`, переименовать в `formatting.py` — но это необязательно для Phase 1.

2. **Нужен ли `m.space.parent` event в дочерних комнатах?**
   - Matrix spec позволяет устанавливать `m.space.parent` в дочерней комнате, чтобы Element показывал ссылку "назад к Space"
   - Не является обязательным — `m.space.child` в Space достаточно для включения комнаты в Space
   - Рекомендация: не добавлять в Phase 1, отложить если понадобится.

3. **`via` в `m.space.child` — один сервер или несколько?**
   - Для single-homeserver деплоя: `["homeserver_domain"]` достаточно
   - Для федерации: нужны несколько серверов
   - Рекомендация: парсить из `matrix_user_id.split(":")[-1]` — достаточно для текущего использования.

---

## Sources

### Primary (HIGH confidence)
- matrix-nio installed package — `AsyncClient.room_create`, `room_put_state`, `room_invite`, `join` — сигнатуры и docstrings проверены через `inspect.signature` и `help()`
- `nio.responses.RoomCreateResponse`, `RoomCreateError`, `RoomPutStateResponse`, `RoomPutStateError` — поля проверены через `inspect.getsource`
- Весь codebase прочитан напрямую

### Secondary (MEDIUM confidence)
- Matrix Spec v1.x — `m.space.child` event format (content `{"via": [...]}`, state_key = child room_id) — стандартное поведение, описано в Matrix spec

---

## Metadata

**Confidence breakdown:**
- matrix-nio API: HIGH — проверено против installed package через Python introspection
- Space creation pattern: HIGH — `space=True` параметр подтверждён в room_create signature
- `m.space.child` content format: MEDIUM — стандарт Matrix spec, не проверен против конкретного homeserver
- Archive via empty content: MEDIUM — Matrix spec behaviour, может зависеть от homeserver version
- Тест-план: HIGH — основан на прямом анализе существующих тестов

**Research date:** 2026-04-02
**Valid until:** 2026-05-02 (matrix-nio обновляется редко, Space API стабилен с Matrix v1.2)
