From f4c4d458d6391aa30ba20cbbd0bd7fd34e851a9d Mon Sep 17 00:00:00 2001 From: "Claude (backend session)" Date: Sat, 1 Aug 2026 18:43:47 +0300 Subject: [PATCH] Land softsign decl study and both decisions as D39.76: seed stopgap for two names, finding closed outside the core, engine anchor rule gated behind a cross-pair measure --- docs/PROGRESS.md | 12 +- docs/README.md | 6 +- docs/architecture/05-decisions-log.md | 4 +- .../SOFTSIGN_DECL_FINDING1_2026-08-02.md | 99 +++++ eval/softsign-decl/README.md | 136 +++++++ eval/softsign-decl/build.sh | 43 +++ eval/softsign-decl/main.go | 337 ++++++++++++++++++ 7 files changed, 630 insertions(+), 7 deletions(-) create mode 100644 docs/archive/reports/SOFTSIGN_DECL_FINDING1_2026-08-02.md create mode 100644 eval/softsign-decl/README.md create mode 100755 eval/softsign-decl/build.sh create mode 100644 eval/softsign-decl/main.go diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 640fa8c4..1893c2d2 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -1,8 +1,8 @@ # Журнал прогресса -> **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (на 2026-08-02, голова D39.75). **Источник истины по РЕШЕНИЯМ — `architecture/05-decisions-log.md` (D1–D39.75); этот файл — ЖУРНАЛ.** +> **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (на 2026-08-02, голова D39.76). **Источник истины по РЕШЕНИЯМ — `architecture/05-decisions-log.md` (D1–D39.76); этот файл — ЖУРНАЛ.** > - **Сделано (сводно; детали — 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–75): ОБЩНОСТЬ ✅ → КАЧЕСТВО БАНКА ✅ (ядро D39.69 + фикс-пак D39.75).** **Текущее: очередь за ПАКЕТОМ-ЧЕКЕРОВ (промт `BACKEND_CHECKER_PACKAGE_SESSION_PROMPT.md` выдан, теперь можно запускать — фикс-пак заленден) + строка 91 у владельца (Decl мягкознаковых имён: forms[] vs anchor-gated).** ⚠ **DeepSeek-V4-Flash-0731: платные прогоны СТОП до ре-пробы (строка 74; там же 6/6-порог классификатора и слоты Q2/Q7). Перекупок нет (D39.63).** +> - **Курс (D39.59–75): ОБЩНОСТЬ ✅ → КАЧЕСТВО БАНКА ✅ (ядро D39.69 + фикс-пак D39.75).** **Текущее: очередь за ПАКЕТОМ-ЧЕКЕРОВ (промт `BACKEND_CHECKER_PACKAGE_SESSION_PROMPT.md` выдан, теперь можно запускать — фикс-пак заленден) + строка 91: решения приняты, стопгэп A в работе (D39.76).** ⚠ **DeepSeek-V4-Flash-0731: платные прогоны СТОП до ре-пробы (строка 74; там же 6/6-порог классификатора и слоты Q2/Q7). Перекупок нет (D39.63).** > - **Горизонт (D39.62/67):** пакет-чекеров (строка 25) + фикс-лист банка (87) → ре-проба flash (74; + платный порог 6/6 классификатора + пробы 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 · строка 74 перед любым платным прогоном. > - **Стек:** draft deepseek-v4-flash thinking-ON (+банкнота) **⚠0731** → терминолог (та же модель, батчи, экран `target_script`) → editor deepseek-v4-pro БИЛИНГВ ИНТЕРИМ (вендором НЕ тронут; glm-5 резерв) → судья gemini (Ф2, полигон); канал B Mistral+grok; ~$0.85/ранобэ (D30.4) — пере-калибровка после развилки 74. > - **ЕДИНЫЙ БЭКЛОГ — секция «Бэклог» ниже** (одна таблица, единственный трекер; каждая петля обязана иметь диспозицию: решено / отложено-с-записью / отклонено; ведёт оркестратор). @@ -30,7 +30,7 @@ | 13б | `speech-cue.txt` zh НЕ шипить до замера голос-флаггера на холодном прогоне (данные для недоказанного детектора = инерция; вывод обсуждения 31.07): оси пустые → файл не нужен, оси ловят → решение владельца о шипе (байты двигают `LangpackVersion`) | владелец | отложено-до-замера | первый прогон добора идеала (оси голоса, строка 24) → отдельное решение | D39.56, PACK19_BUILD §7.2, чат 31.07 | | 14 | Четыре data-инженерных флага фазы-2 общности, каждый с рекомендованным дизайном (§5 отчёта): ё-фолд → target-данные (`char_fold`; смена сигнатуры core-примитива через вердикт-несущий банк, сдвиг `memory_version`) · segmentation-фолбэк громким (ломает контракт corridor-фолбэка `pipeline.go:839-848` + голден-фикстура на нём; лечение — объявить блок в голден-yaml) · `Fertility` per-script map (калибровка ждёт боевого токенизатора) · `source-encoding.txt` + Go-реестр декодеров | владелец | когда-нибудь (следующий пак общности) | отдельное решение | D39.64, GENERALITY_PHASE2 §5.1–5.4 | | 86 | Мелкие открытые решения одним списком (свип D39.66): echo-семантика дропнутого c-lite-члена (D39.18 — оставить pre-c-lite?) · magnitude-кап тяжёлой CJK-утечки (D38.3 п.4в; сейчас strip+flag) | владелец | когда-нибудь | отдельные решения | D39.66, D39.69(feed_cap закрыт §1) | -| 91 | Decl-приёмка МЯГКОЗНАКОВЫХ имён (finding-1 фикс-пака, D39.75): стеммером НЕВОЗМОЖНА — «ь» неотличим от коллизии «Синь»↔«синий» (доказано контрфактикой, регресс-гвозди стоят); выбор: (а) сид/терминолог перечисляет `decl.forms[]` для -нь/-ль имён (ложится на Decl-эмиссию) · (б) anchor-gated матч; при решении добавить прямой тест-гвоздь на «Фан Юаню» и заменить инертную пару Линь/линию→Линь/линий | владелец → бэкенд | скоро (малый дизайн) | отдельное решение | D39.75, BANK_FIXPACK §а | +| 91 | Finding-1 (мягкознаковый decl): замер ПРИНЯТ, решения ПРИНЯТЫ (D39.76: A1 отвергнут глобально · anchor-gated СЛОМАН — переносит коллизию, транслит-везение · промах ~4–7/25 глав, почти весь «Винный червь») — **СТОПГЭП A В РАБОТЕ**: сид-формы «Винный червь»+«Фан Юань» (пишет сессия, подпись владельца) + гвоздь Линь/линий + прямой тест «Фан Юаню»; закрытие строки по лендингу стопгэпа; C = загейченный триггер (реопен = вторая пара, кросс-парный замер-гейт) | бэкенд → владелец (подпись сида) | в работе (мини-касание) | стопгэп → лендинг → закрытие | D39.75, D39.76 | | **— ХВОСТЫ ХОЛОДНОГО ПРОГОНА (исполнен достаточно, D39.58; Р7 владельца) —** | | | | | | | 16 | Полная цена холодного старта: edit-волна не гонялась (Р7); измерено всё ДО редактуры (драфт+банкнота $0.0247/волна · терминолог $0.006–0.013/проход) | бэкенд | когда-нибудь (первый прогон добора идеала) | добор идеала | D39.58 | | 18 | Эмиссия головы 蛊: сид-половина = чек-лист (определяющий терм книги сидится `allow_short`, D39.50); эмиссия-КАНАЛ демотирован до ИЗМЕР-ГЕЙТА (Q7): полигон-замер «меняет ли эмиссия головы рендер композитов» — строить ТОЛЬКО по эффекту и ПОСЛЕ #10 (однознаковый пост-чек чинится чекер-паком); перед стройкой резолвить K-гейт и KWIC-грязь одиночной головы | полигон → бэкенд | скоро (санкция Q7 D39.70 — после #10, очередью с ре-пробой) | полигон-замер → решение | D39.58, D39.60, D39.69 | @@ -104,6 +104,10 @@ | 69 | Gemini API «под-18» обязательство на конечный продукт (независимо от лейблов) | владелец | когда-нибудь | Ф3 / отдельное решение | D39.32, D39.34(7) | | 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) | бэкенд | когда-нибудь | Ф3 | D39.34(7) | +## Оркестратор №9 + бэкенд + владелец — SOFTSIGN-DECL: ЗАМЕР ПРИНЯТ, РЕШЕНИЯ ПРИНЯТЫ (D39.76), 02.08 + +Отчёт: [`archive/reports/SOFTSIGN_DECL_FINDING1_2026-08-02.md`](archive/reports/SOFTSIGN_DECL_FINDING1_2026-08-02.md) (ревью-шапка с решениями; харнесс `eval/softsign-decl/` заленден). «Замер до стройки» отработал: наивное «ь» похоронено глобально, anchor-gated сломан адверсариалом самой сессии (перенос коллизии; чистота книги — транслит-везение), промах мал и локализован. Решено: стопгэп A (сид-формы «Винный червь»+«Фан Юань», формы пишет сессия, подпись владельца) + гвоздь Линь/линий + прямой тест «Фан Юаню»; finding-1 закрывается на A; движковый C — загейченный триггер до второй пары. В ядро — ничего. + ## Оркестратор №9 + бэкенд — ФИКС-ПАК БАНКА ПРИНЯТ И ЗАЛЕНДЕН (D39.75), 02.08 Отчёт: [`archive/reports/BANK_FIXPACK_2026-08-02.md`](archive/reports/BANK_FIXPACK_2026-08-02.md) (ревью-шапка). 9/9 пунктов отработаны: 6 сделано (head-finality в данных · правдивый Batch-коммент+WARN · classify fail-loud+тесты · мутационные neuter-тесты · фикстура 4-серий · TokenizeLetters-консолидация, побочно закрывшая combmark-щель), 2 «не делать» с контрактом (voice×стеммер; Q5 уже в промптах с D39.45), 1 ОТКАЧЕН правильно — «ь» ловил бы коллизию Синь/синий (мандат самопроверки сработал: адверсариальный ревьюер сессии поймал до лендинга). Верификация: батарея зелёная, голден маскированно ПУСТ, 14/15 CONFIRMED. Строка 87 закрыта; finding-1 → строка 91 (владелец); плотность-граница → дописка 79. Очередь: ПАКЕТ-ЧЕКЕРОВ можно запускать. @@ -180,6 +184,8 @@ Q1 = П1 цель-шов санкционирован («главное бэке ## Бэкенд +**finding-1 (строка 91, Decl мягкознаковых имён) — ЗАМЕР ПРОВЕДЁН (02.08, бэкенд-сессия по указанию владельца в eval-конвенции, $0); РЕШЕНИЕ — владелец+оркестратор.** Отчёт: [archive/reports/SOFTSIGN_DECL_FINDING1_2026-08-02.md](archive/reports/SOFTSIGN_DECL_FINDING1_2026-08-02.md); харнесс `eval/softsign-decl/` (пин-модуль, боевой стеммер), сырьё+2 адверсариал-прогона durable в `~/books/gu-zhenren/softsign-decl/`. ИТОГ: разрыв реален но МАЛ (~4–7 ложных промахов/25 глав на реальном размере чанка, почти весь — ОДИН термин «Винный червь»; «Фан Юань»→0 per-chunk; occurrence-level 259 — код-верифицирован). Вариант **(б) anchor-gated** НЕ закрывает коллизию «Синь/Линь» — ПЕРЕНОСИТ на форму `[титул]+[ь-фамилия]` + не защищает числовой класс («пятого/шестого ранга» ×23); чистота на gu-zhenren = транслит-везение, доказательство **пара-зависимо**, safety НЕ измерена (M4=3 пары/1 книга). Рекомендация: **(а) сид-перечень 1–2 ВИДИМЫХ мягких канон-имён** — данные `decl.forms`, матч generic `containsWholeWord`, **вне ядра Go**, ноль коллизии (честно: ручной труд — майнер морфологию НЕ майнит, declForms руко-писаны); движок (б) — только на КРОСС-ПАРНОМ гейт-замере (≥2 пары), не на одном. Строку 91 и CURRENT-STATE НЕ трогал. Открытые решения — в §«Что решить» отчёта. + **Фикс-пак банка (строка 87 + Q5) исполнен (02.08, бэкенд-сессия по `BACKEND_BANK_FIXPACK_SESSION_PROMPT`, D39.71, $0 — только фейк-провайдер в тестах, НЕ закоммичено).** Отчёт: [archive/reports/BANK_FIXPACK_2026-08-02.md](archive/reports/BANK_FIXPACK_2026-08-02.md). СДЕЛАНО: **(б)** head_final → данные (`script-series.txt`+`SeriesHeadFinal`, zh/ja/ko байт-идентичны, тест «плотный не-head-final заводится данными») · **(в)** лживый коммент `Batch` переписан (ChunkIdx фолдится в request-hash) + `BatchRunes`-WARN на оверсайз-серию (не резать — убьёт со-батч §1) · **(г)** classify_types при выкл.гейте = fail-loud + 5 config-тестов · **(д)** мутационный neuter-фолд (mined-scope) · **(ж)** дистилл-фикстура `BANK-FULL` пинит ровно 4 серии (转/等/等资质/阶) · **(и)** `lang.TokenizeWords`→`text.TokenizeLetters` (общий шов, чинит combmark-щель). НЕ ДЕЛАТЬ с обоснованием: **(е)** стеммер НЕ в voice (accept-bias не единонаправлен; контракт в код) · **(з)** Q5-реалии УЖЕ в `terminologist.md`/`classifier.md` (пинг: `PromptSHA256` не двинулся). **(а) ОТКАЧЕНО — предложенный «ь» ФЛАВЕД: адверсариальное ревью (author≠reviewer, подтверждено white-box) поймало коллизию «Синь/Линь»→«син/лин»=«синий/линия» → ложный `SpoilerLeaks` (шум); «юань»→«юан» неотличим по форме — min-stem не разделяет.** Откат + регресс-гвоздь; finding 1 (палладий) открыт в safe-направлении, нужен сид-decl-перечень или anchor-gated матч — **РЕШЕНИЕ владельца (пинг §а)**. Приёмка: `build`/`vet`/`test -race ./...` зелёные · майнер-парити EXACT · голден пере-захвачен, маскированный дифф ПУСТ (двинулся только `embedded_version`; `memory_version`/`prompt_sha256`/`content_hash`/вердикты — на месте) · греп-общность чист. **СТОП — приёмка оркестратора.** **Качество банка — этап 1 (дизайн) исполнен (01.08, бэкенд-сессия по `BACKEND_BANK_QUALITY_SESSION_PROMPT`, $0, read-only, НЕ закоммичено).** Отчёт: [archive/reports/BANK_QUALITY_DESIGN_2026-08-01.md](archive/reports/BANK_QUALITY_DESIGN_2026-08-01.md). 6 пунктов свипа сведены в **3 переиспользуемых примитива** (A head-aware кластер-по-морфеме: §1 серия-батчинг + §6 голова 蛊 · B ре-порядок `terminology.Batch`: §1 + feed_cap · C стеммер целевого письма: §3 Decl-шов + omission-бэкстоп чекеров) + дата-правки (тип — ОТДЕЛЬНЫМ классификатором первичным; род neuter enum+seed-lint, ось `RenderFormatVersion`). **Самопроверка — 3 рубежа (грунтинг + анкор-верификация всех 8 tech-debt-якорей CONFIRMED + 5-агентная адверсариальная дизайн-критика): §1 серия-дефиниция «равная длина/1 позиция» и §2 инлайн-тип были BROKEN — переработаны (§1 → head-aware, пере-верифицировано на `BANK-FULL.tsv`: наивное = 19 кластеров вкл. блоб с §2-harm+протагонист, head-aware = 4 годных серии; §2 → классификатор первичным, DO-роль ДО RenderBatch; $0-экран честно пере-скоупнут — провабельно пропускал юаньши/Юаньхай); §5/§6/§7-эмбеддинг WEAKENED вправлены.** §4 (36б второе мнение) и §7-эмбеддинг ЗАКРЫТЫ по нужности; §5 (резюме-слой 80) → пилот-Ф2.5-гейт (проза-суммарайзер закрыт; «дата-состояние свободно от D1» опровергнуто — source-anchored reveal без верификатора); §6-эмиссия головы → полигон-замер + после #10 (seed-половина строится). **First-occurrence-тест ИСПОЛНЕН** ($0, `eval/bank_autonomy/m_firstocc.py`): гипотеза первенства ОПРОВЕРГНУТА (финал==первое 7 == финал==частота 7, большинство — ни то ни другое) ⇒ роль = контекстный ре-решатель, усиливает §1 (со-батч — единственный рычаг). **7 вопросов владельцу** (§9: канон-пин между прогонами · замер 36б · диспозиция резюме · hard-инъективность · вкус авто-транслита реалий юаньци/цзин-ци-шэнь · короткая форма 27 · замер эмиссии головы). **СТОП — ратификация оркестратора/владельца; этап 2 (стройка) — по релею после лендинга пакета-чекеров.** → **РАТИФИЦИРОВАНО ВЛАДЕЛЬЦЕМ, ЭТАП 2 (СТРОЙКА) ИСПОЛНЕН (01.08, та же сессия, $0 — только фейк-провайдер в тестах, НЕ закоммичено, resnapshot НЕ запускался).** diff --git a/docs/README.md b/docs/README.md index 440db200..2a568bfd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,7 +11,7 @@ ## Структура -- `architecture/` — синтез. **Источник истины по решениям — [`05-decisions-log.md`](architecture/05-decisions-log.md) (D1–D39.75); при конфликте с любым доком он выше.** +- `architecture/` — синтез. **Источник истины по решениям — [`05-decisions-log.md`](architecture/05-decisions-log.md) (D1–D39.76); при конфликте с любым доком он выше.** - `01-decisions.md` — принципы Р1–Р10; `02-mvp-plan.md` — фазы и приёмка (v3, 09.07); `03-implementation-notes.md` — контракты Фазы 0; `04-unhappy-paths.md` — ~70 режимов отказа → механизм; `06-memory-risk-registry.md` — реестр рисков банка памяти; **[`09-target-architecture.md`](architecture/09-target-architecture.md) — целевая 7-слойная архитектура (D39; статус стройки — шапка-таблица; инвариант общности §0.1)** · [`10-prompt-architecture.md`](architecture/10-prompt-architecture.md) — консолидированная промпт-заметка (концерн 4) · [`12-go-style-notes.md`](architecture/12-go-style-notes.md) — норматив общности §0 + Go-ответы · [`13-tech-debt-anchors.md`](architecture/13-tech-debt-anchors.md) — якоря техдолга (справочник к бэклогу, НЕ трекер). **Исполненные (архив 25.07, санкция владельца, → `archive/architecture/`):** [`07-strategic-review.md`](archive/architecture/07-strategic-review.md) (стратаудит 09.07 — курс исполнен) · [`08-sync-audit-ledger.md`](archive/architecture/08-sync-audit-ledger.md) (ледджер синк-аудита — отработан) · [`11-implementation-plan.md`](archive/architecture/11-implementation-plan.md) (план пака-11 — исполнен целиком); `components.puml`/`pipeline.puml` — диаграммы (перерисованы 25.07 под пост-пак-16 реальность: волновой исполнитель, пакетный сплит пака-15, repair-петля за `enabled:false`, generic content-labels D39.25; владелец смотрит PlantUML-расширением VS Code; вручную НЕ рендерить). - `experiments/` — эмпирика «Полигона»: `00-provider-quirks` (читать перед любым вызовом провайдера), `01-token-calibration`, `02-refusal-benchmark`, `03-local-stand`, `04-editor-quality`, `06-local-extraction`, `07-coverage-precision`, `08-cost-model-v2` (актуальная денежная модель), `09-pilot-protocol` (пилот Ф2.5 + поправки D13), `10-explicit-benchmark` (18+ violence-рука канала B), `11-erotica-benchmark` (erotica по трём парам/регистрам — закрытие D14.4, D22), `12-quality-diagnosis`/`13-translator-bakeoff`/`14-quality-empirics`/`14b-meaning-battery` (дуга качества «мерить→строить», закрыты → D30/D32/D37/D38), `15-segmentation-empirics` (exp15: когезия под floor-шумом, фертильность — закрыт → D39.7/D39.8), `16-bank-mining` (exp16: WHICH/WHAT банк-майнинга — закрыт → D39.10). - `research/` — фактура исследований 04–05.07: `01–10` базовые, `11-gap-*` добор критиком, `12-*` режимы отказа/отзывы/таксономии (+ два внешних материала с провенанс-шапками), `13` валидация памяти, `14` адаптивная память, `15` голос и состояние (принят, D21), **`16` ридер-IDE (принят с ревью-шапкой, D29)**, **`17` внешняя критика GPT-5.6 (принят с ревью-шапкой, D25)** — у 16/17 читать шапку прежде тела. **`18` рычаги качества (два отчёта, D36)** · **`19` нарезка+когезия+контракт t/e (D39.1)** · **`20` банк-майнинг W1.5 (D39.6)** · **`21` обзор LLM-транспорта чужих харнессов (23.07: наш транспорт опережает/вровень со всеми 11)** · **`22` доменные харнессы перевода (24.07: калибрующий — ядро подтверждено, впереди COGS/18+/общность/измеренность, позади Q4-выпуск и gate+repair-петля; сиквел `02`/`05`)** — у всех ревью-шапки. ⚠ Часть под superseded-баннерами (01/02/03/04/05/09 и gap-1/2/5) — **читай баннер прежде содержимого**. @@ -19,9 +19,9 @@ - **Активные хендофф-промты (01.08; всё остальное — в `archive/prompts/` с баннерами-исходами):** [`ORCHESTRATOR_SESSION_PROMPT.md`](ORCHESTRATOR_SESSION_PROMPT.md) (роль/нормы; состояние НЕ дублирует) · [`BACKEND_CHECKER_PACKAGE_SESSION_PROMPT.md`](BACKEND_CHECKER_PACKAGE_SESSION_PROMPT.md) (**ТЕКУЩИЙ бэкендный**: пакет-чекеров против labels + чистка ведра D — фикс-пак заленден, МОЖНО ЗАПУСКАТЬ) · [`POLYGON_PACKAGE4_SESSION_PROMPT.md`](POLYGON_PACKAGE4_SESSION_PROMPT.md) (residual-трекер пилота/18+/echo, отложен; P4-хвост из него закрыт D39.61). Закрытые 30.07–01.08 (промты в `archive/prompts/`, отчёты с ревью-шапками в `archive/reports/`): пак-19 «голос/состояние» (D39.54–56) · холодный мини-прогон (D39.58) · ToS-речек (D39.57) · ресёрч общности фаза 1 (D39.60) · полигон Р6+P4 (D39.61) · **фаза 2 общности (D39.64)** · **полигон «сырьё арки банка»+пробы (D39.65)** · **качество банка, оба этапа (D39.69)** · **фикс-пак банка (D39.75)**. - `archive/` — `prompts/`: закрытые сессионные промты (только история, инструкции оттуда не исполнять) · `reports/`: отчёты паков/ресёрч-сессий с ревью-шапками (живая фактура приёмок — на них ссылаются D-лог и активные промты) · `architecture/`: исполненные арх-доки (07 стратревью · 08 синк-ледджер · 11 план пака-11) · четыре слайса хроники `PROGRESS-2026-07-{04-10,10-13,13-25,25-31}.md`. -## Статус (2026-08-01, голова D39.75 — эра «общность → качество банка») +## Статус (2026-08-01, голова D39.76 — эра «общность → качество банка») -Полная карта состояния НЕ здесь: фазы/курс/очередь/горизонт — в шапке [`PROGRESS.md`](PROGRESS.md) (CURRENT-STATE + единый бэклог), решения — в [`architecture/05-decisions-log.md`](architecture/05-decisions-log.md) (карта актуальности; свежая голова — с хвоста файла). Коротко: Ф0/Ф1 ✅ · арх-ресет D39 (паки 11–16) ✅ · паки 17–20 ✅ · мини-прогон и холодный прогон приняты (D39.37/58) · карта языковой привязки движка снята (D39.60: книго-ось чиста; ja→ru безопасна без правки Go, en→ru — нет) · Р1–Р4 закрыты (D39.63) · пак-21 растворён (D39.62) · **фаза 2 общности принята (D39.64: цель/скрипт-швы по данным, эмбед-хеш, манифест-по-каналам; голден вердикт-нейтрален)**. Полигон-ветка закрыта D39.65 (строки 21/22, фиксы 36а/36в проверены живьём). Банк готов целиком (D39.69 ядро + D39.75 фикс-пак; вопросы §9 закрыты D39.70). Текущее: пакет-чекеров — можно запускать; строка 91 у владельца. Свип полноты исполнен и пост-сверен кодом (D39.66: 951 обязательство, потери возвращены; якоря — architecture/13). ⚠ DeepSeek-0731 сменил веса под слагом — платные прогоны СТОП до ре-пробы (бэклог-строка 74). Дальше: стройка банка ∥ пакет-чекеров → ре-проба flash → ДОБОР ИДЕАЛА (мини-прогон и свип гипотез влиты, D39.67) → вторая пара (ja→ru) → МАСШТАБ → пилот Ф2.5 → Ф3 ридер-IDE. +Полная карта состояния НЕ здесь: фазы/курс/очередь/горизонт — в шапке [`PROGRESS.md`](PROGRESS.md) (CURRENT-STATE + единый бэклог), решения — в [`architecture/05-decisions-log.md`](architecture/05-decisions-log.md) (карта актуальности; свежая голова — с хвоста файла). Коротко: Ф0/Ф1 ✅ · арх-ресет D39 (паки 11–16) ✅ · паки 17–20 ✅ · мини-прогон и холодный прогон приняты (D39.37/58) · карта языковой привязки движка снята (D39.60: книго-ось чиста; ja→ru безопасна без правки Go, en→ru — нет) · Р1–Р4 закрыты (D39.63) · пак-21 растворён (D39.62) · **фаза 2 общности принята (D39.64: цель/скрипт-швы по данным, эмбед-хеш, манифест-по-каналам; голден вердикт-нейтрален)**. Полигон-ветка закрыта D39.65 (строки 21/22, фиксы 36а/36в проверены живьём). Банк готов целиком (D39.69 ядро + D39.75 фикс-пак; вопросы §9 закрыты D39.70). Текущее: пакет-чекеров — можно запускать; строка 91 — стопгэп A в работе (D39.76). Свип полноты исполнен и пост-сверен кодом (D39.66: 951 обязательство, потери возвращены; якоря — architecture/13). ⚠ DeepSeek-0731 сменил веса под слагом — платные прогоны СТОП до ре-пробы (бэклог-строка 74). Дальше: стройка банка ∥ пакет-чекеров → ре-проба flash → ДОБОР ИДЕАЛА (мини-прогон и свип гипотез влиты, D39.67) → вторая пара (ja→ru) → МАСШТАБ → пилот Ф2.5 → Ф3 ридер-IDE. ## Доступные ключи от моделей DEEPSEEK_API_KEY, ZAI_API_KEY, KIMI_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, XAI_API_KEY, MISTRAL_API_KEY diff --git a/docs/architecture/05-decisions-log.md b/docs/architecture/05-decisions-log.md index d0415435..8b044652 100644 --- a/docs/architecture/05-decisions-log.md +++ b/docs/architecture/05-decisions-log.md @@ -1,4 +1,4 @@ -# Журнал решений оркестратора — контракт D1–D39.75 (развязки 04.07 · пакеты 09–10.07 · приёмка/качество-первым/пивот/эмпирика 11–12.07 · арх-ресет+стройка пере-прогонного стека 13–19.07) +# Журнал решений оркестратора — контракт D1–D39.76 (развязки 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–63 живут хронологически в теле файла, **свежая голова — С ХВОСТА** (новые ноты аппендятся вниз). Сводка текущей головы и очередь — CURRENT-STATE в `../PROGRESS.md`. @@ -1227,3 +1227,5 @@ API-529-долг закрыт: 8-осевой refute-by-default воркфлоу ## D39.74 — АУДИТ РЕСЁРЧЕЙ СОСЕДНИХ ХАРНЕССОВ (research/21 транспорт · research/22 домен; вопрос владельца 02.08 «есть ли утерянное из /home/ubuntu/projects/tmp?»): **утерянного НЕТ** — из 15 клонов оба ресёрча покрыли корпус, взятое ПОСТРОЕНО (research/21: 9 транспорт-пунктов паками 12/13 — quota-429/Retry-After/keepalive/джиттер/xAI-гард/GLM-finish/retryable-пин/billed-decode-кап/models-валидация + CostSource и луп-гард; вердикт «наш транспорт опережает/вровень со всеми 11, не трогаем» ратифицирован; research/22: разрыв Q6 gating+repair закрыт паком-16 по прецеденту AiNiee ProofreadTask, Problem-классы №5 → строка 25, планы → строка 71), отклонённое отклонено ЗАПИСЬЮ, кандидаты Q7-квирков ждут вендор-сверки по правилу 10.07 (не абсорбированы — так и задумано). Два хвоста, уроненные МОИМ триажем внутри пучковых находок (класс D39.73), добиты: **(1) Mistral-strip гейт message-builder** (research/21 §1.1: extra_forbidden на reasoning-поля; ЛАТЕНТЕН — наш wire single-shot без reasoning-полей; станет несущим при multi-turn/continuation или прод-пути mistral в канале B) → дописка в строку 59 (транспорт-гигиена); **(2) halve-chunk-on-retry** (research/22 §2 №7) → ОТКЛОНЁН ЯВНО: волновая архитектура с чекпойнтами + флор max_tokens + ре-ген-ручка 77 делают полу-чанк малоценным; реопен — при живых max_tokens-упорах (строка 45, дописка) (02.08, оркестратор №9). ✅ ## D39.75 — ФИКС-ПАК БАНКА ПРИНЯТ И ЗАЛЕНДЕН (все 9 пунктов строки 87 отработаны; приёмка исполнением + 3-агентное адверсариальное ревью: 14/15 CONFIRMED · 1 PARTIAL · 0 REFUTED; голден независимо: маскированный дифф ПУСТ, двинулся только `embedded_version`): **СДЕЛАНО** — (б) head-finality В ДАННЫХ (`script-series.txt` + `SeriesHeadFinal`; zh/ja/ko функционально идентичны — пин-тест; синтетический head-initial скрипт заводится данными) · (в) лживый комментарий Batch переписан на правду (ChunkIdx фолдится в RequestHash) + `BatchRunes`-WARN на оверсайз-серию (НЕ режет — резка убила бы со-батч) · (г) `classify_types` при выключенном гейте = fail-loud + 5 config-тестов · (д) мутационные тесты neuter-фолда (версия двигается; mined-scope: BASE байт-идентичен — draft-волна не перепокупает) · (ж) дистилл-фикстура BANK-FULL (150 поверхностей побайтно = боевому TSV; тест требует ТОЧНОГО членства 4 серий и id=0 для 方源/元石/元海) · (и) `TokenizeWords`→`text.TokenizeLetters` — консолидация чистая (слои однонаправленны) И ПОБОЧНО ЗАКРЫЛА РЕАЛЬНУЮ ЩЕЛЬ: старый токенизатор рвал слово на mid-word U+0301 («сло́во»→[сло во] — воспроизведено на копии старого кода), decl-пост-чек промахивался на ударениях. **НЕ ДЕЛАТЬ с контрактом в код** — (е) стеммер НЕ заводится в voice (направления несимметричны: SelfRef-стемминг безопасен, NG-стемминг = шум; контракт-блок voice.go:368-378, фактура сверена) · (з) Q5-инструкция реалий УЖЕ в `terminologist.md`/`classifier.md` — причём с D39.45 (мисатрибуция отчёта «D39.58» только усиливает клейм), PromptSHA256 не двигался. **(а) ОТКАЧЕН — МАНДАТ САМОПРОВЕРКИ СРАБОТАЛ ПО НАЗНАЧЕНИЮ:** «ь» в `decl_suffix` закрыл бы «Фан Юаню», но неотличим по форме от коллизии мягкознаковых имён с лексикой («Синь»↔«синий/синим» — контрфактика воспроизведена: SameStem=true с «ь»); адверсариальный ревьюер сессии поймал ДО лендинга, откат + два регресс-гвоздя (SameStem + dstFormPresent; пара Линь/линию инертна — заменить на Линь/линий при касании) + NB-комментарий в данных. **Finding-1 ОСТАЁТСЯ ОТКРЫТЫМ → строка 91 (выбор владельца):** приёмка склонений мягкознаковых имён стеммером НЕВОЗМОЖНА — (а) сид/терминолог перечисляет `decl.forms[]` для -нь/-ль имён (естественно ложится на Decl-эмиссию) или (б) anchor-gated матч; + прямой тест-гвоздь на дизайн-кейс добавить при решении. Граница плотности (`enabled` серий через Go-константу `cjkScriptNames`) — пре-существующая, дописана в строку 79. Строка 87 ЗАКРЫТА (02.08, оркестратор №9). ✅ + +## D39.76 — SOFTSIGN-DECL ЗАМЕР ПРИНЯТ, ОБА ВОПРОСА РЕШЕНЫ (строка 91; замер исполнен бэкенд-сессией по слову владельца «замеры → решать вместе с оркестратором»; eval-конвенция `eval/softsign-decl/`, пин-харнесс на боевом стеммере, $0, два адверсариал-прогона author≠reviewer): **факты** — (1) разрыв реален, но МАЛ и локализован: пер-чанковый честный промах ~4–7 на 25 глав, почти весь = «Винный червь»; «Фан Юань» = 0 (номинатив всегда рядом); раннее «~26» снято вторым путём (артефакт мелкого блока, свип 800→12000); (2) наивное «ь» (A1) отвергнуто ОКОНЧАТЕЛЬНО — вред глобальный, книго-независимый (SameStem → Postcheck И SpoilerLeaks везде); (3) **anchor-gated (C) СЛОМАН адверсариал-прогоном самой сессии**: коллизию ПЕРЕНОСИТ на класс «[титул]+[ь-фамилия]», числовой класс не защищает («пятого ранга»×13 — якорь общий), ноль коллизий этой книги = ТРАНСЛИТ-ВЕЗЕНИЕ ⇒ в ядро на одно-парном свидетельстве нельзя; (4) сид-перечень честно назван ручным трудом, но для 1–2 имён = ~10 строк YAML ВНЕ ядра (generic containsWholeWord, ноль строк Go). **РЕШЕНИЯ (оркестратор + владелец): (Q1) стопгэп A — ДА**: сид-формы «Винный червь» И «Фан Юань»; формы пишет бэкенд-сессия, подпись владельца обычным сид-циклом; тем же касанием гвоздь Линь/линию→Линь/линий + прямой позитивный тест «Фан Юаню» через forms[] (закрывает PARTIAL D39.75); memory_version → штатный repin, перекупок нет; **(Q2) finding-1 ЗАКРЫТ на A; C снят в ЗАГЕЙЧЕННЫЙ ТРИГГЕР** — реопен только при второй паре живьём, где кросс-парный полигон-замер (перенос + числовой класс, ≥2 пары) — ГЕЙТ любой правки ядра. Общность: путь A = ноль строк Go, grep ядра на пара/книго-литералы пуст. Строка 91 → «стопгэп в работе, закрытие по его лендингу» (02.08, оркестратор №9). ✅ diff --git a/docs/archive/reports/SOFTSIGN_DECL_FINDING1_2026-08-02.md b/docs/archive/reports/SOFTSIGN_DECL_FINDING1_2026-08-02.md new file mode 100644 index 00000000..d18b71ec --- /dev/null +++ b/docs/archive/reports/SOFTSIGN_DECL_FINDING1_2026-08-02.md @@ -0,0 +1,99 @@ +# SOFTSIGN-DECL (finding-1) — отчёт оркестратору для совместного решения с владельцем + +> **✅ ПРИНЯТ И ЗАЛЕНДЕН; ОБА ВОПРОСА РЕШЕНЫ (оркестратор №9 + владелец, 02.08.2026, D39.76).** Спот-чеки приёмки: пин харнесса через rev-parse (build.sh:17) · счёт dst сверен (150 строк / 147 уник.) · durable-сырьё и оба адверсариал-прогона на месте · контрфактика «Синь/синим» бьётся с независимой проверкой фикс-пака. **Решения: (Q1) СТОПГЭП A — ДА**: сид-формы «Винный червь» И «Фан Юань» (цена нулевая, спойлер-точность протагониста важна); формы пишет бэкенд-сессия (детерминированная морфология, не вкус), подпись владельца обычным сид-циклом; сдвиг `memory_version` → штатный repin, перекупок нет; тем же касанием — гвоздь Линь/линию→Линь/линий и ПРЯМОЙ позитивный тест «Фан Юаню» через forms[] (закрывает PARTIAL ревью D39.75). **(Q2) finding-1 ЗАКРЫВАЕТСЯ на A; C (движковый anchor) — в ЗАГЕЙЧЕННЫЙ ТРИГГЕР**: реопен только при второй паре живьём, кросс-парный полигон-замер (перенос «[титул]+[ь-фамилия]» + числовой класс, ≥2 пары) = гейт любой правки ядра. Основание: A1 глобально вреден; anchor-gated сломан адверсариал-прогоном (переносит коллизию; чистота книги = транслит-везение); измеренный промах мал и локализован (~4–7 пер-чанк/25 глав, почти весь «Винный червь», «Фан Юань»=0). + +**Что:** исследование finding-1 (мягкознаковый декл-разрыв банк-пост-чека), отпочковавшегося от +BANK_FIXPACK §а при лендинге D39.75 (бэклог-строка 91). **Кто/где:** бэкенд-сессия; по прямому указанию +владельца замер проведён в eval-конвенции (`eval/softsign-decl/`, пин-харнесс, $0). **Дисциплина:** 2-й +путь счёта · вскрытие единиц глазами · author≠reviewer ДВА прогона (дизайн + код/рекомендация). Владелец: +«провести замеры, решать вместе с оркестратором» — отсюда этот отчёт. **Строить/править — по слову +владельца; ядро Go этой сессией НЕ трогалось.** + +## Вопрос +`declinedFormPresent` не принимает склонённую мягкознаковую форму («Фан Юаню»~«Фан Юань»): стеммер §3 «ь» +не стрипает. Наивное «добавить «ь» в `decl_suffix`» отвергнуто в fix-pack (глобальная коллизия +«Синь/Линь»→«синий/линия»). Чем закрывать: (A) сид-перечень декл-форм · (C) движковый anchor-gated +матч · (B) ничего. И — не ляжет ли решение книго/языко-зависимо в ЯДРО (вопрос владельца). + +## Метод +Харнесс `eval/softsign-decl/main.go` на БОЕВОМ стеммере (`lang.NewTargetStemmer`), `build.sh` пинит +бэкенд коммитом (78b7b34) через `git archive`, `replace`-модуль вне репо — прецедент pkg7/minerharness. +Корпус: банк `~/books/gu-zhenren/coldrun-a/BANK-FULL.tsv` (147 уник. dst) × боевой 25-глав-выход +`acceptance/guzhenren-25ch-ru.txt` (37 480 слов). Сырьё+2 адверсариал-прогона — durable +`~/books/gu-zhenren/softsign-decl/`. Четыре правила матчинга: A0 (боевое, «ь» не стрипается) · A1 +(наивное глобальное «ь») · A2 (anchor=точный) · A3 (anchor=норм-стем). + +## Находки (с исправлениями самопроверки — обе ошибки мои) + +1. **Разрыв реален, но МАЛ.** Occurrence-level 259 окон (184 склонённых «Фан Юань» = Юаня 152/Юаню 19/Юанем 7/Юане 6 + 67 «Винного червя» + хвост). + Но пост-чек срабатывает ПО-ЧАНКОВО: на реальном размере чанка (~1–2k токенов) честный промах = + **~4–7 на 25 глав, почти весь — ОДИН термин «Винный червь»**; «Фан Юань» → **0 per-chunk** (номинатив + всегда рядом). ⚠ Моё раннее «~26» было артефактом мелкого блока (1.5k симв) — снято 2-м путём (свип + 800→12000 симв: 57→26→13→7→4). Число — прокси (не боевой чанкер). + +2. **Код харнесса аудирован TRUSTWORTHY** (author≠reviewer, статик): 6/6 NO-BUG — `stemAug` доказуемо + faithful (реестровые суффиксы все на гласную → ь-финал no-op под реальным реестром), якорь не + лицензирует сам себя (`norm[k]=false ⟹ exact[k]=false`), `loadBankDst`=147 сверено с файлом (150 + строк, 3 дубля dst). Числа окон код-верифицированы + воспроизведены 2-й Python-реализацией. + +3. **A1 (наивное «ь») отвергнут корректно — вред ГЛОБАЛЬНЫЙ, книго-независимый.** M4: + `softSameStem("Синь","синим")=true`, `("Линь","линий")=true`. Голое «ь» поражает глобальный + `SameStem`, а через него весь путь `declinedFormPresent` → и Postcheck, и SpoilerLeaks, в любой книге. + +4. **Adversarial-прогон 1 (дизайн) сломал A3:** anchor-gated НЕ закрывает коллизию — **ПЕРЕНОСИТ** её с + одно-словного класса на форму `[титул]+[ь-фамилия]` (гипотетич. «старейшина Синь» ложно примет + «старейшина синего»). В gu-zhenren 0 коллизий — **транслит-везение** (CJK-стемы юан/шэн не русские + слова), НЕ свойство гейта → доказательство безопасности **пара-зависимо**. Числовой класс НЕ защищён + реально на корпусе: «пятого ранга» ×13, «шестого ранга» ×10 — якорь «ранга» общий, гейт даёт ноль + защиты. Поправка: A3 ТРОГАЕТ SpoilerLeaks (общий путь), спасает лишь observability-статус. + +5. **Adversarial-прогон 2 (рекомендация) поймал мою противоречивость:** я звал сид-перечень «нулём + действия», а он — РУЧНОЙ труд. Сверено по коду: `decl.forms` руко-писаны в сиде + ([memseed.go:89](../../../backend/internal/membank/memseed.go#L89)→[memory.go:315](../../../backend/internal/membank/memory.go#L315)); + майнер морфологию НЕ майнит (его «decl» = отвергнутые поверхности, miner_emit.go:60); генератора + склонений в Go нет. Для ВСЕГО мягкого класса сид-перечень = «142/142-null», что §3 и убивал. + +## Общность (ключевой вопрос владельца) — решение остаётся ВНЕ ядра +- Рекомендуемый путь (A) — данные сида `decl.forms`, матч **generic** `containsWholeWord` + ([mempostcheck.go:158](../../../backend/internal/membank/mempostcheck.go#L158)): **ноль строк Go**, новая + пара кладёт свои формы в свой сид, Go не трогается → инвариант «пара, которой в репо нет, без правки + Go» выполнен. Grep ядра `membank`/`stemmer` на книго/пара-ЛИТЕРАЛЫ и lang-ВЕТКИ — пусто (единственный + хит — иллюстративный коммент-пример 方源→Фан Юань в `mempostcheck.go:128`, не ветка кода). +- Движковый (C) ТРОНУЛ бы алгоритм `declinedFormPresent` (ядро membank) + «ь» в `target-ru.txt` + (lang-данные). Алгоритм generic — архитектурно чисто ЕСЛИ строить; но его БЕЗОПАСНОСТЬ валидирована на + одной паре → в ядро на одно-парном свидетельстве нельзя. Именно это и есть «плохое решение в ядре», + которого владелец не хочет. + +## Опции для решения + +| | Что | Плюсы | Минусы | +|---|---|---|---| +| **A** | Сид-перечень декл-форм 1–2 ВИДИМЫХ мягких канон-имён («Винный червь»; «Фан Юань» если важен SpoilerLeaks) | дёшево (~10 строк YAML), точный матч, ноль коллизии, **вне ядра**, закрывает ~весь измеренный промах | ручной труд; не масштабируется на хвост; per-book | +| **B** | Ничего + NB-коммент/регресс-гвоздь (уже в HEAD) | ноль риска/работы | ~4–7 ложняков/25 глав остаются (у шумовой планки) | +| **C** | Движковый СЛОЙНЫЙ (tail-restricted anchor + стоплист ь-кардиналов) | общий once-done, любая будущая книга бесплатно; данные в lang, алгоритм generic | трогает ядро; safety (перенос/числа) НЕ измерена (M4=3 пары, 1 книга); нужен кросс-парный гейт-замер ПРЕЖДЕ стройки | + +## Рекомендация (на совместное решение) +- **Сейчас: A (+B).** Сид-перечень «Винного червя» (+«Фан Юань» при нужде SpoilerLeaks) — закрывает + измеренный промах вне ядра; NB-коммент + гвоздь остаются; поправить инертный гвоздь «Линь/линию» → + «Линь/линий». В ядро — ничего. +- **C — НЕ строить на одно-парном свидетельстве.** Если решаем закрывать системно, сперва ЗАКАЗАТЬ + полигону кросс-парный корпус-замер частоты ложняков переноса ([титул]+[ь-фамилия]) и числового класса + на ≥2 парах (zh→ru + ja→ru/en→ru) как ГЕЙТ; только положительный замер оправдывает правку ядра. +- Честный исход-кандидат: **закрыть finding-1 на A, снять со строки 91**, C оставить как загейченный + бэклог-триггер (не открытый долг). + +## Что решить вместе с владельцем +1. Делаем стопгэп A? Кто пишет сид-формы (полигон / владелец канона)? +2. Заказываем кросс-парный гейт-замер под C — или закрываем finding-1 на A и C снимаем в загейченный триггер? + +## Артефакты +- `eval/softsign-decl/` — `main.go` · `build.sh` · `README.md` (методика+цифры+пределы, исправлен). +- `~/books/gu-zhenren/softsign-decl/` — `softsign_probe_out.txt` · `adversarial-review.txt` (дизайн) · + `code-recommendation-audit.txt` (код+рекомендация). +- backend HEAD (D39.75): NB-коммент `target-ru.txt:231-237` · гвозди `stemmer_test.go`/`memdecl_test.go`. + +## Честные пределы +Одна книга (gu-zhenren zh→ru); M4 = 3 руко-выбранные пары; per-chunk промах — прокси (не боевой чанкер, +faithful — через `tmctl export`); чистота A3 на корпусе — транслит-везение, НЕ доказательство общей +безопасности. Числа окон код-верифицированы и 2-м путём; интерпретации (перенос/числовой класс) — +предварительны по CLAUDE.md §4 до вебновелл-кросс-парного среза. diff --git a/eval/softsign-decl/README.md b/eval/softsign-decl/README.md new file mode 100644 index 00000000..0a078672 --- /dev/null +++ b/eval/softsign-decl/README.md @@ -0,0 +1,136 @@ +# softsign-decl — разрыв мягкого знака в декл-пост-чеке банка (fix-pack §а, finding-1) + +Замер $0 к backend-бэклог-строке 91 (finding-1): чем закрывать разрыв, когда мягкознаковое имя +(«Фан Юань», «Винный червь») склоняется («Фан Юаня», «Винного червя»), а стеммер §3 «ь» не стрипает, +и потому декл-форму НЕ принимает. Наивное «добавить «ь» в `decl_suffix`» отвергнуто в fix-pack (глобальная +коллизия «Синь/Линь»→«синий/линия»). Вопрос: движковый anchor-gated матч ЛУЧШЕ ли сид-перечисления — и +безопасен ли он на реальном корпусе, а не на бумаге. + +## Метод (конвенция полигона) + +- Харнесс `main.go` — БОЕВОЙ стеммер `lang.NewTargetStemmer(lang.TargetChecksFor("ru"))`; `build.sh` + материализует `go.mod`+`replace` ВНЕ репо, пин бэкенда на коммит (`git archive`) — прецедент + pkg7/minerharness. Ноль сети, ноль моделей, $0. Пин прогона: `78b7b34` (HEAD на момент замера — «ь» + уже реверсирован, D39.75). +- Корпус: банк `~/books/gu-zhenren/coldrun-a/BANK-FULL.tsv` (147 уник. dst) × боевой 25-глав-выход + `~/books/gu-zhenren/acceptance/guzhenren-25ch-ru.txt` (37 480 слов, editor=glm-5, приёмка Ф1). +- Сырьё/выход — durable `~/books/gu-zhenren/softsign-decl/` (правило 9): `softsign_probe_out.txt` + (отчёт харнесса), `adversarial-review.txt` (author≠reviewer прогон). +- Правило 10 (вскрытие единиц + 2-й путь счёта): каждый агрегат ниже сверен независимым `grep -oE` + по боевому выходу; A0-реимплементация харнесса сверена с реальным (unexported) `dstFormPresent` + in-package пробой — 0/29 расхождений — до порта на экспортные символы. + +Четыре правила матчинга декл-окна (база-цельнословная не меняется — она уже ловит номинатив): +- **A0** — боевое: `declinedFormPresent` (mempostcheck.go:169-191), «ь» НЕ стрипается. +- **A1** — наивное: «ь» в реестр, на КАЖДЫЙ компонент, БЕЗ якоря (отвергнутое; для контроля вреда). +- **A2** — anchor-gated: мягкий стрип разрешён, если ДРУГОЙ компонент совпал ТОЧНО. +- **A3** — anchor-gated: мягкий стрип разрешён, если ДРУГОЙ компонент совпал по НОРМАЛЬНОМУ стему (⊇ A2). + +## Цифры + +### Разрыв реален, но КОНЦЕНТРИРОВАН и много-словный +29 из 147 dst несут мягкознаковое слово. Захват окон над A0 (occurrence-level): + +| dst | слов | soft→stem | A0 | A3 | A3−A0 | статус | +|---|---|---|---|---|---|---| +| Фан Юань | 2 | юань→юан | 324 | 508 | **+184** | TRUE (склонение хвоста «Юаня/Юаню/Юанем/Юане», якорь «фан» точный) | +| Винный червь | 2 | червь→черв | 46 | 113 | **+67** | TRUE (оба слова склонны: «Винного червя»; A2 промахивает ВСЕ 67) | +| ветвь Чи | 2 | ветвь→ветв | 0 | 3 | +3 | TRUE (soft в ГОЛОВЕ, якорь «чи» точный) | +| ветвь Фан | 2 | ветвь→ветв | 1 | 3 | +2 | TRUE | +| демонический путь | 2 | путь→пут | 1 | 2 | +1 | TRUE («демонического пути») | +| горная крепость | 2 | крепость→крепост | 0 | 1 | +1 | TRUE («горной крепости») | +| девять рангов | 2 | девять→девят | 1 | 2 | +1 | **МЯГКАЯ НЕТОЧНОСТЬ** (см. ниже) | + +Итого A3−A0 ≈ **259 окон**, 258 истинных + 1 мягкая. A2−A0 = 189 (A2 промахивает 70 истинных +«Винного червя» и др. — нет ТОЧНОГО якоря → A2 хуже A3). + +### Практический эффект — на ПОРЯДОК меньше 259, и число НЕ робастно +Пост-чек срабатывает ПО-ЧАНКОВО (наличие любой принятой формы в чанке), не по вхождениям. Частые +имена почти всегда имеют номинатив в том же чанке. «Склонён-ТОЛЬКО (номинатив отсутствует)» — +СИЛЬНО зависит от размера блока (2-й путь счёта, самопроверка поймала завышение): + +| блок ~симв | Фан Юань | Винный червь | Мочэнь | Чилянь | ИТОГО/25 глав | +|---|---|---|---|---|---| +| 800 | 32 | 22 | 2 | 1 | 57 | +| 1500 | 8 | 16 | 1 | 1 | 26 | +| 3000 | 0 | 12 | 1 | 0 | 13 | +| **6000** | **0** | **6** | **1** | **0** | **7** | +| **12000** | **0** | **4** | **0** | **0** | **4** | + +Боевой чанк ≈ 1–2k ТОКЕНОВ (~6–12k симв) → честный per-chunk промах = **~4–7 на 25 глав, почти весь — +«Винный червь»**; «Фан Юань» на реальном размере чанка → **0** (номинатив всегда рядом). Прежняя оценка +«~26» была артефактом блока 1.5k симв (меньше боевого чанка) — снята. ⚠ Это ПРОКСИ (не боевой чанкер; +faithful — через `tmctl export` границ чанков). Порядок величины (одноцифровой, один термин) робастен. + +### A1 отвергнут корректно — вред ГЛОБАЛЬНЫЙ, книго-независимый +M4: `softSameStem("Синь","синим")=true`, `("Линь","линий")=true`, `("Тень","теней")=true` — стем +совпадает («син/лин/тен»). Голое «ь» в реестр поражает глобальный `SameStem`, а через него — весь путь +`declinedFormPresent`, т.е. и `Postcheck`, и `SpoilerLeaks`, в любой книге. Единственный функциональный +потребитель `SameStem` — `declinedFormPresent` (mempostcheck.go:181); `seeding.go:138` только +конструирует стеммер; `voice.go` его НЕ подключает. + +## Adversarial-ревью (author≠reviewer) — что оно сломало + +A3 **не закрывает** коллизию Синь/Линь — оно её **ПЕРЕНОСИТ** с одно-словного класса на форму +`[общий-титул] + [ь-фамилия]` (самая частая много-словная форма имени в жанре): + +- **Перенос (структурно ВЕРНО, здесь НЕ реализовано):** гипотетический банк-термин «старейшина Синь» + под A3 ложно примет «старейшина синего» (титул «старейшина» — нормальный якорь, «синего»→«син» + мягко). В gu-zhenren этого НЕТ: 18 из 30 soft-строк — CJK-транслит (юан/шэн/чилян/мочэн), чьи + 3-руновые стемы просто не русские слова. Чистый прогон — **транслит-везение корпуса, не свойство + гейта.** Форма-риска в банке есть («старейшина школы», «вождь четвёртого поколения»); реализуется в + книге с русско-коллизионной фамилией. (Проверка: «старейшины синеватый» ×1 в выходе есть, но + «синеватый»→«синеват»≠«син» — не сработало бы; точный «старейшина синего» — конструкт, не корпус.) +- **Числовой класс — системно и НЕ защищён (реально на корпусе):** каждый ь-финальный кардинал + (девять/десять/пять/шесть…) мягко сливается с ординалом/косвенной формой. В выходе: «пятого ранга» + ×13, «шестого ранга» ×10, «девятого ранга» ×1 — поверхность 24+ вхождения; якорь («ранга») — общее + слово, гейт даёт **ноль защиты**. «девять рангов»~«девятого ранга» — та самая 1 реализованная мягкая + неточность. Класс растёт со счётом рангов (плотный в культивации). +- **Поправка по SpoilerLeaks:** мой тезис «A3 не трогает SpoilerLeaks» — неверно обоснован. + `declinedFormPresent` достижим из `SpoilerLeaks` (mempostcheck.go:254) → A3 его МЕНЯЕТ. Спасает не + «неприкосновенность», а то, что SpoilerLeaks — observability-only, не диспозиция (mempostcheck.go:241): + мягкий ложняк там = шум, не флип гейта. A3 всё же СТРОГО лучше A1 для SpoilerLeaks (закрывает + одно-словный глобальный яд), но не чист (много-словный титул+фамилия остаётся). +- **Ни один вариант не доминирует:** min-стем-гейт бесполезен («юан» и «син» оба 3 руны — не разделить); + tail-restricted (soft только на последнем компоненте) точнее здесь, но теряет «ветвь Чи/Фан» (голова + склоняется) и всё равно ловит хвост-фамилию; стоплист коллизий — хорош как ДОБАВКА, плох соло + (открытый список — форма, от которой `target-ru.txt:19` уже предостерегает); сид-перечисление — золото + для подписного канона, но не масштабируется на авто-майненный хвост. + +## Вывод и рекомендация (на ратификацию, НЕ строить без слова владельца) + +> Заострено после 2-го адверсариал-прогона (аудит кода+рекомендации). Код харнесса — TRUSTWORTHY +> (6/6 NO-BUG, 147 dst сверено с файлом). Рекомендация исправлена: §3 ниже НЕ «ноль действия». + +1. **Разрыв реален, но мал:** ~4–7 ложных промахов/25 глав на реальном размере чанка, почти весь — + ОДИН термин «Винный червь» («Фан Юань» → 0 per-chunk). Числа окон код-верифицированы. +2. **Голое A3 НЕ рекомендуется:** переносит коллизию на титул+фамилию (риск в другой книге) и не + защищает числовой класс; чистота на gu-zhenren — транслит-везение (пара-зависимое доказательство). +3. **Стопгэп для finding-1 — сид-перечисление декл-форм для 1–2 ВИДИМЫХ мягких канон-имён** + («Винный червь»; «Фан Юань» если важен SpoilerLeaks): `declForms` матчатся `containsWholeWord` ТОЧНО — + ноль стемминга, ноль коллизии, ноль правок Go; данные книжного канона в сиде — легитимная общность. + ⚠ Честно: `decl.forms` — РУКО-ПИСАНЫЙ сид ([memseed.go:89](../../backend/internal/membank/memseed.go#L89)), + майнер морфологию НЕ майнит (его «decl» = ОТВЕРГНУТЫЕ поверхности, miner_emit.go:60); генератора + склонений в Go нет. Значит для ВСЕГО мягкого класса это = ручной труд «142/142-null», который §3 + и убивал → сид-перечень оправдан ТОЛЬКО как ограниченный стопгэп на горстку видимых имён, НЕ системно. +4. **Движковый anchor-gated матч — не «отложить» отмашкой, а ЗАГЕЙТИТЬ на корпус-замере** частоты + ложняков переноса ([титул]+[ь-фамилия]) и числового класса — они НЕ измерены (M4 = 3 руко-выбранные + пары, одна книга = предварительно, CLAUDE.md §4), а не опровергнуты. Форма, если строить — СЛОЙНАЯ + (tail-restricted + стоплист ь-кардиналов), данные в `target-ru.txt`, алгоритм generic; но ядро Go не + трогать, пока кросс-парный замер (≥2 пары) не оправдает. +5. Backend-действие сейчас: NB-коммент + регресс-гвоздь (`stemmer_test.go`, + `TestSoftSignNamesDoNotCollideWithCommonWords`) ОСТАЮТСЯ как страховка от наивного ре-адда «ь». + Гвоздь «Линь/линию» инертен — заменить на «Линь/линий» (M4 подтверждает: «линий»→«лин»). +6. **Долгий общий дом (НЕ существует сегодня):** майнер-морфология (наблюдать склонённые поверхности, + а не кодировать «ь») был бы пара-агностичен — но это ОТДЕЛЬНЫЙ механизм bank-autonomy, не бесплатный + и не «минер уже майнит» (ранняя формулировка была неверна — такого пути нет). + +## Воспроизвести +``` +cd eval/softsign-decl && ./build.sh HEAD +/softsign-decl/softsign-decl \ + -bank ~/books/gu-zhenren/coldrun-a/BANK-FULL.tsv \ + -output ~/books/gu-zhenren/acceptance/guzhenren-25ch-ru.txt \ + -report ~/books/gu-zhenren/softsign-decl/softsign_probe_out.txt +``` +Прогон ~135с (O(слова×термины×режимы)). go.mod/replace — вне репо; второго модуля в git не появляется. diff --git a/eval/softsign-decl/build.sh b/eval/softsign-decl/build.sh new file mode 100755 index 00000000..5fd82f1b --- /dev/null +++ b/eval/softsign-decl/build.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Собирает softsign-decl КАК МОДУЛЬ ВНЕ РЕПО (прецедент pkg7/minerharness): исходник живёт в eval/ (ревью, +# воспроизводимость), а go.mod + `replace` материализуются в рабочем каталоге вне git. Второго go.mod в +# репозитории не появляется — сборка/vet бэкенда его не видят. +# +# ./build.sh [commit] +# +# work-dir — куда положить модуль и пин-копию бэкенда (скретчпад сессии). +# commit — какой коммит бэкенда пинить (по умолчанию HEAD). Пин обязателен: параллельная бэкенд-сессия +# правит рабочее дерево, и `replace` на него дал бы невоспроизводимый прогон. +set -euo pipefail + +WORK="${1:?usage: build.sh [commit]}" +COMMIT="${2:-HEAD}" +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +SHA="$(git -C "$REPO" rev-parse --short "$COMMIT")" +PIN="$WORK/pinned-$SHA" +MOD="$WORK/softsign-decl" + +mkdir -p "$PIN" "$MOD" +if [ ! -d "$PIN/backend" ]; then + git -C "$REPO" archive "$COMMIT" backend | tar -x -C "$PIN" +fi + +cp "$(dirname "${BASH_SOURCE[0]}")/main.go" "$MOD/main.go" +cp "$PIN/backend/go.sum" "$MOD/go.sum" + +{ + echo "module textmachine/backend/softsign-decl" + echo + sed -n '/^go /p' "$PIN/backend/go.mod" + echo + echo "require textmachine/backend v0.0.0" + echo + sed -n '/^require (/,/^)/p' "$PIN/backend/go.mod" + echo + echo "replace textmachine/backend => $PIN/backend" +} > "$MOD/go.mod" + +cd "$MOD" +GOFLAGS=-mod=mod GOPROXY=off go build -o "$MOD/softsign-decl" . +echo "built: $MOD/softsign-decl (backend pinned at $SHA in $PIN/backend)" diff --git a/eval/softsign-decl/main.go b/eval/softsign-decl/main.go new file mode 100644 index 00000000..5f673428 --- /dev/null +++ b/eval/softsign-decl/main.go @@ -0,0 +1,337 @@ +// Command softsign-decl измеряет разрыв мягкого знака в декл-пост-чеке банка (fix-pack §а finding-1) на +// БОЕВОМ стеммере, НЕ ТРОГАЯ репозиторий. Прецедент — pkg6 `labels/` и pkg7 `minerharness`: отдельный +// Go-модуль ВНЕ репо с `module textmachine/backend/<имя>` и `replace textmachine/backend => <пин-копия>`; +// имя обязано начинаться с `textmachine/backend/`, иначе Go запретит импорт `internal/`. Сборка/пин — +// `build.sh` (git archive ), чтобы незакоммиченные правки параллельных сессий не меняли результат. +// +// Что мерит: для каждого dst банка — приходит ли ПРИНЯТАЯ форма в боевой 25-глав-выход, под четырьмя +// правилами матчинга декл-окна: +// A0 — БОЕВОЕ поведение: declinedFormPresent (mempostcheck.go:169-191) — «ь» НЕ стрипается. +// A1 — наивное: «ь» добавлен в реестр стрипа, применяется к КАЖДОМУ компоненту, БЕЗ якоря (отвергнутый). +// A2 — anchor-gated: мягкий стрип на компоненте разрешён, если ДРУГОЙ компонент совпал ТОЧНО. +// A3 — anchor-gated: мягкий стрип разрешён, если ДРУГОЙ компонент совпал по НОРМАЛЬНОМУ стему (⊇ A2). +// A0 — построчный порт боевого кода на экспортных символах lang; паритет с реальным (unexported) +// dstFormPresent сверен отдельно в membank-пробе (0/29 расхождений) до порта. Ноль сети, ноль моделей, $0. +package main + +import ( + "flag" + "fmt" + "os" + "sort" + "strings" + "unicode" + + "textmachine/backend/internal/lang" + "textmachine/backend/internal/text" +) + +type mode int + +const ( + modeA0 mode = iota + modeA1 + modeA2 + modeA3 +) + +// stemAug моделирует «добавить голый «ь» в реестр decl_suffix»: стрипнуть длиннейший реестровый суффикс; +// если ничего не стрипнуто, а слово кончается на «ь» и стем остаётся ≥ minStemRunes(3), стрипнуть «ь». +// Зеркалит longest-first (реальный суффикс всегда бьёт 1-руновый «ь»). +func stemAug(s lang.TargetStemmer, w string) string { + w = strings.ToLower(w) + if st := s.Stem(w); st != w { + return st + } + r := []rune(w) + if len(r) >= 4 && r[len(r)-1] == 'ь' { + return string(r[:len(r)-1]) + } + return w +} + +func softSameStem(s lang.TargetStemmer, a, b string) bool { + la, lb := strings.ToLower(a), strings.ToLower(b) + if la == lb { + return true + } + return stemAug(s, la) == stemAug(s, lb) +} + +// containsWholeWord — порт mempostcheck.go:197-211 (буквенная граница на обоих концах, по рунам). +func containsWholeWord(hay, form []rune) bool { + n := len(form) + if n == 0 || n > len(hay) { + return false + } + for i := 0; i+n <= len(hay); i++ { + eq := true + for k := 0; k < n; k++ { + if hay[i+k] != form[k] { + eq = false + break + } + } + if !eq { + continue + } + if (i == 0 || !unicode.IsLetter(hay[i-1])) && (i+n == len(hay) || !unicode.IsLetter(hay[i+n])) { + return true + } + } + return false +} + +// windowMatch — совпадает ли окно outWords[i:i+n] с базовыми словами bw под правилом m. +func windowMatch(s lang.TargetStemmer, outWords, bw []string, i int, m mode) bool { + n := len(bw) + anyExact, anyNorm := false, false + norm := make([]bool, n) + soft := make([]bool, n) + for k := 0; k < n; k++ { + ow, bwk := outWords[i+k], strings.ToLower(bw[k]) + if ow == bwk { + anyExact = true + } + norm[k] = s.SameStem(ow, bw[k]) + soft[k] = softSameStem(s, ow, bw[k]) + if norm[k] { + anyNorm = true + } + } + for k := 0; k < n; k++ { + switch m { + case modeA0: + if !norm[k] { + return false + } + case modeA1: + if !soft[k] { + return false + } + case modeA2: + if !(norm[k] || (soft[k] && anyExact)) { + return false + } + case modeA3: + if !(norm[k] || (soft[k] && anyNorm)) { + return false + } + } + } + return true +} + +func countWindows(s lang.TargetStemmer, outWords, bw []string, m mode) int { + n := len(bw) + if n == 0 || n > len(outWords) { + return 0 + } + c := 0 + for i := 0; i+n <= len(outWords); i++ { + if windowMatch(s, outWords, bw, i, m) { + c++ + } + } + return c +} + +func firstWindowText(s lang.TargetStemmer, outWords, bw []string, m mode) string { + n := len(bw) + for i := 0; i+n <= len(outWords); i++ { + if windowMatch(s, outWords, bw, i, m) { + return strings.Join(outWords[i:i+n], " ") + } + } + return "" +} + +// present — порт dstFormPresent: базовая цельнословная форма ИЛИ декл-окно под правилом m. +func present(s lang.TargetStemmer, noutRunes, baseRunes []rune, outWords, bw []string, m mode) bool { + if containsWholeWord(noutRunes, baseRunes) { + return true + } + return countWindows(s, outWords, bw, m) > 0 +} + +func loadBankDst(path string) ([]string, error) { + raw, err := os.ReadFile(path) + if err != nil { + return nil, err + } + lines := strings.Split(string(raw), "\n") + seen := map[string]bool{} + var out []string + for i, ln := range lines { + if i == 0 || strings.TrimSpace(ln) == "" { + continue + } + f := strings.Split(ln, "\t") + if len(f) < 2 { + continue + } + dst := strings.TrimSpace(f[1]) + if dst == "" || seen[dst] { + continue + } + seen[dst] = true + out = append(out, dst) + } + return out, nil +} + +func main() { + var ( + bankP = flag.String("bank", "", "BANK-FULL.tsv (col2 = dst)") + outP = flag.String("output", "", "боевой target-выход (txt)") + repP = flag.String("report", "", "куда писать отчёт (durable ~/books/...)") + tgt = flag.String("tgt", "ru", "целевой язык") + ) + flag.Parse() + if *bankP == "" || *outP == "" || *repP == "" { + fmt.Fprintln(os.Stderr, "usage: softsign-decl -bank B -output O -report R [-tgt ru]") + os.Exit(2) + } + dsts, err := loadBankDst(*bankP) + if err != nil { + fmt.Fprintln(os.Stderr, "bank:", err) + os.Exit(1) + } + rawOut, err := os.ReadFile(*outP) + if err != nil { + fmt.Fprintln(os.Stderr, "output:", err) + os.Exit(1) + } + s := lang.NewTargetStemmer(lang.TargetChecksFor(*tgt)) + if !s.Enabled() { + fmt.Fprintln(os.Stderr, "stemmer inert for", *tgt) + os.Exit(1) + } + nout := text.NormalizeTargetForm(string(rawOut)) + noutRunes := []rune(nout) + outWords := lang.TokenizeWords(nout) + + var b strings.Builder + pf := func(f string, a ...any) { fmt.Fprintf(&b, f, a...) } + pf("SOFT-SIGN DECL PROBE — bank=%d dst, output=%d words, tgt=%s\n", len(dsts), len(outWords), *tgt) + pf("A0=live(no «ь») · A1=naive global «ь» · A2=anchor:exact · A3=anchor:norm\n") + pf("========================================================\n\n") + + type row struct { + dst string + nWords int + softWord, softStem string + a0Win, a1Win, a2Win, a3Win int + a0Base bool + a2New, a3New string + } + var soft []row + for _, dst := range dsts { + base := text.NormalizeTargetForm(dst) + bw := lang.TokenizeWords(base) + var sw, ss string + for _, w := range bw { + r := []rune(w) + if len(r) >= 4 && r[len(r)-1] == 'ь' { + sw, ss = w, string(r[:len(r)-1]) + break + } + } + if sw == "" { + continue + } + soft = append(soft, row{ + dst: dst, nWords: len(bw), softWord: sw, softStem: ss, + a0Win: countWindows(s, outWords, bw, modeA0), + a1Win: countWindows(s, outWords, bw, modeA1), + a2Win: countWindows(s, outWords, bw, modeA2), + a3Win: countWindows(s, outWords, bw, modeA3), + a0Base: containsWholeWord(noutRunes, []rune(base)), + a2New: firstWindowText(s, outWords, bw, modeA2), + a3New: firstWindowText(s, outWords, bw, modeA3), + }) + } + sort.Slice(soft, func(i, j int) bool { return soft[i].a3Win-soft[i].a0Win > soft[j].a3Win-soft[j].a0Win }) + + pf("M1/M2. SOFT-SIGN TERMS (%d of %d dst) — occurrence-level windows captured\n", len(soft), len(dsts)) + pf("dst | words | soft→stem | nomBase? | A0 A1 A2 A3 | A3−A0 | A3 first-new\n----\n") + var gA3multi, gA3single, gA2 int + for _, r := range soft { + g3 := r.a3Win - r.a0Win + gA2 += r.a2Win - r.a0Win + if r.nWords > 1 { + gA3multi += g3 + } else { + gA3single += g3 + } + nt := "" + if g3 > 0 { + nt = r.a3New + } + pf("%-30s | %d | %s→%s | nom=%v | A0=%d A1=%d A2=%d A3=%d | +%d | %s\n", + r.dst, r.nWords, r.softWord, r.softStem, r.a0Base, r.a0Win, r.a1Win, r.a2Win, r.a3Win, g3, nt) + } + pf("\nGAP (occurrence windows over A0): A3 multi=%d single=%d | A2 total=%d\n", gA3multi, gA3single, gA2) + + // M3: presence flips over ALL dst (worst case: every term vs whole book) + pf("\nM3. PRESENCE FLIPS over ALL %d dst (A0-miss → variant-present)\n", len(dsts)) + type flip struct{ dst, win string } + var f1, f2, f3 []flip + for _, dst := range dsts { + base := text.NormalizeTargetForm(dst) + br := []rune(base) + bw := lang.TokenizeWords(base) + if present(s, noutRunes, br, outWords, bw, modeA0) { + continue + } + if present(s, noutRunes, br, outWords, bw, modeA1) { + f1 = append(f1, flip{dst, firstWindowText(s, outWords, bw, modeA1)}) + } + if present(s, noutRunes, br, outWords, bw, modeA2) { + f2 = append(f2, flip{dst, firstWindowText(s, outWords, bw, modeA2)}) + } + if present(s, noutRunes, br, outWords, bw, modeA3) { + f3 = append(f3, flip{dst, firstWindowText(s, outWords, bw, modeA3)}) + } + } + dump := func(name string, fs []flip) { + pf("%s new presences: %d\n", name, len(fs)) + for _, x := range fs { + pf(" %s ⟶ [%s]\n", x.dst, x.win) + } + } + dump("A1 (naive «ь»)", f1) + dump("A2 (anchor=exact)", f2) + dump("A3 (anchor=norm)", f3) + in := func(fs []flip, d string) bool { + for _, x := range fs { + if x.dst == d { + return true + } + } + return false + } + pf("\nA1-ONLY presences (removed by anchor gate A3) — collision-suspect set:\n") + nOnly := 0 + for _, x := range f1 { + if !in(f3, x.dst) { + pf(" %s ⟶ [%s]\n", x.dst, x.win) + nOnly++ + } + } + pf(" (count=%d)\n", nOnly) + + // M4: confirmed collision class (book-independent) under global soft SameStem vs anchor gate + pf("\nM4. CONFIRMED COLLISION CLASS (fix-pack §а) — global soft (A1) vs single-word anchor gate\n") + for _, p := range [][2]string{{"Синь", "синим"}, {"Линь", "линий"}, {"Тень", "теней"}} { + pf(" A1 softSameStem(%q,%q)=%v (stems %q/%q) | anchor gate: lone word, no anchor ⟶ never soft\n", + p[0], p[1], softSameStem(s, p[0], p[1]), stemAug(s, p[0]), stemAug(s, p[1])) + } + + if err := os.WriteFile(*repP, []byte(b.String()), 0o644); err != nil { + fmt.Fprintln(os.Stderr, "write report:", err) + os.Exit(1) + } + fmt.Print(b.String()) + fmt.Fprintf(os.Stderr, "\nreport written: %s\n", *repP) +}