diff --git a/CLAUDE.md b/CLAUDE.md index 4fd55959..4e603bf1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,8 +18,8 @@ Go-бэкенд издательского художественного пер | **Бэкенд** | `backend/` | нет — лендит оркестратор | читать можно; расхождения — пингом оркестратору в `docs/PROGRESS.md` | | **Полигон (eval)** | `eval/` + `docs/experiments/` | только пре-рег фризы | то же | | **Оркестратор** | `docs/` (architecture/research/PROGRESS) + корневые онбординг-доки, ревью чужого кода read-only | да | ратифицирует решения, пишет хендофф-промты | -| **Фронт** | `frontend/` | нет — лендит оркестратор | то же; продуктовые требования — `docs/product-requirements.md`, НЕ из памяти | -| **Платформа** | `platform/` (SaaS control plane: пользователи/квоты/очередь/HTTP; движок дёргает процессами, D39.81) | нет — лендит оркестратор | то же | +| **Фронт** | `frontend/` | нет — лендит оркестратор | читать можно; пинги, итоги и вопросы — ЗОННЫЙ журнал `frontend/docs/frontend-PROGRESS.md` (решение владельца 02.08/04.08; в `docs/PROGRESS.md` фронт НЕ пишет), туда же пишет и оркестратор; продуктовые требования — `docs/product-requirements.md`, НЕ из памяти | +| **Платформа** | `platform/` (SaaS control plane: пользователи/квоты/очередь/HTTP; движок дёргает процессами, D39.81) | нет — лендит оркестратор | читать можно; пинги и итоги — зонные доки `platform/`, в `docs/PROGRESS.md` не пишет | Координация — журнал `docs/PROGRESS.md` (секции «Бэкенд»/«Полигон»/«Память»/«Голос и состояние»/«Ридер-IDE»; сверху CURRENT-STATE). Записывай туда краткие итоги своей сессии. Закрытые хроники вынесены в слайсы `docs/archive/PROGRESS-*.md` — НЕ читать при онбординге, только по конкретной ссылке. Параллельные сессии — норма: чужие незакоммиченные файлы в дереве не трогать; сессия считается ЖИВОЙ, пока владелец не сказал обратное. diff --git a/docs/ORCHESTRATOR_SESSION_PROMPT.md b/docs/ORCHESTRATOR_SESSION_PROMPT.md index 398ce96a..0b81c95f 100644 --- a/docs/ORCHESTRATOR_SESSION_PROMPT.md +++ b/docs/ORCHESTRATOR_SESSION_PROMPT.md @@ -43,6 +43,8 @@ ## Методология (продолжай как предшественники — она себя оплатила) - **Двухступенчатая верификация:** промты сессий несут мандат самопроверки ИСПОЛНЕНИЕМ (код+запросы+результаты); твоя пост-хок адверсариальная верификация при лендинге — второй рубеж, author≠reviewer, воркфлоу-инструментом, по СЫРЬЮ с пере-выводом чисел. Эра №6: рубеж-2 поймал CRITICAL-артефакт (окно судьи HEAD_CHARS) и спас контракт от трёх ложных заголовков — не ослабляй. +- **Ревью хендофф-промтов — ПОСТРОЧНОЕ, не структурное (урок D39.99 п.4):** каждая инструкция промта с путём или каналом ВНЕ зоны сессии сверяется с зонной таблицей CLAUDE.md (куда пишет · кому пингует · кто коммитит); совпадение с общим каноном не оправдание — зонные решения владельца его перевешивают. +- **Свип зонных решений владельца (урок D39.99 п.4):** при каждом лендинге зоны вычитать из зонного журнала НОВЫЕ «решения владельца» и вынести на confirm списком; решение, живущее только в зонном доке при противоречащем каноне, — дефект синхронизации, чинится ратификацией или опровержением, не молчанием. - **Анти-паттерны приёмки:** приёмка проверяет НАПРАВЛЕНИЕ и решения пака, не только клеймы отчёта. Шесть ловушек: след отчёта (проверяешь лишь названные автором места) · связность вместо истинности (форма отчёта убеждает сама) · соглашательство с чёткой позицией (распространенная ошибка ллм моделей склонных соглашаться с человеком) · рационализация задним числом · слепой участок общих моделей обученных на одинх и тех же данных (выводы одной модели усиливают уверенность в правильности другой) · экономия усилия на неудобном. Механика-минимум: ≥1 верификатор выносит мнение по промту+диффу ДО чтения отчёта · ≥1 ищет дефекты ВНЕ карты отчёта · экспериментальный клейм без ре-рана (хотя бы редуцированного) в D-ноте помечается «со слов сессии», не «исполнением» · «решение владельца», известное только со слов сессии, НЕ ратифицируется — список confirm/deny владельцу ДО лендинга, если решение несущее · к утверждениям самой приёмки та же дисциплина «заявление=команда» (догадку фактом не подавать) · оценочные эпитеты в ноты не писать. - **Пре-рег дисциплина полигона:** фриз коммитом ДО платных вызовов (единственный коммит сессии); изменения после = новый experiment-ID; девиации — явно в отчёте; стоп-гейты по бюджету легитимны; «если не влезает — стоп и пинг, не резать молча». - **Лендинг:** микро-дефекты доков чинишь сам с пометкой «испр. оркестратором»; отчёты получают ревью-шапку; код не правишь — находки в фикс-лист; коммиты скоуп-раздельные, стейджинг пофайловый; `git status` перед каждым коммитом (в дереве бывают ≥2 живые сессии) + **`git diff --cached` перед `commit`** — коммит уносит ВЕСЬ индекс: чужой staged `git mv` уедет в твой коммит (инцидент 77dd9b8); `add` общего файла сметает чужую секцию — diff-контент/`add -p`. **Лендинг, двигающий файлы или состав активных промтов, обновляет `docs/README.md` тем же коммитом** (норма D39.80; урок 02.08: README объявлял заленденный пак «можно запускать» с мёртвой ссылкой — поймал не процесс, а новая сессия). Статус/голову README не несёт вовсе — единственный носитель состояния = PROGRESS CURRENT-STATE. **Перед коммитом ратификации — механический чек головы:** `grep "голова D" docs/PROGRESS.md` обязан показать номер ТОЛЬКО ЧТО ратифицированной ноты (голова отставала дважды: D39.81 и D39.83 — оба раза «Текущее» обновлено, число головы забыто). diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 92c2c1b9..bbc665f4 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -1,13 +1,14 @@ # Журнал прогресса -> **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (на 2026-08-04, голова D39.98). +> **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (на 2026-08-04, голова D39.99). +> - **КОНТРАКТ API v0 РАТИФИЦИРОВАН (D39.99, 04.08): строка 95 ЗАКРЫТА.** Дом — `docs/architecture/14-api-contract/` (нормативная спека OpenAPI 3.1 + компаньон-README с провенансом ✓/◆/○ и К-вопросами); написан фронт-сессией S3, принят тремя адверсариальными пассами (0 опровергнутых из 19 подтверждённых находок за все пассы, все вправлены). Типы фронта генерятся из спеки (дрифт-тест = голден-паттерн). **Строки 99/100/101/103 перевешены в «скоро» — БЛОКЕРЫ контракта**; новые 125 (банк-экспорт) · 126 (поднятие потолка) · 127 (DDL-коммент). **Открыто на владельце:** продуктовые К-1/К-3/К-5/К-6/К-8 контракта + фразы В-3 (заготовка — приложение А компаньона) + прежние (confirm двух решений 04.08 из журнала фронта · авторство подчистки D-лога · приёмка банк-арбитража — артефакты в дереве не залендены). **Следующий разговор на берегу — бутстрап платформы (П-1): у второй стороны контракта ноль строк кода.** > - **ФРОНТ S2 + ДОФИКС ПО РЕВЬЮ ПРИНЯТЫ И ЗАЛЕНДЕНЫ (D39.98, 04.08: `ece94ef` сессией по старому канону + `02872b6` лендинг оркестратора).** Git-канон v2 разнесён по носителям: коммитит ТОЛЬКО оркестратор (владелец 04.08, supersede D39.88 — запись в D39.98 п.3; ⚠ там же процедурная нота о подчистке тел D-лога). **В дереве НЕТРЕКНУТЫЕ артефакты банк-арбитража** (`docs/research/24-bank-arbitration.md` · `eval/bank_arbitration/`) — ждут отдельной приёмки (строка 5). **Открыто на владельце (+D39.98 п.4):** confirm двух «решений 04.08» из журнала фронта · авторство подчистки D-лога. > - **ПАК СТАНДАРТОВ + ДОФИКС ARCHGUARD ПРИНЯТЫ И ЗАЛЕНДЕНЫ (D39.96/97, 03–04.08; семь pathspec-коммитов `e8d4ac0`…`0e64efe`). Строки 107/112/117 ЗАКРЫТЫ.** Репо на стандартах: `.golangci.yml` 0 issues · `make battery` = единая точка входа · типовые анализаторы archguard (38 посадок, 9/9 мутаций, семь осознанных пропусков — 121(д)) · x/net 0.57 (живая проба PASS 3/3, `reasoning_content` подтверждён на проводе, $0.0053) · пять шиппинг-конфигов на `low`. **Задачи №12:** (1) выдать промт пака экрана 46 — D39.94 п.3 (фикстура 113 первой; денежный шаг ТОЛЬКО с подписью владельца); (2) малый пак tools/go.mod — решение принято D39.97 п.4 (строка 118). **ОТКРЫТО НА ВЛАДЕЛЬЦЕ:** 116 (порог «≥4/5 при N=5» подан) · строка 5 (находки прерванного банк-ресёрча или пере-запуск; промт подрезать до $1.20) · мини-проба 44 «low+16k» (~$0.05) · подпись денежного шага 46. **Вахта:** маппинг эффорта `deepseek-v4-pro` «early August 2026» (реестр 108, триггер >05.08 — БЛИЗКО). **Источник истины по РЕШЕНИЯМ — `architecture/05-decisions-log.md`; этот файл — ЖУРНАЛ.** > - **Сделано (сводно; детали — D-лог и архив-слайсы):** Ф0 ✅ · Ф1-инфра ✅ · арх-ресет D39 (7 слоёв, паки 11–16) ✅ · паки 17 «канал B» · 18 «долги» · 19 «голос/состояние» · 20 «банк+терминолог» ✅ (D39.26–28/31/41–56) · мини-прогон (D39.37) и ХОЛОДНЫЙ прогон (D39.58: recall банка 0.918/0.980, банк приходит переведённым) приняты · полигон-пакеты 5–8, ToS-речек (D39.57), Р6+P4 (D39.61) закрыты · **карта языковой привязки движка (D39.60): 149 сайтов/45 файлов, книго-ось чиста (4), ja→ru безопасно без правки Go, en→ru — нет** · Р1–Р4 закрыты целиком (D39.63: Р2 = ЗНАЧЕНИЕ, Р4 = норма потолка recall) · **ФАЗА 2 ОБЩНОСТИ ✅ (D39.64: П0 эмбед-хеш · П1 цель-шов · П2 скрипт-шов, ko-баг закрыт · П3 нарезка · П4 манифест-по-каналам; голден вердикт-нейтрален 0/172, майнер-парити EXACT)** · жанровый словарь отменён как класс (D39.47) · петля ремонта построена и НЕ включена (D39.38). > - **Курс (D39.59–78): ОБЩНОСТЬ ✅ → КАЧЕСТВО БАНКА ✅ (D39.69/75/77) → ПАКЕТ-ЧЕКЕРОВ ✅ (D39.78: строка 25 целиком; харнесс labels В GIT 12/12; K2 r0.85 · K4b r0.22 · K6 fp 14→6; Р2 hard/soft + Р4-потолки в контрактах).** **Текущее: МЕЛКАЯ ПАЧКА ✅ ПРИНЯТА И ЗАЛЕНДЕНА (D39.82: строки 89/53/83 закрыты · 93 сужена до остатков · F4-бэкап+pre-flight построен · register/жанр → книжный слой · DC7 пере-гейчен сид-покрытием → строка 98); решения §6 исполнены (D39.79/82); **РЕ-ПРОБА + ОТЛАДОЧНЫЙ ХОЛОДНЫЙ ПРОГОН coldrun-b САНКЦИОНИРОВАНЫ (D39.83: промт выдан и холодно аудирован — 3 major вправлены; потолки $0.15 ре-проба / $5 прогон; ступени 2+8 глав, автобанк по D39.77)**; ре-проба flash (строка 74) — по слову владельца; finding-1 ЗАКРЫТ (D39.77); онбординг-оверхол доков исполнен (D39.80); **ФРОНТ ОТКРЫТ владельцем (D39.81: продукт = SaaS; зоны `frontend/` + `platform/` живые, фронт работает на моках; движковая очередь НЕ пере-упорядочена — ранняя вставка одна: строка 95 «контракт API» $0; строки 96/97 и перевесы 49/94 заведены)**; **СТЕК ФРОНТА/ПЛАТФОРМЫ ЗАПИНЕН, 8 решений владельца ратифицированы (D39.84: SEO Google-only · PWA-первый · денег в MVP-UI нет · стоп-на-банке = параметр запуска, `--verify-bank` готов · перевод НИКОГДА на индексируемый URL = ПТ-34 · якорь чтения = edit-unit, ПТ-21; работы движка под фронт = строки 99–102; фронт/платформа ведут ЗОННЫЕ бэклоги — `frontend/docs/BACKLOG.md` · `platform/BACKLOG.md`, строки 96/97 → П-1/П-2; зоны залендены, фронт-промт S0–S7 в `frontend/docs/`)**; **ШОВ ДВИЖОК↔ПЛАТФОРМА РАТИФИЦИРОВАН ПО РЕСЁРЧУ (D39.85, `research/23`): NDJSON-поток событий → материализация в Postgres → SSE; артефакты границ стадий; анти-паттерны запрещены (живой SQLite не читать · HTTP в движок не тащить · read-пакет из ядра не выносить); строка 103 заведена, входы контракта 95 зафиксированы)**.** **coldrun-b ПРИНЯТ И ЗАЛЕНДЕН (D39.86): фазы A/B исполнены за $0.114378 из $0.15, фаза C заморожена чекпойнтом — легитимный исход по D39.83.** Блокер 0731 воспроизведён на боевой форме (0 из 9 пригодных при `max_tokens=8496`); **вариант (а) флор-16000 ФАЛЬСИФИЦИРОВАН** (4/5 пустых при 16992, добавка уходит в размышление); **(б) `reasoning_effort:"low"` оживляет волну, но требует правки Go** (у бэнк-ролей ручки нет — НОВАЯ строка 104; посылка об амендменте гейта неверна только для СТАДИЙНОЙ ручки); **(в′) thinking OFF замерен карантинно — эхо-мина ЖИВА на весах 0731 (4/20)**, флаг `echoes_when_thinking_off` сохраняет основание; **(г) pro ≈×8.9, а не ×3.1**, и на pro ручка эффорта не работает (вендор `low→high`, смена маппинга объявлена «early August 2026»). Порог классификатора **6/6 взят**; две дыры наблюдаемости закрыты; live-риг не собирался с D39.64 — починен. НОВЫЕ строки **104** (ручки синтетических стадий) и **105** (мис-вердикт классификатора + честность поверхностей); **46** повышена до «скоро» с живым носителем; **16 НЕ закрыта**. ✅ **РАЗВИЛКА 74 РЕШЕНА ВЛАДЕЛЬЦЕМ (D39.87): вариант (б)** — `reasoning_effort: "low"` черновику ПЛЮС правка Go, дающая ручку эффорта ролям, у которых её нет (строка 104), ПЛЮС экран целевого языка (строка 46). Мандат владельца на исполнение — «элегантно встроить в текущее решение по коду, чтоб было чисто»: правка обязана быть использованием СУЩЕСТВУЮЩЕГО поля `Stage.Reasoning`, одним швом на все три рукописные `config.Stage`, так чтобы четвёртое такое место не могло появиться молча. **Пак ИСПОЛНЕН И ПРИНЯТ (D39.91, код `553f1a3`; промт заархивирован)** — работы A+проба зелёные, работа B стала паком экрана 46 (дизайн заморожен D39.92 п.1+D39.93), прогон C не стартовал ⇒ строка 16 ЖИВА и оси голоса 13б по-прежнему без данных. Пак стандартов (`BACKEND_REPO_STANDARDS_SESSION_PROMPT.md`, D39.92) исполнен — приёмка и гейты лендинга в верхнем буллете (D39.96). Перекупок нет (D39.63). > - **Горизонт (D39.62/67, освежён D39.95):** ре-проба flash ИСПОЛНЕНА и строка 74 закрыта (D39.86/91; порог 6/6 → пере-ратификация выборочным, строка 116; пробы 36б Q2/эмиссии Q7 — по слову) → **ДОБОР ИДЕАЛА** (первым прогоном: оси голоса 24 · авто-режим · итерация №2 редакторов 65 · цена 16 · веса K1–K12 13а · вне-претрейн чекпоинт 55) → ВТОРАЯ ПАРА живьём (ja→ru; преп 81) → МАСШТАБ → пилот Ф2.5 (гейт резюме-строки 80; строки 62–68, 85) → Ф3 ридер-IDE (69–71). **Стоячие:** ToS-триггер 25.10 · Ш-2 до go1.27 (⚠ + x/text-таблицы Unicode 17 тем же тулчейном — строка 119/реестр 108) · проба провода исполнена и 117 закрыта (D39.97), конфиги 112 залендены — платные прогоны разблокированы. > - **Стек:** draft deepseek-v4-flash thinking-ON (+банкнота) **⚠0731** → терминолог (та же модель, батчи, экран `target_script`) → editor deepseek-v4-pro БИЛИНГВ ИНТЕРИМ (⚠ вендор АНОНСИРОВАЛ смену маппинга эффорта pro «early August 2026» — вахта в реестре 108; glm-5 резерв) → судья gemini (Ф2, полигон); канал B Mistral+grok; ~$0.85/ранобэ (D30.4) — 112 залендена (D39.97), пере-калибровка цены при следующем платном прогоне. -> - **ЕДИНЫЙ БЭКЛОГ — секция «Бэклог» ниже** (одна таблица, единственный трекер; каждая петля обязана иметь диспозицию: решено / отложено-с-записью / отклонено; ведёт оркестратор). **СЧЁТ ОЧЕРЕДИ на 04.08 (D39.97, скриптом по таблице; обновлять при каждом лендинге):** всего **101** строка · зона бэкенд **52** строго / **65** широко · **блокеров очереди 0**, платные прогоны разблокированы (117 закрыта) · «скоро» **17** (2·4·5·18·46·49·55·95·108·109·113·114·116·118·119·120·121); остальное «когда-нибудь». ⚠ **Счёт — НИЖНЯЯ граница долга, а не потолок:** трекер содержит только замеченное, и 02.08 дважды за $0 свежие глаза вытащили из кода дефекты, которых в нём не было (релей фронта → строки 99/100/102; приёмочное ревью → третий шов `repair.go:384` в 104 и честность `quality.go:436` в 105). Лента ✅ выше — про ЗАКРЫТЫЕ паки, читать её как «долга нет» неверно (разбор — вопрос владельца 02.08, D39.89). +> - **ЕДИНЫЙ БЭКЛОГ — секция «Бэклог» ниже** (одна таблица, единственный трекер; каждая петля обязана иметь диспозицию: решено / отложено-с-записью / отклонено; ведёт оркестратор). **СЧЁТ ОЧЕРЕДИ на 04.08 (D39.99, скриптом по таблице; обновлять при каждом лендинге):** всего **103** строки · зона бэкенд **58** строго / **66** широко · **блокеров очереди 0**, платные прогоны разблокированы (117 закрыта) · «скоро» **21** (2·4·5·18·46·49·55·99·100·101·103·108·109·113·114·116·118·119·120·121·125); остальное «когда-нибудь». ⚠ **Счёт — НИЖНЯЯ граница долга, а не потолок:** трекер содержит только замеченное, и 02.08 дважды за $0 свежие глаза вытащили из кода дефекты, которых в нём не было (релей фронта → строки 99/100/102; приёмочное ревью → третий шов `repair.go:384` в 104 и честность `quality.go:436` в 105). Лента ✅ выше — про ЗАКРЫТЫЕ паки, читать её как «долга нет» неверно (разбор — вопрос владельца 02.08, D39.89). > - Архивы хроники: `archive/PROGRESS-2026-07-04-10.md` (D31) · `-10-13` (D39.6-гигиена) · `-13-25` (стройка паков 11–16, rerun2) · **`-25-31` (паки 17–20 · мини-прогон · полигон-пакеты 5–8 · ToS · холодный прогон; D39.26–58)**. Записи ниже — живой хвост (эра D39.59+). ## Бэклог (ЕДИНЫЙ, собран 26.07, актуализация 02.08 D39.80; правки — только через оркестратора) @@ -102,14 +103,16 @@ | 70 | Action-security gate перед выдачей tools/webfetch (D25 п.5) | бэкенд | когда-нибудь | Ф3 | D39.34(7) | | 71 | **Планы research/22**: epub-tag-rewrite · Q7-леджер ; + F3-brief из D29 п.3 (chat-edit · Not-useful-петля · cost-of-fix) + Ф3-скоуп 02-mvp-plan (TMX/TBX · Bertalign · дистилляция 7–14B); + V5: фронт ВЕБ-ПЕРВЫЙ, SEO/ранжирование (куки · страницы-сироты · актуальные алгоритмы Google) — определяет выбор фреймворка (SSR vs SPA), в Ф3-дизайн ДО выбора стека (ПТ-23) | бэкенд | когда-нибудь | Ф3 | D39.34(7), START_PROMT V5, D39.80 | | 94 | **Продукт-механика входа и прогона** (V2, ПТ-16..18): абьюз/misuse-прескрин дёшево и точно ДО трат токенов (H8) + UI-контракт отказа (проект-решение D39.80) · лимит размера входа настройкой (H7) · продукт-кнопки «стоп/продолжить» поверх построенных graceful stop и redrive, абьюзивный прогон НЕ продолжать (H10); API-форма — через контракт 95 | бэкенд | к подключению фронта (ПОДТЯНУТА D39.81; лимит можно раньше, к пилоту) | отдельный пак при Ф3-препе (после 95) | START_PROMT V2, H7/H8/H10, D39.80, D39.81 | -| 95 | **Контракт API v0 + продуктовый словарь статусов ($0, ранняя вставка):** зафиксировать артефактом ДО большого кода обеих сторон — фронт пишется на моках, без пришпиленного контракта моки и API разойдутся (класс «док↔код», 26/180 переписи); дом — `docs/architecture/14-api-contract.md`; продуктовые статусы «загрузка → разбор → перевод → подпись банка → финал → готово», слой перевода внутренних вердиктов в продуктовые (flag_reason/стадии/модели НЕ протекают — инвариант D39.81); двухфазный поток уже в движке (`--verify-bank`, банк-пауза) — ВЫВЕСТИ, не проектировать; резерв полей прогресс/ETA (54) и стоп/продолжить (94); ревью-вопрос контракта: «сменится стадия конвейера — придётся ли править фронт?»; входы D39.84: контракт подписи учитывает пересборку банка (подпись ≠ UPDATE — пишем в файлы-источники) · формы пофазного прогресса/манифеста/таблицы подписи = строки 99–101; входы D39.85 (`research/23`): (i) NDJSON-поток событий (словарь-enum + version-хендшейк + событие потолка — строка 103), (ii) артефактный контракт (export-JSON · 101 · 100), (iii) правило ре-синка (идемпотентный апсерт run_id+seq; `status --json` = канал согласования); словарь событий и продуктовый словарь статусов (ПТ-33) проектируются вместе | оркестратор/бэкенд/фронт | скоро (единственная ранняя вставка Ф3) | дизайн-промт по слову владельца | релей фронта 02.08, D39.81, research/16 | | 96 | **→ переехала в `platform/BACKLOG.md` П-1** (зонные бэклоги, D39.84: HTTP/SSE-слой, обвязка, аутентификация, порядок деплоя); здесь остаётся ратифицированным сам ИНВАРИАНТ D39.81 — движок = процесс-на-прогон (EXCLUSIVE flock `store.go:43`), сервер в backend НЕ пишется | платформа | — | зонный бэклог | D39.81, D39.84 | | 97 | **→ переехала в `platform/BACKLOG.md` П-2** (глобальный брокер рейт-лимитов; гейт «до второго параллельного пользователя» зафиксирован там же) | платформа | — | зонный бэклог | D39.81, D39.84 | -| 99 | **Пофазный прогресс `draft N/M ∥ edit N/M`**: unit = done только при всех draft-строках членов + edit-строке (`status.go:328-331/365`) — индикатор «готово N/M» стоит 0% ВСЮ черновую волну; данные уже есть (`chunk_status` — строка-на-стадию, `migrate.go:111-126`; status уже делит стадии по волнам `status.go:332-334`) — вывод пофазных счётчиков в StatusReport/JSON без миграции схемы | бэкенд | к подключению фронта (первая по критичности из четырёх D39.84) | малое касание read-model | релей фронта 02.08, D39.84 | -| 100 | **Персист манифеста глав/чанков** (+ `chunker_version` из снапшота + хеш источника): каждый read-вызов заново ингестит и режет исходник (`status.go:177-195` bookChunks; redrive — дважды за вызов; замер фронта 1.42–1.51 с CPU на книге 23 МБ, умножается на число книг) — нужен под экран разбора и дерево глав; дизайн обязан пережить смену чанкера (`--resnapshot`) | бэкенд | к подключению фронта | отдельное решение (дизайн с 95) | релей фронта 02.08, D39.84 | -| 101 | **Машиночитаемая таблица подписи банка**: кап 20 — только stdout (`render.go:98`), полная таблица — текстовый сайдкар `.bank-stop.txt`, JSON/структурного выхода нет (mined-signature.yaml — сид-дельта без freq/spread/evidence); нужна фронту S5; помнить ловушку «подпись ≠ UPDATE» (банк пересобирается: `seeding.go:18/110`, `glossary.go:158-162` — контракт подписи пишет в файлы-источники) | бэкенд | к подключению фронта | малое касание (форма — с контрактом 95) | релей фронта 02.08, D39.84 | +| 99 | **Пофазный прогресс `draft N/M ∥ edit N/M`**: unit = done только при всех draft-строках членов + edit-строке (`status.go:328-331/365`) — индикатор «готово N/M» стоит 0% ВСЮ черновую волну; данные уже есть (`chunk_status` — строка-на-стадию, `migrate.go:111-126`; status уже делит стадии по волнам `status.go:332-334`) — вывод пофазных счётчиков в StatusReport/JSON без миграции схемы | бэкенд | скоро (БЛОКЕР контракта API, D39.99) | малое касание read-model | релей фронта 02.08, D39.84 | +| 100 | **Персист манифеста глав/чанков** (+ `chunker_version` из снапшота + хеш источника): каждый read-вызов заново ингестит и режет исходник (`status.go:177-195` bookChunks; redrive — дважды за вызов; замер фронта 1.42–1.51 с CPU на книге 23 МБ, умножается на число книг) — нужен под экран разбора и дерево глав; дизайн обязан пережить смену чанкера (`--resnapshot`) | бэкенд | скоро (БЛОКЕР контракта API — стабильный id главы, D39.99) | отдельное решение (дизайн с 95) | релей фронта 02.08, D39.84 | +| 101 | **Машиночитаемая таблица подписи банка**: кап 20 — только stdout (`render.go:98`), полная таблица — текстовый сайдкар `.bank-stop.txt`, JSON/структурного выхода нет (mined-signature.yaml — сид-дельта без freq/spread/evidence); нужна фронту S5; помнить ловушку «подпись ≠ UPDATE» (банк пересобирается: `seeding.go:18/110`, `glossary.go:158-162` — контракт подписи пишет в файлы-источники) | бэкенд | скоро (БЛОКЕР контракта API — экран подписи, D39.99) | малое касание (форма — с контрактом 95) | релей фронта 02.08, D39.84 | | 102 | **Приём внешнего trace-контекста в tmctl**: TraceID минтится заново каждым вызовом (`main.go:67-70`; внутри вызова трасса цельная, `request_log.trace_id` есть) — прогон, запущенный платформой, должен быть одной трассой | бэкенд | когда-нибудь (Ф3, с 96) | малое касание | релей фронта 02.08, D39.84 | -| 103 | **NDJSON-эмиттер прогонных событий со стабильным словарём** (машинный канал живого статуса для платформы, `research/23` §2): NDJSON-логи уже есть (`LOG_FORMAT=json`, авто-оси trace/book/chapter/stage/role — `obs/logging.go:15-54`), но сообщения прозой, version-хендшейка нет, а **событие потолка отсутствует вовсе** (деньги только в тексте ошибки `stagerun.go:472-494`); нужен словарь-enum + version-хендшейк первой строкой (образец terraform -json: минор = ignore-unknown, мажор = reject) + событие потолка; все границы — единичные call-sites в pipeline рядом с готовыми slog; человеческие логи уже на stderr — stdout-дисциплина соблюдена | бэкенд | к подключению фронта (словарь — вместе с контрактом 95) | малое касание | research/23, D39.85 | +| 103 | **NDJSON-эмиттер прогонных событий со стабильным словарём** (машинный канал живого статуса для платформы, `research/23` §2): NDJSON-логи уже есть (`LOG_FORMAT=json`, авто-оси trace/book/chapter/stage/role — `obs/logging.go:15-54`), но сообщения прозой, version-хендшейка нет, а **событие потолка отсутствует вовсе** (деньги только в тексте ошибки `stagerun.go:472-494`); нужен словарь-enum + version-хендшейк первой строкой (образец terraform -json: минор = ignore-unknown, мажор = reject) + событие потолка; все границы — единичные call-sites в pipeline рядом с готовыми slog; человеческие логи уже на stderr — stdout-дисциплина соблюдена | бэкенд | скоро (БЛОКЕР контракта API: словарь §6 + событие note per-unit + ceiling, D39.99) | малое касание | research/23, D39.85 | +| 125 | **Артефакт экспорта БАНКА** (полный банк тремя статусами — канал чтения `/bank` контракта 14): строка 101 это таблица ПОДПИСИ (кап 20 stdout, `cmd/tmctl/render.go:98`), не экспорт; сам банк в приватном SQLite, платформе закрыт (D39.85) — нужен экспорт-артефакт движка на границе стадии/стопе | бэкенд | скоро (БЛОКЕР контракта API, D39.99) | дизайн + малое касание export | ревью контракта 04.08, D39.99 | +| 126 | **Механизм поднятия потолка для resume после ceiling-стопа**: движок продолжает «once the ceiling is raised» (`stagerun.go:488-489`), канала поднятия в контракте нет и в MVP-UI быть не может (деньги, D39.84) — платформенный механизм/политика; продуктовое слово стопа — К-8 у владельца | платформа/бэкенд | когда-нибудь (до подключения фронта к живым прогонам) | дизайн-вход П-1 | ревью контракта 04.08, D39.99 | +| 127 | **Устаревший DDL-коммент `glossary.source`** (`migrate.go:193`: «seed|ruby|auto», фактические пути записи — seed/ruby/mined, `auto` не пишет никто; фантом уже утёк во фронт-доки дважды) — поправить коммент | бэкенд | когда-нибудь (попутно любым паком store) | однострочная правка | ревью контракта 04.08, D39.99 | | 105 | **Роль `classifier` мис-вердиктится как эхо + честность двух новых строк наблюдаемости (D39.86).** (а) `classifyOutput` даёт поблажку `SourceEchoExpected` только `roleTerminologist` (`chunkrun.go:37`), а ответ классификатора по формату состоит из исходных ханьских термов ⇒ доля исходного письма 0.20–0.29 > порога 0.15 на каждом батче: живой артефакт пробы 6/6 несёт `verdict=cjk_artifact` при `bad_lines=0`. Ущерб узкий — `runBankRoleBatches` кладёт `run.texts[i] = att.text` без проверки диспозиции (`terminologist.go:603`), посчитанные эхо-ставки (`quality.go:313-321`) берутся из `chunk_status`, куда бэнк-путь не пишет ⇒ портятся только строки `tmctl report`/лога; **латентно** (`classify_types` выключен во всех шиппинг-конфигах). ⚠ Цена дисциплинированного фикса, которую надо знать заранее: бамп `classifierVersion` (литерал, сам не двигается) меняет payload снапшота → snapshot id → `RequestHash` ⇒ промах ВСЕХ чекпойнтов книги, то есть `--resnapshot` + пере-покупка. (б) тем же касанием — честность поверхностей: строка `VOICE` берёт `rules=` из конфига ВРЕМЕНИ ОТЧЁТА (`quality.go:436`), поэтому включение `gates.voice` после прогона печатает «измерено, чисто» для неизмеренной книги (воспроизведено); и она печатается при выключенном гейте, если `SpoilerLeaks>0` (дизъюнкция — поведение верное, формулировка отчёта/коммента неверна); знаменатель `BANKNOTE` = `len(states)` включая призрачные строки `retrieval_state`, тогда как соседний агрегатор их фильтрует (`quality.go:259-260`) ⇒ после пере-чанковки покрытие занижается | бэкенд | когда-нибудь (ближайшее касание банка/наблюдаемости; (б) — дешёвая часть, вердиктов не двигает) | малое касание + вердикт-двигающая половина (а) через СТОП-решение | D39.86 | | 106 | **Слой 3 НЕ достроен: контракт редактора = full-regen, а целевой — узкие мандаты + ДИФФЫ.** `09-target-architecture.md:121-126` дословно: «research/07 предписывает узкие мандаты + диффы, а не полную перегенерацию — сейчас ровно наоборот (4 мандата, full-regen), что и породило хвост инверсий»; целевой контракт — редактор правит anchored search/replace-спанами с детерминированным Go-apply (ограниченный blast-radius, дешевле, аудируемо, структурно omission-safe). Пак-16 построил петлю РЕМОНТА (починка флагнутого спана постфактум), а не diff-контракт роли ⇒ это разные вещи. **Строки не было ни одной** — дыра учёта, найденная сверкой 02.08 (грепы `diff`/`мандат`/`span` по бэклогу = 0). Смежное: арм Q4b «diff-fidelity+style редактор» (`exp15:108`) не прогонялся НИ РАЗУ, хотя трек B объявлен завершённым | владелец → бэкенд | когда-нибудь (смена контракта ролей = решение владельца) | ресёрч-арм → дизайн → крупная стройка | 09 §Слой 3, D39.90 | | 113 | **Запас боевого гейта не зафиксирован тестом, а харнесс, давший число, УДАЛЁН (Д-хвост D39.90).** Числа «18 из 20 · 0 ложных на 1055 · запас 13× (0.0117 против 0.15)» и «санитайзер поймал бы английский черновик» получены ВРЕМЕННЫМ тестом в пакете pipeline (`splitBanknote`+`StripThink`+`sourceScriptShare`), который сессия удалила после снятия. Число ушло в контракт (строка 46), воспроизвести его нечем. **Делать:** корпус-фикстура в репо (~20 off-target + ~40 здоровых, с движковой предобработкой; снапшот НЕ двигает) + регресс-тест на реальный запас — это база, которую обязано побить любое предложение по 46. СТОП, если фикстура не воспроизводит 18/20 при 0 FP, или если запас меньше 3× | бэкенд | скоро (перед любой правкой по строке 46) | $0 фикстура + регресс-тест | D39.90, EFFORT_HANDLE §5.4 шаги 1–2, §7 | @@ -129,7 +132,11 @@ | 124 | **Калибровка снапшота (вопрос владельца 04.08, разбор D39.97 п.5): нацеленность, не агрессивность.** Три оси одним дизайн-паком: (а) недо-чувствительность — вердикт-несущие вызовы x/text не свёрнуты (вход: строка 119); (б) пере-охват — `EmbeddedVersion` фолдит все пары разом → пер-парный хеш (родня 115/120); (в) дешёвая перекупка: `tmctl resnapshot --dry-run` — байт-сверка тел по чекпойнтам (`msgsContentHash`), идентичные юниты пере-привязываются без пере-вызова, платятся только изменившиеся. «Поведенческий отпечаток вместо строки версии» отложен (пол ≠ доказательство) | бэкенд | когда-нибудь (перед МАСШТАБОМ — когда перекупки станут реальными деньгами) | дизайн-пак по D39.97 п.5 | D39.97 | -## Оркестратор №12 — ревью фронта S2, приёмка дофикса и лендинг `02872b6`, git-канон v2 по носителям (D39.98), 04.08 +## Оркестратор №12 — КОНТРАКТ API v0 РАТИФИЦИРОВАН: строка 95 закрыта, форма OpenAPI, три пасса приёмки (D39.99), 04.08 + +S3 упёрлась в замок (контракта нет) — по слову владельца написала вход Ф-14 + черновик; форма по решению владельца сменена на OpenAPI 3.1 (типы генерятся, спека линтуется — класс «фикстура выдумала форму», случившийся четырежды, закрыт механически). Приёмка: три воркфлоу-пасса (16+6 агентов), 19 подтверждённых находок (2 high: withheld ≠ «текст не выдан» — движок отгружает текст под флагом; чтения без канала шва), ноль опровергнутых; все вправлены двумя раундами дофикса, каждый раунд перепроверен. Пост-мортем процесса — в D39.99 п.4 (промт S3 велел пинговать в чужую зону — противоречие канона и зонного решения владельца закрыто правкой CLAUDE.md; чек-пункты в промт оркестратора). Детали — D39.99. + + Приёмка S2 — адверсариальный воркфлоу (5 линз × скептики с ре-раном): батарея и виртуализация подтверждены исполнением, стек без дрифта, три дефекта (пар-слепой шрифтовой токен · выдуманный тип `org` с фабрикованной цитатой журнала · ключ рядов банка без учёта полисемии) вправлены дофиксом и залендены; три находки ревьюеров опровергнуты скептиками. Детали и остаточное — D39.98. @@ -302,7 +309,7 @@ Q1 = П1 цель-шов санкционирован («главное бэке ## Полигон -**Арбитраж банка (строка 5, пере-запуск промта D39.89 по слову владельца, потолок $1.00) — ИСПОЛНЕН, отчёт `docs/research/24-bank-arbitration.md`, потрачено $0.0896 (04.08).** Главное: (1) голд 53 approved сида → 45 в BANK-FULL, из них у 27 промахов прод-банка верный dst лежал в вариантах черновиков лишь у 5 ⇒ **потолок любого «выбора из вариантов» = 17/45, селекционный арбитраж закрыт классом**; (2) свежий терминолог-соло (flash-low) бьёт бесплатный базис §C2-3 **+8/−0**, а **равноправный консилиум НЕ окупается**: +3 к само-согласованности = одна конвенция 古月; лучший одиночка glm-5 25/45 > лучший совет 23/45; добавление grok РОНЯЕТ совет 23→20; C3conf(взвешенный уверенностью) ≡ C3; ⟦TM-NO-DST⟧ = 0 на 35 вызовах трёх семейств; (3) **вербализованная ЧИСЛОВАЯ уверенность разделяет внутри модели** (AUC 0.77–0.85, слова хуже 0.63) при тотальной overconfidence (~88 при точности ~50%) — жить ей ordinal-очередью ревью, не арбитром; (4) **химера семьи 古月 воспроизведена причинно на границе батча** (свежий deepseek: «Гу Юэ» ровно в батче 2, «Гуюэ» ровно в батче 0), а по-терминная МАРШРУТИЗАЦИЯ (улики-первым) сама создала химеру 转-серии ⇒ единица арбитража/роутинга — кластер; DetectSeries разнодлинные семьи (古月×15, 蛊×14) не видит; (5) **проба инжекта: инъекция поднимает следование канону 43%→95%, НЕВЕРНАЯ строка законом принимается 6/6, с маркером ⟨проверить⟩ — тоже 6/6 (Δ=0 п.п.): пометка на транслятор-проводе поведенчески пуста**, цена неверной строки банка = ~100% заражение; (6) индустриальный обзор: стандарта арбитража термов не существует, наш дизайн впереди ближайшего аналога (DelTA first-wins); пропущенных стандартов нет. Рекомендации тремя корзинами в §D (строить: кластер-канал семей · intra-run CanonConflicts · наблюдаемость Signals/invented/conf · нормализованный голос foldVariants; закрыть: равноправный совет · кросс-модельные веса · self-consistency · селекция · отказ-сигнал). 4 вопроса владельцу в отчёте (мини-голд алиасов · конвенция составных фамилий · проба редакторского провода · фикс-пак корзины 1). Два адверсариальных раунда author≠reviewer пройдены. Сырьё durable `~/books/gu-zhenren/bank-arbitration/` (59 json), харнесс `eval/bank_arbitration/`. НЕ закоммичено — лендит оркестратор. +**Арбитраж банка (строка 5, пере-запуск промта D39.89 по слову владельца, потолок $1.00) — ИСПОЛНЕН, отчёт `docs/research/24-bank-arbitration.md`, потрачено $0.0896 (04.08).** Главное: (1) голд 53 approved сида → 45 в BANK-FULL, из них у 27 промахов прод-банка верный dst лежал в вариантах черновиков лишь у 5 ⇒ **потолок любого «выбора из вариантов» = 17/45, селекционный арбитраж закрыт классом**; (2) свежий терминолог-соло (flash-low) бьёт бесплатный базис §C2-3 **+8/−0**, а **равноправный консилиум НЕ окупается**: +3 к само-согласованности = одна конвенция 古月; лучший одиночка glm-5 25/45 > лучший совет 23/45; добавление grok РОНЯЕТ совет 23→20; C3conf(взвешенный уверенностью) ≡ C3; ⟦TM-NO-DST⟧ = 0 на 35 вызовах трёх семейств; (3) **вербализованная ЧИСЛОВАЯ уверенность разделяет внутри модели** (AUC 0.77–0.85, слова хуже 0.63) при тотальной overconfidence (~88 при точности ~50%) — жить ей ordinal-очередью ревью, не арбитром; (4) **химера семьи 古月 воспроизведена причинно на границе батча** (свежий deepseek: «Гу Юэ» ровно в батче 2, «Гуюэ» ровно в батче 0), а по-терминная МАРШРУТИЗАЦИЯ (улики-первым) сама создала химеру 转-серии ⇒ единица арбитража/роутинга — кластер; DetectSeries разнодлинные семьи (古月×15, 蛊×14) не видит; (5) **проба инжекта: инъекция поднимает следование канону 43%→95%, НЕВЕРНАЯ строка законом принимается 6/6, с маркером ⟨проверить⟩ — тоже 6/6 (Δ=0 п.п.): пометка на транслятор-проводе поведенчески пуста**, цена неверной строки банка = ~100% заражение; (6) индустриальный обзор: стандарта арбитража термов не существует, наш дизайн впереди ближайшего аналога (DelTA first-wins); пропущенных стандартов нет. Рекомендации тремя корзинами в §D (строить: кластер-канал семей · intra-run CanonConflicts · наблюдаемость Signals/invented/conf · нормализованный голос foldVariants; закрыть: равноправный совет · кросс-модельные веса · self-consistency · селекция · отказ-сигнал). 4 вопроса владельцу в отчёте (мини-голд алиасов · конвенция составных фамилий · проба редакторского провода · фикс-пак корзины 1). Два адверсариальных раунда author≠reviewer пройдены (р.2 поймал 4 неточности, все вправлены; моя ложная оговорка про grok-ростер снята сырьём). **Достройка по вопросам владельца (тот же день, +$0.0161, итог $0.0960):** (а) $0-демо ценности на живом банке — intra-run чек связности поймал бы **18 невидимых противоречий на 149 финалов (12%)**, а нормализованный фолд голосов двигает топ лишь 1/75 (понижен до косметики); (б) **мультиязычная проверка ja→ru и en→ru (`multilang_probe.py`): направление инжект-выводов ПЕРЕНОСИТСЯ** — закон слушается, неверная строка принимается (ja 2/2, en 1/1), маркер Δ=0 на обеих парах; попутно ja-батч бэнк-вызова вернул термы упрощённым китайским — off-language класс, который парослепой экран `OffLanguage` отбил бы (несущая защита для будущей ja-пары). **Кросс-пары по слову владельца (тот же день, +$0.0113, итог $0.1073): §F/§G отчёта.** F1 zh→en: свежая en-редакция по тому же ростеру дала «Rank 1..9 · Grade A/B/C/D · Aperture · Gu Master» — **41/45 (91%) совпадений класса решения с подписями владельца** (банк НЕ однобокий; буквенный маппинг 甲→A, недостижимый для ru-моделей, en-моделью взят — довод за сид/бриф-канал); fan-канон RI по памяти сессии ещё ближе к владельцу (元石 primeval stone = «первобытный камень»); ру-специфика ровно одна — слитность «Гуюэ». F2 en→zh (Кристофф-2026): deepseek идеально связал семью *-blood (X血者), **glm сломал корень внутри ОДНОГО батча** и разошёлся в имени Aaron между соседними окнами (艾伦↔亚伦) — дрейф, от которого банк существует, воспроизведён на en→zh; deepseek выдумал 5 незапрошенных строк (нужность гарда `want[key]` кросс-парно подтверждена); личная оценка прозы обеих моделей — «редактируемо-издательская», узкое место всех направлений — термы, не слог. **§G — дизайн-синк алгоритмов бэкенда** (готовый вход фикс-пака класса D39.71): G1 `DetectFamilies` (данные-гейтед, имя=префикс/реалия=суффикс, без транзитивности, мёрж в seriesID) · G2 `ConsolidationConflicts` (18/149 на живом банке) · G3 наблюдаемость (Signals/Invented/Conf + фикс ParseReply-джойна) · G4 кластерный роутинг рецензента · G5 фолд-косметика · G6 явные «не делать». **Финальный рерайт отчёта по слову владельца (04.08):** сага/хроника правок убраны, факты и оговорки сохранены, §G сведён к уровню алгоритмов; **раунд 3 адверсариала (независимый агент по диффу v1→v2): числа 100% чисты пере-раном, новые клеймы подтверждены первоисточниками; его блокер (рерайт сжал зарегистрированные пре-реги §0/§E/§F) закрыт восстановлением пре-регов дословно с явной пометкой девиаций, потери фактов возвращены.** v1-черновик — `docs/archive/reports/DRAFT_BANK_ARBITRATION_V1_2026-08-04.md` (помечен черновиком в шапке и именем; перенесён по слову владельца 04.08). Сырьё durable `~/books/gu-zhenren/bank-arbitration/` (84 json), харнесс `eval/bank_arbitration/`. НЕ закоммичено — лендит оркестратор. **Сырьё арки банка + пробы фиксов (01.08) — приняты и залендены, детали в журнале выше (D39.65).** diff --git a/docs/README.md b/docs/README.md index c887a4f9..3e34ee2a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,7 +10,7 @@ - [product-requirements.md](product-requirements.md) — реестр «что продукт обязан уметь»: START_PROMT V0–V5 + находки H6–H18, статусы сверены кодом. - `architecture/` — синтез и контракты: - [05-decisions-log.md](architecture/05-decisions-log.md) — **источник истины по решениям**. Читать: карта актуальности (шапка) + живая голова (с хвоста); корпус D1–D38 — справочник по ссылкам/grep, целиком не читать. - - [09-target-architecture.md](architecture/09-target-architecture.md) — целевая 7-слойная архитектура (статус стройки — шапка-таблица; §3 — карта находок H/L) · [10-prompt-architecture.md](architecture/10-prompt-architecture.md) — промпт-слой · [12-go-style-notes.md](architecture/12-go-style-notes.md) — норматив общности §0 · [13-tech-debt-anchors.md](architecture/13-tech-debt-anchors.md) — якоря техдолга (справочник к бэклогу, НЕ трекер). + - [09-target-architecture.md](architecture/09-target-architecture.md) — целевая 7-слойная архитектура (статус стройки — шапка-таблица; §3 — карта находок H/L) · [10-prompt-architecture.md](architecture/10-prompt-architecture.md) — промпт-слой · [12-go-style-notes.md](architecture/12-go-style-notes.md) — норматив общности §0 · [13-tech-debt-anchors.md](architecture/13-tech-debt-anchors.md) — якоря техдолга (справочник к бэклогу, НЕ трекер) · [14-api-contract/](architecture/14-api-contract/) — **контракт API v0 фронт↔платформа (D39.99)**: нормативная спека OpenAPI 3.1 (типы фронта генерятся из неё) + компаньон-README (провенанс ✓/◆/○, К-вопросы; зонная копия `frontend/docs/api-contract/` — байт-зеркало, сверять при лендинге). - Исторические, читать через ⚠-баннеры: [01-decisions.md](architecture/01-decisions.md) (Р1–Р10) · [02-mvp-plan.md](architecture/02-mvp-plan.md) · [03-implementation-notes.md](architecture/03-implementation-notes.md) · [04-unhappy-paths.md](architecture/04-unhappy-paths.md) · [06-memory-risk-registry.md](architecture/06-memory-risk-registry.md). - `experiments/` — эмпирика полигона: [00-provider-quirks.md](experiments/00-provider-quirks.md) — **читать перед любым вызовом провайдера**; [08-cost-model-v2.md](experiments/08-cost-model-v2.md) — денежная модель; [09-pilot-protocol.md](experiments/09-pilot-protocol.md) — пилот Ф2.5; остальные 01–16 — отчёты закрытых экспериментов (судьба — в баннерах/D-логе). - `research/` — фактура ресёрчей 01–22; у принятых — ревью-шапки, часть тел под ⚠ superseded: **читай баннер прежде содержимого**. Ключевые для навигации: 15 голос · 16 ридер-IDE · 17 внешняя критика · 18 рычаги качества · 19 нарезка · 20 банк-майнинг · 21 обзор транспорта · 22 доменные харнессы · 23 шов движок↔платформа (читать перед любым кодом стыка). diff --git a/docs/architecture/05-decisions-log.md b/docs/architecture/05-decisions-log.md index b1131e01..c0715c1e 100644 --- a/docs/architecture/05-decisions-log.md +++ b/docs/architecture/05-decisions-log.md @@ -1,4 +1,4 @@ -# Журнал решений оркестратора — контракт D1–D39.98 (развязки 04.07 · пакеты 09–10.07 · приёмка/качество-первым/пивот/эмпирика 11–12.07 · арх-ресет+стройка пере-прогонного стека 13–19.07) +# Журнал решений оркестратора — контракт D1–D39.99 (развязки 04.07 · пакеты 09–10.07 · приёмка/качество-первым/пивот/эмпирика 11–12.07 · арх-ресет+стройка пере-прогонного стека 13–19.07) > **⟶ КАРТА АКТУАЛЬНОСТИ (ревизия D31, продлена до D38.2 [12.07]; исторические записи ниже НЕ переписываются — дисциплина D23.3).** Читая контракт целиком, держи под рукой, что чем перекрыто: > ⚠ **Навигация (актуализация 01.08):** карта ниже детально покрывает D1–D39.28; решения D39.29+ живут хронологически в теле файла, **свежая голова — С ХВОСТА** (новые ноты аппендятся вниз). Сводка текущей головы и очередь — CURRENT-STATE в `../PROGRESS.md`. @@ -1432,3 +1432,15 @@ API-529-долг закрыт: 8-осевой refute-by-default воркфлоу **3. Git-канон v2 заленден по носителям (решение владельца 04.08, правка `CLAUDE.md` его рукой): коммитит ТОЛЬКО оркестратор; право самокоммита фронта/платформы (D39.88) ОТОЗВАНО — supersede-запись = этот блок.** Носители: `CLAUDE.md` (таблица зон + гардрейлы) · промты фронта (переписаны фронт-сессией в своей зоне) · `platform/README.md` · подчистки D-лога/PROGRESS. ⚠ **Процедурная нота (D23.3):** синхронизация переписала тела D39.84 п.8 и D39.88 НА МЕСТЕ — снято в т.ч. дословное решение владельца 02.08; это против правила «исторические записи не переписываются». История цела в git (`2b166b7`, pre-rewrite HEAD); авторство подчистки — на подтверждении владельца; впредь supersede — только пометкой в карте + новым блоком. **4. Открыто на владельце (+2 к списку D39.97):** confirm/deny двух «решений владельца 04.08» из журнала фронта («+» в левой панели закрывает половину Ф-7 · референсы возвращены на диск) · авторство подчистки D-лога (п.3). **В дереве появились нетрекнутые артефакты банк-арбитража** (`docs/research/24-bank-arbitration.md` · `eval/bank_arbitration/`) — заход строки 5, судя по всему, пере-исполнен; НЕ залендены, ждут отдельной приёмки. (04.08.2026, оркестратор №12). ✅ + +## D39.99 — КОНТРАКТ API v0 РАТИФИЦИРОВАН: строка 95 закрыта; форма = OpenAPI 3.1; три пасса приёмки, 19/19 находок вправлены; пост-мортем промт-ревью в канон (04.08). ✅ + +**1. Артефакт и форма.** S3 упёрлась в замок (контракта не существовало, строка 95 без промта) — по слову владельца написала вход Ф-14 (`frontend/docs/API_CONTRACT_INPUT.md`) и черновик контракта. **Решение владельца о форме: нормативная поверхность = OpenAPI 3.1** (`openapi.yaml`: JSON Schema моделей, spectral-линт шестым шагом `npm run check`, типы фронта ГЕНЕРЯТСЯ `openapi-typescript` с дрифт-тестом по голден-паттерну, MSW типизируется от них) — класс «фикстура выдумала форму», случившийся ЧЕТЫРЕЖДЫ (org/fullname · титульная политика · «Раздел N» · девять статусов), закрыт механически; прозаический md остался компаньоном (провенанс ✓/◆/○ · обоснования · К-вопросы). AsyncAPI отложен с записью (Ф-24). **Дом ратифицированной копии — `docs/architecture/14-api-contract/`** (спека байт-копией + компаньон-README с ревью-шапкой; байт-сверка копий — обязанность каждого лендинга; генерация типов далее из ратифицированной копии — контракт первичен). + +**2. Приёмка — три адверсариальных воркфлоу-пасса, 0 опровергнутых из 19 подтверждённых** (черновик: 4 линзы×скептики, 2 high — «withheld=текст не выдан» ложно о движке (sanitizer_stripped и c-lite drop отгружают текст под флагом, `disposition.go:96-104`/`export.go:203-215`) и чтения без канала шва (текст юнитов посреди прогона · полный банк — строка 101 это таблица подписи, не экспорт) + 8 medium — сквозной паттерн «инфляция провенанса ✓ на ◆-предложениях»; round-1 дофикс: 6/6 применён, перепроверено кодом, генерённая цепочка подтверждена пере-генерацией байт-в-байт; round-2 свежий взгляд на новые схемы: 5 medium — экспорт ненаблюдаем · promote без dst (движок hard-reject, `memseed.go:84-86`) · закрытый required kind против легального пустого (`memseed.go:323-326`) · самопротиворечие §2.7 · resume после потолка без канала поднятия; все вправлены, включая if/then-констрейнт с честным замером «openapi-typescript игнорирует 3.1-условия» → К-11). Сессия сама завела К-10 (несплитованный счётчик главы) и К-12 (poll vs push экспорта). State выведен из ПАРЫ (вердикт, наличие текста); фразы приложения А — плейсхолдеры (В-3 за владельцем); ПТ-34-инвариант вписан (noindex · no-store · выгрузка только владельцу). + +**3. Очередь.** Строка 95 ЗАКРЫТА. **99/100/101/103 перевешены «к подключению фронта» → «скоро, БЛОКЕРЫ контракта»** (аргументы сессии подтверждены кодом: без 100 у главы нет стабильного id — нумерация плотная, `chunker.go:99-105`; без 99 индикатор 0% всю черновую волну — `status.go:328-331`; без 101 подписной экран читает сайдкар с капом 20 — `cmd/tmctl/render.go:98`). НОВЫЕ строки: **125** банк-экспорт артефакт (канал `/bank`) · **126** механизм поднятия потолка (платформа; продуктовое слово — К-8) · **127** DDL-коммент `migrate.go:193` устарел (`auto` не пишет никто — фантом утёк во фронт-доки дважды). Счёт скриптом: 103 строки · скоро 21 · бэкенд 58/66. + +**4. Пост-мортем процесса (вопрос владельца «где мы ошиблись на берегу») — три слоя, фиксы в канон.** (а) Ошибка последовательности оркестратора: ратифицирован S3-промт с мёртвым основным сценарием (замок закрыт, промта строки 95 не существовало) — фронт три сессии упирался в один вход. (б) Ревью промта было структурным, не построчным: инструкция «пинг в docs/PROGRESS.md» дословно следовала таблице ролей CLAUDE.md, но противоречила решению владельца 02.08 из шапки зонного журнала. (в) Системная дыра: решения владельца, принятые в зонных сессиях, не доезжают до канона. **Фиксы:** CLAUDE.md — пинг-канал фронта/платформы = ЗОННЫЙ журнал (`frontend/docs/frontend-PROGRESS.md` — переименован по слову владельца, писать туда всем включая оркестратора), не docs/PROGRESS.md; промт оркестратора — два чек-пункта (хендофф-промты сверяются ПОСТРОЧНО: каждая инструкция с путём/каналом вне зоны — против зонной таблицы; при каждом лендинге зоны — свип новых «решений владельца» из зонного журнала на confirm списком); шапка `frontend/docs/BACKLOG.md` разведена «предлагать обязан / ратифицирует оркестратор» (просьба №3 сессии — прежняя формулировка спровоцировала остановку первого захода). + +**5. Открыто на владельце:** продуктовые К контракта — К-1 (десять статусов) · К-3 (метка главы в дереве) · К-5 (ETA в MVP; ПТ-19 существует) · К-6 (ось ступеней замечаний — операторская унаследована) · К-8 (слово и статус стопа по потолку) + заполнение приложения А (В-3) + прежний список D39.98 п.4 (confirm двух решений · авторство подчистки · приёмка банк-арбитража). **Следующий разговор на берегу — бутстрап платформы П-1: у второй стороны контракта ноль строк кода, К-4/К-7/К-12 адресованы зоне, которой нет** (урок строки 95 — не ждать, пока три сессии упрутся). (04.08.2026, оркестратор №12). ✅ diff --git a/docs/architecture/14-api-contract/README.md b/docs/architecture/14-api-contract/README.md new file mode 100644 index 00000000..eda2a37e --- /dev/null +++ b/docs/architecture/14-api-contract/README.md @@ -0,0 +1,339 @@ + + +# Контракт API v0 — спутник спеки: провенанс, обоснования, вопросы + +> **Нормативная поверхность контракта — [`api-contract/openapi.yaml`](api-contract/openapi.yaml)** +> (OpenAPI 3.1). Этот файл её НЕ дублирует: он несёт то, чего YAML не выражает — откуда взято +> каждое решение, чем оно обосновано, что осталось открытым. При расхождении по ФОРМЕ +> побеждает YAML; при вопросе «почему так» — этот файл. +> +> **Статус: черновик фронт-сессии S3 на ратификацию.** Дом ратифицированной копии — +> `docs/architecture/14-api-contract/`, зона оркестратора; перенос делает он, генерация типов +> после ратификации идёт из перенесённой копии — контракт первичен, код вторичен. +> +> **Язык.** Спека английская: из неё генерятся типы, а исходники фронта по конвенции +> английские (слово владельца 04.08). Ссылки на К-вопросы внутри YAML набраны латинской +> `K-N` — это те же вопросы §4. Спутник и остальные доки зоны — русские. +> +> **Зона строки 95 — «оркестратор/бэкенд/фронт».** Фронт авторитетен в одной трети: форма +> read-модели и продуктовые словари. Транспорт платформы (пути, аутентификация, коды) и +> работы движка (99–103) здесь ПРЕДЛОЖЕНЫ и без подтверждения своих зон не действуют. + +## 0. Пометки провенанса + +| Пометка | Что значит | +|---|---| +| **✓ выведено** | следует из кода движка или ратифицированного решения; грунт `file:line` рядом | +| **◆ предложено** | решение фронта, разумное по его сведениям; подтверждает названная зона | +| **○ открыто** | развилка, на которую у фронта ответа нет; перечень — §4 | + +⚠ Пометка ставится **на утверждение, а не на раздел**: у одного пункта половина бывает +выведенной, а половина предложенной. Первая редакция черновика этим и грешила — восемь мест +несли ✓ там, где верно было ◆; ниже разведено. + +--- + +## 1. Почему YAML, а не проза + +Контракт — машинный артефакт: из него генерируются типы, по нему линтуется форма, им +типизируются моки. Прозаический контракт расходится с кодом ровно тем способом, ради +предотвращения которого заведена строка 95. + +Инструменты, пины и отклонение по пиру TS не дублирую: они в `STACK_DECISIONS.md` §3 и в +бэклоге зоны — Ф-23 (`overrides` вместо `--legacy-peer-deps`), Ф-24 (AsyncAPI отложен с +причиной). Четыре формы нарушения гейта, каждая проверена живьём, — `FRONTEND_PLAN.md` §5.4.2. + +--- + +## 2. Решения и их происхождение + +### 2.1. Язык — код, никогда не имя — ✓ выведено + +Движок держит коды (`backend/internal/config/book.go:26-27`), ключ пары — `zh-ru` +(`configs/langpacks/zh-ru/`). Имя языка в данных — пар-специфика в общем слое, запрещённая +§2 канона. Вторая цена, дороже: `lang` элемента берётся из данных книги, и пара ja→ru с именем +вместо кода отрисует кандзи китайскими начертаниями молча. + +### 2.2. Идентификаторы непрозрачны — ✓ выведено + +`glossary.id` — свежий автоинкремент на каждой пересборке банка и намеренно не хешируется +(`store/migrate.go:173-174`); номер главы плотный, «Chapters that yield no text … do NOT +consume a chapter number» (`chunk/chunker.go:99-105`), поэтому правка исходника сдвигает +номера последующих глав. Стабильность обеспечивает платформа при персисте манифеста (100). + +### 2.3. Заголовок главы отдельным полем — ◆ предложено + +**Движок сегодня делает ОБРАТНОЕ**, и это надо назвать прямо: титул рендерится +детерминистически из шаблона пары (`configs/langpacks/zh-ru/heading.txt`: `template Глава {n}`), +исходный маркер вырезается из текста для модели (`chunk/chunker.go:110-114`), а на экспорте +титул **вклеивается внутрь текста первого юнита** (`pipeline/export.go:215`), причём колонка +исходника остаётся без него. + +Выведена здесь только МЕХАНИКА. Само поле `heading` — предложение фронта, и у него есть цена +на другой стороне: движку придётся отдавать титул отдельно. Альтернатива (оставить вклейку, +фронт отрезает строку) хуже: отрезание титула из текста — это парсинг прозы, и он сломается +на первой главе без заголовка. Развилка — К-2. + +**Расхождение фикстуры, найденное разбором:** дерево витрины показывает «Раздел 2. …», колонка +оригинала — неснятый «第二节:». Для zh→ru движок не порождает ни одной из форм. Не чинится +до ответа на К-2. + +### 2.4. Состояние — у прогона; у главы выполнение — ✓ выведено + +Подпись банка это один стоп на всю книгу (`pipeline/mining.go:201`), поэтому «глава ждёт +подписи, пока соседняя финализируется» — невозможная картина. У главы движок держит +`ChapterPassport` (`pipeline/status.go:37-55`). + +### 2.5. Прогресс пофазно и в юнитах — ✓ выведено + +«A unit is DONE when every member draft AND the unit's edit resolved ok» +(`pipeline/status.go:328-331`), редактура не стартует до стопа банка ⇒ сквозной счётчик стоит +на нуле всю черновую волну. Зависимость — строка 99. + +### 2.6. Словарь статусов — ✓ лестница, ◆ ненормальные исходы + +Лестница дословно из строки 95: «загрузка → разбор → перевод → подпись банка → финал → +готово». **`not_started` — дыра, найденная самопроверкой черновика:** лестница описывает +идущий прогон, а библиотека обязана показывать разобранную книгу, которую не запускали. +`stopped`, `rejected`, `not_started` контракт ВЫВОДИТ из поведения процесса, а не получает +полем: механика стопа у движка есть (`cmd/tmctl/main.go:63`), но «кто нажал» знает платформа. + +**Стоп по потолку — не `failed`** ✓ выведено: «Ceiling is a hard, book-wide stop (not a +per-chunk flag): the job stays 'pending' and resume continues once the ceiling is raised» +(`pipeline/stagerun.go:488-489`). Мапить его в `failed` запрещено — это соврало бы про +резюмируемость. Каким статусом и словом он показывается — К-8, вопрос владельцу. + +### 2.7. Состояние пары выводится из ПАРЫ — ✓ выведено (исправление первой редакции) + +Первая редакция утверждала «`withheld` = текст не выдан» как факт о движке. **Это было +ложно:** флагнутый юнит легально приходит С ТЕКСТОМ в двух случаях — + +- косметическая зачистка санитайзера: «the chunk is NOT lost — the cleaned text is committed + as the export» (`pipeline/disposition.go:96-104`); +- c-lite member-drop: редактор отгружает отредактированный чистый остаток, а юнит флагнут + из-за выпавшего члена (`pipeline/export.go:203-207`). + +Поэтому состояние выводится из ПАРЫ (вердикт, наличие финального текста): флаг+текст → +`translated` с замечанием; флаг+пусто → `withheld`. + +**Причина флага при этом ПРОИЗВОЛЬНА, и «единственный легальный случай» — снято** (ревью +оркестратора, round-2, пункт 4; утверждение противоречило выводу строкой выше). При c-lite drop +юнит несёт `FlagReason` ПЕРВОГО выпавшего члена, каким бы он ни был (`export.go:203-207`: +`ce.FlagReason = drops[0].Reason`, и тут же `ce.FinalText … still ships`). Значит «текст + +замечание» — это класс, а не один случай, и карта вердиктов обязана иметь фразу для каждой +причины, а не для двух. + +**Но `glossary_miss` в этот класс НЕ входит, и это проверено отдельно** (иначе правка выше +воскресила бы невоспроизводимый пример). `memberDrops` берёт причину из ЧЕРНОВОЙ строки члена +(`status.go:242-257`: `draftStages[cs.Stage] && flagged`), а `glossary_miss` ставится +пост-чеком только там, где отгружается финал: в черновой волне — лишь когда она сама финальная +(`waverun.go:373-381`, draft-only), в c-lite — на строке РЕДАКТУРЫ (`waverun.go:494`). В +пайплайне, где текст отгружает редактура (то есть где c-lite drop вообще возможен), черновая +строка `glossary_miss` нести не может. При включённом гейте текст удерживается целиком +(`export.go:295-300`), при выключенном мисса нет вовсе. **Итог: пара «текст + промах словаря» +невозможна ни одним каналом — фикстура витрины, показывавшая её, переведена на c-lite drop.** + +**Свежесть** ✓ выведено: `target` обновляется на границах стадий и на стопах, а не +непрерывно — посреди прогона канала чтения не существует (эксклюзивный лок движка; +санкционированное чтение — завершённый либо остановленный прогон, `research/23` §0, §4). + +### 2.8. Банк: словари ✓, имена ◆, `kind` ◆ с дырой + +Значения выведены из схемы и гейтов Go; **имена полей контракта — предложение фронта.** + +| Поле | Словарь | Грунт | +|---|---|---| +| `status` | `auto · draft · approved` | `store/migrate.go:191`; только `approved` — канон | +| `kind` ◆ | `name · place · title · term · nickname` **плюс отсутствие значения** | `terminology/classify.go:15` + `pipeline/banknote.go:74`; пустое — `membank/memseed.go:323-326` | +| `origin` | `seed · ruby · mined` | пути записи, см. ниже | +| `sense` | свободный текст | `store/migrate.go:182` | +| `since_chapter`/`until_chapter` | целые, `0` = без границы | `store/migrate.go:189-190` | + +**Фантом `auto` в провенансе убран.** Первая редакция взяла словарь из комментария схемы +(`migrate.go:193`: `seed|ruby|auto`) — комментарий устарел. По путям записи `"auto"` пишет +**статус**, не провенанс (`membank/memseed.go:328`: `Status:"auto", Source:"ruby"`), а майнинг +ставит `mined` (`pipeline/mining.go:424`). Правка комментария в движке — за оркестратором. + +**`kind` пере-размечен ✓→◆, и вот почему это не косметика** (ревью round-2, пункт 3). Словарь +из пяти значений выведен верно, но ЗАКРЫТЫМ и обязательным он делает нелегальной легальную +строку: ruby-кандидат получает `Type: ""`, если его класс не `name` — то есть gloss и +ambiguous живут без типа по построению (`membank/memseed.go:323-326`: `typ := ""`, и только +`class == rubyClassName` даёт `"name"`). Материализатору read-модели такую строку было +физически нечем заполнить. **Правило пустого:** `kind` присутствует всегда и допускает `null`; +`null` значит «движок не решил», строка при этом остаётся подписываемой, и клиенту запрещено +и выбрасывать её, и додумывать тип за движок. Проекция `""` → `null` — работа платформы. + +**Ложный друг устранён.** У движка колонка `source` — это ПРОВЕНАНС. Первая редакция назвала +провенанс `origin`, а имя `source` отдала ДРУГОЙ колонке (тексту термина) — то есть завела +между схемами ложного друга. Теперь: провенанс `origin`, формы термина `src`/`dst`, как их +зовёт сам движок; имя `source` в схеме банка не используется вовсе. + +### 2.9. Подпись — набор решений — ✓ выведено + +Конвейер заменяет банк целиком (`store/migrate.go:169-170`), поэтому `PATCH /term/{id}` молча +не работает. Механика дословно: «for EACH term either promote it into the mined-delta file OR +decline it in the mined-rejects file, then resume — the stop clears once every proposed term +is promoted or rejected» (`pipeline/mining.go:201`). Отсюда: решение `promote|decline` · +счётчик «решено N из M» · запрет «продолжить» при неполном наборе · частичное сохранение ◆. + +`POST /runs/{id}/resume` — **нормативная операция, а не резерв** (первая редакция помечала её +«○ резерв строки 94», хотя тут же делала её носителем снятия стопа банка). Резерв строки 94 — +это `stop`, продуктовая кнопка. + +### 2.10. Ревизия — ✓ у ре-синка, ◆ у чтений + +**✓ выведено:** правило ре-синка — идемпотентный апсерт по `(run_id, seq)`, канал согласования +— `status --json` (`research/23` §2, §8; D39.85). + +**◆ предложено фронтом:** что ревизию несут и ЧТЕНИЯ, и что счётчик у потока и у чтений ОДИН. +Обоснование — гонка, которую иначе нечем разрешить: фронт живёт на снимке и потоке разом, +а рефетч по возврату фокуса окна у ратифицированного `@tanstack/react-query` включён по +умолчанию, то есть гонка на каждое переключение вкладки. Но это просьба, не вывод; выбор — +К-4. + +**Скоуп ревизии** (дыра первой редакции: она отдавала `revision` на межкнижной библиотеке при +пер-прогонном определении): в спеке ревизия объявлена НА РЕСУРС — у библиотеки своя, у +прогона своя. Единая сквозная или пер-ресурсная — часть К-4. + +### 2.11. Разрыв потока — ◆ предложено + +При переподключении клиент шлёт `Last-Event-ID`. Если сервер докачать не может, он обязан +ответить событием `resync_required`, а не молча начать с текущего момента: **реплей истории +запрещён**, иначе разовое событие вроде `note` теряется молча и замечание не появится +до перезагрузки. Клиент по этому событию перечитывает снимки. + +### 2.12. Разрешающий список — ✓ инвариант, ◆ форма + +Проекция «read-модель → фронт» строится как allowlist. Что лежит в операторских структурах +(`pipeline/status.go:58-130`, `:37-55`) и не может доехать: пять денежных полей плюс +`cost_usd` главы (§4.8 — денег в MVP-интерфейсе нет вовсе) · `routing` вида «stage=model», +`content_labels`, `content_routing_problems` (ПТ-33) · снапшот, дрифт, ре-билл · +`escalations`, `postcheck_misses`, `style_flags`, `glossary_miss_flagged`, `stages_skipped`, +`repair_applied`, `worst_flag_reason` · `flag_reason` и `detail` — последний несёт сырой текст +движка вида «CJK leak in the ru output: 第一节»; строку собирает `checks/sanitizer.go:658`, +а `pipeline/export.go:34` — лишь объявление поля, куда она доезжает. + +### 2.13. ПТ-34 — перевод не индексируется — ✓ инвариант + +Реестр требований назначает носителем ПТ-34 в том числе контракт, а первая редакция пункта +не имела вовсе. В спеке: приложение живёт под `X-Robots-Tag: noindex`, ответы с текстом +перевода несут `Cache-Control: no-store`, ссылка на выгрузку выдаётся только владельцу. + +--- + +## 3. Зависимости: без чего контракт не заработает + +| Что | Строка | Без чего именно | +|---|---|---| +| Пофазный прогресс `draft ∥ edit` | 99 | прогресс (§2.5) | +| Персист манифеста + `chunker_version` | 100 | стабильный `id` главы (§2.2) | +| Машиночитаемая таблица ПОДПИСИ | 101 | экран подписи (§2.9) | +| Событийный эмиттер + событие потолка | 103 | весь поток (§2.11), событие `note`, событие `ceiling` | +| **Артефакт экспорта БАНКА** | **строки нет — заводит оркестратор** | чтение `GET /books/{id}/bank` | +| **Механизм поднятия потолка** | **строки нет — заводит оркестратор** | `POST /runs/{id}/resume` после стопа по потолку (см. ниже) | +| HTTP/SSE, аутентификация, воркер | П-1 | всё; в `platform/` ноль строк кода | + +⚠ **Отдельно про банк — дыра, найденная ревью оркестратора.** Строка 101 даёт таблицу +ПОДПИСИ (стоп-таблица, кап 20 на stdout — `cmd/tmctl/render.go:98`), а не экспорт всего банка; +сам банк живёт в приватном SQLite движка, читать который платформе запрещено (D39.85). То есть +у чтения `/bank` сегодня **нет канала вообще**. Фронт этого не решает — нужна строка единого +бэклога, и заводит её оркестратор. + +Та же природа у события `note`: пер-юнитных замечаний посреди прогона движок не эмитит — +зависимость на словарь строки 103. + +⚠ **Потолок: `resume` сам по себе не сдвинет прогон** (ревью round-2, пункт 5). Движок +продолжает «once the ceiling is raised» (`pipeline/stagerun.go:488-489`), а канала поднятия +в контракте нет — и в MVP-интерфейсе быть не может: денег на экране нет вовсе (D39.84). Значит +между стопом по потолку и продолжением обязан стоять механизм ПЛАТФОРМЫ (поднятие по политике, +или явное действие вне интерфейса книги), и до него `resume` после потолка возвращает прогон +в то же состояние. Что при этом видит пользователь — К-8, вопрос владельцу; чем поднимают — +строка единого бэклога, которой нет. + +--- + +## 4. Открытые вопросы + +| # | Вопрос | Кому | +|---|---|---| +| К-1 | Десять статусов (§2.6) — принять или поправить? Три контракт выводит, а не получает | владелец / автор контракта | +| К-2 | Титул главы: отдать полем `heading` (предложено) — или оставить вклейку в текст, и фронт отрезает строкой? | автор контракта + бэкенд | +| К-3 | Метка главы в дереве: титул это ровно «Глава N», узлов 2284 — дерево одинаковых по форме строк | владелец (продуктовое) | +| К-4 | Ревизия: одна сквозная на прогон или своя на ресурс? И несут ли её чтения вообще (§2.10) | платформа | +| К-5 | Показывать ли оценку времени. **ПТ-19 существует** (`docs/product-requirements.md:49`: «видимый прогресс/ETA — из Ф3-видения ридер-IDE»), то есть посылка «не запрошено» неверна; вопрос в том, показываем ли в MVP | владелец (продуктовое) | +| К-6 | Ступени замечания: сколько их и где граница. Сегодняшние две — проекция ОПЕРАТОРСКОЙ лестницы рангов, а она не обязана совпадать с продуктовой осью | владелец (продуктовое) | +| К-7 | Пагинация: 2284 главы и 1200 терминов одним ответом или курсором? Фронт виртуализует, ему годится любой | платформа | +| К-8 | Стоп по потолку: каким статусом и каким словом? В `failed` мапить нельзя — стоп резюмируемый | владелец (продуктовое) | +| К-9 | **Отказ прескрина не выразим ни одним из десяти статусов.** Абьюз/misuse-прескрин до трат токенов и UI-контракт отказа — строка 94 (ПТ-16); книга, отклонённая прескрином, это не `rejected` (тот про неразобранный файл) и не `failed` | владелец + автор контракта | +| К-10 | **Выполнение главы — тот самый несплитованный счётчик, который §2.5 объявляет негодным.** `Chapter.units_done` не разведён по фазам, значит дерево глав показывает ноль всю черновую волну — ровно то, из-за чего прогресс книги сделан пофазным. Развести и тут (цена — пофазные счётчики НА ГЛАВУ в строке 99) или показывать в дереве другое | автор контракта + бэкенд | +| К-11 | **Условная обязательность полей — выражена у банка, не выражена у чтений.** У `BankDecision` констрейнт поставлен (`if action=promote → dst` непустой), и вот что это стоило, измерено: spectral его валидирует, а **openapi-typescript его игнорирует** — в генерённых типах `dst?: string` как был. То есть 3.1-условие защищает сервер, но не экран; клиентское сужение (юнион `promote`-с-`dst` ↔ `decline`) — работа подписного экрана S5, писать его до экрана не на чем проверить. Остаётся решить то же для чтений: `Note` не требует ни `chapter_id`, ни `unit_id`, `Unit.target` не обязателен при `translated`; обе схемы служат и вложенно, и отдельно, поэтому простое `required` соврало бы | автор контракта | +| К-12 | **Завершение выгрузки: опрос или событие?** Чтение `GET /books/{id}/exports/{id}` заведено — без него создающий вызов был тупиком (`ready:false` и ни слова дальше). Но пушить ли завершение ещё и кадром потока, чтобы не опрашивать, решает платформа: у неё воркер и её цена | платформа | + +--- + +## 5. Проверка ревью-вопросом строки 95 + +**«Сменится стадия конвейера — придётся ли править фронт?»** + +| Изменение в движке | Правит ли фронт | +|---|---| +| переименована стадия / добавлена волна | **нет** — имена стадий не пересекают шов, прогресс пофазный, а не постадийный | +| сменилась модель или маршрутизация | **нет** — `routing`/`content_labels` в allowlist не входят | +| добавлена новая причина флага | **нет** — на провод идёт продуктовая фраза, карта живёт в контракте | +| добавлен новый тип термина | **нет** — словарь расширяется минором, ветка неизвестного стоит на шве | +| добавлено новое продуктовое состояние | **да, один файл** — карта «статус → вид» на шве `src/api/`; это и есть контрольный вопрос владельца | +| сменился чанкер, главы пере-разобраны | **частично** — код фронта не правится (ключ непрозрачный, номер отображаемый), но **сохранность соответствия старых `id` новым главам контрактом не гарантируется**: это работа персиста манифеста (строка 100). Если соответствие потеряно, у пользователя разъезжаются открытые вкладки и закладки — не правка кода, но видимый ущерб, и решать его строке 100 | + +Единственное безусловное «да» — то, которое и должно быть «да». + +--- + +## Приложение А. Карта «вердикт → продуктовая фраза» — ЗАГОТОВКА + +Заполняет автор контракта вместе с бэкендом и владельцем. **Правило: фраза пишется по +доккомменту `disposition.go`, а не по имени константы, и рядом кладётся цитата** — иначе +повторяется инверсия, стоившая двух фраз (`glossary_miss` подан как «термин не подписан», +хотя термин ПОДПИСАН и его проигнорировали, `disposition.go:78-79`; `sanitizer_stripped` подан +как потеря текста, хотя «the chunk is NOT lost», `disposition.go:99`). + +⚠ **Русские фразы ниже — плейсхолдеры, а не предложение фронта.** Словарь продуктовый, его +слова выбирает владелец (ПТ-33, В-3). Ступень — тоже: сегодняшние `attention`/`glance` +унаследовали ОПЕРАТОРСКУЮ ось рангов, а она не обязана совпадать с продуктовой (К-6). + +| Причина движка | Ранг | Продуктовая фраза | Ступень | +|---|---|---|---| +| `hard_refusal` · `soft_refusal` · `content_filter` · `hard_block` | 0 | ⬜ | ⬜ | +| `cjk_artifact` · `excision_suspect` · `coverage_fail` | 1 | ⬜ | ⬜ | +| `sanitizer_defect` | 2 | ⬜ | ⬜ | +| `loop_degenerate` | 3 | ⬜ | ⬜ | +| `decode_error` | 4 | ⬜ | ⬜ | +| `glossary_miss` | 5 | плейсхолдер: «Подписанный термин не применён в переводе» | ⬜ | +| `length` · `empty` | 6 | ⬜ | ⬜ | +| `sanitizer_stripped` | 7 | плейсхолдер: «Служебная разметка вычищена автоматически» | ⬜ | +| `upstream_not_ok` | 8 (по умолчанию) | ⬜ | ⬜ | +| незнакомая причина | 8 (по умолчанию) | ⬜ нейтральная, НЕ «ошибка» | ⬜ | + +Причин пятнадцать; `upstream_not_ok` в первой редакции отсутствовал — у него нет своей ветки +в `flagReasonSeverity`, поэтому он падает в ранг по умолчанию (`pipeline/status.go:174`), +как и любая будущая причина. Последняя строка — не формальность: контракт обязан иметь фразу +для причины, которой ещё не существует. diff --git a/docs/architecture/14-api-contract/openapi.yaml b/docs/architecture/14-api-contract/openapi.yaml new file mode 100644 index 00000000..ec9cd8a3 --- /dev/null +++ b/docs/architecture/14-api-contract/openapi.yaml @@ -0,0 +1,983 @@ +openapi: 3.1.0 + +info: + title: TextMachine API + version: 0.1.0-draft + summary: Contract between the frontend and the TextMachine platform (draft, awaiting ratification). + description: | + **DRAFT of frontend session S3 — not a ratified contract.** The ratified copy belongs in + `docs/architecture/14-api-contract/` (orchestrator's zone). + + Companion document: `../API_CONTRACT_DRAFT.md` — provenance of every decision (derived from + engine code / proposed by the frontend / open), rationale, dependencies and open questions. + This file is normative for the FORM; the companion explains where the form comes from. + + ## Boundaries + + The frontend reads the platform read-model only. The engine is never addressed by any path + below (D39.85). + + Pipeline vocabulary does not cross this boundary: no model names, no stage names, no money. + The read-model → frontend projection is an allowlist — a field not named here never reaches + the browser. + + No response is served from an indexable URL. The app lives on `app.` under + `X-Robots-Tag: noindex`; responses carrying translated text MUST be sent with + `Cache-Control: no-store`. + + ## Versioning + + Semver. **Minor** — backwards-compatible additions: a client MUST ignore unknown fields and + MUST tolerate unknown enum values without failing. **Major** — a client MUST refuse an + unsupported version and tell the user. + + Every `enum` below is the vocabulary of version 0.1, not a closed world. Generated types are + closed unions and do NOT protect against an unknown value, so the unknown-value branch + belongs on the client seam (`src/api/`), where values enter, not in every component. + + license: + name: UNLICENSED + identifier: LicenseRef-proprietary + +servers: + - url: https://app.example.org/v0 + description: Platform. The base path is a frontend proposal; the platform confirms it. + +security: + - sessionCookie: [] + - bearerToken: [] + +tags: + - name: library + description: Book library and book card. + - name: reading + description: Chapters, source/translation pairs, notes. + - name: bank + description: Memory bank and term signing. + - name: runs + description: Translation runs, live progress, control. + - name: export + description: Export of a finished book. + +paths: + /books: + get: + tags: [library] + operationId: listBooks + summary: Book library. + description: | + Flat list of the user's books. `revision` is the revision of the LIBRARY itself + (membership and statuses), not of any run. Revision scope is open — companion §4 (K-4). + responses: + '200': + description: Library. + content: + application/json: + schema: { $ref: '#/components/schemas/Library' } + '401': { $ref: '#/components/responses/Unauthorized' } + post: + tags: [library] + operationId: createBook + summary: Add a book. + description: | + Accepts the file and the properties declared by the user. Responds immediately; the book + enters `uploading` and parsing is a separate visible step. + requestBody: + required: true + content: + multipart/form-data: + schema: { $ref: '#/components/schemas/BookIntake' } + responses: + '201': + description: Book accepted. + content: + application/json: + schema: { $ref: '#/components/schemas/Book' } + '400': { $ref: '#/components/responses/BadRequest' } + '401': { $ref: '#/components/responses/Unauthorized' } + '413': { $ref: '#/components/responses/TooLarge' } + + /books/{bookId}: + parameters: + - $ref: '#/components/parameters/BookId' + get: + tags: [library] + operationId: getBook + summary: Book card. + description: Book metadata plus the current or last run. + responses: + '200': + description: Book card. + content: + application/json: + schema: { $ref: '#/components/schemas/BookDetail' } + '401': { $ref: '#/components/responses/Unauthorized' } + '404': { $ref: '#/components/responses/NotFound' } + + /books/{bookId}/chapters: + parameters: + - $ref: '#/components/parameters/BookId' + get: + tags: [reading] + operationId: listChapters + summary: Chapter tree. + description: | + Chapters in reading order. A chapter has NO status, only unit progress: bank signing is a + single book-wide stop, so "one chapter awaits signing while its neighbour finalizes" + cannot happen. + responses: + '200': + description: Chapters of the book. + content: + application/json: + schema: { $ref: '#/components/schemas/ChapterList' } + '401': { $ref: '#/components/responses/Unauthorized' } + '404': { $ref: '#/components/responses/NotFound' } + + /books/{bookId}/chapters/{chapterId}/units: + parameters: + - $ref: '#/components/parameters/BookId' + - $ref: '#/components/parameters/ChapterId' + get: + tags: [reading] + operationId: listUnits + summary: Source/translation pairs of a chapter. + description: | + The unit of shipping is the EDIT UNIT, not a paragraph and not a chunk: roughly 1.9 units + per chapter, and a whole chapter is sometimes a single block. Alignment is coarse and + accepted as such. + responses: + '200': + description: Pairs of the chapter. + content: + application/json: + schema: { $ref: '#/components/schemas/UnitList' } + '401': { $ref: '#/components/responses/Unauthorized' } + '404': { $ref: '#/components/responses/NotFound' } + + /books/{bookId}/notes: + parameters: + - $ref: '#/components/parameters/BookId' + get: + tags: [reading] + operationId: listNotes + summary: Notes of a book. + description: | + A note addresses a unit or a whole chapter. Byte offsets do not exist in the engine's + checks and are not planned. + responses: + '200': + description: Notes of the book. + content: + application/json: + schema: { $ref: '#/components/schemas/NoteList' } + '401': { $ref: '#/components/responses/Unauthorized' } + '404': { $ref: '#/components/responses/NotFound' } + + /books/{bookId}/bank: + parameters: + - $ref: '#/components/parameters/BookId' + get: + tags: [bank] + operationId: listBankTerms + summary: Memory bank of a book. + description: | + ⚠ **No backing channel exists for this read today.** The engine ships a signing table, + not a bank export, and its private store must not be read by the platform. The export + artifact is a dependency the frontend cannot create — companion §3. + responses: + '200': + description: Bank of the book. + content: + application/json: + schema: { $ref: '#/components/schemas/Bank' } + '401': { $ref: '#/components/responses/Unauthorized' } + '404': { $ref: '#/components/responses/NotFound' } + + /books/{bookId}/bank/decisions: + parameters: + - $ref: '#/components/parameters/BookId' + post: + tags: [bank] + operationId: submitBankDecisions + summary: Submit term decisions. + description: | + **Signing is not a row edit.** The pipeline replaces a book's whole glossary from its + deterministic inputs, so a direct write would be erased by the next run. A decision is + `promote` (with a translation) or `decline`, following the stop mechanics exactly. + + Submission is PARTIAL and accumulates on the server: there are hundreds of terms, and a + closed tab must not cost an hour of work. + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/BankDecisionsRequest' } + responses: + '200': + description: Decisions accepted; the response carries what is left. + content: + application/json: + schema: { $ref: '#/components/schemas/BankDecisionsResult' } + '400': { $ref: '#/components/responses/BadRequest' } + '401': { $ref: '#/components/responses/Unauthorized' } + '404': { $ref: '#/components/responses/NotFound' } + + /books/{bookId}/runs: + parameters: + - $ref: '#/components/parameters/BookId' + post: + tags: [runs] + operationId: startRun + summary: Start a translation run. + description: | + `verify_bank` is a parameter of the RUN, not a global setting. With it the run stops at + the bank boundary; without it the unsigned bank is carried forward marked as unverified. + It is the user's choice between "I will sign" and "translate as is". + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/RunRequest' } + responses: + '202': + description: Run accepted. + content: + application/json: + schema: { $ref: '#/components/schemas/Run' } + '401': { $ref: '#/components/responses/Unauthorized' } + '404': { $ref: '#/components/responses/NotFound' } + '409': { $ref: '#/components/responses/Conflict' } + + /runs/{runId}/events: + parameters: + - $ref: '#/components/parameters/RunId' + get: + tags: [runs] + operationId: streamRunEvents + summary: Live run events (SSE). + description: | + `text/event-stream`. Required: HTTP/2 at the edge, `Cache-Control: no-store`, + `X-Accel-Buffering: no`, a heartbeat every ~20 s, a monotonic `id` and `Last-Event-ID` + support. Events are PUSHED by the platform worker; the frontend never polls the + read-model. + + **The first event is always `hello`** — the version handshake. A client that does not + support the major version closes the stream and tells the user. + + **Reconnect.** The client sends `Last-Event-ID`. If the server cannot resume from it, it + MUST answer with `resync_required` instead of silently starting from the present moment; + the client then re-reads snapshots. Replaying history is forbidden — a one-shot event + such as `note` would otherwise be lost silently. + + OpenAPI does not type SSE frames, so the event name → payload schema mapping is a table + in the `EventEnvelope` description. + responses: + '200': + description: Event stream. + content: + text/event-stream: + schema: { $ref: '#/components/schemas/EventEnvelope' } + '401': { $ref: '#/components/responses/Unauthorized' } + '404': { $ref: '#/components/responses/NotFound' } + + /runs/{runId}/stop: + parameters: + - $ref: '#/components/parameters/RunId' + post: + tags: [runs] + operationId: stopRun + summary: Stop a run. + description: | + The product "stop" action. The engine stops gracefully on a signal; deciding who pressed + it belongs to the platform. + responses: + '202': + description: Stop accepted. + content: + application/json: + schema: { $ref: '#/components/schemas/Run' } + '401': { $ref: '#/components/responses/Unauthorized' } + '404': { $ref: '#/components/responses/NotFound' } + '409': { $ref: '#/components/responses/Conflict' } + + /runs/{runId}/resume: + parameters: + - $ref: '#/components/parameters/RunId' + post: + tags: [runs] + operationId: resumeRun + summary: Resume a stopped run. + description: | + Clears the bank-signing stop and continues after a user stop. + + ⚠ **After a ceiling stop this call alone does not move the run:** the engine continues + only once the ceiling has been raised, and no channel for raising it exists here — money + is absent from the MVP interface by decision. The mechanism belongs to the platform + (companion §3), and what the user sees meanwhile is companion §4 (K-8). + + **Answers 409 while the set of bank decisions is incomplete** — the stop clears only on a + complete set. + responses: + '202': + description: Resume accepted. + content: + application/json: + schema: { $ref: '#/components/schemas/Run' } + '401': { $ref: '#/components/responses/Unauthorized' } + '404': { $ref: '#/components/responses/NotFound' } + '409': { $ref: '#/components/responses/Conflict' } + + /books/{bookId}/exports: + parameters: + - $ref: '#/components/parameters/BookId' + post: + tags: [export] + operationId: createExport + summary: Build a book export. + description: Formats and their contents are stage S7 work; only the call shape is fixed here. + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/ExportRequest' } + responses: + '202': + description: Export is being prepared. + content: + application/json: + schema: { $ref: '#/components/schemas/Export' } + '401': { $ref: '#/components/responses/Unauthorized' } + '404': { $ref: '#/components/responses/NotFound' } + + /books/{bookId}/exports/{exportId}: + parameters: + - $ref: '#/components/parameters/BookId' + - $ref: '#/components/parameters/ExportId' + get: + tags: [export] + operationId: getExport + summary: State of an export. + description: | + Without this read the creating call is a dead end: it answers `ready: false` and nothing + ever says otherwise. Whether completion is ALSO pushed as a stream event, making polling + unnecessary, is the platform's call — companion §4 (K-12). + responses: + '200': + description: State of the export. + content: + application/json: + schema: { $ref: '#/components/schemas/Export' } + '401': { $ref: '#/components/responses/Unauthorized' } + '404': { $ref: '#/components/responses/NotFound' } + +components: + securitySchemes: + sessionCookie: + type: apiKey + in: cookie + name: __Host-tm_session + description: | + Browser presentation of one server-side session: HttpOnly, Secure, SameSite=Lax. CSRF + protection is mandatory on the cookie path. The form belongs to the platform. + bearerToken: + type: http + scheme: bearer + description: | + Desktop and CLI present the same server-side session as an opaque token. The principal is + established in middleware only; no endpoint may assume a cookie — that is what keeps the + API portable to the desktop client. + + parameters: + BookId: + name: bookId + in: path + required: true + description: Opaque book identifier. + schema: { $ref: '#/components/schemas/Id' } + ChapterId: + name: chapterId + in: path + required: true + description: Opaque chapter identifier. + schema: { $ref: '#/components/schemas/Id' } + RunId: + name: runId + in: path + required: true + description: Opaque run identifier. + schema: { $ref: '#/components/schemas/Id' } + ExportId: + name: exportId + in: path + required: true + description: Opaque export identifier. + schema: { $ref: '#/components/schemas/Id' } + + responses: + BadRequest: + description: Request rejected. + content: + application/problem+json: + schema: { $ref: '#/components/schemas/Problem' } + Unauthorized: + description: Session missing or invalid. + content: + application/problem+json: + schema: { $ref: '#/components/schemas/Problem' } + NotFound: + description: Object not found. + content: + application/problem+json: + schema: { $ref: '#/components/schemas/Problem' } + Conflict: + description: Action impossible in the current state. + content: + application/problem+json: + schema: { $ref: '#/components/schemas/Problem' } + TooLarge: + description: File exceeds the intake size limit. + content: + application/problem+json: + schema: { $ref: '#/components/schemas/Problem' } + + schemas: + Id: + type: string + minLength: 1 + description: | + Opaque identifier. The client MUST NOT parse, sort by or construct it. Stability across + runs is the platform's job when it persists the manifest. + examples: ['bk_7c1'] + + Revision: + type: integer + minimum: 0 + description: | + Monotonic revision of a resource. A read whose revision is LOWER than an event the client + already applied MUST be dropped rather than rendered, otherwise the interface rolls + progress backwards on every refetch. + examples: [1841] + + LangCode: + type: string + pattern: '^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$' + description: | + Language code, never a name. The display name is computed by the screen via + `Intl.DisplayNames`. + examples: ['zh'] + + Counter: + type: object + description: Wave counter, in UNITS. + required: [done, total] + properties: + done: { type: integer, minimum: 0 } + total: { type: integer, minimum: 0 } + + Progress: + type: object + description: | + Progress PER PHASE, in units: a unit is done only once its edit resolved, and editing does + not start before the bank stop, so a single end-to-end counter reads zero for the whole + draft wave. + + The phases exist for the DATA, not for the screen — the user sees one fraction with no + phase names. No ready-made percentage is shipped: the formula is a product decision. + required: [draft, edit] + properties: + draft: { $ref: '#/components/schemas/Counter' } + edit: { $ref: '#/components/schemas/Counter' } + + BookStatus: + type: string + description: | + Product status of a book: `uploading` file is being accepted · `parsing` split into + chapters · `not_started` parsed, never run · `translating` translation in progress · + `awaiting_bank` waiting for the glossary to be signed · `finalizing` final pass · + `ready` done · `stopped` stopped by the user · `rejected` file could not be parsed · + `failed` run aborted by an error. + + The engine has no run-state vocabulary at all, so `not_started`, `stopped` and `rejected` + are DERIVED by the contract rather than received as a field. + + **A ceiling stop is NOT `failed`.** It is a resumable book-wide stop; mapping it to + `failed` is forbidden, as that would lie about resumability. Which status and which word + the user sees is an open product question — companion §4 (K-8). + + A prescreen refusal maps to none of these values either — companion §4 (K-9). + enum: + - uploading + - parsing + - not_started + - translating + - awaiting_bank + - finalizing + - ready + - stopped + - rejected + - failed + + Book: + type: object + description: A book in the library. + required: + [id, title, source_lang, target_lang, status, chapter_count, added_at, progress, note_count] + properties: + id: { $ref: '#/components/schemas/Id' } + title: { type: string } + source_lang: { $ref: '#/components/schemas/LangCode' } + target_lang: { $ref: '#/components/schemas/LangCode' } + genre: { type: string, description: Genre as declared by the user. } + chapter_count: { type: integer, minimum: 0 } + character_count: + type: integer + minimum: 0 + description: Size in characters. Not an engine field; the platform knows it from intake. + added_at: { type: string, format: date-time } + status: { $ref: '#/components/schemas/BookStatus' } + progress: { $ref: '#/components/schemas/Progress' } + note_count: { type: integer, minimum: 0 } + + Library: + type: object + required: [revision, books] + properties: + revision: { $ref: '#/components/schemas/Revision' } + books: + type: array + items: { $ref: '#/components/schemas/Book' } + + Run: + type: object + description: | + A run over a book. `status` reuses the book vocabulary, but the book-level values + (`uploading`, `parsing`, `not_started`, `rejected`) never appear on a run. + required: [id, revision, status, verify_bank, started_at] + properties: + id: { $ref: '#/components/schemas/Id' } + revision: { $ref: '#/components/schemas/Revision' } + status: { $ref: '#/components/schemas/BookStatus' } + verify_bank: + type: boolean + description: The run was requested with a stop for bank signing. + started_at: { type: string, format: date-time } + finished_at: + type: [string, 'null'] + format: date-time + + BookDetail: + type: object + required: [revision, book] + properties: + revision: { $ref: '#/components/schemas/Revision' } + book: { $ref: '#/components/schemas/Book' } + run: + oneOf: + - $ref: '#/components/schemas/Run' + - type: 'null' + description: Current or last run; `null` if the book was never run. + + BookIntake: + type: object + description: Add-a-book form. + required: [file, source_lang, target_lang] + properties: + file: + type: string + format: binary + description: Book file. + source_lang: { $ref: '#/components/schemas/LangCode' } + target_lang: { $ref: '#/components/schemas/LangCode' } + genre: { type: string } + + Chapter: + type: object + required: [id, number, units_total, units_done, note_count] + properties: + id: { $ref: '#/components/schemas/Id' } + number: + type: integer + minimum: 1 + description: | + Displayed ordinal. **Not a key:** numbering is dense — chapters that yield no text do + not consume a number — so editing the source shifts every later chapter. + heading: + type: string + description: | + Chapter title as a field is a frontend PROPOSAL: today the engine glues the rendered + title into the text of the first unit and leaves the source column without it. + Unresolved — companion §4 (K-2). + units_total: { type: integer, minimum: 0 } + units_done: + type: integer + minimum: 0 + description: | + Units finished in this chapter. Whether "finished" needs the same draft/edit split as + `Progress` — without it a chapter reads zero for the whole draft wave — is open, + companion §4 (K-10). + note_count: { type: integer, minimum: 0 } + + ChapterList: + type: object + required: [revision, chapters] + properties: + revision: { $ref: '#/components/schemas/Revision' } + chapters: + type: array + items: { $ref: '#/components/schemas/Chapter' } + + UnitState: + type: string + description: | + State of a pair, derived from the PAIR (chunk verdict plus presence of final text), not + from the verdict alone: a flagged unit legally arrives WITH text. + + - `translated` — text shipped. This includes a flagged unit whose text shipped anyway, + such as a cosmetic sanitizer cleanup; such a unit carries a `note`; + - `withheld` — verdict flagged AND no text; + - `pending` — not translated yet. + + The word "flagged" never goes on the wire: it is pipeline vocabulary. + enum: [translated, withheld, pending] + + Unit: + type: object + description: | + A source/translation pair, one edit unit wide. + + **Freshness.** `target` is updated at stage boundaries and at stops, not continuously — + mid-run there is no read channel at all. The live "something changed" signal arrives as an + event; the text arrives with a read after the boundary. + required: [id, source, state] + properties: + id: { $ref: '#/components/schemas/Id' } + source: + type: string + description: Source text, aligned to the edit unit. + target: + type: string + description: Translated text. Empty for `withheld` and `pending`. + state: { $ref: '#/components/schemas/UnitState' } + note: + oneOf: + - $ref: '#/components/schemas/Note' + - type: 'null' + + UnitList: + type: object + required: [revision, units] + properties: + revision: { $ref: '#/components/schemas/Revision' } + units: + type: array + items: { $ref: '#/components/schemas/Unit' } + + NoteSeverity: + type: string + description: | + Severity step. Two steps are a frontend PROPOSAL projected from the engine's operator + severity ranks, and that axis need not match the product one. How many steps there are and + where the boundary lies is an open product question — companion §4 (K-6). + enum: [attention, glance] + + Note: + type: object + description: | + A note in product terms. Neither the engine's flag reason nor its detail text crosses the + boundary; the reason → phrase map belongs to the contract and is not filled in this draft + (companion, appendix A). + required: [severity, message] + properties: + severity: { $ref: '#/components/schemas/NoteSeverity' } + message: + type: string + description: | + Ready human phrase; the wording is the owner's call. A phrase MUST exist for every + reason, including one this contract does not know yet, and it MUST read neutrally + rather than as an error. + chapter_id: { $ref: '#/components/schemas/Id' } + unit_id: { $ref: '#/components/schemas/Id' } + + NoteList: + type: object + required: [revision, notes] + properties: + revision: { $ref: '#/components/schemas/Revision' } + notes: + type: array + items: { $ref: '#/components/schemas/Note' } + + TermKind: + type: string + description: | + Kind of term. Not cosmetic: `name` and `place` ROUTE a term into transliteration, so + signing a term without seeing its kind means signing blind. + enum: [name, place, title, term, nickname] + + TermStatus: + type: string + description: | + Signing status, THREE-VALUED; only `approved` is injected as canon. A boolean `signed` + would merge "proposed by the engine, nobody looked" with "a human started and did not + finish" — on a screen of hundreds of rows that is the main filter of work. + enum: [auto, draft, approved] + + TermOrigin: + type: string + description: Provenance of a bank row — who created it. An axis independent of `status`. + enum: [seed, ruby, mined] + + BankTerm: + type: object + description: | + A memory bank row. + + ⚠ **The name `source` is deliberately unused here.** In the engine that column means + PROVENANCE; this contract calls provenance `origin` and the term's surfaces `src`/`dst`. + Naming the term's text `source` would create a false friend between the two schemas. + required: [id, src, dst, kind, status, origin, since_chapter, until_chapter] + properties: + id: { $ref: '#/components/schemas/Id' } + src: + type: string + description: Source surface of the term. + dst: + type: string + description: Translation; empty for a candidate with no proposed form. + kind: + oneOf: + - $ref: '#/components/schemas/TermKind' + - type: 'null' + description: | + `null` when the engine could not decide the kind: ruby candidates that are neither a + name nor a place legally carry none. A client MUST show such a row as "kind not + decided" and MUST NOT drop it or invent a kind — the row still needs signing. + status: { $ref: '#/components/schemas/TermStatus' } + origin: { $ref: '#/components/schemas/TermOrigin' } + sense: + type: string + description: Polysemy disambiguator; part of the uniqueness key. + since_chapter: + type: integer + minimum: 0 + description: | + Start of the spoiler window; `0` means from the beginning of the book. A term is + unique by `(book, src, sense, since, until)`, so the same `src` legally arrives as + several rows — without the window they look like duplicates and get deleted. + until_chapter: + type: integer + minimum: 0 + description: End of the spoiler window; `0` means open-ended. + + Bank: + type: object + required: [revision, total, signed, terms] + properties: + revision: { $ref: '#/components/schemas/Revision' } + total: { type: integer, minimum: 0 } + signed: + type: integer + minimum: 0 + description: How many rows are in status `approved`. + terms: + type: array + items: { $ref: '#/components/schemas/BankTerm' } + + BankDecision: + type: object + description: | + A decision on one proposed term. `dst` is mandatory and non-empty for `promote`: the + engine refuses a signed term with an empty translation on the next run, because such a + term matches nothing yet reads as an intended rendering. + required: [term_id, action] + if: + properties: + action: { const: promote } + required: [action] + then: + required: [dst] + properties: + dst: { minLength: 1 } + properties: + term_id: { $ref: '#/components/schemas/Id' } + action: + type: string + enum: [promote, decline] + description: '`promote` — accept the term (with a translation in `dst`); `decline` — reject it.' + dst: + type: string + description: Translation. Required and non-empty when `action` is `promote`. + + BankDecisionsRequest: + type: object + required: [decisions] + properties: + decisions: + type: array + minItems: 1 + items: { $ref: '#/components/schemas/BankDecision' } + + BankDecisionsResult: + type: object + required: [revision, pending_decisions, complete] + properties: + revision: { $ref: '#/components/schemas/Revision' } + pending_decisions: + type: integer + minimum: 0 + description: How many proposed terms still have no decision. + complete: + type: boolean + description: | + The set is complete. The stop clears ONLY on a complete set, so the screen must show + "N of M decided" and must not offer to resume while this is `false`. + + RunRequest: + type: object + required: [verify_bank] + properties: + verify_bank: + type: boolean + description: Stop for bank signing before the final pass. + + ExportRequest: + type: object + required: [format] + properties: + format: + type: string + description: Export format; the set of formats is stage S7 work. + + Export: + type: object + required: [id, ready] + properties: + id: { $ref: '#/components/schemas/Id' } + ready: { type: boolean } + url: + type: string + format: uri + description: | + Link to the finished export. Served to the authenticated owner only and never + indexed. + + EventEnvelope: + type: object + description: | + An SSE frame. OpenAPI does not type stream frames, so the mapping is fixed here: + + | `event` | `data` schema | When | + |---|---|---| + | `hello` | `EventHello` | always the first frame | + | `status` | `EventStatus` | product status changed | + | `progress` | `EventProgress` | counters moved | + | `chapter` | `EventChapter` | a chapter's progress changed | + | `note` | `EventNote` | a note appeared | + | `bank` | `EventBank` | the bank changed or a signing stop occurred | + | `ceiling` | `EventCeiling` | the run was halted by a ceiling | + | `resync_required` | `EventResyncRequired` | resuming the stream is impossible | + + The frame `id` carries a monotonic revision. Whether it is the same counter that reads + carry is open — companion §4 (K-4). + required: [event, data] + properties: + event: { type: string } + data: + description: | + Frame payload. Schemas are listed as a union rather than tied by a discriminator: + `event` lives in the SSE frame, not inside `data`, so an OpenAPI discriminator does + not apply. Dispatch by event name, per the table above. + oneOf: + - $ref: '#/components/schemas/EventHello' + - $ref: '#/components/schemas/EventStatus' + - $ref: '#/components/schemas/EventProgress' + - $ref: '#/components/schemas/EventChapter' + - $ref: '#/components/schemas/EventNote' + - $ref: '#/components/schemas/EventBank' + - $ref: '#/components/schemas/EventCeiling' + - $ref: '#/components/schemas/EventResyncRequired' + + EventHello: + type: object + description: Version handshake. A client that does not support the major version closes the stream. + required: [contract, run_id, revision] + properties: + contract: + type: string + description: Contract version, e.g. `0.1.0`. + run_id: { $ref: '#/components/schemas/Id' } + revision: { $ref: '#/components/schemas/Revision' } + + EventStatus: + type: object + required: [status] + properties: + status: { $ref: '#/components/schemas/BookStatus' } + + EventProgress: + type: object + required: [progress] + properties: + progress: { $ref: '#/components/schemas/Progress' } + + EventChapter: + type: object + required: [chapter_id, units_done, note_count] + properties: + chapter_id: { $ref: '#/components/schemas/Id' } + units_done: { type: integer, minimum: 0 } + note_count: { type: integer, minimum: 0 } + + EventNote: + type: object + description: | + A note appeared. Depends on the event emitter — the engine does not emit per-unit notes + mid-run today (companion §3). + required: [note] + properties: + note: { $ref: '#/components/schemas/Note' } + + EventBank: + type: object + required: [total, signed, pending_decisions] + properties: + total: { type: integer, minimum: 0 } + signed: { type: integer, minimum: 0 } + pending_decisions: { type: integer, minimum: 0 } + + EventCeiling: + type: object + description: | + The run hit a ceiling and halted. **Carries no figures** — the fact of the stop, not a + sum: money does not appear in the MVP interface at all. The stop is resumable, so it does + not yield `failed`. Depends on the event emitter (companion §3). + required: [halted] + properties: + halted: { type: boolean } + + EventResyncRequired: + type: object + description: | + The server cannot resume the stream from the presented `Last-Event-ID`. The client MUST + re-read snapshots. Replaying history is forbidden — a one-shot event such as `note` would + be lost silently. + required: [reason] + properties: + reason: + type: string + description: Product-level reason; carries no internals. + + Problem: + type: object + description: | + Error per RFC 9457. ⚠ `detail` NEVER carries engine text: the engine's own detail strings + read like "CJK leak in the ru output: 第一节", which exposes how the pipeline works. Here + `detail` is either empty or already a product phrase. + required: [type, title, status] + properties: + type: { type: string, format: uri } + title: { type: string } + status: { type: integer } + detail: { type: string } + instance: { type: string, format: uri-reference }