surfaces/.planning/phases/01-matrix-qa-polish/01-RESEARCH.md

528 lines
33 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 — 106110 тестов.
Критическая деталь: `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 (целевой диапазон 106110 после добавления новых)
### Численный ориентир для "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)