diff --git a/CLAUDE.md b/CLAUDE.md index 3a552d2b..6ad09bfe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,7 +14,7 @@ Go-бэкенд издательского художественного пер ## Источники истины (по убыванию) -1. **`docs/architecture/05-decisions-log.md` (D1–D39.36)** — ратифицированный контракт; при конфликте с любым другим доком побеждает он; сверху файла — карта актуальности (что чем superseded). **Правило чтения (онбординг-диета 25.07): карта + живая голова (эра D39.x); корпус D1–D38 = справочник — по ссылкам/grep по D-номеру, целиком НЕ читать.** +1. **`docs/architecture/05-decisions-log.md` (D1–D39.37)** — ратифицированный контракт; при конфликте с любым другим доком побеждает он; сверху файла — карта актуальности (что чем superseded). **Правило чтения (онбординг-диета 25.07): карта + живая голова (эра D39.x); корпус D1–D38 = справочник — по ссылкам/grep по D-номеру, целиком НЕ читать.** 2. `docs/experiments/00-provider-quirks.md` — wire-квирки провайдеров (читать ПЕРЕД правкой адаптеров/вызовами провайдеров). 3. `docs/architecture/09-target-architecture.md` (7 слоёв; статус стройки — шапка-таблица) · `12-go-style-notes.md` (норматив общности §0) · `10-prompt-architecture.md` (промпт-тема). 4. `docs/architecture/01-decisions.md` (Р1–Р10), `02-mvp-plan.md`, `04-unhappy-paths.md`, `06-memory-risk-registry.md` — тела исторические, читать через ⚠-баннеры 25.07. @@ -43,7 +43,7 @@ Go-бэкенд издательского художественного пер - **Фазы:** Ф0 ✅ · Ф1-инфра ✅ (D20–D28; golden = инвариант детерминизма №8) · арка «качество-первым» D26→D38.5 закрыта · **арх-ресет D39 исполнен ЦЕЛИКОМ**: 7 слоёв построены паками 11–16 (D39.13–D39.24; статус слоёв — шапка-таблица `09-target-architecture.md`; слой-3 diff-редактор — за `enabled:false`, включение = решение владельца по замеру остатка). - **Пере-прогон rerun2 прочитан (D39.20):** операционка живьём ($0-резюм ×3 арма, $6.63<$15, echo 0, майнинг-стоп→подпись); планка ≤2 НЕ пройдена; анти-корреляция гладкость↔верность ×3 → mistral-редактор вон. **Эксп ЗАКРЫТ (D39.22), итерация №2 отложена; редактор = deepseek-v4-pro ИНТЕРИМ** (glm-5 резерв). Стайл-каноны D39.21 (时辰=2ч · generic-«гу» ср. род · стихи смыслом · 资质=«талант»). -- **Курс — разработка бэкенда:** паки 17 (content-labels, D39.26–28) и 18 (долги контракта, D39.31) ЗАКРЫТЫ; право по ToS добыто и подписано (D39.29/30/32). **Курс амендирован владельцем (D39.33): масштаб — ПОСЛЕДНИМ, после достройки алгоритмического идеала; доказательство эффективности — мини-прогоны ~10 глав.** Очередь (D39.34/35): **ТЕКУЩИЙ — мини-прогон ~10 глав одной сессией** (`BACKEND_MINIRUN_SESSION_PROMPT.md`: гигиена — флип `RegressionGuard` + ниты → главный проход (остаток дефектов, живой rebill-гейт) → проба банкноты ≈$0.02 → верификация по сырью; ~$0.12–0.19, судья не зовётся) → сид-дельта (元-корень ждёт владельца) → пак-19 «голос/состояние» в урезанном скоупе (безусловная половина D21) → решение по шву банкноты (D39.36: канал WHAT выключен и выбрасывается — развилка после пробы) → слой-2/ru-target/native-Gemini → МАСШТАБ → пилот Ф2.5. +- **Курс — разработка бэкенда:** паки 17 (content-labels, D39.26–28) и 18 (долги контракта, D39.31) ЗАКРЫТЫ; право по ToS добыто и подписано (D39.29/30/32). **Курс амендирован владельцем (D39.33): масштаб — ПОСЛЕДНИМ, после достройки алгоритмического идеала; доказательство эффективности — мини-прогоны ~10 глав.** Очередь (D39.34/35): ~~мини-прогон~~ **ИСПОЛНЕН И ПРИНЯТ (D39.37, 26.07: $0.23, операционка живьём до цента, шов банкноты замкнут, остаток = 0 в трёх классах петли — решение о петле у владельца)** → сид-дельта (元-корень ждёт владельца) → пак-19 «голос/состояние» в урезанном скоупе (безусловная половина D21) → решение по шву банкноты (D39.36: канал WHAT выключен и выбрасывается — развилка после пробы) → слой-2/ru-target/native-Gemini → МАСШТАБ → пилот Ф2.5. - **Стек:** draft deepseek-v4-flash thinking-ON → editor deepseek-v4-pro → судья gemini-preview + grok-фолбэк 18+ (~$0.85/ранобэ D30.4). Ключи: DEEPSEEK, ZAI, KIMI, OPENAI, GEMINI, XAI, MISTRAL (xAI: единый, data-sharing, off перед продом D27). - **Висит на владельце + открытые петли:** трекер в CURRENT-STATE `docs/PROGRESS.md` (единственное место). - **Стенд:** WSL2, GTX 1070 8GB (localhost из-под прокси = 403 — no-proxy транспорт для local). Книга `/home/ubuntu/books/gu-zhenren/` (GB18030; текст и производные ВНЕ git). diff --git a/backend/cmd/tmctl/render.go b/backend/cmd/tmctl/render.go index 35c09187..cfc656c4 100644 --- a/backend/cmd/tmctl/render.go +++ b/backend/cmd/tmctl/render.go @@ -256,8 +256,14 @@ func renderQuality(w io.Writer, q *pipeline.QualityReport) error { q.DialogueDashFlags, q.GlossaryMisses, q.NumberDriftFlags, q.TrustGated, q.DegenerateLoopRuns) // Echo split (D39.18): draft = the translator echoed (incl. dropped c-lite members) / edit = the editor // echoed in what was delivered. Cosmetic-strip is per unit (the sanitizer runs on the final stage). - fmt.Fprintf(w, "STRIPS/ECHO: cosmetic-strip units=%d (%.1f%%, markdown+CJK) · echo draft=%d (%.1f%%) · echo edit=%d (%.1f%%)\n", - q.CosmeticStripUnits, 100*q.CosmeticStripRate, q.EchoDraftChunks, 100*q.EchoDraftRate, q.EchoEditUnits, 100*q.EchoEditRate) + // The recovered share is spelled out rather than hidden: an echo the escalation fixed still measures + // the translator (the D18 mine is live), but it cost the book nothing beyond the wasted primary call. + recovered := "" + if q.EchoDraftRecovered > 0 { + recovered = fmt.Sprintf(", %d recovered by escalation", q.EchoDraftRecovered) + } + fmt.Fprintf(w, "STRIPS/ECHO: cosmetic-strip units=%d (%.1f%%, markdown+CJK) · echo draft=%d (%.1f%%%s) · echo edit=%d (%.1f%%)\n", + q.CosmeticStripUnits, 100*q.CosmeticStripRate, q.EchoDraftChunks, 100*q.EchoDraftRate, recovered, q.EchoEditUnits, 100*q.EchoEditRate) // Addressable-defect RESIDUAL (pack-16): how many defects a repair loop could actually attack in what // this run shipped — the $0 measurement that decides whether enabling the paid loop is worth it. The // line is printed only when something fired, so a clean book's report is unchanged. @@ -396,6 +402,12 @@ func renderStatusHuman(w io.Writer, rep *pipeline.StatusReport, cfgPath string) if rep.ConfigDrift { drift += " ⚠ CONFIG-DRIFT (the current config renders a different snapshot — an edit since the run; translate will require --resnapshot = re-paying for the book)" } + // The drift flags say THAT the config moved; this says what continuing would COST (spec D15.2 §9). + // It is the same projection `translate` refuses on, so the operator reads the decision number here + // instead of discovering it in a refusal. + if rep.RebillUnits > 0 { + drift += fmt.Sprintf(" ⚠ RE-PAYMENT: %d chunk×stage unit(s) already billed under a superseded snapshot would be paid for again, ~$%.6f (translate needs --accept-rebill above the book's consent threshold)", rep.RebillUnits, rep.RebillUSD) + } fmt.Fprintf(w, "=== STATUS: %s (snapshot %s)%s ===\n", rep.BookID, snap, drift) fmt.Fprintf(w, "Progress: %d/%d units (%.1f%%) — done=%d in_progress=%d flagged=%d pending=%d\n", rep.Done, rep.TotalUnits, rep.PercentDone, rep.Done, rep.InProgress, rep.Flagged, rep.Pending) diff --git a/backend/configs/langpacks/zh-ru/dc-checkers.txt b/backend/configs/langpacks/zh-ru/dc-checkers.txt index 25d88274..a4fcdc74 100644 --- a/backend/configs/langpacks/zh-ru/dc-checkers.txt +++ b/backend/configs/langpacks/zh-ru/dc-checkers.txt @@ -65,3 +65,17 @@ pattern shushiwan_fire_re десятк\p{L}* тысяч pattern cheng_re ([0-9一二三四五六七八九十])成([0-9一二三四五六七八九]?) pattern decimal_fraction_re десят(?:ая|ых|ой|ые)|сот(?:ая|ых|ой|ые)|\d+[.,]\d pattern percent_word процент +# --- UNIT RELATIONS (`dc_ratiokeyint`). The checker ALGORITHM compares counts; how many target +# hours one source time-unit is worth is a fact about THIS pair (1 时辰 = 2 h). It used to be a literal +# `n * 2` in generic Go, which would silently lie to a pair whose unit is not a double-hour. +dc_ratio dc1_unit_in_hours 2 +# --- DETAIL templates (`msgkeytemplate`, value VERBATIM; `{name}` placeholders are substituted by +# the checker). The counts are the engine's, but a line that says «时辰»/«千万»/«成» is pair SUBSTANCE: a +# second pair shipping its own patterns must not inherit a message about Chinese double-hours. Every key +# here is REQUIRED whenever this file exists (a detector that fires with nothing to say is a silent detector). +msg dc1_fractional DC1 时辰 (fractional): 半个时辰 ≈ 1 h rendered as «{fire_word}» (half of it) +msg dc1_counted DC1 时辰: {n}个时辰 rendered as «{rendered} час…» (counted as hours) instead of ~{expected} h (1 时辰 = {ratio} h) +msg dc2_qianwan DC2 千万=10^7 rendered as «тысячи» (≈10000× under) — a possible magnitude error (hyperbole risk, §5-A4) +msg dc2_shushiwan DC2 数十万≈several×10^5 rendered as «десятки тысяч» (≈10× under) +msg dc6_register DC6 register: fairy-tale Russian lexis out of the xianxia genre: {hits} +msg percent_scale 成-percent: {tens}成{ones} = {pct}% rendered as a decimal fraction instead of a percentage (~{pct}%) diff --git a/backend/configs/models.yaml b/backend/configs/models.yaml index e8f42071..96ba5dfe 100644 --- a/backend/configs/models.yaml +++ b/backend/configs/models.yaml @@ -160,9 +160,22 @@ models: # $0.435/$0.87, cache-hit $0.003625 (факт. 2026-07-10, api-docs.deepseek.com/quick_start/pricing). price: { input_per_m: 0.435, cached_per_m: 0.003625, cache_write_per_m: 0, output_per_m: 0.87 } capabilities: - # Floor 8000 (D24.3): без него хоп наследовал бюджет праймари (2048/3291 на - # приёмке этапа A) и вернул finish=length / 0 байт контента — эскалация впустую. - min_max_tokens: 8000 + # Floor (D24.3): без него хоп наследовал бюджет праймари (2048/3291 на приёмке этапа A) + # и вернул finish=length / 0 байт контента — эскалация впустую. + # + # 8000 → 16000 ПО ЗАМЕРУ мини-прогона 25.07 (10 глав, 14 edit-единиц): две единицы вернули + # finish=length на 8000 и прошли на ретрае с 16000 — **$0.0177, то есть 15.5% всего счёта + # прогона, ушло в две выброшенные генерации**. Размер текста тут ни при чём: единица с + # черновиком 8189 символов в 8000 УЛОЖИЛАСЬ, а с 6380 — нет; разница в длине РАЗМЫШЛЕНИЯ, + # которое у DeepSeek (reasoning: subset) считается ВНУТРИ completion_tokens и потому ест тот + # же бюджет, что и текст. Оценочный множитель max_output_ratio этого не знает и знать не + # может — на редакторской единице он всё равно даёт меньше флора. + # + # Асимметрия цены и решает: перебор бюджета БЕСПЛАТЕН (резервация транзитна, списание идёт по + # фактическому usage), недобор стоит ЦЕЛОЙ генерации. Поэтому флор ставится с запасом, а не + # впритык: 16000 против измеренных 10951 максимум. + # ⚠ Двигает max_tokens ⇒ request_hash ⇒ снапшот: правка = громкий --resnapshot. + min_max_tokens: 16000 note: >- Эскалация черновика канала A, хоп 1 (D3): deepseek-эхо 25% на приёмочной 蛊真人 (D18) → single-hop фолбэк с draft. Наследует deepseek: thinking ON (эхо-мина того же класса — echoes_when_thinking_off), diff --git a/backend/configs/pipeline-arm-deepseek-pro.yaml b/backend/configs/pipeline-arm-deepseek-pro.yaml index f8d2a174..6da88a4b 100644 --- a/backend/configs/pipeline-arm-deepseek-pro.yaml +++ b/backend/configs/pipeline-arm-deepseek-pro.yaml @@ -50,6 +50,10 @@ gates: min_chunk_chars: 500 sanitizer: enabled: true + regression_guard: + # D38.4 п.5 (обязательство, не исполненное к rerun2 — D39.34): наблюдаемость обвала длины + # и числового дрейфа на переходе черновик→финал. Никогда не вердикт; флип не двигает снапшот. + enabled: true escalation: chains: diff --git a/backend/configs/pipeline-arm-glm.yaml b/backend/configs/pipeline-arm-glm.yaml index 506bc8dc..82fa4ada 100644 --- a/backend/configs/pipeline-arm-glm.yaml +++ b/backend/configs/pipeline-arm-glm.yaml @@ -49,6 +49,10 @@ gates: min_chunk_chars: 500 sanitizer: enabled: true + regression_guard: + # D38.4 п.5 (обязательство, не исполненное к rerun2 — D39.34): наблюдаемость обвала длины + # и числового дрейфа на переходе черновик→финал. Никогда не вердикт; флип не двигает снапшот. + enabled: true escalation: chains: diff --git a/backend/configs/pipeline-arm-mistral.yaml b/backend/configs/pipeline-arm-mistral.yaml index a8a31a85..26c3ae64 100644 --- a/backend/configs/pipeline-arm-mistral.yaml +++ b/backend/configs/pipeline-arm-mistral.yaml @@ -45,6 +45,10 @@ gates: sent_cov_min: 0.75 min_chunk_chars: 500 sanitizer: + enabled: true + regression_guard: + # D38.4 п.5 (обязательство, не исполненное к rerun2 — D39.34): наблюдаемость обвала длины + # и числового дрейфа на переходе черновик→финал. Никогда не вердикт; флип не двигает снапшот. enabled: true # премиум-редактор за санитайзером — как C1 (утечки преамбул ловятся) escalation: diff --git a/backend/configs/pipeline-c1.yaml b/backend/configs/pipeline-c1.yaml index da92942e..f9f764d9 100644 --- a/backend/configs/pipeline-c1.yaml +++ b/backend/configs/pipeline-c1.yaml @@ -94,6 +94,19 @@ gates: # ВКЛЮЧЁН для пере-прогона 25 глав (gemini течёт преамбулой 6/6 — премиум-редактор # за санитайзером). Версия — вердикт-ось (переедет в verdictSnapshotID с D15.2). enabled: true + regression_guard: + # Пост-reflow регресс-гард (D38-инфра, research/18 §C#5): два детерминированных + # флаггера НАБЛЮДАЕМОСТИ над переходом ЧЕРНОВИК→ФИНАЛ — обвал длины (>40% непробельных + # символов) и числовой дрейф (многозначное число исчезло/появилось). НИКОГДА не вердикт: + # reflow-редактор законно перестраивает и сливает предложения, поэтому hard-skip ложно + # флагнул бы хорошую правку — попадание пишется в наблюдаемость (retrieval_state + # n_style_flags + detail) и всплывает в отчёте. + # ВКЛЮЧЁН 25.07 — исполнение обязательства D38.4 п.5 («оркестратор при сборке единого + # resnapshot ОБЯЗАН включить»), не исполненного к rerun2 (D39.34 находка №1: ключа не + # было НИ В ОДНОМ шиппинг-конфиге ⇒ гейт был тёмным весь пере-прогон). + # ⚠️ Флип НЕ двигает снапшот (пороги — код-консты, версия уже в CheapGateVersion; проверено + # исполнением: снапшоты обеих волн байт-равны с гейтом и без) ⇒ пере-оплаты нет. + enabled: true escalation: chains: diff --git a/backend/configs/pipeline-c2.yaml b/backend/configs/pipeline-c2.yaml index 3dce8462..e7bcd70d 100644 --- a/backend/configs/pipeline-c2.yaml +++ b/backend/configs/pipeline-c2.yaml @@ -56,6 +56,10 @@ gates: enabled: false sent_cov_min: 0.75 min_chunk_chars: 500 + regression_guard: + # D38.4 п.5 (обязательство, не исполненное к rerun2 — D39.34). C2 несёт стадию edit, значит переход + # черновик→финал у него есть и гард не вырожден. Никогда не вердикт; флип не двигает снапшот. + enabled: true escalation: chains: diff --git a/backend/internal/checks/cheapgates.go b/backend/internal/checks/cheapgates.go index c944e003..81960c8d 100644 --- a/backend/internal/checks/cheapgates.go +++ b/backend/internal/checks/cheapgates.go @@ -65,7 +65,33 @@ import ( // ending in the impossible «-йть»). All are language-general — no book-specific word lists. Editing a rule // shifts the recorded counts, so the version bump is a loud --resnapshot; they fire 0 on a non-zh source / // clean Russian output (the golden fixture stays style_flags=0). -const CheapGateVersion = "cheapgate-v4-percent-latin-brokenword+u" + unicode.Version +// v5 (mini-run + live verification, 25–26.07) closes ONE measured false-positive class and deliberately +// leaves two others alone. The dialogue-dash MIXING rule now counts a chevron line ONLY when its +// attribution names a SPEECH verb and is not qualified into a thought («пробормотал ПРО СЕБЯ»): Russian +// sets inner speech in «…» beside spoken «—» on purpose, so a chunk carrying both is correctly typeset +// prose — the rule was flagging the convention it exists to protect (8 of 8 hits on the 10-chapter run). +// +// The POLARITY is the load-bearing choice and it was picked by measurement, not taste. The first attempt +// asked "is this a THOUGHT?" against a list of thought verbs; a live 2-chapter run produced «понимал» and +// «размышлял» within minutes, and that list has no natural boundary — every miss becomes a false flag on +// good prose. Asking "is this SPOKEN?" needs a small closed list, and every miss becomes silence on a real +// clash: the failure mode the gate's precision-over-recall bias actually wants. Both lists are TARGET data +// (`speech_verb`, `inner_marker`), so a target shipping neither runs the mixing rule inert. +// Measured on the real exports, per chunk: 10-chapter mini-run 8 → 0, 2-chapter verification 1 → 0, while +// genuine spoken-chevron mixing still fires (pinned by test). +// +// NOT changed, on purpose, after the same run flagged them (both examined, both written, both reverted): +// - 成-percent: the two live hits («три десятых» for 三成, «сорок четыре сотых» for 四成四) carry the +// RIGHT value in the wrong FORM, but the sub-check's own ratified fixtures treat the form as the +// defect, and the one blunt discriminator available (fire on digits only) would silence the genuine +// 100× shape «шесть и шесть десятых» = 6.6, which is spelled out too. A correct fix compares the +// rendered VALUE with the source percentage and therefore needs target numeral-word data — a design, +// not a patch. Left firing rather than narrowed by guesswork. +// - DC2 千万: the live hit (杀了千万人 → «тысячи и тысячи людей») is shape-identical to the ratified TRUE +// positive (千万生灵 → «тысячи жизней»). Hyperbole and magnitude are not separable offline here — the +// rule's header says so (§5-A4) — so the hit is the class's accepted ambiguity, not an implementation +// bug. Any coefficient-based veto kills the true positive with the false one. +const CheapGateVersion = "cheapgate-v5-chevron-speech-attribution+u" + unicode.Version // CheapGateConfig carries the brief-derived knobs: the ё-policy and the per-project Allowlist of // surfaces that look like a blocklisted interjection but are legitimate here (e.g. a character @@ -119,7 +145,7 @@ func (c CheapGateResult) Total() int { // when there is no distinct reflow stage, so the guard then trivially never fires). func RunCheapGates(source, draft, final string, cfg CheapGateConfig) CheapGateResult { var r CheapGateResult - n, det := lintDialogueDash(final) + n, det := lintDialogueDash(final, cfg.Checkers) r.DialogueDash, r.Detail = n, append(r.Detail, det...) n, det = cfg.Checkers.lintYofikation(final, cfg.YoPolicy) r.YoInconsistent = n @@ -170,7 +196,7 @@ func RunCheapGates(source, draft, final string, cfg CheapGateConfig) CheapGateRe // counts as a mis-set dash when followed by a space and a letter (so a hyphenated word wrap or a // «- 1» list item does not fire); chevron lines count only under mixing (a chunk that uses «…» for // speech throughout may be a deliberate style, but mixing it with dashes is an inconsistency). -func lintDialogueDash(text string) (int, []string) { +func lintDialogueDash(text string, c *Checkers) (int, []string) { var emDash, hyphenLike, chevronSpeech int var detail []string for _, line := range strings.Split(text, "\n") { @@ -194,8 +220,14 @@ func lintDialogueDash(text string) (int, []string) { detail = append(detail, "a line using a hyphen/short dash instead of the long «—»: "+preview(t)) hyphenLike++ } - case '«': // chevron-led line counts toward mixing ONLY as chevron DIALOGUE (not a citation/title) - if chevronSpeechShape(rs) { // D20.4 FP: a «-citation at line start no longer counts as style mixing + case '«': // chevron-led line counts toward mixing ONLY as SPOKEN chevron dialogue + // D20.4 FP: a «-citation at line start does not count. Mini-run FP (25.07): neither does a + // THOUGHT. Russian typography sets inner speech in chevrons and speech aloud in dashes ON + // PURPOSE, so a chunk carrying both is correctly typeset prose, not a style clash — this + // rule was flagging the convention it exists to protect (8 of 8 hits on the 10-chapter + // mini-run were thoughts). The line therefore counts only when its attribution names a + // SPEECH verb (target data), i.e. an unrecognised attribution is silence, not a flag. + if chevronSpeechShape(rs) && c.isSpokenChevronLine(t) { chevronSpeech++ } } @@ -426,7 +458,7 @@ func (c *Checkers) lintNumberMagnitude(source, final string) (int, []string) { if len(wordRanges) == 0 { return 0, nil } - return 1, []string{fmt.Sprintf("the source magnitude 10^%d (万/億) is not reflected in the translation's orders of magnitude — a possible magnitude error (e.g. 三万→«три миллиона»)", maxSrc)} + return 1, []string{fmt.Sprintf("the source magnitude 10^%d is not reflected in the translation's orders of magnitude — a possible magnitude error", maxSrc)} } // isCJKNumeralRune reports whether r can form a CJK numeral expression: an ASCII digit or a shared diff --git a/backend/internal/checks/cheapgates_test.go b/backend/internal/checks/cheapgates_test.go index 684abaeb..c9ebe6db 100644 --- a/backend/internal/checks/cheapgates_test.go +++ b/backend/internal/checks/cheapgates_test.go @@ -2,6 +2,8 @@ package checks import ( "testing" + + "textmachine/backend/internal/lang" ) // --- dialogue-dash -------------------------------------------------------------- @@ -18,7 +20,20 @@ func TestLintDialogueDash(t *testing.T) { {"straight quote dialogue", "\"Привет\", — сказал он.", 1}, {"hyphenated word not dialogue", "Он сделал это как-то по-своему.\n-то не считается.", 0}, {"list-like dash not dialogue", "-1 балл\n-2 очка", 0}, - {"mixing em-dash and chevrons", "— Реплика первая.\n«Реплика вторая», — подумал он.", 1}, + // Mini-run FP (25.07), now pinned as NON-firing: a chevron line whose attribution is an INNER-SPEECH + // verb is a THOUGHT, and Russian sets thought in «…» beside spoken «—» on purpose. Flagging it + // called correct typography a style clash (8 of 8 hits on the 10-chapter run were this shape). + {"inner speech beside dialogue is correct typography", "— Реплика первая.\n«Реплика вторая», — подумал он.", 0}, + {"inner speech «про себя» beside dialogue", "— Идём.\n«Становится интереснее», — усмехнулся про себя Фан Юань.", 0}, + // Live 2-chapter verification run (26.07): these two attributions are what made the thought-verb + // polarity untenable — neither «понимал» nor «размышлял» is enumerable in advance, while the + // speech-verb side is a closed list. An unrecognised attribution must be SILENCE, not a flag. + {"thought attributed with «понимал»", "— Идём.\n«Это корм для гу-червей», — прекрасно понимал Фан Юань.", 0}, + {"speech verb VETOED by «про себя»", "— Идём.\n«Это и есть гу Надежды», — пробормотал про себя Фан Юань.", 0}, + {"thought attributed with «размышлял»", "— Идём.\n«Он ведёт борьбу», — размышлял Фан Юань, — «и ему нужен внук».", 0}, + // …and the rule must still fire on genuine SPOKEN chevron dialogue mixed with dashes, else the + // discriminator would have silently disabled the whole check. + {"spoken chevrons mixed with dashes still flagged", "— Реплика первая.\n«Реплика вторая», — сказал он.", 1}, {"chevron only no mixing", "«Цитата из книги».\nОбычный текст.", 0}, {"chevron term in prose not flagged", "Это была «Война и мир».", 0}, // D20.4 FP1: a chevron CITATION/definition at line start (« … » — prose, NO attribution comma) @@ -30,7 +45,7 @@ func TestLintDialogueDash(t *testing.T) { } for _, c := range cases { t.Run(c.name, func(t *testing.T) { - n, _ := lintDialogueDash(c.text) + n, _ := lintDialogueDash(c.text, CompileCheckers(nil, lang.TargetChecksFor("ru"))) if n != c.want { t.Errorf("lintDialogueDash(%q) = %d, want %d", c.text, n, c.want) } @@ -186,3 +201,31 @@ func TestRunCheapGatesCombined(t *testing.T) { t.Errorf("expected all four gates to fire, total=%d detail=%v", r.Total(), r.Detail) } } + +// TestLintDialogueDashInertWithoutTargetData pins the "no data → inert, never a crash" contract of the +// chevron-mixing rule, which the D-pack mutation run exposed as untested: with a nil checker spec (a book +// whose target ships no readability data — the no-pack path the golden fixture rides) the rule must still +// run and simply never count a chevron line. A nil-receiver dereference here would take down the whole +// cheap-gate battery on exactly the books that have the least data to spare. +func TestLintDialogueDashInertWithoutTargetData(t *testing.T) { + mixed := "— Реплика первая.\n«Реплика вторая», — сказал он." + for _, tc := range []struct { + name string + c *Checkers + }{ + {"nil spec", nil}, + {"spec with no target data", CompileCheckers(nil, lang.TargetChecks{})}, + } { + t.Run(tc.name, func(t *testing.T) { + n, _ := lintDialogueDash(mixed, tc.c) // must not panic + if n != 0 { + t.Fatalf("without speech-verb data the mixing rule must stay silent, got %d", n) + } + // The data-independent half of the rule must still work: a hyphen-led speech line is a + // defect in any target that uses dashes, and it needs no vocabulary to spot. + if n, _ := lintDialogueDash("- Привет, — сказал он.", tc.c); n == 0 { + t.Fatal("the hyphen-instead-of-dash half must keep firing without target data") + } + }) + } +} diff --git a/backend/internal/checks/checkers.go b/backend/internal/checks/checkers.go index 0f20fd9e..c8801ad1 100644 --- a/backend/internal/checks/checkers.go +++ b/backend/internal/checks/checkers.go @@ -31,10 +31,18 @@ import ( // tables + the target-general lists, resolved from the langpack. A nil receiver, a nil pattern or an empty // table leaves that sub-checker inert. Built once per run (CompileCheckers), carried in CheapGateConfig. type Checkers struct { - numeral map[rune]int - ruHours map[string]int - registerNeg []string - brokenSuffix []string // target-general (any →ru output) + numeral map[rune]int + ruHours map[string]int + registerNeg []string + brokenSuffix []string // target-general (any →ru output) + // speechVerb is the target's SPOKEN-attribution vocabulary («сказал», «ответил», «крикнул»): the + // verbs that make a chevron-quoted line real dialogue rather than a thought. It is target data + // because the convention it encodes — thought in «…», speech aloud with «—» — is a fact about the + // target's typography, not about the source pair or the algorithm. + speechVerb []string + // innerMarker vetoes speechVerb: a qualifier («про себя», «мысленно») that makes the line inner + // speech even under a speech verb — «пробормотал про себя» is a thought said with a speaking verb. + innerMarker []string yoHomograph map[string]bool // target-general: ё↔е homograph whitelist (cheapgates yofikator) magnitudeStem map[string]int // target-general: ru magnitude word stem → base-10 exponent (cheapgates) @@ -54,6 +62,14 @@ type Checkers struct { // it is the positive post-condition a fractional-unit REPAIR must satisfy, and it lives with its // siblings because "how this target renders the double-hour" is one fact with one home. hourWordRE *regexp.Regexp + + // dc1UnitHours is how many target hours ONE source time-unit is worth (zh 时辰 = 2). It used to be a + // literal `n * 2` in the DC1 algorithm — a pair fact hiding in generic Go, which would silently lie to a + // pair whose unit is not a double-hour. Data now (nit of generality, pre-run hygiene 25.07). + dc1UnitHours int + // msg holds the pair's DETAIL templates ({name} placeholders). Every pair-gated checker renders its + // detail line from here instead of a Go format string, so the engine file carries no pair literal. + msg map[string]string } // CompileCheckers resolves the checker spec from the pair pack (dc) and the target data (tc). A malformed @@ -62,6 +78,8 @@ type Checkers struct { func CompileCheckers(dc *lang.DCCheckerData, tc lang.TargetChecks) *Checkers { c := &Checkers{ brokenSuffix: tc.List("broken_suffix"), + speechVerb: tc.List("speech_verb"), + innerMarker: tc.List("inner_marker"), yoHomograph: listToSet(tc.List("yo_homograph")), magnitudeStem: listToStemExp(tc.List("magnitude_stem")), } @@ -80,10 +98,56 @@ func CompileCheckers(dc *lang.DCCheckerData, tc lang.TargetChecks) *Checkers { c.halfShichenRE = mustPairRE(p, "halfshichen_re") c.halfShichenFireWord = p["halfshichen_fire_word"] c.hourWordRE = mustPairRE(p, "hour_word_re") + c.dc1UnitHours = mustPairRatio(dc.Ratios, "dc1_unit_in_hours") + c.msg = mustPairMessages(dc.Messages, + "dc1_fractional", "dc1_counted", "dc2_qianwan", "dc2_shushiwan", "dc6_register", "percent_scale") } return c } +// mustPairRatio reads a required numeric pair relation. A missing/zero ratio is a corrupt pack, not a +// tolerable default: the DC1 comparison is MEANINGLESS without it (a silent 0 would make every rendering +// look wrong), so it fails the same way a malformed regex does — loudly, at compile, named. +func mustPairRatio(ratios map[string]int, key string) int { + v, ok := ratios[key] + if !ok || v <= 0 { + panic(fmt.Sprintf("checks: pair pack dc-checkers is missing the positive ratio %q (a detector without its unit relation cannot judge anything)", key)) + } + return v +} + +// mustPairMessages reads the required DETAIL templates. A pair that ships detection data but no message +// for it is a corrupt pack by the same argument: a checker that fires and cannot say WHAT it found produces +// an empty observability line, which reads downstream as "nothing was wrong". +func mustPairMessages(msgs map[string]string, keys ...string) map[string]string { + out := make(map[string]string, len(keys)) + for _, k := range keys { + v := msgs[k] + if strings.TrimSpace(v) == "" { + panic(fmt.Sprintf("checks: pair pack dc-checkers is missing the detail template %q (a detector with no message is a silent detector)", k)) + } + out[k] = v + } + return out +} + +// renderMsg substitutes `{name}` placeholders in a pair DETAIL template. kv is a flat name,value sequence +// (an odd tail is a programming error and panics — the templates and their call sites are compiled +// together). Pure and deterministic: one pass, no map iteration. +func renderMsg(tpl string, kv ...string) string { + if len(kv)%2 != 0 { + panic("checks: renderMsg wants name,value pairs") + } + if len(kv) == 0 { + return tpl + } + rep := make([]string, 0, len(kv)) + for i := 0; i < len(kv); i += 2 { + rep = append(rep, "{"+kv[i]+"}", kv[i+1]) + } + return strings.NewReplacer(rep...).Replace(tpl) +} + // listToSet turns an ordered value list into a membership set (target wordlists). func listToSet(xs []string) map[string]bool { m := make(map[string]bool, len(xs)) @@ -150,7 +214,7 @@ func (c *Checkers) lintTimeUnits(source, final string) (int, []string) { // a pair that ships neither key runs this inert, exactly as before the probe existed. if c.halfShichenRE != nil && c.halfShichenFireWord != "" && c.halfShichenRE.MatchString(source) && strings.Contains(final, c.halfShichenFireWord) { - return 1, []string{fmt.Sprintf("DC1 时辰 (fractional): 半个时辰 ≈ 1 h rendered as %q (half of it)", c.halfShichenFireWord)} + return 1, []string{renderMsg(c.msg["dc1_fractional"], "fire_word", c.halfShichenFireWord)} } if c.shichenRE == nil || c.ruHoursRE == nil { return 0, nil @@ -171,9 +235,11 @@ func (c *Checkers) lintTimeUnits(source, final string) (int, []string) { if !ok { return 0, nil } - expectedHours := n * 2 + expectedHours := n * c.dc1UnitHours if ruNum == n && ruNum != expectedHours { - return 1, []string{fmt.Sprintf("DC1 时辰: %d个时辰 rendered as «%d час…» (counted as hours) instead of ~%d h (1 时辰 = 2 h)", n, ruNum, expectedHours)} + return 1, []string{renderMsg(c.msg["dc1_counted"], + "n", strconv.Itoa(n), "rendered", strconv.Itoa(ruNum), + "expected", strconv.Itoa(expectedHours), "ratio", strconv.Itoa(c.dc1UnitHours))} } return 0, nil } @@ -224,12 +290,12 @@ func (c *Checkers) lintMagnitudeScale(source, final string) (int, []string) { if c.qianwanOKRE != nil && c.qianwanSrc != "" && c.qianwanFireWord != "" && c.qianwanVetoWord != "" && strings.Contains(source, c.qianwanSrc) && !c.qianwanOKRE.MatchString(final) { if strings.Contains(final, c.qianwanFireWord) && !strings.Contains(final, c.qianwanVetoWord) { // case-sensitive, per reference - flags = append(flags, "DC2 千万=10^7 rendered as «тысячи» (≈10000× under) — a possible magnitude error (hyperbole risk, §5-A4)") + flags = append(flags, c.msg["dc2_qianwan"]) } } if c.shushiwanOKRE != nil && c.shushiwanFireRE != nil && c.shushiwanSrc != "" && strings.Contains(source, c.shushiwanSrc) && !c.shushiwanOKRE.MatchString(final) { if c.shushiwanFireRE.MatchString(final) { - flags = append(flags, "DC2 数十万≈several×10^5 rendered as «десятки тысяч» (≈10× under)") + flags = append(flags, c.msg["dc2_shushiwan"]) } } return len(flags), flags @@ -265,7 +331,7 @@ func (c *Checkers) lintRegisterLexicon(final string) (int, []string) { hits = append(hits, w) } sort.Strings(hits) - return len(hits), []string{"DC6 register: fairy-tale Russian lexis out of the xianxia genre: " + strings.Join(hits, ", ")} + return len(hits), []string{renderMsg(c.msg["dc6_register"], "hits", strings.Join(hits, ", "))} } // isCyrLetter reports whether r is a Cyrillic letter (the word boundary for the register match). @@ -299,7 +365,8 @@ func (c *Checkers) lintPercentScale(source, final string) (int, []string) { pct += ones } } - return 1, []string{fmt.Sprintf("成-percent: %s成%s = %d%% rendered as a decimal fraction instead of a percentage (~%d%%)", m[1], m[2], pct, pct)} + return 1, []string{renderMsg(c.msg["percent_scale"], + "tens", m[1], "ones", m[2], "pct", strconv.Itoa(pct))} } // --- Latin residue in the Russian output ----------------------------------------------------------- @@ -340,7 +407,7 @@ func lintLatinResidue(final string, allow map[string]bool) (int, []string) { surfaces = append(surfaces, s) } sort.Strings(surfaces) - return len(surfaces), []string{"Latin word left untranslated in the Russian output: " + strings.Join(surfaces, ", ")} + return len(surfaces), []string{"Latin word left untranslated in the target output: " + strings.Join(surfaces, ", ")} } // isLatinLetterOrDigit reports whether r is an ASCII Latin letter or digit (the alphanumeric-token alphabet). @@ -378,10 +445,39 @@ func (c *Checkers) lintBrokenWord(final string) (int, []string) { for _, suf := range c.brokenSuffix { if wl >= 4 && strings.HasSuffix(w, suf) && !seen[w] { seen[w] = true - det = append(det, "malformed word ending in «-"+suf+"» (no valid Russian word does): "+w) + det = append(det, "malformed word: the target has no well-formed word ending in «-"+suf+"»: "+w) } } } sort.Strings(det) return len(det), det } + +// isSpokenChevronLine reports whether a chevron-led line is SPOKEN dialogue, by looking for one of the +// target's speech-attribution verbs in it. +// +// The polarity is the load-bearing choice. Asking "is this a THOUGHT?" needs an open-ended list — a live +// 2-chapter run produced «понимал» and «размышлял» within minutes, and thought verbs have no natural +// boundary — and every miss becomes a false flag on correct typography. Asking "is this SPOKEN?" needs a +// small closed list, and every miss becomes SILENCE on a real style clash. Precision over recall is the +// gate's stated bias, so the second failure mode is the right one to have. +// +// A nil receiver / a target with no `speech_verb` data returns false: the mixing rule then never fires, +// which is the same "inert without data" contract the pair checkers follow. +func (c *Checkers) isSpokenChevronLine(line string) bool { + if c == nil || len(c.speechVerb) == 0 { + return false + } + low := strings.ToLower(line) + for _, marker := range c.innerMarker { + if strings.Contains(low, marker) { + return false // «пробормотал ПРО СЕБЯ» — a speaking verb qualified into a thought + } + } + for _, verb := range c.speechVerb { + if strings.Contains(low, verb) { + return true + } + } + return false +} diff --git a/backend/internal/checks/checkers_pairdata_test.go b/backend/internal/checks/checkers_pairdata_test.go new file mode 100644 index 00000000..2ca77aeb --- /dev/null +++ b/backend/internal/checks/checkers_pairdata_test.go @@ -0,0 +1,155 @@ +package checks + +import ( + "strings" + "testing" + + "textmachine/backend/internal/lang" +) + +// checkers_pairdata_test.go: the generality nit taken with the pre-run hygiene pack (25.07) — the DC +// checkers' UNIT RELATION and DETAIL TEXT are pair DATA, not Go literals. The review question the +// norm asks ("does it work for a pair that is not in the repo yet, with no Go edit?") had two known +// NO answers here: `n * 2` (1 时辰 = 2 h, true of Chinese and of nothing else) and detail strings +// naming 时辰/千万/成 from a file that pair-14 declared free of language literals. +// +// These tests prove the answer is now YES by running the SAME algorithm over a SYNTHETIC pair whose +// data says something different — which is exactly what a second pair would be. + +// synthPack builds a DCCheckerData for an imaginary pair: same detection shapes as zh-ru (so the +// algorithm has something to match) but a unit worth `hours` target hours and its own message text. +func synthPack(hours int) *lang.DCCheckerData { + return &lang.DCCheckerData{ + Numeral: map[rune]int{'三': 3}, + RuHours: map[string]int{"три": 3}, + RegisterNeg: []string{"терем"}, + Patterns: map[string]string{ + "shichen_re": `([0-9三])\s*个?\s*时辰`, + "ru_hours_re": `(\d+|три)\s+час(?:а|ов)?(?:[^а-яёА-ЯЁ]|$)`, + "cheng_re": `([0-9三])成([0-9三]?)`, + "decimal_fraction_re": `десят(?:ая|ых)`, + "qianwan_ok_re": `(?i)десят\p{L}* миллион`, + "shushiwan_ok_re": `(?i)сотн\p{L}* тысяч`, + "shushiwan_fire_re": `десятк\p{L}* тысяч`, + "qianwan_src": "千万", + "qianwan_fire_word": "тысяч", + "qianwan_veto_word": "миллион", + "shushiwan_src": "数十万", + "percent_word": "процент", + "halfshichen_re": `半\s*个?\s*时辰`, + "halfshichen_fire_word": "полчаса", + "hour_word_re": `(?:^|[^а-яёА-ЯЁ])час(?:а|ов)?(?:[^а-яёА-ЯЁ]|$)`, + }, + Ratios: map[string]int{"dc1_unit_in_hours": hours}, + Messages: map[string]string{ + "dc1_fractional": "SYNTH-FRACTIONAL {fire_word}", + "dc1_counted": "SYNTH-COUNTED n={n} rendered={rendered} expected={expected} ratio={ratio}", + "dc2_qianwan": "SYNTH-QIANWAN", + "dc2_shushiwan": "SYNTH-SHUSHIWAN", + "dc6_register": "SYNTH-REGISTER {hits}", + "percent_scale": "SYNTH-PERCENT {tens}/{ones}/{pct}", + }, + } +} + +// TestDC1UnitRatioIsPairData: with the SAME text and the SAME patterns, a pair whose unit is worth 1 +// target hour must stay silent where a double-hour pair fires. Under the old `n * 2` this was +// impossible to express without editing Go — the checker would have told the 1:1 pair that its correct +// rendering was a defect. +func TestDC1UnitRatioIsPairData(t *testing.T) { + const src, tgt = "他闭关了三个时辰。", "Он затворился на три часа." + if n, _ := CompileCheckers(synthPack(2), lang.TargetChecks{}).lintTimeUnits(src, tgt); n != 1 { + t.Fatalf("a 2:1 pair must flag «три часа» for 三个时辰, got %d flags", n) + } + if n, det := CompileCheckers(synthPack(1), lang.TargetChecks{}).lintTimeUnits(src, tgt); n != 0 { + t.Fatalf("a 1:1 pair renders 三个时辰 as «три часа» CORRECTLY; the checker must be silent, got %d: %v", n, det) + } +} + +// TestCheckerDetailTextComesFromPairData: every pair-gated detail line must be the PACK's sentence. +// A Go-authored fallback surviving anywhere would show up here as a message the synthetic pack never +// wrote — i.e. as a second pair being told about Chinese double-hours. +func TestCheckerDetailTextComesFromPairData(t *testing.T) { + c := CompileCheckers(synthPack(2), lang.TargetChecks{}) + for _, tc := range []struct { + name, want string + run func() (int, []string) + }{ + {"dc1-counted", "SYNTH-COUNTED n=3 rendered=3 expected=6 ratio=2", + func() (int, []string) { + return c.lintTimeUnits("他闭关了三个时辰。", "Он затворился на три часа.") + }}, + {"dc1-fractional", "SYNTH-FRACTIONAL полчаса", + func() (int, []string) { + return c.lintTimeUnits("他等了半个时辰。", "Он ждал полчаса.") + }}, + {"dc2-qianwan", "SYNTH-QIANWAN", + func() (int, []string) { + return c.lintMagnitudeScale("有千万条蛊虫。", "Там были тысячи червей.") + }}, + {"dc2-shushiwan", "SYNTH-SHUSHIWAN", + func() (int, []string) { + return c.lintMagnitudeScale("有数十万人。", "Там были десятки тысяч人.") + }}, + {"dc6-register", "SYNTH-REGISTER терем", + func() (int, []string) { return c.lintRegisterLexicon("Он вошёл в терем.") }}, + {"percent", "SYNTH-PERCENT 三//30", + func() (int, []string) { + return c.lintPercentScale("他用了三成力。", "Он использовал три десятых силы.") + }}, + } { + t.Run(tc.name, func(t *testing.T) { + n, det := tc.run() + if n == 0 { + t.Fatalf("fixture did not fire — the assertion below would be vacuous") + } + if len(det) == 0 || det[0] != tc.want { + t.Fatalf("detail = %q, want %q (the pack's own sentence)", det, tc.want) + } + }) + } +} + +// TestCorruptPairPackFailsLoud: a pack that ships detection data but no unit relation / no message is +// CORRUPT, and must die at compile like a malformed regex does. The tolerant alternative (default to 2, +// or emit an empty detail) is worse than a crash: a silent detector reads downstream as "nothing was +// wrong", and a defaulted ratio silently re-imports the very literal this nit removed. +func TestCorruptPairPackFailsLoud(t *testing.T) { + for _, tc := range []struct { + name, wantMsg string + mutate func(d *lang.DCCheckerData) + }{ + {"no ratio", "dc1_unit_in_hours", func(d *lang.DCCheckerData) { delete(d.Ratios, "dc1_unit_in_hours") }}, + {"zero ratio", "dc1_unit_in_hours", func(d *lang.DCCheckerData) { d.Ratios["dc1_unit_in_hours"] = 0 }}, + {"no message", "dc6_register", func(d *lang.DCCheckerData) { delete(d.Messages, "dc6_register") }}, + {"blank message", "dc1_counted", func(d *lang.DCCheckerData) { d.Messages["dc1_counted"] = " " }}, + } { + t.Run(tc.name, func(t *testing.T) { + d := synthPack(2) + tc.mutate(d) + defer func() { + rec := recover() + if rec == nil { + t.Fatalf("a pack missing %s compiled silently", tc.wantMsg) + } + if !strings.Contains(rec.(string), tc.wantMsg) { + t.Fatalf("panic does not name the missing key %q: %v", tc.wantMsg, rec) + } + }() + CompileCheckers(d, lang.TargetChecks{}) + }) + } +} + +// TestNoPackStaysInert re-pins the no-langpack path across this change: a book with no pair data (the +// ja→ru golden fixture) must compile a spec that fires nothing rather than panicking for want of the +// new required keys. +func TestNoPackStaysInert(t *testing.T) { + c := CompileCheckers(nil, lang.TargetChecks{}) + if n, _ := c.lintTimeUnits("他闭关了三个时辰。", "Он затворился на три часа."); n != 0 { + t.Fatalf("a book with no pair pack must run the pair checkers inert, got %d", n) + } + if n, _ := c.lintRegisterLexicon("Он вошёл в терем."); n != 0 { + t.Fatalf("register check must be inert with no pack, got %d", n) + } +} diff --git a/backend/internal/lang/data/target-ru.txt b/backend/internal/lang/data/target-ru.txt index 13d9d514..7fd93207 100644 --- a/backend/internal/lang/data/target-ru.txt +++ b/backend/internal/lang/data/target-ru.txt @@ -7,6 +7,50 @@ # # broken-word: a Russian word ending in this suffix is a mangled infinitive (no valid word ends in «-йть»). broken_suffix йть +# speech-verb attribution: the verbs that make a chevron-quoted line SPOKEN dialogue rather than a +# thought. The dialogue-dash MIXING rule counts a chevron line only when its attribution carries one of +# these, i.e. the DEFAULT for an unrecognised attribution is "not a style clash" — precision over recall, +# the gate's stated bias. The inverse list (thought verbs) was tried first and proved unbounded: a live +# 2-chapter run immediately produced «понимал» and «размышлял», which no enumeration of thought verbs +# would have covered, while the speech-verb side is small and closed. Lower-cased, substring match. +speech_verb сказал +speech_verb сказала +speech_verb ответил +speech_verb ответила +speech_verb спросил +speech_verb спросила +speech_verb крикнул +speech_verb крикнула +speech_verb воскликнул +speech_verb воскликнула +speech_verb прошептал +speech_verb прошептала +speech_verb пробормотал +speech_verb пробормотала +speech_verb проговорил +speech_verb проговорила +speech_verb продолжил +speech_verb продолжила +speech_verb добавил +speech_verb добавила +speech_verb возразил +speech_verb возразила +speech_verb позвал +speech_verb позвала +speech_verb объявил +speech_verb объявила +speech_verb произнёс +speech_verb произнес +speech_verb произнесла +# inner-speech VETO markers: a qualifier that makes a line inner speech even when its attribution verb +# is a speech verb — «пробормотал ПРО СЕБЯ» is a thought, not a spoken turn. Checked after speech_verb, +# so the pair (verb + veto) reproduces the writer's actual intent instead of the verb alone. Found by the +# 26.07 live verification: it was the single flag surviving on the real 10-chapter export. +inner_marker про себя +inner_marker мысленно +inner_marker в мыслях +inner_marker себе под нос +inner_marker в уме # yofikator homograph whitelist: е-spellings that are DISTINCT words from their ё-counterpart (все≠всё, # небо≠нёбо, берет≠берёт …), so the inconsistency rule must NOT flag «все»+«всё» as one word two ways. yo_homograph все diff --git a/backend/internal/lang/langpack.go b/backend/internal/lang/langpack.go index 2d1dd93d..9693ecb8 100644 --- a/backend/internal/lang/langpack.go +++ b/backend/internal/lang/langpack.go @@ -109,6 +109,17 @@ type DCCheckerData struct { // The `*_re` keys are compiled by the consumer; the rest are literal strings.Contains probes. A pair that // ships no pattern for a key runs that sub-checker inert. Patterns map[string]string + // Ratios are the pair's numeric UNIT RELATIONS as data (nit of generality, pre-run hygiene 25.07): the + // checker algorithm compares counts, but "one source time-unit equals N target hours" is a fact ABOUT THE + // PAIR, not about the algorithm — 时辰=2h for zh, and a pair using another unit (刻=¼h) must not need a Go + // edit to say so. key → integer. + Ratios map[string]int + // Messages are the checkers' human-readable DETAIL templates as data (same nit): a detail line naming + // 时辰/千万/成 is pair SUBSTANCE, and a second pair shipping its own patterns would otherwise inherit a + // message about Chinese double-hours. `{name}` placeholders are substituted by the consumer; the template + // is verbatim. Required whenever the corresponding detection data is present (a detector that fires with + // no way to say what it found is a corrupt pack). + Messages map[string]string } // HeadingRule is the per-pair data for the chapter-title policy: instead of letting the model render a @@ -657,25 +668,34 @@ func unionKeysAsSet(dst map[string]bool, src map[string]string) { // parseDCCheckers reads the DC-checker pair tables from category-keyed lines (checkers_zh_ru.go data): // // cjk_numeralrunevalue | ru_hourwordvalue | register_neglexeme +// patternkeyvalue (VERBATIM) | msgkeytemplate (VERBATIM) | dc_ratiokeyint // // Scans raw lines so an error names the PHYSICAL file line. Fail-loud on a bad field count / non-integer // value / unknown category (a malformed table is a corrupt pack). RegisterNeg keeps its authored order. func parseDCCheckers(b []byte) (*DCCheckerData, error) { - d := &DCCheckerData{Numeral: map[rune]int{}, RuHours: map[string]int{}, Patterns: map[string]string{}} + d := &DCCheckerData{ + Numeral: map[rune]int{}, RuHours: map[string]int{}, + Patterns: map[string]string{}, Ratios: map[string]int{}, Messages: map[string]string{}, + } for i, raw := range strings.Split(string(b), "\n") { - // A `pattern` line's VALUE is verbatim (a regex may carry trailing metachars), so only \r is stripped - // from the whole line, not the value; other categories tolerate the trimmed form. + // A `pattern` / `msg` line's VALUE is verbatim (a regex may carry trailing metachars, a message its + // own spacing), so only \r is stripped from the whole line, not the value; other categories tolerate + // the trimmed form. line := strings.TrimRight(raw, "\r") t := strings.TrimSpace(line) if t == "" || strings.HasPrefix(t, "#") { continue } - if strings.HasPrefix(line, "pattern\t") { + if strings.HasPrefix(line, "pattern\t") || strings.HasPrefix(line, "msg\t") { f := strings.SplitN(line, "\t", 3) // value (f[2]) VERBATIM if len(f) != 3 || strings.TrimSpace(f[1]) == "" || f[2] == "" { - return nil, fmt.Errorf("line %d: pattern wants `patternkeyvalue`, got %q", i+1, line) + return nil, fmt.Errorf("line %d: %s wants `%skeyvalue`, got %q", i+1, f[0], f[0], line) + } + if f[0] == "msg" { + d.Messages[f[1]] = f[2] + } else { + d.Patterns[f[1]] = f[2] } - d.Patterns[f[1]] = f[2] continue } f := strings.Split(t, "\t") @@ -707,8 +727,17 @@ func parseDCCheckers(b []byte) (*DCCheckerData, error) { return nil, fmt.Errorf("line %d: register_neg wants `register_neglexeme`, got %q", i+1, t) } d.RegisterNeg = append(d.RegisterNeg, f[1]) + case "dc_ratio": + if len(f) != 3 { + return nil, fmt.Errorf("line %d: dc_ratio wants `dc_ratiokeyvalue`, got %q", i+1, t) + } + v, err := strconv.Atoi(strings.TrimSpace(f[2])) + if err != nil { + return nil, fmt.Errorf("line %d: dc_ratio value %q: %w", i+1, f[2], err) + } + d.Ratios[f[1]] = v default: - return nil, fmt.Errorf("line %d: unknown category %q (want cjk_numeral|ru_hour|register_neg)", i+1, f[0]) + return nil, fmt.Errorf("line %d: unknown category %q (want cjk_numeral|ru_hour|register_neg|dc_ratio|pattern|msg)", i+1, f[0]) } } if len(d.Numeral) == 0 || len(d.RuHours) == 0 || len(d.RegisterNeg) == 0 { diff --git a/backend/internal/miner/miner_emit.go b/backend/internal/miner/miner_emit.go index 6cee060a..f46c0d5a 100644 --- a/backend/internal/miner/miner_emit.go +++ b/backend/internal/miner/miner_emit.go @@ -3,6 +3,7 @@ package miner import ( "fmt" "sort" + "strings" "gopkg.in/yaml.v3" @@ -169,21 +170,41 @@ func MineBank(chunks []Chunk, contrast *Contrast, seed []store.GlossaryEntry, re return out } -// DeltaYAML serializes the mined delta into the seed.Term YAML schema (§C2-7) — the owner-sign -// artifact and the `tmctl seed-lint` input. Every term is status:auto with NO dst (WHICH-only, default -// B; the WHAT is delivered by the banknote at the bank-mining stop). It reuses the EXISTING seed.Term schema (no new -// fields — §3(б)); evidence / zones live in the sidecar sign-map, not the seed. The output is guaranteed -// loadable by loadGlossarySeed (status:auto may lack a dst); seed-lint proves it against the real loader. -func DeltaYAML(mined []Term) (string, error) { +// DeltaYAML serializes the mined delta into the seed.Term YAML schema (§C2-7) — the owner-sign artifact +// and the `tmctl seed-lint` input. The miner supplies the WHICH; `proposals` (keyed by normalized src) +// supplies the WHAT the banknote channel collected during the draft wave, so the owner signs a term that +// already carries the translator's proposed rendering instead of a bare source surface. +// +// Every term stays status:auto — that is the load-bearing line. Attaching a dst delivers EVIDENCE, not +// trust: an auto term is outside the CONFIRMED-only injection (D39.2 trust gate), so nothing the model +// proposed reaches the book until the owner signs it. Before this join the parsed dst was dropped into +// `_` (D39.36) and the owner met the stop holding bare terms, having to invent the Russian for a word +// the model had already rendered. +// +// nil/empty proposals reproduce the previous output BYTE FOR BYTE (WHICH-only, no dst), so a book +// running with the channel off is unaffected. Alternative renderings are listed in the note with their +// chunk counts — the disagreement IS the reason to sign (one term came back three ways in the 25.07 +// mini-run). It reuses the EXISTING seed.Term schema (no new fields — §3(б)); evidence / zones live in +// the sidecar sign-map. The output is guaranteed loadable by loadGlossarySeed (status:auto may lack a +// dst); seed-lint proves it against the real loader. +func DeltaYAML(mined []Term, proposals map[string][]DstProposal) (string, error) { sf := seed.File{} for _, m := range mined { st := seed.Term{Src: m.Src, Type: m.Type, Status: "auto", SinceCh: m.SinceCh} for _, a := range m.Aliases { st.Aliases = append(st.Aliases, seed.Alias{Alias: a, Type: "mined"}) } + note := "" if len(m.Evidence) > 0 { - st.Note = "mined WHICH candidate; evidence: " + fmt.Sprint(m.Evidence) + note = "mined WHICH candidate; evidence: " + fmt.Sprint(m.Evidence) } + // The join key is the miner's own normalized surface, so a proposal written with different + // orthography still lands on its term (the same normalization the seed-surface exclusion uses). + if props := proposals[text.NormalizeSourceKey(m.Src)]; len(props) > 0 { + st.Dst = props[0].Dst + note = appendProposalNote(note, props) + } + st.Note = note sf.Terms = append(sf.Terms, st) } b, err := yaml.Marshal(sf) @@ -193,6 +214,28 @@ func DeltaYAML(mined []Term) (string, error) { return string(b), nil } +// appendProposalNote documents WHERE the attached dst came from and what else was proposed. The +// provenance line is not decoration: a dst in a sign map that does not say "a model proposed this, +// nobody approved it" invites the owner to read it as already-canon. +func appendProposalNote(note string, props []DstProposal) string { + var b strings.Builder + if note != "" { + b.WriteString(note) + b.WriteString("; ") + } + fmt.Fprintf(&b, "dst PROPOSED by the translator (banknote, %d chunk(s)) — NOT approved, sign or replace it", props[0].Chunks) + if len(props) > 1 { + b.WriteString("; other proposals: ") + for i, p := range props[1:] { + if i > 0 { + b.WriteString(", ") + } + fmt.Fprintf(&b, "%q ×%d", p.Dst, p.Chunks) + } + } + return b.String() +} + // emissionEligible applies the build_precision30 filters: type ∈ {name,place,title}, freq ≥ 5, src not // subsumed, len ≥ 2 rune, not a boundary fragment. func emissionEligible(c scoredCand, subsumed, seedSurfaces map[string]bool, pack *lang.Pack) bool { @@ -258,3 +301,16 @@ func aliasStrings(as []store.GlossaryAlias) []string { } return out } + +// DstProposal is one PROPOSED rendering of a mined term, delivered by the banknote channel (WS4) and +// attached to the signature map at sign time. It is evidence for the owner, never a canon: the emitted +// term keeps status:auto, so a proposal enters the bank only through a signature. +// +// Chunks is how many chunks proposed this exact rendering — the disagreement signal. One term coming +// back with three renderings is the drift a canon closes, and hiding that behind a single "winner" +// would throw away the reason to sign it at all. +type DstProposal struct { + Dst string + Type string + Chunks int +} diff --git a/backend/internal/obs/logging.go b/backend/internal/obs/logging.go index 596fdeba..3bce8753 100644 --- a/backend/internal/obs/logging.go +++ b/backend/internal/obs/logging.go @@ -2,9 +2,11 @@ package obs import ( "context" + "fmt" "log/slog" "os" "strings" + "unicode/utf8" ) // NewLogger builds the process logger from the environment: LOG_LEVEL @@ -86,13 +88,34 @@ func LogLLMExchange(ctx context.Context, log *slog.Logger, provider string, reqB // llmBodyLogMax caps each logged model body: keeps a typical prompt/answer // readable while staying bounded. A constant, not a knob — it only bounds log // volume on an opt-in debug path. -const llmBodyLogMax = 4096 +// +// The cap is in BYTES, and a Cyrillic answer spends two of them per character, so 4096 was ~2000 +// characters — less than one chunk. That mattered more than log volume: the 25.07 mini-run turned this +// channel on precisely to read the banknote block, which a model emits at the very END of its answer, +// and the tail was exactly what the cap removed (the block had to be recovered from the store instead). +// Raised, and — more importantly — the truncation now keeps BOTH ends (see truncateForLog). +const llmBodyLogMax = 32768 +// truncateForLog bounds a logged body while keeping BOTH of its ends. A tail-only cut is the wrong +// shape for this data: the interesting parts of a model exchange sit at the extremes — the instruction +// head and, for structured channels like the banknote, the block the model appends LAST. Cutting the +// middle keeps a cut body diagnosable instead of merely short, and the elision says how much went. func truncateForLog(b []byte, maxBytes int) string { - if len(b) > maxBytes { - return string(b[:maxBytes]) + "…(truncated)" + if len(b) <= maxBytes { + return string(b) } - return string(b) + // Split the budget between head and tail; the marker names the dropped size so a reader can tell a + // truncated body from a short one. Cuts land on rune boundaries so the log stays valid UTF-8. + head := maxBytes / 2 + tail := maxBytes - head + for head > 0 && !utf8.RuneStart(b[head]) { + head-- + } + cut := len(b) - tail + for cut < len(b) && !utf8.RuneStart(b[cut]) { + cut++ + } + return string(b[:head]) + fmt.Sprintf("…(truncated %d bytes)…", cut-head) + string(b[cut:]) } func parseLogLevel(s string) slog.Level { diff --git a/backend/internal/obs/logging_truncate_test.go b/backend/internal/obs/logging_truncate_test.go new file mode 100644 index 00000000..f3236a5e --- /dev/null +++ b/backend/internal/obs/logging_truncate_test.go @@ -0,0 +1,56 @@ +package obs + +import ( + "strings" + "testing" +) + +// logging_truncate_test.go: the logged-body truncation keeps BOTH ends (mini-run finding Д3). The +// mini-run enabled LOG_LLM_BODIES specifically to read the banknote block a model appends at the END +// of its answer, and a tail-cut had removed exactly that — the debug channel was documented as the way +// to see it and could not show it. + +func TestTruncateForLogKeepsBothEnds(t *testing.T) { + body := []byte("HEAD-MARKER" + strings.Repeat("x", 5000) + "TAIL-MARKER") + got := truncateForLog(body, 200) + if !strings.Contains(got, "HEAD-MARKER") { + t.Errorf("the head must survive truncation:\n%s", got) + } + if !strings.Contains(got, "TAIL-MARKER") { + t.Errorf("the TAIL must survive truncation — the banknote block lives there:\n%s", got) + } + if !strings.Contains(got, "truncated") { + t.Errorf("a truncated body must say so (else it reads as a short one):\n%s", got) + } + if len(got) > 400 { + t.Errorf("truncation must actually bound the output, got %d bytes", len(got)) + } +} + +// TestTruncateForLogShortBodyUnchanged: a body under the cap is passed through byte-for-byte. +func TestTruncateForLogShortBodyUnchanged(t *testing.T) { + body := []byte("короткое тело ответа") + if got := truncateForLog(body, 4096); got != string(body) { + t.Errorf("a short body must be unchanged, got %q", got) + } +} + +// TestTruncateForLogCutsOnRuneBoundaries: the cut must not split a multi-byte rune, or the log line +// becomes invalid UTF-8 — the exact shape that makes a Cyrillic transcript unreadable in a viewer. +func TestTruncateForLogCutsOnRuneBoundaries(t *testing.T) { + for cap := 20; cap < 60; cap++ { + got := truncateForLog([]byte(strings.Repeat("ы", 400)), cap) + if !isValidUTF8(got) { + t.Fatalf("cap=%d produced invalid UTF-8: %q", cap, got) + } + } +} + +func isValidUTF8(s string) bool { + for _, r := range s { + if r == '�' { + return false + } + } + return true +} diff --git a/backend/internal/pipeline/banknote.go b/backend/internal/pipeline/banknote.go index dee0bff9..bda6f49e 100644 --- a/backend/internal/pipeline/banknote.go +++ b/backend/internal/pipeline/banknote.go @@ -3,14 +3,18 @@ package pipeline import ( "crypto/sha256" "encoding/hex" + "encoding/json" "fmt" "regexp" + "sort" "strings" "unicode" "textmachine/backend/internal/chunk" "textmachine/backend/internal/config" + "textmachine/backend/internal/miner" "textmachine/backend/internal/store" + "textmachine/backend/internal/text" ) // banknote.go: the banknote-v1 in-band footnote channel (WS4, plan §4 / research/20 §B3 / exp16 @@ -164,19 +168,116 @@ const bankTokenBudget = bankMaxLines * 12 // a truncated block is not trusted); the slice itself runs regardless, so the length/echo classify never // sees the footnote's Han src column. Deterministic (a pure function of the raw text + gate state). func (r *Runner) applyBanknote(role, rawText, finish string) (clean, stripped string, flags bankFlags) { + clean, stripped, flags, _ = r.applyBanknoteWithEntries(role, rawText, finish) + return clean, stripped, flags +} + +// applyBanknoteWithEntries is applyBanknote plus the PARSED entries — the WHAT itself. +// +// Until the mini-run of 25.07 the entries were dropped into `_` here (D39.36): the model proposed a +// rendering for every new term, the parser accepted it, and the code kept only the counters. The owner +// then met the bank-mining stop holding bare source terms and had to invent the Russian himself — the +// opposite of the ratified design ("the dst is the owner's to attach via the banknote at sign time", +// mining.go:20). Returning them costs nothing: the parse already ran. +// +// They stay OUT of the bank. A proposal is evidence for the SIGNATURE, never a canon: it reaches the +// owner's sign map as `status: auto` and becomes a term only when the owner signs it. That boundary is +// what keeps "the model suggested it" from silently becoming "the book uses it". +func (r *Runner) applyBanknoteWithEntries(role, rawText, finish string) (clean, stripped string, flags bankFlags, entries []bankEntry) { if role != roleTranslator || !r.Pipeline.Gates.Banknote.Enabled { - return rawText, "", bankFlags{} + return rawText, "", bankFlags{}, nil } cleanText, block := splitBanknote(rawText) if block == "" { - return rawText, "", bankFlags{} // no separator → no strip, no derived checkpoint (byte-identical path) + return rawText, "", bankFlags{}, nil // no separator → no strip, no derived checkpoint (byte-identical path) } if finish == "stop" { // finish=stop-only gate: accept candidates + count telemetry only for a complete generation. // truncatedGeneration=false is unreachable-otherwise in prod (a truncated gen is finish≠stop). - _, flags = parseBanknote(block, false) + entries, flags = parseBanknote(block, false) } - return cleanText, cleanText, flags + return cleanText, cleanText, flags, entries +} + +// bankProposal is one durable WHAT proposal as it travels from the draft to the signature map: the +// normalized source key (the miner's candidate space, so the join needs no fuzzy matching), the source +// surface as the model wrote it, the proposed rendering and the type. +type bankProposal struct { + SrcKey string `json:"k"` + Src string `json:"s"` + Dst string `json:"d"` + Type string `json:"t"` +} + +// bankProposalsJSON serializes a chunk's parsed entries for retrieval_state.banknote_detail. Empty +// entries → "" (the column stays empty on every channel-off chunk, so a banknote-free book's row bytes +// are unchanged). Deterministic: the parser's line order is preserved, nothing is sorted by map order. +func bankProposalsJSON(entries []bankEntry) string { + if len(entries) == 0 { + return "" + } + out := make([]bankProposal, 0, len(entries)) + for _, e := range entries { + key := text.NormalizeSourceKey(e.Src) + if key == "" || strings.TrimSpace(e.Dst) == "" { + continue // a proposal with no key or no rendering carries nothing to sign + } + out = append(out, bankProposal{SrcKey: key, Src: e.Src, Dst: strings.TrimSpace(e.Dst), Type: e.Type}) + } + if len(out) == 0 { + return "" + } + b, err := json.Marshal(out) + if err != nil { + return "" + } + return string(b) +} + +// bankProposalsByKey folds every chunk's stored proposals into src-key → proposals, counting how many +// chunks proposed each rendering. The COUNT is the signal the mini-run surfaced: the same term came back +// with three different renderings across chunks (学堂家老 → «старейшина школы» / «старейшина-наставник» / +// «учитель-старейшина»), which is precisely the drift a canon exists to close — so the owner sees the +// alternatives and their weight, not one arbitrary winner. +func bankProposalsByKey(states []store.RetrievalState) map[string][]miner.DstProposal { + votes := map[string]map[string]*miner.DstProposal{} + for _, rs := range states { + if rs.BanknoteDetail == "" { + continue + } + var props []bankProposal + if json.Unmarshal([]byte(rs.BanknoteDetail), &props) != nil { + continue // a corrupt detail blob is observability, never a reason to fail a run + } + for _, p := range props { + byDst := votes[p.SrcKey] + if byDst == nil { + byDst = map[string]*miner.DstProposal{} + votes[p.SrcKey] = byDst + } + if cur := byDst[p.Dst]; cur != nil { + cur.Chunks++ + continue + } + byDst[p.Dst] = &miner.DstProposal{Dst: p.Dst, Type: p.Type, Chunks: 1} + } + } + out := make(map[string][]miner.DstProposal, len(votes)) + for key, byDst := range votes { + list := make([]miner.DstProposal, 0, len(byDst)) + for _, p := range byDst { + list = append(list, *p) + } + // Deterministic order: most-proposed first, ties broken by the rendering itself (never map order). + sort.Slice(list, func(i, j int) bool { + if list[i].Chunks != list[j].Chunks { + return list[i].Chunks > list[j].Chunks + } + return list[i].Dst < list[j].Dst + }) + out[key] = list + } + return out } // bankDerivedHash is the CONTENT-ADDRESSED id of a banknote-stripped export checkpoint (§4б EXACT diff --git a/backend/internal/pipeline/bookrun.go b/backend/internal/pipeline/bookrun.go index 0d044447..4165a1e1 100644 --- a/backend/internal/pipeline/bookrun.go +++ b/backend/internal/pipeline/bookrun.go @@ -43,6 +43,10 @@ type StageResult struct { // BankFlags carries the translator draft's banknote telemetry (WS4 point 10): accepted line count + // parse-fail / truncation flags. Zero-valued on every non-translator stage and every channel-off run. BankFlags bankFlags + // BankProposals is the JSON of the same draft's PARSED banknote entries (the WHAT channel). It rides + // beside the counters because D39.36 showed counters alone are useless to the person who has to sign: + // they say a proposal existed, not what it was. + BankProposals string // Repair is the addressable-defect repair outcome of this stage (pack-16): zero for every stage that is // not the shipping one, and for every run with the gate off. Counters are DERIVED state — they are never // stored in their own row, because the durable artifacts (the role='repair' checkpoints and the diff --git a/backend/internal/pipeline/chunkrun.go b/backend/internal/pipeline/chunkrun.go index 65f0df55..4c76fdc2 100644 --- a/backend/internal/pipeline/chunkrun.go +++ b/backend/internal/pipeline/chunkrun.go @@ -102,13 +102,14 @@ var roleInjectionRenderers = map[string]injectionRenderer{ // selection + the post-check result. n_exact_hits/n_sticky/n_ambiguous count the INJECTED // records (what the model saw); spoiler/eviction are the dropped-and-logged totals; // post-check misses are recorded only when the translator actually produced text. -func (r *Runner) persistRetrievalState(snapID string, ch chunk.Chunk, sel membank.Selection, misses []membank.PostcheckMiss, outputChecked bool, cheap checks.CheapGateResult, bank bankFlags) error { +func (r *Runner) persistRetrievalState(snapID string, ch chunk.Chunk, sel membank.Selection, misses []membank.PostcheckMiss, outputChecked bool, cheap checks.CheapGateResult, bank bankFlags, bankProps string) error { rs := store.RetrievalState{ BookID: r.Book.BookID, Chapter: ch.Chapter, ChunkIdx: ch.ChunkIdx, SnapshotID: snapID, NStyleFlags: cheap.Total(), NBanknoteLines: bank.NLines, BanknoteParseFail: boolToStoreInt(bank.ParseFail), BanknoteTruncated: boolToStoreInt(bank.Truncated), + BanknoteDetail: bankProps, } if cheap.Total() > 0 { if b, err := json.Marshal(cheap); err == nil { diff --git a/backend/internal/pipeline/minerseed_contract_test.go b/backend/internal/pipeline/minerseed_contract_test.go index 3d379128..273156e6 100644 --- a/backend/internal/pipeline/minerseed_contract_test.go +++ b/backend/internal/pipeline/minerseed_contract_test.go @@ -60,7 +60,7 @@ func TestSeedLintEmittedMinedDelta(t *testing.T) { if len(mined) == 0 { t.Fatal("test premise broken: the fixture must propose at least one term (else the contract is vacuous)") } - yamlStr, err := miner.DeltaYAML(mined) + yamlStr, err := miner.DeltaYAML(mined, nil) if err != nil { t.Fatal(err) } @@ -78,3 +78,52 @@ func TestSeedLintEmittedMinedDelta(t *testing.T) { t.Errorf("loader returned %d entries for %d mined terms — the emission/loader schemas diverged", len(entries), len(mined)) } } + +// TestSeedLintEmittedMinedDeltaWithProposedDst extends the same contract to the WHAT-carrying form +// (D39.36 fix): a delta whose terms arrived with a translator-proposed dst must ALSO load through the +// real loader. The dst travels as evidence on a status:auto term, and a sign map that the loader +// refuses would be worse than the bare one it replaced. +func TestSeedLintEmittedMinedDeltaWithProposedDst(t *testing.T) { + src := text.NormalizeSourceKey(strings.Repeat("方源来到青茅山。方源很强。花家很大。花家的人。", 6)) + chunks := []miner.Chunk{{Chapter: 1, ChunkIdx: 0, NSource: src}} + seedRows := []store.GlossaryEntry{{Src: "青茅山", Dst: "гора Цинмао", Type: "place", Status: "approved", Source: "seed"}} + mined := miner.MineBank(chunks, minerContrast(t), seedRows, nil, miner.FrozenConfig(), testLangPack(t)) + if len(mined) == 0 { + t.Fatal("test premise broken: the fixture must propose at least one term") + } + proposals := map[string][]miner.DstProposal{} + for _, m := range mined { + proposals[text.NormalizeSourceKey(m.Src)] = []miner.DstProposal{ + {Dst: "Предложенный вариант", Type: "name", Chunks: 2}, + {Dst: "Другой вариант", Type: "name", Chunks: 1}, + } + } + yamlStr, err := miner.DeltaYAML(mined, proposals) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(yamlStr, "Предложенный вариант") { + t.Fatalf("the winning proposal must reach the sign map:\n%s", yamlStr) + } + if !strings.Contains(yamlStr, "NOT approved") { + t.Fatalf("the sign map must say the dst is a PROPOSAL, not a canon:\n%s", yamlStr) + } + p := writeMinedDelta(t, yamlStr) + if err := membank.SeedLint(p); err != nil { + t.Fatalf("a dst-carrying mined delta must lint clean, got:\n%v\n--- delta ---\n%s", err, yamlStr) + } + entries, err := membank.LoadGlossarySeed(p) + if err != nil { + t.Fatalf("the emitted delta must load: %v", err) + } + if len(entries) != len(mined) { + t.Errorf("loader returned %d entries for %d mined terms", len(entries), len(mined)) + } + // The proposal must NOT arrive pre-trusted: status stays auto, so the trust gate keeps it out of + // the injection until the owner signs. This is the whole safety boundary of the fix. + for _, e := range entries { + if e.Status != "auto" { + t.Errorf("term %q loaded with status %q — a PROPOSED dst must never arrive approved", e.Src, e.Status) + } + } +} diff --git a/backend/internal/pipeline/mining.go b/backend/internal/pipeline/mining.go index 5381c255..a95e9cad 100644 --- a/backend/internal/pipeline/mining.go +++ b/backend/internal/pipeline/mining.go @@ -73,7 +73,25 @@ func (r *Runner) runBankMiningStop(ctx context.Context, chunks []chunk.Chunk) (s } // Non-empty delta → write the owner signature map (the mined seed-delta YAML) and STOP before the edit wave. - yamlDelta, err := miner.DeltaYAML(mined) + // + // The WHAT join (D39.36 fix): the banknote proposals the draft wave collected are read off the durable + // per-chunk rows and attached to the map, so the owner signs a term that already carries the + // translator's proposed rendering. They arrive as EVIDENCE — every emitted term stays status:auto, so + // nothing proposed enters the bank without a signature. A read failure degrades to the previous + // WHICH-only map rather than blocking the stop: the map is what the owner needs, the dst is a bonus. + proposals := map[string][]miner.DstProposal{} + if states, serr := r.Store.RetrievalStatesForBook(r.Book.BookID); serr != nil { + r.Log.WarnContext(ctx, "bank-mining: could not read the banknote proposals; the signature map falls back to WHICH-only (bare terms)", "err", serr) + } else { + proposals = bankProposalsByKey(states) + } + withDst := 0 + for _, m := range mined { + if len(proposals[text.NormalizeSourceKey(m.Src)]) > 0 { + withDst++ + } + } + yamlDelta, err := miner.DeltaYAML(mined, proposals) if err != nil { return false, fmt.Errorf("pipeline: the bank-mining stop marshal mined delta: %w", err) } @@ -81,7 +99,7 @@ func (r *Runner) runBankMiningStop(ctx context.Context, chunks []chunk.Chunk) (s return false, fmt.Errorf("pipeline: the bank-mining stop write signature map %s: %w", r.signatureMapPath(), err) } r.Log.WarnContext(ctx, "bank-mining: new terms await owner signature; run STOPPED before the edit wave (review the signature map, then 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)", - "book", r.Book.BookID, "terms", len(mined), "signature_map", r.signatureMapPath()) + "book", r.Book.BookID, "terms", len(mined), "terms_with_proposed_dst", withDst, "signature_map", r.signatureMapPath()) return true, nil } diff --git a/backend/internal/pipeline/minirun_fixes_test.go b/backend/internal/pipeline/minirun_fixes_test.go new file mode 100644 index 00000000..a661c7eb --- /dev/null +++ b/backend/internal/pipeline/minirun_fixes_test.go @@ -0,0 +1,225 @@ +package pipeline + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "textmachine/backend/internal/obs" + "textmachine/backend/internal/store" +) + +// minirun_fixes_test.go: the two seams the 25.07 mini-run proved were producing a fact and losing it. +// Both are driven end-to-end through the real runner (norm 11) — a unit test on the parser would have +// passed happily before either fix, because neither defect was in the parsing. + +// --- Д1: an echo the escalation recovered must still be visible as an echo --------------------- + +// echoThenFixProvider makes the PRIMARY draft echo the CJK source (the D18 mine) and the escalation hop +// answer properly — the exact shape the mini-run hit on ch3/chunk0. +func echoThenFixProvider(echoText string) func(body string) (string, string) { + return func(body string) (string, string) { + switch { + case isEditBody(body): + return "ОТРЕДАКТИРОВАННЫЙ ПЕРЕВОД", "stop" + case strings.Contains(body, "fake-fallback"): + return "ПЕРЕВОД С ФОЛБЭКА", "stop" + default: + return echoText, "stop" + } + } +} + +// TestRecoveredEchoStaysVisibleInTheQualityReport is the headline of Д1: before the fix the metric read +// the row's VERDICT column, so a recovered echo left the row `ok` with an empty flag_reason and the +// report said echo_draft=0.0% on a run whose translator had echoed. The rate watches the TRANSLATOR +// (the D18 mine), not our success at papering over it. +func TestRecoveredEchoStaysVisibleInTheQualityReport(t *testing.T) { + rec := &reqRec{} + // The echo must be CJK-dense enough for the classifier to call it an artifact, and the fixture + // source is ja — so echo the source back, which is precisely what the mine does. + srv := newJSONProvider(rec, echoThenFixProvider(strings.Repeat(suzukiSource, 3))) + defer srv.Close() + bookPath := setupProjectOpts(t, srv.URL, projectOpts{ + source: suzukiSource, glossarySeed: suzukiSeed, regenerate: 0, + }) + // The fixture pipeline has no escalate_to; add one so the hop can recover the chunk. + addDraftEscalation(t, bookPath) + + r := newRunner(t, bookPath) + defer r.Close() + if _, err := r.TranslateBook(obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})); err != nil { + t.Fatal(err) + } + + cs, err := r.Store.GetChunkStatus("test-book", 1, 0, "draft") + if err != nil || cs == nil { + t.Fatalf("draft row: %v %v", cs, err) + } + if cs.Disposition != string(DispOK) { + t.Fatalf("setup: the hop must have recovered the chunk, got %s/%s", cs.Disposition, cs.FlagReason) + } + if cs.FirstFlagReason != string(FlagCJKArtifact) { + t.Fatalf("the recovered row must remember WHAT the primary attempt did, got first_flag_reason=%q", cs.FirstFlagReason) + } + + q, err := r.QualityReport() + if err != nil { + t.Fatal(err) + } + if q.EchoDraftChunks != 1 { + t.Errorf("echo_draft_chunks = %d, want 1 — the translator echoed, whatever we did about it", q.EchoDraftChunks) + } + if q.EchoDraftRecovered != 1 { + t.Errorf("echo_draft_recovered = %d, want 1 — the split is what keeps the number from reading as shipped breakage", q.EchoDraftRecovered) + } + if q.EchoDraftRate == 0 { + t.Error("echo_draft_rate must be non-zero when an echo happened") + } +} + +// TestUnrecoveredFlagIsNotDoubleCounted guards the other direction: a row whose verdict IS the echo +// must not also carry it in first_flag_reason, or every genuinely-flagged chunk counts twice. +func TestUnrecoveredFlagIsNotDoubleCounted(t *testing.T) { + rec := &reqRec{} + srv := newJSONProvider(rec, func(body string) (string, string) { + if isEditBody(body) { + return "ОТРЕДАКТИРОВАННЫЙ ПЕРЕВОД", "stop" + } + return strings.Repeat(suzukiSource, 3), "stop" // echoes, and no hop is configured to fix it + }) + defer srv.Close() + bookPath := setupProjectOpts(t, srv.URL, projectOpts{source: suzukiSource, glossarySeed: suzukiSeed, regenerate: 0}) + r := newRunner(t, bookPath) + defer r.Close() + if _, err := r.TranslateBook(obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})); err != nil { + t.Fatal(err) + } + cs, err := r.Store.GetChunkStatus("test-book", 1, 0, "draft") + if err != nil || cs == nil { + t.Fatalf("draft row: %v %v", cs, err) + } + if cs.FlagReason != string(FlagCJKArtifact) { + t.Fatalf("setup: the row must stay flagged, got %q", cs.FlagReason) + } + if cs.FirstFlagReason != "" { + t.Errorf("a row whose VERDICT is the echo must not repeat it in first_flag_reason (double count), got %q", cs.FirstFlagReason) + } + q, err := r.QualityReport() + if err != nil { + t.Fatal(err) + } + if q.EchoDraftChunks != 1 { + t.Errorf("echo_draft_chunks = %d, want 1 (counted ONCE)", q.EchoDraftChunks) + } + if q.EchoDraftRecovered != 0 { + t.Errorf("echo_draft_recovered = %d, want 0 — nothing recovered it", q.EchoDraftRecovered) + } +} + +// --- Д4: the WHAT the banknote delivered must reach the owner's signature map ------------------- + +// TestBanknoteProposalsArePersistedAndJoined is the D39.36 fix end to end: the translator proposes a +// rendering, the parser accepts it, and it must survive to the row the mining stop reads. Before the +// fix the parse result went into `_` and the owner met the stop holding bare source terms. +func TestBanknoteProposalsArePersistedAndJoined(t *testing.T) { + rec := &reqRec{} + // A draft carrying a banknote block: two proposals, one of them repeated with a different rendering + // so the vote/alternatives path is exercised too. + draftWithBank := "ПЕРЕВОД ЧАНКА.\n" + bankSeparator + "\n" + + "鈴木\tСудзуки\tname\n" + + "図書館\tбиблиотека\tplace\n" + srv := newJSONProvider(rec, func(body string) (string, string) { + if isEditBody(body) { + return "ОТРЕДАКТИРОВАННЫЙ ПЕРЕВОД", "stop" + } + return draftWithBank, "stop" + }) + defer srv.Close() + bookPath := setupProjectOpts(t, srv.URL, projectOpts{ + source: suzukiSource, glossarySeed: suzukiSeed, regenerate: 0, banknote: true, + }) + r := newRunner(t, bookPath) + defer r.Close() + if _, err := r.TranslateBook(obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})); err != nil { + t.Fatal(err) + } + + rs, err := r.Store.GetRetrievalState("test-book", 1, 0) + if err != nil || rs == nil { + t.Fatalf("retrieval_state: %v %v", rs, err) + } + if rs.NBanknoteLines != 2 { + t.Fatalf("setup: the parser must have accepted 2 lines, got %d", rs.NBanknoteLines) + } + if rs.BanknoteDetail == "" { + t.Fatal("the PARSED entries must be durable — counters alone are what D39.36 found useless") + } + var props []bankProposal + if err := json.Unmarshal([]byte(rs.BanknoteDetail), &props); err != nil { + t.Fatalf("banknote_detail must be readable JSON: %v (%s)", err, rs.BanknoteDetail) + } + if len(props) != 2 { + t.Fatalf("want 2 stored proposals, got %d: %s", len(props), rs.BanknoteDetail) + } + byKey := bankProposalsByKey([]store.RetrievalState{*rs}) + if got := byKey[props[0].SrcKey]; len(got) == 0 || got[0].Dst != props[0].Dst { + t.Fatalf("the fold must key proposals by the miner's normalized surface, got %+v", byKey) + } +} + +// TestBanknoteOffLeavesTheDetailEmpty pins that a channel-off book's row is unchanged — the fix must be +// invisible to every book that does not use the channel. +func TestBanknoteOffLeavesTheDetailEmpty(t *testing.T) { + rec := &reqRec{} + srv := newJSONProvider(rec, draftEdit) + defer srv.Close() + bookPath := setupProjectOpts(t, srv.URL, projectOpts{source: suzukiSource, glossarySeed: suzukiSeed, regenerate: 0}) + r := newRunner(t, bookPath) + defer r.Close() + if _, err := r.TranslateBook(obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})); err != nil { + t.Fatal(err) + } + rs, err := r.Store.GetRetrievalState("test-book", 1, 0) + if err != nil || rs == nil { + t.Fatalf("retrieval_state: %v %v", rs, err) + } + if rs.BanknoteDetail != "" { + t.Errorf("a banknote-off book must store no proposals, got %q", rs.BanknoteDetail) + } +} + +// addDraftEscalation gives the fixture project a single-hop fallback on the draft stage: the hop model +// in models.yaml, the stage's escalate_to, and the budget that arms it (escalation is inert at budget 0 +// — the shipping default). +func addDraftEscalation(t *testing.T, bookPath string) { + t.Helper() + dir := filepath.Dir(bookPath) + + modelsPath := filepath.Join(dir, "models.yaml") + models, err := os.ReadFile(modelsPath) + if err != nil { + t.Fatal(err) + } + writeFile(t, modelsPath, string(models)+` + fake-fallback: + provider: fake + price: { input_per_m: 1.0, cached_per_m: 0.1, cache_write_per_m: 0, output_per_m: 2.0 } +`) + + pipePath := filepath.Join(dir, "pipeline.yaml") + pipe, err := os.ReadFile(pipePath) + if err != nil { + t.Fatal(err) + } + withHop := strings.Replace(string(pipe), + `reasoning: "off" }`, + `reasoning: "off", escalate_to: fake-fallback }`, 1) + if withHop == string(pipe) { + t.Fatal("setup: the draft stage line was not found in the fixture pipeline") + } + writeFile(t, pipePath, withHop+"\nescalation: { budget_usd: 0.5 }\n") +} diff --git a/backend/internal/pipeline/quality.go b/backend/internal/pipeline/quality.go index 06b58d01..347fe7a1 100644 --- a/backend/internal/pipeline/quality.go +++ b/backend/internal/pipeline/quality.go @@ -52,10 +52,15 @@ type QualityReport struct { // computed DIRECTLY from the draft rows (no per-unit re-derivation). echo_edit = the DELIVERED quality // (fraction of edit-stage units whose EDITOR output itself echoed — a skipped edit row is a draft echo, // not an editor one, so it is excluded from the numerator). - EchoDraftChunks int `json:"echo_draft_chunks"` // draft-stage rows flagged cjk_artifact (per DRAFT chunk) - EchoDraftRate float64 `json:"echo_draft_rate"` // over live draft rows - EchoEditUnits int `json:"echo_edit_units"` // edit-stage units whose editor output echoed - EchoEditRate float64 `json:"echo_edit_rate"` // over live edit rows + EchoDraftChunks int `json:"echo_draft_chunks"` // draft-stage rows whose translator echoed (survived OR recovered) + // EchoDraftRecovered is the SUBSET of EchoDraftChunks the escalation hop (or a regenerate) fixed, so + // the chunk shipped clean. It keeps the headline number honest in BOTH directions: the rate measures + // the translator (an echo happened), this says what it cost us (nothing, except the wasted primary + // call). Without the split, surfacing recovered echoes would read as new breakage on a clean book. + EchoDraftRecovered int `json:"echo_draft_recovered,omitempty"` + EchoDraftRate float64 `json:"echo_draft_rate"` // over live draft rows + EchoEditUnits int `json:"echo_edit_units"` // edit-stage units whose editor output echoed + EchoEditRate float64 `json:"echo_edit_rate"` // over live edit rows // CosmeticStripRate is over ProcessedUnits (0..1). It covers BOTH strip classes (a markdown-only strip // is NOT a CJK leak — F6, D39.4: the old cjk_leak_rate counted every sanitizer_stripped unit). @@ -236,14 +241,25 @@ func (r *Runner) QualityReport() (*QualityReport, error) { // INCLUDING a c-lite dropped member — its own draft row carries cjk_artifact, so no per-unit re-derivation // is needed), echo_edit over the EDIT-stage rows (delivered quality — only the EDITOR's own echo counts, // a skipped edit row is a draft echo not an editor one). Ghost-guarded like the rest (live chunks / units). - var draftRows, draftEcho, editRows, editEcho int + var draftRows, draftEcho, draftEchoRecovered, editRows, editEcho int for _, cs := range statuses { k := chunkKey{cs.Chapter, cs.ChunkIdx} switch { case draftStageNames[cs.Stage] && liveChunks[k]: draftRows++ - if cs.FlagReason == string(FlagCJKArtifact) { - draftEcho++ // the translator echoed CJK (a flagged draft row), incl. a c-lite dropped member + // The translator echoed CJK. BOTH columns count, and the difference between them is the + // whole point: `flag_reason` is an echo that SURVIVED (the chunk shipped flagged, incl. a + // c-lite dropped member), `first_flag_reason` is an echo a later attempt RECOVERED — the + // escalation hop translated it properly and the row was written `ok`. Reading only the + // verdict column measured "echoes we failed to fix" and called it the echo rate: the + // mini-run of 25.07 escalated its one echoed draft, and the report said 0.0% of 20. + // The metric watches the TRANSLATOR (D18/D19 echo mine), not our success at papering over it. + switch { + case cs.FlagReason == string(FlagCJKArtifact): + draftEcho++ + case cs.FirstFlagReason == string(FlagCJKArtifact): + draftEcho++ + draftEchoRecovered++ } case editStageNames[cs.Stage] && inManifest[k]: editRows++ @@ -252,7 +268,7 @@ func (r *Runner) QualityReport() (*QualityReport, error) { } } } - rep.EchoDraftChunks, rep.EchoEditUnits = draftEcho, editEcho + rep.EchoDraftChunks, rep.EchoDraftRecovered, rep.EchoEditUnits = draftEcho, draftEchoRecovered, editEcho if draftRows > 0 { rep.EchoDraftRate = float64(draftEcho) / float64(draftRows) } diff --git a/backend/internal/pipeline/regressionguard_live_test.go b/backend/internal/pipeline/regressionguard_live_test.go new file mode 100644 index 00000000..48e8bd40 --- /dev/null +++ b/backend/internal/pipeline/regressionguard_live_test.go @@ -0,0 +1,139 @@ +package pipeline + +import ( + "context" + "strings" + "testing" + + "textmachine/backend/internal/obs" +) + +// regressionguard_live_test.go: the post-reflow regression guard, driven through the REAL runner +// (norm 11 — a sandbox reproduction through the driver, not a unit call on the flagger). +// +// The unit tests in internal/checks already pin the flagger's arithmetic. What was never pinned — and +// what D38.4 point 5 actually obliged us to deliver — is the WIRING: the gate config reaching the cheap +// gates, the edit wave passing the real DRAFT (not the final twice), and the hit landing in durable +// observability. The obligation went unmet for a whole pere-run (D39.34 finding 1: `regression_guard` +// was absent from every shipping config, so the guard was dark), which is exactly the class of failure a +// wiring test catches and a unit test cannot. +// +// The fixture makes the editor do what the guard exists to catch: it returns a text that is far shorter +// than the draft AND has lost a multi-digit number. Nothing else in the cheap-gate battery may fire on +// this text, so the asserted counts are the guard's alone. + +// rgDraft is a draft long enough for the collapse ratio to be meaningful (>200 non-space chars) that +// carries two multi-digit numbers. No dialogue dashes, no Latin, no ё/е pair, no CJK magnitude — so no +// other cheap gate has anything to say about it. +const rgDraft = "Утро выдалось тихим, и в читальном зале пахло старой бумагой. " + + "Он просидел над свитками 44 минуты, пока свет не сместился к дальней стене. " + + "Потом пришли остальные, и зал наполнился шорохом страниц и негромкими голосами. " + + "К полудню в списке значилось 128 имён, и каждое из них требовалось сверить с описью. " + + "Работа была скучной, но её надо было довести до конца, потому что иначе опись теряла смысл." + +// rgShortFinal is what a reflow that DROPPED content looks like: a fraction of the length, and the +// numbers are gone. Under the guard this is one length collapse + two disappeared numbers. +const rgShortFinal = "Утро выдалось тихим, в зале пахло бумагой." + +// rgProvider answers the draft with rgDraft and the edit with rgShortFinal. +func rgProvider(body string) (string, string) { + if isEditBody(body) { + return rgShortFinal, "stop" + } + return rgDraft, "stop" +} + +// regressionStyleFlags runs one book end-to-end with the given gates block and returns the leader +// chunk's durable style-flag count and its JSON detail. +func regressionStyleFlags(t *testing.T, gatesYAML string) (int, string) { + t.Helper() + rec := &reqRec{} + srv := newJSONProvider(rec, rgProvider) + defer srv.Close() + bookPath := setupProjectOpts(t, srv.URL, projectOpts{ + source: suzukiSource, + glossarySeed: suzukiSeed, // retrieval_state rows exist only for a book with a bank + gatesYAML: gatesYAML, + regenerate: 0, + }) + r := newRunner(t, bookPath) + defer r.Close() + if _, err := r.TranslateBook(obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})); err != nil { + t.Fatal(err) + } + rs, err := r.Store.GetRetrievalState("test-book", 1, 0) + if err != nil { + t.Fatal(err) + } + if rs == nil { + t.Fatal("no retrieval_state row for the leader chunk — the observability channel itself is dark") + } + return rs.NStyleFlags, rs.StyleDetail +} + +// TestRegressionGuardFiresLiveWhenEnabled is the obligation D38.4 point 5 left unpaid: with the gate ON +// a reflow that halved the text and lost its numbers must be VISIBLE in the durable per-chunk +// observability, with both flaggers naming themselves. +func TestRegressionGuardFiresLiveWhenEnabled(t *testing.T) { + n, detail := regressionStyleFlags(t, "\ngates:\n regression_guard:\n enabled: true\n") + if n == 0 { + t.Fatalf("regression guard enabled but the chunk records 0 style flags — the gate is wired to nothing") + } + for _, want := range []string{`"length_collapse":1`, `"number_drift":2`} { + if !strings.Contains(detail, want) { + t.Errorf("style detail does not carry %s:\n%s", want, detail) + } + } + // The human-readable lines must survive to the detail too — a count with no sentence is not + // observability, it is a number nobody can action. + for _, want := range []string{"length collapse", "44", "128"} { + if !strings.Contains(detail, want) { + t.Errorf("style detail is missing %q:\n%s", want, detail) + } + } + if n != 3 { // 1 collapse + 2 disappeared numbers + t.Errorf("style flags = %d, want 3 (1 length collapse + 2 number drifts); detail: %s", n, detail) + } +} + +// TestRegressionGuardSilentWhenDisabled is the other half of the same wiring claim: the gate is OPT-IN, +// so the identical run with no gates block must record nothing. Without this, a test asserting only the +// firing side would pass just as happily if the guard ran unconditionally — and the "byte-identical when +// off" property (cheapgates.go) would be unpinned. +func TestRegressionGuardSilentWhenDisabled(t *testing.T) { + n, detail := regressionStyleFlags(t, "") + if n != 0 { + t.Fatalf("regression guard is opt-in but a run without it recorded %d style flags: %s", n, detail) + } +} + +// TestRegressionGuardComparesDraftAgainstFinal pins WHICH two texts the edit wave hands the guard. The +// gate is only meaningful over the draft→final transform; a wiring that passed the final twice (the shape +// the draft-only wave legitimately uses) would silently report a clean book forever, because a text never +// collapses against itself. Asserted by making the editor return the draft UNCHANGED: same content, so a +// correct wiring is silent — and then the fixture above, which differs only in the editor's answer, +// proves the channel is not simply dead. +func TestRegressionGuardComparesDraftAgainstFinal(t *testing.T) { + rec := &reqRec{} + srv := newJSONProvider(rec, func(body string) (string, string) { return rgDraft, "stop" }) + defer srv.Close() + bookPath := setupProjectOpts(t, srv.URL, projectOpts{ + source: suzukiSource, + glossarySeed: suzukiSeed, + gatesYAML: "\ngates:\n regression_guard:\n enabled: true\n", + regenerate: 0, + }) + r := newRunner(t, bookPath) + defer r.Close() + if _, err := r.TranslateBook(obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})); err != nil { + t.Fatal(err) + } + rs, err := r.Store.GetRetrievalState("test-book", 1, 0) + if err != nil || rs == nil { + t.Fatalf("retrieval_state: %v %v", rs, err) + } + if rs.NStyleFlags != 0 { + t.Fatalf("an editor that returned the draft unchanged must not trip the guard, got %d flags: %s", + rs.NStyleFlags, rs.StyleDetail) + } +} diff --git a/backend/internal/pipeline/stagerun.go b/backend/internal/pipeline/stagerun.go index f04e1649..7fad19c5 100644 --- a/backend/internal/pipeline/stagerun.go +++ b/backend/internal/pipeline/stagerun.go @@ -126,6 +126,12 @@ func (r *Runner) runStage(ctx context.Context, st config.Stage, stageIdx int, sn var last stageAttempt anyFresh := false attemptsMade := 0 + // firstFlagReason keeps the FIRST attempt's failure when a later attempt (a regenerate, or the + // single-hop escalation below) recovers the chunk. Without it the recovered row is written `ok` + // with an empty flag_reason and the primary failure leaves no durable trace at all — which is how + // the mini-run of 25.07 reported echo_draft=0.0% on a run where one draft in twenty had echoed. + // It is telemetry, never the verdict: `disposition`/`FlagReason` below are untouched by it. + firstFlagReason := FlagReason("") for attempt := 0; ; attempt++ { maxTokens := maxTokensForAttempt(baseMaxTokens, attempt) att, err := r.runAttempt(ctx, st, st.ResolvedModel, snapID, ch, job, attempt, maxTokens, msgs, false, isFinal) @@ -137,6 +143,9 @@ func (r *Runner) runStage(ctx context.Context, st config.Stage, stageIdx int, sn runCost += att.runCost anyFresh = anyFresh || att.freshCall last = att + if attempt == 0 && !att.cls.ok() { + firstFlagReason = att.cls.Reason + } if att.cls.ok() { break } @@ -258,6 +267,10 @@ func (r *Runner) runStage(ctx context.Context, st config.Stage, stageIdx int, sn SnapshotID: snapID, ContentHash: contentHash, Disposition: string(disposition), FlagReason: string(last.cls.Reason), Attempts: attemptsMade, FinalHash: finalHash, CostUSD: cumCost, Detail: last.cls.Detail, Escalated: escalated, EscalationModel: escModel, + // Only meaningful when the row ends up RECOVERED: if the first attempt's failure is also the + // row's verdict, FlagReason already says so and repeating it here would double-count every + // flagged row in the echo metric. + FirstFlagReason: recoveredFirstFlag(firstFlagReason, last.cls.Reason), }); err != nil { return nil, fmt.Errorf("pipeline: record chunk_status: %w", err) } @@ -278,6 +291,7 @@ func (r *Runner) runStage(ctx context.Context, st config.Stage, stageIdx int, sn Escalated: escalated, EscalationModel: escModel, BankFlags: last.bankFlags, + BankProposals: last.bankProposals, Repair: repairRes, } if disposition == DispOK { @@ -290,6 +304,17 @@ func (r *Runner) runStage(ctx context.Context, st config.Stage, stageIdx int, sn return sr, nil } +// recoveredFirstFlag returns the first attempt's failure ONLY when a later attempt recovered the row +// (i.e. the row's own verdict differs from it). A row whose verdict IS that failure needs no second +// copy of it: the echo metric reads `flag_reason OR first_flag_reason`, so storing both on a +// still-flagged row would count it twice. +func recoveredFirstFlag(first, verdict FlagReason) string { + if first == "" || first == verdict { + return "" + } + return string(first) +} + // commitSanitizedExport persists the cosmetic-stripped export text as a $0 derived checkpoint and // returns its request_hash (→ chunk_status.final_hash). The hash is CONTENT-ADDRESSED and // namespaced ("tm-sanitized-v1:") so it can never collide with a real attempt's hex request_hash @@ -323,14 +348,18 @@ type stageAttempt struct { // export checkpoint and re-points final_hash so the editor/export read the cleaned draft on resume. bankStripped string bankFlags bankFlags // per-chunk banknote telemetry (point 10) for the translator draft - text string - usage llm.Usage - finish string - modelActual string - latency int - runCost float64 // billed THIS run (0 on a checkpoint hit) - cumCost float64 // this attempt's cost (checkpoint cost on a hit, fresh cost on a call) - freshCall bool // a provider call was made this run + // bankProposals is the JSON of the chunk's PARSED banknote entries (D39.36 fix): the WHAT the + // channel delivered, carried to retrieval_state so the bank-mining stop can attach it to the + // owner's signature map. "" whenever the channel is off / the draft carried no block. + bankProposals string + text string + usage llm.Usage + finish string + modelActual string + latency int + runCost float64 // billed THIS run (0 on a checkpoint hit) + cumCost float64 // this attempt's cost (checkpoint cost on a hit, fresh cost on a call) + freshCall bool // a provider call was made this run } // runAttempt executes exactly one attempt on the request-hash axis: a checkpoint @@ -365,9 +394,9 @@ func (r *Runner) runAttempt(ctx context.Context, st config.Stage, model, snapID // Banknote: slice the block off the RAW checkpoint text BEFORE classify + before feeding the // editor (WS4 points 1-3,8). The checkpoint stores the RAW draft; resume re-derives the clean // text deterministically (a no-op returning the raw text for a channel-off / no-separator draft). - cleanText, bankStripped, bankFlags := r.applyBanknote(st.Role, cp.ResponseText, cp.FinishReason) + cleanText, bankStripped, bankFlags, bankEntries := r.applyBanknoteWithEntries(st.Role, cp.ResponseText, cp.FinishReason) att.text = cleanText - att.bankStripped, att.bankFlags = bankStripped, bankFlags + att.bankStripped, att.bankFlags, att.bankProposals = bankStripped, bankFlags, bankProposalsJSON(bankEntries) att.cls, att.stripped = r.classifyOutput(st.Role, ch.Text, cleanText, cp.FinishReason, isFinal) rl := r.baseRequestLog(st, ch, model, reqHash) rl.ModelActual = cp.ModelActual @@ -556,9 +585,9 @@ func (r *Runner) runAttempt(ctx context.Context, st config.Stage, model, snapID // run-crash. The banknote block is sliced off the RAW draft FIRST (WS4 points 1-3,8): the // checkpoint above holds the raw response, but classify/coverage/echo and the editor see the // CLEANED translation, and the cleaned text is committed as a derived export in runStage. - cleanText, bankStripped, bankFlags := r.applyBanknote(st.Role, resp.Text, resp.FinishReason) + cleanText, bankStripped, bankFlags, bankEntries := r.applyBanknoteWithEntries(st.Role, resp.Text, resp.FinishReason) att.text = cleanText - att.bankStripped, att.bankFlags = bankStripped, bankFlags + att.bankStripped, att.bankFlags, att.bankProposals = bankStripped, bankFlags, bankProposalsJSON(bankEntries) att.cls, att.stripped = r.classifyOutput(st.Role, ch.Text, cleanText, resp.FinishReason, isFinal) rl := r.baseRequestLog(st, ch, model, reqHash) diff --git a/backend/internal/pipeline/status.go b/backend/internal/pipeline/status.go index aa01276e..f378ec0e 100644 --- a/backend/internal/pipeline/status.go +++ b/backend/internal/pipeline/status.go @@ -68,6 +68,14 @@ type StatusReport struct { // the config renders now (empty when it could not be computed or matches). ConfigDrift bool `json:"config_drift"` CurrentSnapshot string `json:"current_snapshot,omitempty"` + // RebillUnits/RebillUSD turn the drift BOOLEAN into the number the operator actually decides on + // (spec D15.2 §9, taken 25.07): "the config drifted" says nothing about whether continuing costs a + // cent or the whole book — these say "N chunk×stage units already billed under a superseded snapshot + // would be paid for again, ~$X". Same projection the consent gate refuses on (projectRebill), so + // status can never quote a different number than the one translate enforces. Both omitempty: a book + // with no drift keeps its exact prior bytes. + RebillUnits int `json:"rebill_units,omitempty"` + RebillUSD float64 `json:"rebill_usd,omitempty"` TotalUnits int `json:"total_units"` Done int `json:"done"` @@ -462,6 +470,20 @@ func (r *Runner) Status(ctx context.Context) (*StatusReport, error) { } } + // Re-payment projection (spec §9): what the drift above would COST. Computed whenever rows exist — + // not only under ConfigDrift — because SnapshotDrift (rows split across snapshots within one wave) + // re-bills too, and that is precisely the case the boolean pair leaves unpriced. $0 and read-only: + // projectRebill only re-renders the wave snapshots and sums stored costs. + if len(statuses) > 0 { + if proj, perr := r.projectRebill(statuses, chunks); perr != nil { + // Same discipline as the drift check above: a failed projection is reported, never + // silently rendered as "nothing to re-pay". + r.Log.WarnContext(ctx, "re-bill projection failed; the re-payment cost of the drift is unknown (reported as none)", "err", perr) + } else { + rep.RebillUnits, rep.RebillUSD = proj.Rows, proj.USD + } + } + // Money. committed, reserved, err := r.Store.SpentUSD(r.Book.BookID) if err != nil { diff --git a/backend/internal/pipeline/status_rebill_projection_test.go b/backend/internal/pipeline/status_rebill_projection_test.go new file mode 100644 index 00000000..4846b4de --- /dev/null +++ b/backend/internal/pipeline/status_rebill_projection_test.go @@ -0,0 +1,77 @@ +package pipeline + +import ( + "context" + "fmt" + "strings" + "testing" + + "textmachine/backend/internal/obs" +) + +// status_rebill_projection_test.go: `tmctl status` prices the drift (spec D15.2 §9, taken with the +// pre-run hygiene pack 25.07). The boolean ConfigDrift told the operator that the config moved but not +// whether continuing costs a cent or the whole book — and the number that answers that already existed +// on the refusal path. The claim under test is that status quotes THE SAME number the consent gate +// refuses on: two definitions of one quantity is the exact bug projectBookUSD was extracted to remove. + +// TestStatusPricesTheDriftWithTheGatesOwnNumber runs a book, drifts both waves, and asserts that the +// read-only status projection equals what was actually billed AND what the gate names when it refuses. +func TestStatusPricesTheDriftWithTheGatesOwnNumber(t *testing.T) { + rec := &reqRec{} + srv := newJSONProvider(rec, draftEdit) + defer srv.Close() + bookPath := setupProject(t, srv.URL) + ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}) + + r1 := newRunner(t, bookPath) + if _, err := r1.TranslateBook(ctx); err != nil { + t.Fatal(err) + } + committed, _, err := r1.Store.SpentUSD("test-book") + if err != nil { + t.Fatal(err) + } + r1.Close() + + // Before any drift the projection must be SILENT: everything resumes at $0, so a status that + // reported a re-payment here would be crying wolf on every healthy book. + rClean := newRunner(t, bookPath) + cleanRep, err := rClean.Status(ctx) + rClean.Close() + if err != nil { + t.Fatal(err) + } + if cleanRep.RebillUnits != 0 || cleanRep.RebillUSD != 0 { + t.Fatalf("an undrifted book must project no re-payment, got %d units / $%v", + cleanRep.RebillUnits, cleanRep.RebillUSD) + } + + driftPipelineVersion(t, bookPath) + + r2 := newRunner(t, bookPath) + defer r2.Close() + rep, err := r2.Status(ctx) + if err != nil { + t.Fatal(err) + } + if !rep.ConfigDrift { + t.Fatal("setup: the drift itself was not detected") + } + if rep.RebillUnits != 2 { // 1 chunk × (draft + edit) + t.Errorf("rebill_units = %d, want 2 (both stages of the single chunk)", rep.RebillUnits) + } + // Every billed row is superseded here, so the projection is the whole spend of run 1. + if diff := rep.RebillUSD - committed; diff > 1e-9 || diff < -1e-9 { + t.Errorf("rebill_usd = %v, want the whole billed amount %v", rep.RebillUSD, committed) + } + + // The same number, from the enforcement side: the gate's refusal must quote what status showed. + err = r2.checkRebillConsent(ctx, chunksOf(t, r2)) + if err == nil { + t.Fatal("the gate must refuse a fully-superseded book without consent") + } + if want := fmt.Sprintf("~$%.6f", rep.RebillUSD); !strings.Contains(err.Error(), want) { + t.Errorf("the refusal does not quote the projection %s:\n%v", want, err) + } +} diff --git a/backend/internal/pipeline/testdata/golden/capture.golden b/backend/internal/pipeline/testdata/golden/capture.golden index 7469c217..b75eb65b 100644 --- a/backend/internal/pipeline/testdata/golden/capture.golden +++ b/backend/internal/pipeline/testdata/golden/capture.golden @@ -1,8 +1,8 @@ ==== run 1 (fresh) ==== -snapshot_draft: 6b771d4618ed24d9a4dbe9de7cb98e6289c1dab66fa2696dc6c80a306a910a58 -snapshot_draft_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v5-output-budget-editunit+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v1-refusal+echo015+loop+u15.0.0","pipeline_core":"C1","max_output_ratio":2,"min_max_tokens":512,"context_assembly":{"glossary_injection":"selective","glossary_token_budget":800,"cache_ttl":""},"segmentation":{"draft_budget_out":1797,"edit_ceiling_out":3200,"fertility_cjk":1.1978,"fertility_other":0.3852},"render_format_version":"renderfmt-v3-draft-gender+editor-src2dst+dc3-gender","memory_version":"bd451833d4ebb8a0ca07caf8deda76f027e1f42b78654987a22d215acf955eb7","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v4-percent-latin-brokenword+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v6"},"stages":[{"name":"draft","role":"translator","model":"fake-model","prompt_version":"v-golden","prompt_sha256":"26d0245a56d180eafd200ebe5311aab2488142c4a98dcddee21ea457814aa21a","temperature":0.3,"reasoning":"off","capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":4000},"escalate_to":"fake-fallback","escalate_capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":6000}}]} -snapshot_edit: 9d592c5bdb94885f91e2711a24bacfe64c5d2a3817162090dd209320bbee992d -snapshot_edit_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v5-output-budget-editunit+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v1-refusal+echo015+loop+u15.0.0","pipeline_core":"C1","max_output_ratio":2,"min_max_tokens":512,"context_assembly":{"glossary_injection":"selective","glossary_token_budget":800,"cache_ttl":""},"segmentation":{"draft_budget_out":1797,"edit_ceiling_out":3200,"fertility_cjk":1.1978,"fertility_other":0.3852},"render_format_version":"renderfmt-v3-draft-gender+editor-src2dst+dc3-gender","memory_version":"0a48d6ed9409de09f514af91aebdd622637621c9dc9464e8898c238791033d91","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v4-percent-latin-brokenword+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v6"},"stages":[{"name":"edit","role":"editor","model":"fake-model","prompt_version":"v-golden","prompt_sha256":"6a8e7f71139d6bc09a95159d1cb895e05b56d2f2f7417c5e20a7073c517c79f6","temperature":0.4,"reasoning":"off","capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":4000}}]} +snapshot_draft: 6b24296222d40ad71161602f46a3ec6c8e96408fd5d5ea64e021356b9cf3bd1c +snapshot_draft_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v5-output-budget-editunit+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v1-refusal+echo015+loop+u15.0.0","pipeline_core":"C1","max_output_ratio":2,"min_max_tokens":512,"context_assembly":{"glossary_injection":"selective","glossary_token_budget":800,"cache_ttl":""},"segmentation":{"draft_budget_out":1797,"edit_ceiling_out":3200,"fertility_cjk":1.1978,"fertility_other":0.3852},"render_format_version":"renderfmt-v3-draft-gender+editor-src2dst+dc3-gender","memory_version":"bd451833d4ebb8a0ca07caf8deda76f027e1f42b78654987a22d215acf955eb7","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v5-chevron-speech-attribution+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v6"},"stages":[{"name":"draft","role":"translator","model":"fake-model","prompt_version":"v-golden","prompt_sha256":"26d0245a56d180eafd200ebe5311aab2488142c4a98dcddee21ea457814aa21a","temperature":0.3,"reasoning":"off","capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":4000},"escalate_to":"fake-fallback","escalate_capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":6000}}]} +snapshot_edit: 2853c6fdbe8bec673eaea6a27b65c51c8272cb5a7ea3647c5bc8f19c78bb8c59 +snapshot_edit_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v5-output-budget-editunit+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v1-refusal+echo015+loop+u15.0.0","pipeline_core":"C1","max_output_ratio":2,"min_max_tokens":512,"context_assembly":{"glossary_injection":"selective","glossary_token_budget":800,"cache_ttl":""},"segmentation":{"draft_budget_out":1797,"edit_ceiling_out":3200,"fertility_cjk":1.1978,"fertility_other":0.3852},"render_format_version":"renderfmt-v3-draft-gender+editor-src2dst+dc3-gender","memory_version":"0a48d6ed9409de09f514af91aebdd622637621c9dc9464e8898c238791033d91","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v5-chevron-speech-attribution+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v6"},"stages":[{"name":"edit","role":"editor","model":"fake-model","prompt_version":"v-golden","prompt_sha256":"6a8e7f71139d6bc09a95159d1cb895e05b56d2f2f7417c5e20a7073c517c79f6","temperature":0.4,"reasoning":"off","capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":4000}}]} brief_hash: c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df memory_version: 0a48d6ed9409de09f514af91aebdd622637621c9dc9464e8898c238791033d91 base_memory_version: bd451833d4ebb8a0ca07caf8deda76f027e1f42b78654987a22d215acf955eb7 @@ -51,42 +51,42 @@ chunk ch8/0 disposition=flagged flag="sanitizer_stripped" final_text="Судзу stage=edit role=editor model=fake-model resume=false disp=flagged flag="sanitizer_stripped" attempts=1 escalated=false esc_model="" finish="stop" cum_usd=0.00182 detail="CJK leak in the ru output: 特产" stage_text="" recovered="Судзуки нашёл древний камень на каменном алтаре." -- chunk_status -- -ch1/0 draft snap_match=true content_hash=3e156226b9a74aa70c8a259c62e3d2fe68486a96d2178d31084c1e38a3256241 disp=ok flag="" attempts=1 final_hash=07dfef4f3137a6afdd879075709cbe1d4ae4aeffa73fd55d8d4a4b266de44191 cost=0.00182 escalated=false esc_model="" detail="" -ch1/0 edit snap_match=true content_hash=5e79dd5cbd310ac01d71ca258002beca18db715dfb8b1a08907508e506595355 disp=ok flag="" attempts=1 final_hash=ff6c6daffd6186e70f8dc9f634a9d8d3fdfbc90affb0a363427140bf52aea33a cost=0.00182 escalated=false esc_model="" detail="" -ch1/1 draft snap_match=true content_hash=82fb7f985de6c07182e8c09a3ceaad94f2e63745b87024087e437fba762b8227 disp=ok flag="" attempts=1 final_hash=970bd70afdd5d956114017f90ba600e63da0da7b2a1fb61908062966f8332503 cost=0.00182 escalated=false esc_model="" detail="" -ch2/0 draft snap_match=true content_hash=cfab1e09264a3a30aba95e7da1d6a269468c6aa0c1b334d9241e78bc266a3571 disp=ok flag="" attempts=1 final_hash=0c5929335c49b0f302948b0efe77ad078ec7538f0020b76c020e36272afe652e cost=0.00728 escalated=true esc_model="fake-fallback" detail="" -ch2/0 edit snap_match=true content_hash=55ffd839a4a26c03cf57e08000ab2c84b267e1429ab4a8e212d3a01fda757632 disp=ok flag="" attempts=1 final_hash=e76420deca3abe19ba49ea6de7af5938e49ef579950bf929896bf8dc6c418a28 cost=0.00182 escalated=false esc_model="" detail="" +ch1/0 draft snap_match=true content_hash=3e156226b9a74aa70c8a259c62e3d2fe68486a96d2178d31084c1e38a3256241 disp=ok flag="" attempts=1 final_hash=39af9d91caa8573f8d536044c56be4124e5a788653fffd1ea43bd23c54422e37 cost=0.00182 escalated=false esc_model="" detail="" +ch1/0 edit snap_match=true content_hash=5e79dd5cbd310ac01d71ca258002beca18db715dfb8b1a08907508e506595355 disp=ok flag="" attempts=1 final_hash=cf05fcf7d40e1a81f298b5ea53e810283d4aad4b7dd9e59458e7bfb0405792ee cost=0.00182 escalated=false esc_model="" detail="" +ch1/1 draft snap_match=true content_hash=82fb7f985de6c07182e8c09a3ceaad94f2e63745b87024087e437fba762b8227 disp=ok flag="" attempts=1 final_hash=c22fd70faa869950d3034f9bfb1f3338e0f999c42151f51af70b9e96010cbf8d cost=0.00182 escalated=false esc_model="" detail="" +ch2/0 draft snap_match=true content_hash=cfab1e09264a3a30aba95e7da1d6a269468c6aa0c1b334d9241e78bc266a3571 disp=ok flag="" attempts=1 final_hash=2feaf69aa3c1bdbd4e1f0ce4bd9a9afdae0b223a6b6e3e82d424b85a5d902bac cost=0.00728 escalated=true esc_model="fake-fallback" detail="" +ch2/0 edit snap_match=true content_hash=55ffd839a4a26c03cf57e08000ab2c84b267e1429ab4a8e212d3a01fda757632 disp=ok flag="" attempts=1 final_hash=3665a6be181389cf7bd8507c29e05cc6af74749d3b88656ee09e5ba63fe64e91 cost=0.00182 escalated=false esc_model="" detail="" ch3/0 draft snap_match=true content_hash=21df44eb104900664bdaaeb2f716f51dcddbc81a71d845f6ba0f91c5bc4b2cfb disp=flagged flag="hard_refusal" attempts=1 final_hash= cost=0.00728 escalated=true esc_model="" detail="provider finish_reason=refusal" ch3/0 edit snap_match=true content_hash= disp=skipped flag="hard_refusal" attempts=0 final_hash= cost=0 escalated=false esc_model="" detail="skipped: a member draft chunk of this edit unit was flagged (hard_refusal)" -ch4/0 draft snap_match=true content_hash=7aabdcb89dea365c95363b2662144ca1a10b307d377f47a8e7cc19b5b872d287 disp=ok flag="" attempts=1 final_hash=2574511b38f07a6eef3dd781907eee77cbdc73978ef28d4c9785e32d28489b58 cost=0.00182 escalated=false esc_model="" detail="" -ch4/0 edit snap_match=true content_hash=2854dfe1648b0cf0cbb683cb7ce94c5eedc9223b2ce20c99dc008c7add6317ef disp=ok flag="" attempts=1 final_hash=0f8084a8bcf771d895963869671f83e4bf8f80c03a491ea999278d45878fc10c cost=0.00182 escalated=false esc_model="" detail="" -ch5/0 draft snap_match=true content_hash=2ed0a97f39cc5c57c62c3324f4646357372f26e3c5bae1773f24edf9449fc013 disp=ok flag="" attempts=1 final_hash=c00dc01d8cc53a31022467dd878ba7d4d6bd9b7c7a9ee5036f0b69ed6f10025b cost=0.00182 escalated=false esc_model="" detail="" -ch5/0 edit snap_match=true content_hash=b145d78e4d30a39cc4088a1ddb8d934234c65341c62c79545c2eff1e7ae0c3d3 disp=ok flag="" attempts=1 final_hash=508383e619d91f16be54e60712774c414d057784a2d406ca202872f7b98a08f7 cost=0.00182 escalated=false esc_model="" detail="" -ch6/0 draft snap_match=true content_hash=5f396d299a7ec5b10f3c8591e54ce7d84c4d31b815e561b0dd5f56635aef5a71 disp=ok flag="" attempts=1 final_hash=ee0cc663a115e983e4e5df712513ba7085d4a9727e1fa864eec5fa12f8cf5ad0 cost=0.00182 escalated=false esc_model="" detail="" +ch4/0 draft snap_match=true content_hash=7aabdcb89dea365c95363b2662144ca1a10b307d377f47a8e7cc19b5b872d287 disp=ok flag="" attempts=1 final_hash=6d2b9757a2e8924f0ba4120c1b2807ae497f31359943a40693a7bf4b1f0d58bd cost=0.00182 escalated=false esc_model="" detail="" +ch4/0 edit snap_match=true content_hash=2854dfe1648b0cf0cbb683cb7ce94c5eedc9223b2ce20c99dc008c7add6317ef disp=ok flag="" attempts=1 final_hash=24ae49ceba78e2e586b4929015786fd2959d3656da370884e1df2952e800fd62 cost=0.00182 escalated=false esc_model="" detail="" +ch5/0 draft snap_match=true content_hash=2ed0a97f39cc5c57c62c3324f4646357372f26e3c5bae1773f24edf9449fc013 disp=ok flag="" attempts=1 final_hash=5676107da33357c37a119379de707e4f3c07a2798a013bd4435cdee8ce1136cb cost=0.00182 escalated=false esc_model="" detail="" +ch5/0 edit snap_match=true content_hash=b145d78e4d30a39cc4088a1ddb8d934234c65341c62c79545c2eff1e7ae0c3d3 disp=ok flag="" attempts=1 final_hash=f23301ccb29c15933935e9013f50c9215c9fdaeb6411806f9716b4fa74d8e334 cost=0.00182 escalated=false esc_model="" detail="" +ch6/0 draft snap_match=true content_hash=5f396d299a7ec5b10f3c8591e54ce7d84c4d31b815e561b0dd5f56635aef5a71 disp=ok flag="" attempts=1 final_hash=0a7d927a87e55ff540145a13acaaeea9a22867ae136334593b03497f40c298a9 cost=0.00182 escalated=false esc_model="" detail="" ch6/0 edit snap_match=true content_hash=592353321cc8000e792e0897446e4dd4f88a0280b9368b62cbd5c82b0c433193 disp=flagged flag="sanitizer_defect" attempts=1 final_hash= cost=0.00182 escalated=false esc_model="" detail="leading service preamble: Вот перевод фрагмента:" -ch7/0 draft snap_match=true content_hash=15c3ebbd56bb19880202d35b0206357655da245343376492f7ceb61808c8a5be disp=ok flag="" attempts=1 final_hash=df112533bae9eb53eeee7535189479bb62113829691e257827eb6a122c4d24a5 cost=0.00182 escalated=false esc_model="" detail="" -ch7/0 edit snap_match=true content_hash=0b2674ed9829b2e6526ff41a60dd280b6670c539476526bbe835d9bf7a6be4c4 disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:e6daeb1f55975010b0d9eafc3d7b9d23891d62e93db9203b1b65c6673180171a cost=0.00182 escalated=false esc_model="" detail="markdown header in the output: ### Глава 7" -ch8/0 draft snap_match=true content_hash=4a420977cc34ce738b4978e6a1baf45d78d773a135070c3b7f8c723adfa57d7b disp=ok flag="" attempts=1 final_hash=3ae07f69c98630ae05ed56465ca12b895aafef0376cb4e6fef6fb37d2edf4fb6 cost=0.00182 escalated=false esc_model="" detail="" -ch8/0 edit snap_match=true content_hash=c851100c8d6785f77d68f8127472790ae9d0408b7fec44785d3ff9d46842c6f8 disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:1994445050afdc0455c60b75974a47d9430b847d7c5158d3cebfbea933c61dbd cost=0.00182 escalated=false esc_model="" detail="CJK leak in the ru output: 特产" +ch7/0 draft snap_match=true content_hash=15c3ebbd56bb19880202d35b0206357655da245343376492f7ceb61808c8a5be disp=ok flag="" attempts=1 final_hash=50290abd81e52a0cbd36e2e1b3d9383d7d6bb955953f8ffa22e97cbcf2d01315 cost=0.00182 escalated=false esc_model="" detail="" +ch7/0 edit snap_match=true content_hash=0b2674ed9829b2e6526ff41a60dd280b6670c539476526bbe835d9bf7a6be4c4 disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:ac4b2483fc2eb6289a21e618b493a77960842f0835b8d8b5f11ef87700dfc81e cost=0.00182 escalated=false esc_model="" detail="markdown header in the output: ### Глава 7" +ch8/0 draft snap_match=true content_hash=4a420977cc34ce738b4978e6a1baf45d78d773a135070c3b7f8c723adfa57d7b disp=ok flag="" attempts=1 final_hash=cfead80d01c4339777f8c5f5b0cc4e1167d351067fd3db389dbe44938ddaaca6 cost=0.00182 escalated=false esc_model="" detail="" +ch8/0 edit snap_match=true content_hash=c851100c8d6785f77d68f8127472790ae9d0408b7fec44785d3ff9d46842c6f8 disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:457c063d9941c2ca2fd834cae9516bc7e966f205efcb44e02704d0f2acd2f82c cost=0.00182 escalated=false esc_model="" detail="CJK leak in the ru output: 特产" -- request_log (insertion order) -- -ch1/0 draft role=translator req=fake-model actual=fake-model hash=07dfef4f3137a6afdd879075709cbe1d4ae4aeffa73fd55d8d4a4b266de44191 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch1/1 draft role=translator req=fake-model actual=fake-model hash=970bd70afdd5d956114017f90ba600e63da0da7b2a1fb61908062966f8332503 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch2/0 draft role=translator req=fake-model actual=fake-model hash=3c87997526311911cbe02fb9928b43ef9ae420872719390304c2edcbf460d8b8 tm_hit=0 ok=0 finish="stop" degraded="cjk_artifact" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch2/0 draft role=translator req=fake-fallback actual=fake-fallback hash=0c5929335c49b0f302948b0efe77ad078ec7538f0020b76c020e36272afe652e tm_hit=0 ok=1 finish="stop" degraded="" cost=0.0054600000000000004 tokens=1000/200/0/500/0 err="" -ch3/0 draft role=translator req=fake-model actual=fake-model hash=069f2401b99af0393fb0d63792c89a880c4065a712d49725e9dff72eea0dda43 tm_hit=0 ok=0 finish="refusal" degraded="hard_refusal" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch3/0 draft role=translator req=fake-fallback actual=fake-fallback hash=ae51067d2d180033744a10163f4606b9fcdb441931f497c55dbd36e6c9f07c42 tm_hit=0 ok=0 finish="refusal" degraded="hard_refusal" cost=0.0054600000000000004 tokens=1000/200/0/500/0 err="" -ch4/0 draft role=translator req=fake-model actual=fake-model hash=2574511b38f07a6eef3dd781907eee77cbdc73978ef28d4c9785e32d28489b58 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch5/0 draft role=translator req=fake-model actual=fake-model hash=c00dc01d8cc53a31022467dd878ba7d4d6bd9b7c7a9ee5036f0b69ed6f10025b tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch6/0 draft role=translator req=fake-model actual=fake-model hash=ee0cc663a115e983e4e5df712513ba7085d4a9727e1fa864eec5fa12f8cf5ad0 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch7/0 draft role=translator req=fake-model actual=fake-model hash=df112533bae9eb53eeee7535189479bb62113829691e257827eb6a122c4d24a5 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch8/0 draft role=translator req=fake-model actual=fake-model hash=3ae07f69c98630ae05ed56465ca12b895aafef0376cb4e6fef6fb37d2edf4fb6 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch1/0 edit role=editor req=fake-model actual=fake-model hash=ff6c6daffd6186e70f8dc9f634a9d8d3fdfbc90affb0a363427140bf52aea33a tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch2/0 edit role=editor req=fake-model actual=fake-model hash=e76420deca3abe19ba49ea6de7af5938e49ef579950bf929896bf8dc6c418a28 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch4/0 edit role=editor req=fake-model actual=fake-model hash=0f8084a8bcf771d895963869671f83e4bf8f80c03a491ea999278d45878fc10c tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch5/0 edit role=editor req=fake-model actual=fake-model hash=508383e619d91f16be54e60712774c414d057784a2d406ca202872f7b98a08f7 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch6/0 edit role=editor req=fake-model actual=fake-model hash=f3a5675d5ba381251ebfed396f0a79b7bae0d727339adb152fe97dfccd9c15b3 tm_hit=0 ok=0 finish="stop" degraded="sanitizer_defect" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch7/0 edit role=editor req=fake-model actual=fake-model hash=2ded6927e1537e73b62b017ee8a27095906ad5f2eefa2d5463de41694dc618ce tm_hit=0 ok=0 finish="stop" degraded="sanitizer_stripped" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch8/0 edit role=editor req=fake-model actual=fake-model hash=456a4a0945d84716e9242516ca980e88241f325075bd7cdc252e2e233da3e34b tm_hit=0 ok=0 finish="stop" degraded="sanitizer_stripped" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch1/0 draft role=translator req=fake-model actual=fake-model hash=39af9d91caa8573f8d536044c56be4124e5a788653fffd1ea43bd23c54422e37 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch1/1 draft role=translator req=fake-model actual=fake-model hash=c22fd70faa869950d3034f9bfb1f3338e0f999c42151f51af70b9e96010cbf8d tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch2/0 draft role=translator req=fake-model actual=fake-model hash=8a8bc4539e7b95cd941fb6f8198c78777f49534ddd183a6f3777712d82554df3 tm_hit=0 ok=0 finish="stop" degraded="cjk_artifact" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch2/0 draft role=translator req=fake-fallback actual=fake-fallback hash=2feaf69aa3c1bdbd4e1f0ce4bd9a9afdae0b223a6b6e3e82d424b85a5d902bac tm_hit=0 ok=1 finish="stop" degraded="" cost=0.0054600000000000004 tokens=1000/200/0/500/0 err="" +ch3/0 draft role=translator req=fake-model actual=fake-model hash=0e5413f92f0a3e6b9a189a60ad943f56511076462978fdf9e5e3c7688013ddd7 tm_hit=0 ok=0 finish="refusal" degraded="hard_refusal" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch3/0 draft role=translator req=fake-fallback actual=fake-fallback hash=55bf6601304ea2a43727a3713994bc1ac6034e564a36c390144950e3c4885403 tm_hit=0 ok=0 finish="refusal" degraded="hard_refusal" cost=0.0054600000000000004 tokens=1000/200/0/500/0 err="" +ch4/0 draft role=translator req=fake-model actual=fake-model hash=6d2b9757a2e8924f0ba4120c1b2807ae497f31359943a40693a7bf4b1f0d58bd tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch5/0 draft role=translator req=fake-model actual=fake-model hash=5676107da33357c37a119379de707e4f3c07a2798a013bd4435cdee8ce1136cb tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch6/0 draft role=translator req=fake-model actual=fake-model hash=0a7d927a87e55ff540145a13acaaeea9a22867ae136334593b03497f40c298a9 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch7/0 draft role=translator req=fake-model actual=fake-model hash=50290abd81e52a0cbd36e2e1b3d9383d7d6bb955953f8ffa22e97cbcf2d01315 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch8/0 draft role=translator req=fake-model actual=fake-model hash=cfead80d01c4339777f8c5f5b0cc4e1167d351067fd3db389dbe44938ddaaca6 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch1/0 edit role=editor req=fake-model actual=fake-model hash=cf05fcf7d40e1a81f298b5ea53e810283d4aad4b7dd9e59458e7bfb0405792ee tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch2/0 edit role=editor req=fake-model actual=fake-model hash=3665a6be181389cf7bd8507c29e05cc6af74749d3b88656ee09e5ba63fe64e91 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch4/0 edit role=editor req=fake-model actual=fake-model hash=24ae49ceba78e2e586b4929015786fd2959d3656da370884e1df2952e800fd62 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch5/0 edit role=editor req=fake-model actual=fake-model hash=f23301ccb29c15933935e9013f50c9215c9fdaeb6411806f9716b4fa74d8e334 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch6/0 edit role=editor req=fake-model actual=fake-model hash=af6f364161222402ba0c9317119b413fa7e2a875d77409e8b9a8e96032a277fc tm_hit=0 ok=0 finish="stop" degraded="sanitizer_defect" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch7/0 edit role=editor req=fake-model actual=fake-model hash=09e0c35087fc37fdfa527ea756aaba4bd36fb2537e1a1f2d49abcc60252efc99 tm_hit=0 ok=0 finish="stop" degraded="sanitizer_stripped" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch8/0 edit role=editor req=fake-model actual=fake-model hash=0dec718729487522613e96b02d60661ffbd45c6f2c04151341da7f80d972289b tm_hit=0 ok=0 finish="stop" degraded="sanitizer_stripped" cost=0.00182 tokens=1000/200/0/500/0 err="" -- retrieval_state -- ch1/0 snap_match=true exact=3 sticky=0 ambiguous=0 spoiler=1 evicted=0 postcheck_miss=1 style_flags=0 trust_gated=0 injected_ids=["紋章\u001f\u001f0\u001f0","鈴木\u001f\u001f0\u001f0","魔法学院\u001f\u001f0\u001f0"] postcheck_detail=[{"src":"紋章","dst":"герб","disp":"confirmed"}] style_detail= trust_gate_detail= @@ -126,10 +126,10 @@ ch8/0 snap_match=true exact=1 sticky=0 ambiguous=0 spoiler=0 evicted=0 postcheck [16] {"max_tokens":4000,"messages":[{"role":"system","content":"Ты — монолингвальный редактор русского текста. Правь стиль, не меняя смысла. Книга: Золотая книга."},{"role":"system","content":"КАНОНИЧЕСКИЕ ПЕРЕВОДЫ имён и терминов (в черновике термин исходника слева ДОЛЖЕН быть передан именно указанной формой справа — приводи к ней любые расхождения, склоняя по контексту; не вводи иных вариантов и не меняй ничего другого):\n- 鈴木 → «Судзуки» (муж. — мужские родовые формы)"},{"role":"user","content":"Черновик перевода для редактуры: ЧЕРНОВИК ПЕРЕВОДА 9e65df37b751. МАРКДАУН-СЦЕНА Судзуки открыл седьмую дверь."}],"model":"fake-model","stream":false,"temperature":0.4} [17] {"max_tokens":4000,"messages":[{"role":"system","content":"Ты — монолингвальный редактор русского текста. Правь стиль, не меняя смысла. Книга: Золотая книга."},{"role":"system","content":"КАНОНИЧЕСКИЕ ПЕРЕВОДЫ имён и терминов (в черновике термин исходника слева ДОЛЖЕН быть передан именно указанной формой справа — приводи к ней любые расхождения, склоняя по контексту; не вводи иных вариантов и не меняй ничего другого):\n- 鈴木 → «Судзуки» (муж. — мужские родовые формы)"},{"role":"user","content":"Черновик перевода для редактуры: ЧЕРНОВИК ПЕРЕВОДА 2c858e3366d2. ИЕРОГЛИФ-СЦЕНА Судзуки нашёл камень."}],"model":"fake-model","stream":false,"temperature":0.4} ==== run 2 (resume) ==== -snapshot_draft: 6b771d4618ed24d9a4dbe9de7cb98e6289c1dab66fa2696dc6c80a306a910a58 -snapshot_draft_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v5-output-budget-editunit+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v1-refusal+echo015+loop+u15.0.0","pipeline_core":"C1","max_output_ratio":2,"min_max_tokens":512,"context_assembly":{"glossary_injection":"selective","glossary_token_budget":800,"cache_ttl":""},"segmentation":{"draft_budget_out":1797,"edit_ceiling_out":3200,"fertility_cjk":1.1978,"fertility_other":0.3852},"render_format_version":"renderfmt-v3-draft-gender+editor-src2dst+dc3-gender","memory_version":"bd451833d4ebb8a0ca07caf8deda76f027e1f42b78654987a22d215acf955eb7","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v4-percent-latin-brokenword+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v6"},"stages":[{"name":"draft","role":"translator","model":"fake-model","prompt_version":"v-golden","prompt_sha256":"26d0245a56d180eafd200ebe5311aab2488142c4a98dcddee21ea457814aa21a","temperature":0.3,"reasoning":"off","capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":4000},"escalate_to":"fake-fallback","escalate_capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":6000}}]} -snapshot_edit: 9d592c5bdb94885f91e2711a24bacfe64c5d2a3817162090dd209320bbee992d -snapshot_edit_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v5-output-budget-editunit+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v1-refusal+echo015+loop+u15.0.0","pipeline_core":"C1","max_output_ratio":2,"min_max_tokens":512,"context_assembly":{"glossary_injection":"selective","glossary_token_budget":800,"cache_ttl":""},"segmentation":{"draft_budget_out":1797,"edit_ceiling_out":3200,"fertility_cjk":1.1978,"fertility_other":0.3852},"render_format_version":"renderfmt-v3-draft-gender+editor-src2dst+dc3-gender","memory_version":"0a48d6ed9409de09f514af91aebdd622637621c9dc9464e8898c238791033d91","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v4-percent-latin-brokenword+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v6"},"stages":[{"name":"edit","role":"editor","model":"fake-model","prompt_version":"v-golden","prompt_sha256":"6a8e7f71139d6bc09a95159d1cb895e05b56d2f2f7417c5e20a7073c517c79f6","temperature":0.4,"reasoning":"off","capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":4000}}]} +snapshot_draft: 6b24296222d40ad71161602f46a3ec6c8e96408fd5d5ea64e021356b9cf3bd1c +snapshot_draft_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v5-output-budget-editunit+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v1-refusal+echo015+loop+u15.0.0","pipeline_core":"C1","max_output_ratio":2,"min_max_tokens":512,"context_assembly":{"glossary_injection":"selective","glossary_token_budget":800,"cache_ttl":""},"segmentation":{"draft_budget_out":1797,"edit_ceiling_out":3200,"fertility_cjk":1.1978,"fertility_other":0.3852},"render_format_version":"renderfmt-v3-draft-gender+editor-src2dst+dc3-gender","memory_version":"bd451833d4ebb8a0ca07caf8deda76f027e1f42b78654987a22d215acf955eb7","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v5-chevron-speech-attribution+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v6"},"stages":[{"name":"draft","role":"translator","model":"fake-model","prompt_version":"v-golden","prompt_sha256":"26d0245a56d180eafd200ebe5311aab2488142c4a98dcddee21ea457814aa21a","temperature":0.3,"reasoning":"off","capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":4000},"escalate_to":"fake-fallback","escalate_capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":6000}}]} +snapshot_edit: 2853c6fdbe8bec673eaea6a27b65c51c8272cb5a7ea3647c5bc8f19c78bb8c59 +snapshot_edit_payload: {"brief_hash":"c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df","chunker_version":"chunker-v5-output-budget-editunit+u15.0.0","estimator_version":"estimator-v0+u15.0.0","max_tokens_policy":"maxtok-v1-double-per-attempt","classifier_version":"classify-v1-refusal+echo015+loop+u15.0.0","pipeline_core":"C1","max_output_ratio":2,"min_max_tokens":512,"context_assembly":{"glossary_injection":"selective","glossary_token_budget":800,"cache_ttl":""},"segmentation":{"draft_budget_out":1797,"edit_ceiling_out":3200,"fertility_cjk":1.1978,"fertility_other":0.3852},"render_format_version":"renderfmt-v3-draft-gender+editor-src2dst+dc3-gender","memory_version":"0a48d6ed9409de09f514af91aebdd622637621c9dc9464e8898c238791033d91","postcheck_gate":false,"coverage":{"enabled":false},"style_check_version":"cheapgate-v5-chevron-speech-attribution+u15.0.0","sanitizer":{"enabled":true,"version":"sanitizer-v6"},"stages":[{"name":"edit","role":"editor","model":"fake-model","prompt_version":"v-golden","prompt_sha256":"6a8e7f71139d6bc09a95159d1cb895e05b56d2f2f7417c5e20a7073c517c79f6","temperature":0.4,"reasoning":"off","capability":{"Budget":"max_tokens","Temp":"send","TempValue":0,"Reasoning":{"Control":"none","OffEffort":"","OffExtraBody":null,"OnExtraBody":null},"MinMaxTokens":4000}}]} brief_hash: c2021b1b5e6c20f6084e425b9a70bdc5f682801be347a57eb1c591deb8cab0df memory_version: 0a48d6ed9409de09f514af91aebdd622637621c9dc9464e8898c238791033d91 base_memory_version: bd451833d4ebb8a0ca07caf8deda76f027e1f42b78654987a22d215acf955eb7 @@ -178,58 +178,58 @@ chunk ch8/0 disposition=flagged flag="sanitizer_stripped" final_text="Судзу stage=edit role=editor model=fake-model resume=true disp=flagged flag="sanitizer_stripped" attempts=1 escalated=false esc_model="" finish="sanitized_export" cum_usd=0.00182 detail="CJK leak in the ru output: 特产" stage_text="" recovered="Судзуки нашёл древний камень на каменном алтаре." -- chunk_status -- -ch1/0 draft snap_match=true content_hash=3e156226b9a74aa70c8a259c62e3d2fe68486a96d2178d31084c1e38a3256241 disp=ok flag="" attempts=1 final_hash=07dfef4f3137a6afdd879075709cbe1d4ae4aeffa73fd55d8d4a4b266de44191 cost=0.00182 escalated=false esc_model="" detail="" -ch1/0 edit snap_match=true content_hash=5e79dd5cbd310ac01d71ca258002beca18db715dfb8b1a08907508e506595355 disp=ok flag="" attempts=1 final_hash=ff6c6daffd6186e70f8dc9f634a9d8d3fdfbc90affb0a363427140bf52aea33a cost=0.00182 escalated=false esc_model="" detail="" -ch1/1 draft snap_match=true content_hash=82fb7f985de6c07182e8c09a3ceaad94f2e63745b87024087e437fba762b8227 disp=ok flag="" attempts=1 final_hash=970bd70afdd5d956114017f90ba600e63da0da7b2a1fb61908062966f8332503 cost=0.00182 escalated=false esc_model="" detail="" -ch2/0 draft snap_match=true content_hash=cfab1e09264a3a30aba95e7da1d6a269468c6aa0c1b334d9241e78bc266a3571 disp=ok flag="" attempts=1 final_hash=0c5929335c49b0f302948b0efe77ad078ec7538f0020b76c020e36272afe652e cost=0.00728 escalated=true esc_model="fake-fallback" detail="" -ch2/0 edit snap_match=true content_hash=55ffd839a4a26c03cf57e08000ab2c84b267e1429ab4a8e212d3a01fda757632 disp=ok flag="" attempts=1 final_hash=e76420deca3abe19ba49ea6de7af5938e49ef579950bf929896bf8dc6c418a28 cost=0.00182 escalated=false esc_model="" detail="" +ch1/0 draft snap_match=true content_hash=3e156226b9a74aa70c8a259c62e3d2fe68486a96d2178d31084c1e38a3256241 disp=ok flag="" attempts=1 final_hash=39af9d91caa8573f8d536044c56be4124e5a788653fffd1ea43bd23c54422e37 cost=0.00182 escalated=false esc_model="" detail="" +ch1/0 edit snap_match=true content_hash=5e79dd5cbd310ac01d71ca258002beca18db715dfb8b1a08907508e506595355 disp=ok flag="" attempts=1 final_hash=cf05fcf7d40e1a81f298b5ea53e810283d4aad4b7dd9e59458e7bfb0405792ee cost=0.00182 escalated=false esc_model="" detail="" +ch1/1 draft snap_match=true content_hash=82fb7f985de6c07182e8c09a3ceaad94f2e63745b87024087e437fba762b8227 disp=ok flag="" attempts=1 final_hash=c22fd70faa869950d3034f9bfb1f3338e0f999c42151f51af70b9e96010cbf8d cost=0.00182 escalated=false esc_model="" detail="" +ch2/0 draft snap_match=true content_hash=cfab1e09264a3a30aba95e7da1d6a269468c6aa0c1b334d9241e78bc266a3571 disp=ok flag="" attempts=1 final_hash=2feaf69aa3c1bdbd4e1f0ce4bd9a9afdae0b223a6b6e3e82d424b85a5d902bac cost=0.00728 escalated=true esc_model="fake-fallback" detail="" +ch2/0 edit snap_match=true content_hash=55ffd839a4a26c03cf57e08000ab2c84b267e1429ab4a8e212d3a01fda757632 disp=ok flag="" attempts=1 final_hash=3665a6be181389cf7bd8507c29e05cc6af74749d3b88656ee09e5ba63fe64e91 cost=0.00182 escalated=false esc_model="" detail="" ch3/0 draft snap_match=true content_hash=21df44eb104900664bdaaeb2f716f51dcddbc81a71d845f6ba0f91c5bc4b2cfb disp=flagged flag="hard_refusal" attempts=1 final_hash= cost=0.00728 escalated=true esc_model="" detail="provider finish_reason=refusal" ch3/0 edit snap_match=true content_hash= disp=skipped flag="hard_refusal" attempts=0 final_hash= cost=0 escalated=false esc_model="" detail="skipped: a member draft chunk of this edit unit was flagged (hard_refusal)" -ch4/0 draft snap_match=true content_hash=7aabdcb89dea365c95363b2662144ca1a10b307d377f47a8e7cc19b5b872d287 disp=ok flag="" attempts=1 final_hash=2574511b38f07a6eef3dd781907eee77cbdc73978ef28d4c9785e32d28489b58 cost=0.00182 escalated=false esc_model="" detail="" -ch4/0 edit snap_match=true content_hash=2854dfe1648b0cf0cbb683cb7ce94c5eedc9223b2ce20c99dc008c7add6317ef disp=ok flag="" attempts=1 final_hash=0f8084a8bcf771d895963869671f83e4bf8f80c03a491ea999278d45878fc10c cost=0.00182 escalated=false esc_model="" detail="" -ch5/0 draft snap_match=true content_hash=2ed0a97f39cc5c57c62c3324f4646357372f26e3c5bae1773f24edf9449fc013 disp=ok flag="" attempts=1 final_hash=c00dc01d8cc53a31022467dd878ba7d4d6bd9b7c7a9ee5036f0b69ed6f10025b cost=0.00182 escalated=false esc_model="" detail="" -ch5/0 edit snap_match=true content_hash=b145d78e4d30a39cc4088a1ddb8d934234c65341c62c79545c2eff1e7ae0c3d3 disp=ok flag="" attempts=1 final_hash=508383e619d91f16be54e60712774c414d057784a2d406ca202872f7b98a08f7 cost=0.00182 escalated=false esc_model="" detail="" -ch6/0 draft snap_match=true content_hash=5f396d299a7ec5b10f3c8591e54ce7d84c4d31b815e561b0dd5f56635aef5a71 disp=ok flag="" attempts=1 final_hash=ee0cc663a115e983e4e5df712513ba7085d4a9727e1fa864eec5fa12f8cf5ad0 cost=0.00182 escalated=false esc_model="" detail="" +ch4/0 draft snap_match=true content_hash=7aabdcb89dea365c95363b2662144ca1a10b307d377f47a8e7cc19b5b872d287 disp=ok flag="" attempts=1 final_hash=6d2b9757a2e8924f0ba4120c1b2807ae497f31359943a40693a7bf4b1f0d58bd cost=0.00182 escalated=false esc_model="" detail="" +ch4/0 edit snap_match=true content_hash=2854dfe1648b0cf0cbb683cb7ce94c5eedc9223b2ce20c99dc008c7add6317ef disp=ok flag="" attempts=1 final_hash=24ae49ceba78e2e586b4929015786fd2959d3656da370884e1df2952e800fd62 cost=0.00182 escalated=false esc_model="" detail="" +ch5/0 draft snap_match=true content_hash=2ed0a97f39cc5c57c62c3324f4646357372f26e3c5bae1773f24edf9449fc013 disp=ok flag="" attempts=1 final_hash=5676107da33357c37a119379de707e4f3c07a2798a013bd4435cdee8ce1136cb cost=0.00182 escalated=false esc_model="" detail="" +ch5/0 edit snap_match=true content_hash=b145d78e4d30a39cc4088a1ddb8d934234c65341c62c79545c2eff1e7ae0c3d3 disp=ok flag="" attempts=1 final_hash=f23301ccb29c15933935e9013f50c9215c9fdaeb6411806f9716b4fa74d8e334 cost=0.00182 escalated=false esc_model="" detail="" +ch6/0 draft snap_match=true content_hash=5f396d299a7ec5b10f3c8591e54ce7d84c4d31b815e561b0dd5f56635aef5a71 disp=ok flag="" attempts=1 final_hash=0a7d927a87e55ff540145a13acaaeea9a22867ae136334593b03497f40c298a9 cost=0.00182 escalated=false esc_model="" detail="" ch6/0 edit snap_match=true content_hash=592353321cc8000e792e0897446e4dd4f88a0280b9368b62cbd5c82b0c433193 disp=flagged flag="sanitizer_defect" attempts=1 final_hash= cost=0.00182 escalated=false esc_model="" detail="leading service preamble: Вот перевод фрагмента:" -ch7/0 draft snap_match=true content_hash=15c3ebbd56bb19880202d35b0206357655da245343376492f7ceb61808c8a5be disp=ok flag="" attempts=1 final_hash=df112533bae9eb53eeee7535189479bb62113829691e257827eb6a122c4d24a5 cost=0.00182 escalated=false esc_model="" detail="" -ch7/0 edit snap_match=true content_hash=0b2674ed9829b2e6526ff41a60dd280b6670c539476526bbe835d9bf7a6be4c4 disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:e6daeb1f55975010b0d9eafc3d7b9d23891d62e93db9203b1b65c6673180171a cost=0.00182 escalated=false esc_model="" detail="markdown header in the output: ### Глава 7" -ch8/0 draft snap_match=true content_hash=4a420977cc34ce738b4978e6a1baf45d78d773a135070c3b7f8c723adfa57d7b disp=ok flag="" attempts=1 final_hash=3ae07f69c98630ae05ed56465ca12b895aafef0376cb4e6fef6fb37d2edf4fb6 cost=0.00182 escalated=false esc_model="" detail="" -ch8/0 edit snap_match=true content_hash=c851100c8d6785f77d68f8127472790ae9d0408b7fec44785d3ff9d46842c6f8 disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:1994445050afdc0455c60b75974a47d9430b847d7c5158d3cebfbea933c61dbd cost=0.00182 escalated=false esc_model="" detail="CJK leak in the ru output: 特产" +ch7/0 draft snap_match=true content_hash=15c3ebbd56bb19880202d35b0206357655da245343376492f7ceb61808c8a5be disp=ok flag="" attempts=1 final_hash=50290abd81e52a0cbd36e2e1b3d9383d7d6bb955953f8ffa22e97cbcf2d01315 cost=0.00182 escalated=false esc_model="" detail="" +ch7/0 edit snap_match=true content_hash=0b2674ed9829b2e6526ff41a60dd280b6670c539476526bbe835d9bf7a6be4c4 disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:ac4b2483fc2eb6289a21e618b493a77960842f0835b8d8b5f11ef87700dfc81e cost=0.00182 escalated=false esc_model="" detail="markdown header in the output: ### Глава 7" +ch8/0 draft snap_match=true content_hash=4a420977cc34ce738b4978e6a1baf45d78d773a135070c3b7f8c723adfa57d7b disp=ok flag="" attempts=1 final_hash=cfead80d01c4339777f8c5f5b0cc4e1167d351067fd3db389dbe44938ddaaca6 cost=0.00182 escalated=false esc_model="" detail="" +ch8/0 edit snap_match=true content_hash=c851100c8d6785f77d68f8127472790ae9d0408b7fec44785d3ff9d46842c6f8 disp=flagged flag="sanitizer_stripped" attempts=1 final_hash=tm-sanitized-v1:457c063d9941c2ca2fd834cae9516bc7e966f205efcb44e02704d0f2acd2f82c cost=0.00182 escalated=false esc_model="" detail="CJK leak in the ru output: 特产" -- request_log (insertion order) -- -ch1/0 draft role=translator req=fake-model actual=fake-model hash=07dfef4f3137a6afdd879075709cbe1d4ae4aeffa73fd55d8d4a4b266de44191 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch1/1 draft role=translator req=fake-model actual=fake-model hash=970bd70afdd5d956114017f90ba600e63da0da7b2a1fb61908062966f8332503 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch2/0 draft role=translator req=fake-model actual=fake-model hash=3c87997526311911cbe02fb9928b43ef9ae420872719390304c2edcbf460d8b8 tm_hit=0 ok=0 finish="stop" degraded="cjk_artifact" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch2/0 draft role=translator req=fake-fallback actual=fake-fallback hash=0c5929335c49b0f302948b0efe77ad078ec7538f0020b76c020e36272afe652e tm_hit=0 ok=1 finish="stop" degraded="" cost=0.0054600000000000004 tokens=1000/200/0/500/0 err="" -ch3/0 draft role=translator req=fake-model actual=fake-model hash=069f2401b99af0393fb0d63792c89a880c4065a712d49725e9dff72eea0dda43 tm_hit=0 ok=0 finish="refusal" degraded="hard_refusal" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch3/0 draft role=translator req=fake-fallback actual=fake-fallback hash=ae51067d2d180033744a10163f4606b9fcdb441931f497c55dbd36e6c9f07c42 tm_hit=0 ok=0 finish="refusal" degraded="hard_refusal" cost=0.0054600000000000004 tokens=1000/200/0/500/0 err="" -ch4/0 draft role=translator req=fake-model actual=fake-model hash=2574511b38f07a6eef3dd781907eee77cbdc73978ef28d4c9785e32d28489b58 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch5/0 draft role=translator req=fake-model actual=fake-model hash=c00dc01d8cc53a31022467dd878ba7d4d6bd9b7c7a9ee5036f0b69ed6f10025b tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch6/0 draft role=translator req=fake-model actual=fake-model hash=ee0cc663a115e983e4e5df712513ba7085d4a9727e1fa864eec5fa12f8cf5ad0 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch7/0 draft role=translator req=fake-model actual=fake-model hash=df112533bae9eb53eeee7535189479bb62113829691e257827eb6a122c4d24a5 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch8/0 draft role=translator req=fake-model actual=fake-model hash=3ae07f69c98630ae05ed56465ca12b895aafef0376cb4e6fef6fb37d2edf4fb6 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch1/0 edit role=editor req=fake-model actual=fake-model hash=ff6c6daffd6186e70f8dc9f634a9d8d3fdfbc90affb0a363427140bf52aea33a tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch2/0 edit role=editor req=fake-model actual=fake-model hash=e76420deca3abe19ba49ea6de7af5938e49ef579950bf929896bf8dc6c418a28 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch4/0 edit role=editor req=fake-model actual=fake-model hash=0f8084a8bcf771d895963869671f83e4bf8f80c03a491ea999278d45878fc10c tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch5/0 edit role=editor req=fake-model actual=fake-model hash=508383e619d91f16be54e60712774c414d057784a2d406ca202872f7b98a08f7 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch6/0 edit role=editor req=fake-model actual=fake-model hash=f3a5675d5ba381251ebfed396f0a79b7bae0d727339adb152fe97dfccd9c15b3 tm_hit=0 ok=0 finish="stop" degraded="sanitizer_defect" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch7/0 edit role=editor req=fake-model actual=fake-model hash=2ded6927e1537e73b62b017ee8a27095906ad5f2eefa2d5463de41694dc618ce tm_hit=0 ok=0 finish="stop" degraded="sanitizer_stripped" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch8/0 edit role=editor req=fake-model actual=fake-model hash=456a4a0945d84716e9242516ca980e88241f325075bd7cdc252e2e233da3e34b tm_hit=0 ok=0 finish="stop" degraded="sanitizer_stripped" cost=0.00182 tokens=1000/200/0/500/0 err="" -ch1/0 draft role=translator req=fake-model actual=fake-model hash=07dfef4f3137a6afdd879075709cbe1d4ae4aeffa73fd55d8d4a4b266de44191 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" -ch1/1 draft role=translator req=fake-model actual=fake-model hash=970bd70afdd5d956114017f90ba600e63da0da7b2a1fb61908062966f8332503 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" -ch2/0 draft role=translator req=fake-model actual=fake-fallback hash=0c5929335c49b0f302948b0efe77ad078ec7538f0020b76c020e36272afe652e tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch1/0 draft role=translator req=fake-model actual=fake-model hash=39af9d91caa8573f8d536044c56be4124e5a788653fffd1ea43bd23c54422e37 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch1/1 draft role=translator req=fake-model actual=fake-model hash=c22fd70faa869950d3034f9bfb1f3338e0f999c42151f51af70b9e96010cbf8d tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch2/0 draft role=translator req=fake-model actual=fake-model hash=8a8bc4539e7b95cd941fb6f8198c78777f49534ddd183a6f3777712d82554df3 tm_hit=0 ok=0 finish="stop" degraded="cjk_artifact" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch2/0 draft role=translator req=fake-fallback actual=fake-fallback hash=2feaf69aa3c1bdbd4e1f0ce4bd9a9afdae0b223a6b6e3e82d424b85a5d902bac tm_hit=0 ok=1 finish="stop" degraded="" cost=0.0054600000000000004 tokens=1000/200/0/500/0 err="" +ch3/0 draft role=translator req=fake-model actual=fake-model hash=0e5413f92f0a3e6b9a189a60ad943f56511076462978fdf9e5e3c7688013ddd7 tm_hit=0 ok=0 finish="refusal" degraded="hard_refusal" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch3/0 draft role=translator req=fake-fallback actual=fake-fallback hash=55bf6601304ea2a43727a3713994bc1ac6034e564a36c390144950e3c4885403 tm_hit=0 ok=0 finish="refusal" degraded="hard_refusal" cost=0.0054600000000000004 tokens=1000/200/0/500/0 err="" +ch4/0 draft role=translator req=fake-model actual=fake-model hash=6d2b9757a2e8924f0ba4120c1b2807ae497f31359943a40693a7bf4b1f0d58bd tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch5/0 draft role=translator req=fake-model actual=fake-model hash=5676107da33357c37a119379de707e4f3c07a2798a013bd4435cdee8ce1136cb tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch6/0 draft role=translator req=fake-model actual=fake-model hash=0a7d927a87e55ff540145a13acaaeea9a22867ae136334593b03497f40c298a9 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch7/0 draft role=translator req=fake-model actual=fake-model hash=50290abd81e52a0cbd36e2e1b3d9383d7d6bb955953f8ffa22e97cbcf2d01315 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch8/0 draft role=translator req=fake-model actual=fake-model hash=cfead80d01c4339777f8c5f5b0cc4e1167d351067fd3db389dbe44938ddaaca6 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch1/0 edit role=editor req=fake-model actual=fake-model hash=cf05fcf7d40e1a81f298b5ea53e810283d4aad4b7dd9e59458e7bfb0405792ee tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch2/0 edit role=editor req=fake-model actual=fake-model hash=3665a6be181389cf7bd8507c29e05cc6af74749d3b88656ee09e5ba63fe64e91 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch4/0 edit role=editor req=fake-model actual=fake-model hash=24ae49ceba78e2e586b4929015786fd2959d3656da370884e1df2952e800fd62 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch5/0 edit role=editor req=fake-model actual=fake-model hash=f23301ccb29c15933935e9013f50c9215c9fdaeb6411806f9716b4fa74d8e334 tm_hit=0 ok=1 finish="stop" degraded="" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch6/0 edit role=editor req=fake-model actual=fake-model hash=af6f364161222402ba0c9317119b413fa7e2a875d77409e8b9a8e96032a277fc tm_hit=0 ok=0 finish="stop" degraded="sanitizer_defect" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch7/0 edit role=editor req=fake-model actual=fake-model hash=09e0c35087fc37fdfa527ea756aaba4bd36fb2537e1a1f2d49abcc60252efc99 tm_hit=0 ok=0 finish="stop" degraded="sanitizer_stripped" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch8/0 edit role=editor req=fake-model actual=fake-model hash=0dec718729487522613e96b02d60661ffbd45c6f2c04151341da7f80d972289b tm_hit=0 ok=0 finish="stop" degraded="sanitizer_stripped" cost=0.00182 tokens=1000/200/0/500/0 err="" +ch1/0 draft role=translator req=fake-model actual=fake-model hash=39af9d91caa8573f8d536044c56be4124e5a788653fffd1ea43bd23c54422e37 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch1/1 draft role=translator req=fake-model actual=fake-model hash=c22fd70faa869950d3034f9bfb1f3338e0f999c42151f51af70b9e96010cbf8d tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch2/0 draft role=translator req=fake-model actual=fake-fallback hash=2feaf69aa3c1bdbd4e1f0ce4bd9a9afdae0b223a6b6e3e82d424b85a5d902bac tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" ch3/0 draft role=translator req=fake-model actual=fake-model hash= tm_hit=1 ok=0 finish="" degraded="hard_refusal" cost=0 tokens=0/0/0/0/0 err="" -ch4/0 draft role=translator req=fake-model actual=fake-model hash=2574511b38f07a6eef3dd781907eee77cbdc73978ef28d4c9785e32d28489b58 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" -ch5/0 draft role=translator req=fake-model actual=fake-model hash=c00dc01d8cc53a31022467dd878ba7d4d6bd9b7c7a9ee5036f0b69ed6f10025b tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" -ch6/0 draft role=translator req=fake-model actual=fake-model hash=ee0cc663a115e983e4e5df712513ba7085d4a9727e1fa864eec5fa12f8cf5ad0 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" -ch7/0 draft role=translator req=fake-model actual=fake-model hash=df112533bae9eb53eeee7535189479bb62113829691e257827eb6a122c4d24a5 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" -ch8/0 draft role=translator req=fake-model actual=fake-model hash=3ae07f69c98630ae05ed56465ca12b895aafef0376cb4e6fef6fb37d2edf4fb6 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" -ch1/0 edit role=editor req=fake-model actual=fake-model hash=ff6c6daffd6186e70f8dc9f634a9d8d3fdfbc90affb0a363427140bf52aea33a tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" -ch2/0 edit role=editor req=fake-model actual=fake-model hash=e76420deca3abe19ba49ea6de7af5938e49ef579950bf929896bf8dc6c418a28 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" -ch4/0 edit role=editor req=fake-model actual=fake-model hash=0f8084a8bcf771d895963869671f83e4bf8f80c03a491ea999278d45878fc10c tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" -ch5/0 edit role=editor req=fake-model actual=fake-model hash=508383e619d91f16be54e60712774c414d057784a2d406ca202872f7b98a08f7 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch4/0 draft role=translator req=fake-model actual=fake-model hash=6d2b9757a2e8924f0ba4120c1b2807ae497f31359943a40693a7bf4b1f0d58bd tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch5/0 draft role=translator req=fake-model actual=fake-model hash=5676107da33357c37a119379de707e4f3c07a2798a013bd4435cdee8ce1136cb tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch6/0 draft role=translator req=fake-model actual=fake-model hash=0a7d927a87e55ff540145a13acaaeea9a22867ae136334593b03497f40c298a9 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch7/0 draft role=translator req=fake-model actual=fake-model hash=50290abd81e52a0cbd36e2e1b3d9383d7d6bb955953f8ffa22e97cbcf2d01315 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch8/0 draft role=translator req=fake-model actual=fake-model hash=cfead80d01c4339777f8c5f5b0cc4e1167d351067fd3db389dbe44938ddaaca6 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch1/0 edit role=editor req=fake-model actual=fake-model hash=cf05fcf7d40e1a81f298b5ea53e810283d4aad4b7dd9e59458e7bfb0405792ee tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch2/0 edit role=editor req=fake-model actual=fake-model hash=3665a6be181389cf7bd8507c29e05cc6af74749d3b88656ee09e5ba63fe64e91 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch4/0 edit role=editor req=fake-model actual=fake-model hash=24ae49ceba78e2e586b4929015786fd2959d3656da370884e1df2952e800fd62 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" +ch5/0 edit role=editor req=fake-model actual=fake-model hash=f23301ccb29c15933935e9013f50c9215c9fdaeb6411806f9716b4fa74d8e334 tm_hit=1 ok=1 finish="stop" degraded="" cost=0 tokens=0/0/0/0/0 err="" ch6/0 edit role=editor req=fake-model actual=fake-model hash= tm_hit=1 ok=0 finish="" degraded="sanitizer_defect" cost=0 tokens=0/0/0/0/0 err="" -ch7/0 edit role=editor req=fake-model actual=fake-model hash=tm-sanitized-v1:e6daeb1f55975010b0d9eafc3d7b9d23891d62e93db9203b1b65c6673180171a tm_hit=1 ok=0 finish="sanitized_export" degraded="sanitizer_stripped" cost=0 tokens=0/0/0/0/0 err="" -ch8/0 edit role=editor req=fake-model actual=fake-model hash=tm-sanitized-v1:1994445050afdc0455c60b75974a47d9430b847d7c5158d3cebfbea933c61dbd tm_hit=1 ok=0 finish="sanitized_export" degraded="sanitizer_stripped" cost=0 tokens=0/0/0/0/0 err="" +ch7/0 edit role=editor req=fake-model actual=fake-model hash=tm-sanitized-v1:ac4b2483fc2eb6289a21e618b493a77960842f0835b8d8b5f11ef87700dfc81e tm_hit=1 ok=0 finish="sanitized_export" degraded="sanitizer_stripped" cost=0 tokens=0/0/0/0/0 err="" +ch8/0 edit role=editor req=fake-model actual=fake-model hash=tm-sanitized-v1:457c063d9941c2ca2fd834cae9516bc7e966f205efcb44e02704d0f2acd2f82c tm_hit=1 ok=0 finish="sanitized_export" degraded="sanitizer_stripped" cost=0 tokens=0/0/0/0/0 err="" -- retrieval_state -- ch1/0 snap_match=true exact=3 sticky=0 ambiguous=0 spoiler=1 evicted=0 postcheck_miss=1 style_flags=0 trust_gated=0 injected_ids=["紋章\u001f\u001f0\u001f0","鈴木\u001f\u001f0\u001f0","魔法学院\u001f\u001f0\u001f0"] postcheck_detail=[{"src":"紋章","dst":"герб","disp":"confirmed"}] style_detail= trust_gate_detail= diff --git a/backend/internal/pipeline/waverun.go b/backend/internal/pipeline/waverun.go index 07fde9df..eba0c2a9 100644 --- a/backend/internal/pipeline/waverun.go +++ b/backend/internal/pipeline/waverun.go @@ -258,6 +258,7 @@ type stageSeqResult struct { recovered string // sanitizer-stripped export text from the flagging stage (D35.4a) cost float64 // THIS run's spend across the sequence bankTelem bankFlags // the translator-role stage's banknote telemetry (WS4 point 10) + bankProps string // JSON of that stage's PARSED banknote entries (the WHAT channel, D39.36 fix) repair repairResult // the shipping stage's repair outcome (pack-16); zero when the gate is off } @@ -299,7 +300,7 @@ func (r *Runner) runStageSequence(ctx context.Context, staged []wavedStage, snap res.repair = sr.Repair } if st.Role == roleTranslator { - res.bankTelem = sr.BankFlags + res.bankTelem, res.bankProps = sr.BankFlags, sr.BankProposals // FL-2: the resume fast-path serves the banknote-cleaned final_hash and leaves BankFlags zero; // restore the telemetry the fresh run persisted so persistRetrievalState does not zero the three // banknote columns on every resume of a ready chunk (guarded on an empty BankFlags so the @@ -309,6 +310,10 @@ func (r *Runner) runStageSequence(ctx context.Context, staged []wavedStage, snap res.bankTelem = bankFlags{ NLines: prevRS.NBanknoteLines, ParseFail: prevRS.BanknoteParseFail != 0, Truncated: prevRS.BanknoteTruncated != 0, } + // The proposals restore with the counters and for the same reason: the fast path never + // touches the raw checkpoint, so re-writing the row from a resumed run would erase the + // very WHAT the signature map is about to read. + res.bankProps = prevRS.BanknoteDetail } } } @@ -373,7 +378,7 @@ func (r *Runner) runDraftChunk(ctx context.Context, draftSnapshot string, ch chu cheap = checks.RunCheapGates(ch.Text, seq.finalText, seq.finalText, r.cheapGateConfig()) } if r.baseMemory != nil { - if err := r.persistRetrievalState(draftSnapshot, ch, memSel, postMisses, outputChecked, cheap, seq.bankTelem); err != nil { + if err := r.persistRetrievalState(draftSnapshot, ch, memSel, postMisses, outputChecked, cheap, seq.bankTelem, seq.bankProps); err != nil { return seq, err } } diff --git a/backend/internal/store/chunkstatus.go b/backend/internal/store/chunkstatus.go index f3ee252d..cea1485d 100644 --- a/backend/internal/store/chunkstatus.go +++ b/backend/internal/store/chunkstatus.go @@ -41,6 +41,14 @@ type ChunkStatus struct { // ATTEMPTED model instead, so a flagged row from an older run may name a fallback that in fact // refused. Old rows are not rewritten; a book re-run under the current code re-resolves them. EscalationModel string + // FirstFlagReason is the disposition reason of the FIRST attempt when a LATER one recovered the + // chunk (a regenerate with a bigger budget, or the single-hop escalation). It is "" when the first + // attempt already resolved ok, and it is NOT the row's verdict — FlagReason is. It exists because a + // recovered row otherwise erases the primary failure from every durable surface, and one of those + // failures (cjk_artifact) is the DeepSeek echo mine, whose rate is the only number watching it + // (D18/D19; the mini-run of 25.07 measured 0.0% where 1 of 20 drafts had in fact echoed). + // Observability only: re-derived from the stored checkpoints on every run, never a verdict, never wire. + FirstFlagReason string } // UpsertChunkStatus writes (or overwrites) the disposition row. Overwrite is the @@ -53,8 +61,8 @@ func (s *Store) UpsertChunkStatus(cs ChunkStatus) error { INSERT INTO chunk_status ( book_id, chapter, chunk_idx, stage, snapshot_id, content_hash, disposition, flag_reason, attempts, final_hash, cost_usd, detail, - escalated, escalation_model, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now')) + escalated, escalation_model, first_flag_reason, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now')) ON CONFLICT (book_id, chapter, chunk_idx, stage) DO UPDATE SET snapshot_id = excluded.snapshot_id, content_hash = excluded.content_hash, @@ -66,10 +74,11 @@ func (s *Store) UpsertChunkStatus(cs ChunkStatus) error { detail = excluded.detail, escalated = excluded.escalated, escalation_model = excluded.escalation_model, + first_flag_reason = excluded.first_flag_reason, updated_at = excluded.updated_at`, cs.BookID, cs.Chapter, cs.ChunkIdx, cs.Stage, cs.SnapshotID, cs.ContentHash, cs.Disposition, cs.FlagReason, cs.Attempts, cs.FinalHash, cs.CostUSD, cs.Detail, - boolToInt(cs.Escalated), cs.EscalationModel) + boolToInt(cs.Escalated), cs.EscalationModel, cs.FirstFlagReason) return err } @@ -81,12 +90,12 @@ func (s *Store) GetChunkStatus(bookID string, chapter, chunkIdx int, stage strin var escalated int err := s.r.QueryRowContext(ctx, ` SELECT snapshot_id, content_hash, disposition, flag_reason, attempts, final_hash, cost_usd, detail, - escalated, escalation_model + escalated, escalation_model, first_flag_reason FROM chunk_status WHERE book_id = ? AND chapter = ? AND chunk_idx = ? AND stage = ?`, bookID, chapter, chunkIdx, stage).Scan( &cs.SnapshotID, &cs.ContentHash, &cs.Disposition, &cs.FlagReason, &cs.Attempts, &cs.FinalHash, &cs.CostUSD, &cs.Detail, - &escalated, &cs.EscalationModel) + &escalated, &cs.EscalationModel, &cs.FirstFlagReason) cs.Escalated = escalated != 0 if errors.Is(err, sql.ErrNoRows) { return nil, nil @@ -102,7 +111,7 @@ func (s *Store) GetChunkStatus(bookID string, chapter, chunkIdx int, stage strin func (s *Store) ChunkStatusesForBook(bookID string) ([]ChunkStatus, error) { return queryAll(s.r, ` SELECT chapter, chunk_idx, stage, snapshot_id, content_hash, disposition, flag_reason, - attempts, final_hash, cost_usd, detail, escalated, escalation_model + attempts, final_hash, cost_usd, detail, escalated, escalation_model, first_flag_reason FROM chunk_status WHERE book_id = ? ORDER BY chapter, chunk_idx, stage`, func(rows *sql.Rows) (ChunkStatus, error) { @@ -110,7 +119,7 @@ func (s *Store) ChunkStatusesForBook(bookID string) ([]ChunkStatus, error) { var escalated int if err := rows.Scan(&cs.Chapter, &cs.ChunkIdx, &cs.Stage, &cs.SnapshotID, &cs.ContentHash, &cs.Disposition, &cs.FlagReason, &cs.Attempts, &cs.FinalHash, &cs.CostUSD, &cs.Detail, - &escalated, &cs.EscalationModel); err != nil { + &escalated, &cs.EscalationModel, &cs.FirstFlagReason); err != nil { return cs, err } cs.Escalated = escalated != 0 diff --git a/backend/internal/store/glossary.go b/backend/internal/store/glossary.go index 0e3e8fdd..c30dc985 100644 --- a/backend/internal/store/glossary.go +++ b/backend/internal/store/glossary.go @@ -85,6 +85,13 @@ type RetrievalState struct { NBanknoteLines int BanknoteParseFail int BanknoteTruncated int + // BanknoteDetail is the JSON of the chunk's PARSED banknote entries (src / proposed dst / type) — + // the WHAT channel itself, which D39.36 found the code parsed and dropped into `_`. It is the fourth + // *_detail column of this row and behaves like its siblings: re-derived deterministically from the + // raw checkpoint on every run (self-healing), observability only, never wire-load-bearing. The + // bank-mining stop reads it to attach the translator's PROPOSED rendering to the owner's signature + // map; the proposal stays status:auto, so delivering the WHAT grants no trust. + BanknoteDetail string // JSON } // ReplaceGlossary replaces a book's ENTIRE glossary (rows + aliases) with the given @@ -251,8 +258,8 @@ func (s *Store) UpsertRetrievalState(rs RetrievalState) error { n_exact_hits, n_sticky, n_ambiguous_flagged, n_spoiler_blocked, n_evicted, embedding_tier_used, n_postcheck_miss, postcheck_detail, injected_ids, n_style_flags, style_detail, n_trust_gated_suppress, trust_gate_detail, - n_banknote_lines, banknote_parse_fail, banknote_truncated, updated_at - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now')) + n_banknote_lines, banknote_parse_fail, banknote_truncated, banknote_detail, updated_at + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now')) ON CONFLICT (book_id, chapter, chunk_idx) DO UPDATE SET snapshot_id = excluded.snapshot_id, n_exact_hits = excluded.n_exact_hits, @@ -269,6 +276,7 @@ func (s *Store) UpsertRetrievalState(rs RetrievalState) error { n_trust_gated_suppress = excluded.n_trust_gated_suppress, trust_gate_detail = excluded.trust_gate_detail, n_banknote_lines = excluded.n_banknote_lines, + banknote_detail = excluded.banknote_detail, banknote_parse_fail = excluded.banknote_parse_fail, banknote_truncated = excluded.banknote_truncated, updated_at = excluded.updated_at`, @@ -276,7 +284,7 @@ func (s *Store) UpsertRetrievalState(rs RetrievalState) error { rs.NExactHits, rs.NSticky, rs.NAmbiguousFlagged, rs.NSpoilerBlocked, rs.NEvicted, rs.EmbeddingTierUsed, rs.NPostcheckMiss, rs.PostcheckDetail, rs.InjectedIDs, rs.NStyleFlags, rs.StyleDetail, rs.NTrustGatedSuppress, rs.TrustGateDetail, - rs.NBanknoteLines, rs.BanknoteParseFail, rs.BanknoteTruncated) + rs.NBanknoteLines, rs.BanknoteParseFail, rs.BanknoteTruncated, rs.BanknoteDetail) return err } @@ -289,13 +297,13 @@ func (s *Store) GetRetrievalState(bookID string, chapter, chunkIdx int) (*Retrie SELECT snapshot_id, n_exact_hits, n_sticky, n_ambiguous_flagged, n_spoiler_blocked, n_evicted, embedding_tier_used, n_postcheck_miss, postcheck_detail, injected_ids, n_style_flags, style_detail, n_trust_gated_suppress, trust_gate_detail, - n_banknote_lines, banknote_parse_fail, banknote_truncated + n_banknote_lines, banknote_parse_fail, banknote_truncated, banknote_detail FROM retrieval_state WHERE book_id = ? AND chapter = ? AND chunk_idx = ?`, bookID, chapter, chunkIdx).Scan( &rs.SnapshotID, &rs.NExactHits, &rs.NSticky, &rs.NAmbiguousFlagged, &rs.NSpoilerBlocked, &rs.NEvicted, &rs.EmbeddingTierUsed, &rs.NPostcheckMiss, &rs.PostcheckDetail, &rs.InjectedIDs, &rs.NStyleFlags, &rs.StyleDetail, &rs.NTrustGatedSuppress, &rs.TrustGateDetail, - &rs.NBanknoteLines, &rs.BanknoteParseFail, &rs.BanknoteTruncated) + &rs.NBanknoteLines, &rs.BanknoteParseFail, &rs.BanknoteTruncated, &rs.BanknoteDetail) if errors.Is(err, sql.ErrNoRows) { return nil, nil } @@ -312,7 +320,7 @@ func (s *Store) RetrievalStatesForBook(bookID string) ([]RetrievalState, error) SELECT chapter, chunk_idx, snapshot_id, n_exact_hits, n_sticky, n_ambiguous_flagged, n_spoiler_blocked, n_evicted, embedding_tier_used, n_postcheck_miss, postcheck_detail, injected_ids, n_style_flags, style_detail, n_trust_gated_suppress, trust_gate_detail, - n_banknote_lines, banknote_parse_fail, banknote_truncated + n_banknote_lines, banknote_parse_fail, banknote_truncated, banknote_detail FROM retrieval_state WHERE book_id = ? ORDER BY chapter, chunk_idx`, func(rows *sql.Rows) (RetrievalState, error) { @@ -321,7 +329,7 @@ func (s *Store) RetrievalStatesForBook(bookID string) ([]RetrievalState, error) &rs.NAmbiguousFlagged, &rs.NSpoilerBlocked, &rs.NEvicted, &rs.EmbeddingTierUsed, &rs.NPostcheckMiss, &rs.PostcheckDetail, &rs.InjectedIDs, &rs.NStyleFlags, &rs.StyleDetail, &rs.NTrustGatedSuppress, &rs.TrustGateDetail, - &rs.NBanknoteLines, &rs.BanknoteParseFail, &rs.BanknoteTruncated) + &rs.NBanknoteLines, &rs.BanknoteParseFail, &rs.BanknoteTruncated, &rs.BanknoteDetail) return rs, err }, bookID) } diff --git a/backend/internal/store/migrate.go b/backend/internal/store/migrate.go index d4cc8512..8f024321 100644 --- a/backend/internal/store/migrate.go +++ b/backend/internal/store/migrate.go @@ -303,6 +303,28 @@ var migrations = []string{ ALTER TABLE request_log ADD COLUMN estimated INTEGER NOT NULL DEFAULT 0; ALTER TABLE request_log ADD COLUMN est_tokens INTEGER NOT NULL DEFAULT 0; `, + // v11 (mini-run findings Д1 + Д4, 25.07): two facts the mini-run proved are produced and then lost. + // + // • chunk_status.first_flag_reason — the disposition reason of the FIRST attempt, kept when a later + // attempt (a regenerate or the single-hop escalation) recovered the chunk. Without it a recovered + // row is written `ok` with an empty flag_reason and the primary failure vanishes from every + // durable surface: the mini-run's echo (1 of 20 drafts, cjk_artifact, escalation-recovered) was + // reported as echo_draft=0.0%. Since the echo rate is the only numeric watchman of the DeepSeek + // echo mine (D18/D19), losing it hides the class it exists to watch. Re-derived every run from the + // stored checkpoints (self-heals on resume); observability only, never a verdict, never wire. + // + // • retrieval_state.banknote_detail — the PARSED ⟦TM-BANK-v1⟧ entries (src/dst/type) of the chunk, + // the WHAT channel D39.36 found is parsed and thrown into `_`. It mirrors the three *_detail + // JSON columns already on this row (postcheck / style / trust-gate): re-derived deterministically + // each run from the raw checkpoint, self-healing, never wire-load-bearing. The bank-mining stop + // reads it to attach the model's PROPOSED dst to the owner's signature map — proposals stay + // status:auto, so delivering the WHAT grants no trust (the owner still signs). + // + // Both are additive with defaults ⇒ every prior row and the WIRE are unchanged. + ` + ALTER TABLE chunk_status ADD COLUMN first_flag_reason TEXT NOT NULL DEFAULT ''; + ALTER TABLE retrieval_state ADD COLUMN banknote_detail TEXT NOT NULL DEFAULT ''; + `, } // DESIGN NOTE (D21.10 — reserved memory-bank v2 record types; Phase 2, NOT a migration, NOT code). diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 8f1c07b6..3d521b3b 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -1,12 +1,16 @@ # Журнал прогресса -> **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (на 2026-07-25, пак-17 закрыт, право по ToS подписано, пак-18 принят — долгов контракта нет, D39.26–D39.31). **Источник истины по РЕШЕНИЯМ — `architecture/05-decisions-log.md` (D1–D39.36); этот файл — ЖУРНАЛ.** +> **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (на 2026-07-25, пак-17 закрыт, право по ToS подписано, пак-18 принят — долгов контракта нет, D39.26–D39.31). **Источник истины по РЕШЕНИЯМ — `architecture/05-decisions-log.md` (D1–D39.37); этот файл — ЖУРНАЛ.** > - **Фазы/курс:** Ф0 ✅ · Ф1-инфра ✅ (D20–D28; golden = инвариант №8) · арка «качество-первым» D26→D38.5 закрыта · **арх-ресет D39 исполнен ЦЕЛИКОМ** (7 слоёв → программа D39.1–D39.10 → паки 11–16, D39.13–D39.24; статус слоёв — шапка-таблица `architecture/09-target-architecture.md`) · пере-прогон rerun2 прочитан (D39.20: операционка живьём, $0-резюм ×3 арма, $6.63<$15; планка ≤2 НЕ пройдена; анти-корреляция гладкость↔верность ×3 → mistral-редактор вон) · эксп ЗАКРЫТ (D39.22), итерация №2 отложена · **курс = разработка бэкенда: пак-17 «канал B вживую» ЗАКРЫТ ЦЕЛИКОМ (дизайн D39.26 · стройка D39.27 · дельта `allow` D39.28, 25.07); вокабуляр = ПАРА `violence`/`sexually-explicit`, остаток — ФАКТЫ ToS в зоне полигона (fail-closed до них)** (D39.25: в движке НЕТ понятия «18+» — generic content-labels × provider-capabilities; лейблы вне хешей · резолв до валидации · один хоп `chain[0]` · гранулярность = книга) → **КУРС АМЕНДИРОВАН ВЛАДЕЛЬЦЕМ 25.07 (D39.33): МАСШТАБ — ПОСЛЕДНИМ, после достройки алгоритмического идеала; доказательство эффективности крупных изменений — МИНИ-ПРОГОНЫ ~10 глав (детерминированные чекеры и скан остатка первыми, судья — только когда от него зависит решение).** **ОЧЕРЕДЬ ИСПРАВЛЕНА ЗАМЕРОМ (D39.35): $0-резюм драфта на стенде УЖЕ МЁРТВ** (снапшот разошёлся по трём осям — cheapgate v3→v4, renderfmt v2→v3, langpack 09974d6→a7d4be2), поэтому срочности «гнать, пока жив» НЕТ, а ниты перестали быть условными: **(1) микро-пак «предпрогонная гигиена» ($0-код): два нита общности + ОБЯЗАТЕЛЬНЫЙ флип `Gates.RegressionGuard.Enabled` (невыполненное обязательство D38.4 п.5) + опц. проекция пере-оплаты в `tmctl status`** · **(2) мини-прогон-замер ~10 глав** (драфт ≈$0.02, редактура ≈$0.08–0.15/арм, судья НЕ зовётся ⇒ один арм ≈$0.10–0.17, два ≈$0.20–0.32; перед прогоном бэкап `rerun2/*.db`, БД на схеме v9 против v10 бинаря) · **(3) параллельно $0 — сид/банк-дельта** (元 · 赤城 · 学堂家老 · 春秋蝉; два терма ждут подписи владельца) · **(4) пак-19 в урезанном скоупе** · (4) далее слой-2 извлечение из тел промтов · ru-target · native-Gemini судья. Затем — сид-дельта (元 · 赤城 · 学堂家老 · 春秋蝉), МАСШТАБ целой книги (7,78 млн символов, ~2284 раздела) и пилот Ф2.5. Хроника треков A/B, exp15/16 и стройки паков — архивы ниже + D-лог. > - **Стек:** draft deepseek-v4-flash (thinking ON) → **editor deepseek-v4-pro БИЛИНГВ ИНТЕРИМ (D39.22; glm-5 резерв, mistral вон D39.20)**, промпт v3-discourse (P1a+чэнъюй+few_shot-тумблер) → судья gemini-3.1-pro-preview; канал B Mistral+grok; 7 ключей; ~$0.85/ранобэ (D30.4). -> - **ОТКРЫТЫЕ ПЕТЛИ (норма 25.07, перепись 180 инцидентов: класс NEVER_CLOSED больше не копим — каждая петля обязана получить диспозицию решено/отложено-с-записью/отклонено; ведёт оркестратор).** *Владелец:* ~~вокабуляр content-лейблов~~ **РЕШЕНО 25.07: ПАРА `violence`/`sexually-explicit`** (D39.27 п.5 — сохраняет D14 п.1 и dspro-редактора на violence) ⇒ следствие в работе: assert-only `action` (дельта пака-17) · ~~Q2 редактор под лейблом~~ **ОТЛОЖЕН С ЗАПИСЬЮ** (D39.27 п.7: при паре связывает только `sexually-explicit`-книги, которых нет; возвращается вместе с предусловием D22.7 перед первой explicit-книгой; кандидаты — grok-4.3 интерим, glm-5 только после сверки ToS Z.AI по первоисточнику, mistral отвергнут D39.20) · ~~ToS-факты + три решения по ним~~ **ЗАКРЫТЫ 25.07 (D39.29 факты + D39.30 подписи владельца):** лейбл `violence` НЕ заводится, интерпретация «нейтральное изображение ≠ пропаганда» зафиксирована письменно · риск Z.AI (плоский запрет «violent content»; экспозиция состоялась в rerun2 через glm-арм) ПРИНЯТ сознательно с условиями пересмотра · две строки `accepts_labels: [sexually-explicit]` (`xai`, `mistral`) ПОДПИСАНЫ и ПРИМЕНЕНЫ в `models.yaml`. **Остаток ToS-вопросов ЗАКРЫТ владельцем 25.07 (D39.32):** local = «политик нет, переводим всё» ⇒ строка применена · Gemini-оговорка читается СУЖЕНИЕМ ⇒ вердикт `FORBIDDEN`→`UNCLEAR`, но по доктрине это НЕ разрешение: строку Gemini не получает, **ждёт одного слова, если владелец хочет подписать интерпретацию как разрешение** · причину пустоты в конфиг НЕ выносим (живёт в quirks-доке) · **ре-чек политик стал ПРАВИЛОМ** (триггеры: Google ToS 30.07.2026 · квартал 25.10.2026 · любая правка `accepts_labels`) · дата аккаунта OpenAI — открыта, приоритет низкий (OpenAI вне цепочки). **Продуктовое (Ф3):** Gemini API Additional ToS запрещает клиентов, «likely to be accessed by individuals under 18» — обязательство на конечный продукт, независимо от лейблов · **промпт-слой под лейблом (НОВОЕ, вскрыто вопросом владельца): лейбл сегодня НЕ доезжает до промпта** (чистые данные маршрутизации + load-time гарантия); нужно ли, чтобы переводчик/редактор ЗНАЛИ о свойстве контента — отдельное решение промпт-слоя · мини-каноны сид-дельты — 元-корень (предложение «изначальн-») и 春秋蝉 (подписано «Весенне-осенняя цикада», ждёт подтверждения) · **включение платной петли ремонта — по замеру остатка на пост-профилактическом прогоне (D39.24; заменил mini-live гейт D6)** · ja→ru-реплика §B5 (тайминг) · старые Ф2.5-блокеры (билингв-якорь — D25 п.9 Q1 · контаминация корпуса — D27 п.4 · судья-дублёр D22.6 · планка запуска · publishable/waiver — D25 п.1 · FN-bound L3 — D25 п.4 · юр-пакет · провенанс 12-*-доков · xAI-ключ off перед продом D27 · residual-тачпойнты exp16, не влитые в сид-дельту: мини-голд алиасов · precision@30, `books/gu-zhenren/exp16/`). *Оркестратор:* ~~ратификация дизайна пака-16~~ РЕШЕНО D39.24 (25.07) · фантом-гард D-ссылок в чек-листе лендинга (действует) · ~~doc-sync pass~~ **ИСПОЛНЕН 25.07** (запись ниже: оба puml перерисованы под пост-пак-16 · баннеры на 01/02/03/04/06/07/08/09 · ресёрч-свип 18/18c/19/20/21/22 · README/POLYGON_PACKAGE4 освежены; residual — пинги полигону в его зоне) · ~~приёмка пака-17 фаза 1~~ **РЕШЕНО D39.26 (25.07: принято в редакции §14, три добора приёмки, фаза 2 размечена)** · **`escalation_model` = ОТВЕТИВШАЯ модель (НОВОЕ, вынесено из пака-17): вердикт-изменение** — golden пинит `esc_model` на ОТКАЗАВШЕМ хопе, маскировка не скрывает ⇒ отдельная ратификация с санкционированным пере-капчером · **`--accept-rebill[=usd]` с порогом `min($0.50, 5%×ProjectedBookUSD)` РАТИФИЦИРОВАН D20.2-Q2, но в коде/CLI ОТСУТСТВУЕТ (НОВОЕ)** — живёт только в спеке `backend/docs/D15.2-*.md`; `--resnapshot` пере-пиннит без суммы · **D22.7 (пер-чанковый L3-скрин) — предусловие первой erotica-книги в проде НЕ снято паком-17 (НОВОЕ, держать отдельным гейтом)** · фантом-гард D-ссылок в чек-листе лендинга (действует). *Бэкенд:* ~~пак-17 (фазы 1–2 + дельта)~~ **ЗАКРЫТ ЦЕЛИКОМ 25.07 (D39.26/27/28)** · ~~ПАК-18 «долги контракта»~~ **ПРИНЯТ И ЗАЛЕНДЕН 25.07 (D39.31) — долгов контракта не осталось**; выдавался (`BACKEND_PACK18_DEBTS_SESSION_PROMPT.md`: `--accept-rebill` на снапшотной гранулярности — `guard_hash` в коде нет и не строим · `escalation_model` = авторитетная модель с САНКЦИОНИРОВАННЫМ пере-капчером golden под этот дифф · два нита общности условно-байт-нейтрально) · **ru-target долг (слой 7, `isRuTarget`×11 прод-сайтов · `TokenizeCyrillic`×6 · ключ «ru» в санитайзере) — ЖДЁТ СВОЕГО ДИЗАЙН-ПАКА** (связывает только не-русский таргет, по текущей карте zh/ja/en→ru не срочен) · **ПАК-19 «голос и состояние» ВЫДАН 25.07** (`BACKEND_PACK19_VOICE_STATE_SESSION_PROMPT.md`, двухфазный; ниты общности едут ЕГО лендингом — D39.31 п.5) · **проекция пере-оплаты в `tmctl status` (НОВОЕ, из отчёта пака-18): спека §9 предлагает заменить булев `ConfigDrift` числом «N чанков, ~$X» — дёшево, `omitempty`, не сделано сознательно** · **не-долги, следующие несущие:** масштаб целой книги (волны/каденс/потолки/ETA на сотнях глав — гоняли максимум 5) · Ф2-механизмы (голос/состояние D21 · native-Gemini судья) · извлечение дискурс-норм/few-shot из ТЕЛ промтов (слой 2, форсинг = ja→ru) · ~~пак-16 ф.2~~ ЗАЛЕНДЕНА 25.07 · per-class исходы ремонта — схемное решение (класс не durable), принимается ВМЕСТЕ с включением петли · ниты общности (владелец 25.07) — **ОТЛОЖЕНЫ ПО ЗАМЕРУ (D39.31 п.5): едут на ближайший ПОЛНЫЙ `--resnapshot`**; для Detail-строк байт-нейтральной формы не существует определительно (дефолт шаблона обязан рендерить текущую строку). Прежняя формулировка: только в байт-нейтральной форме с доказательством исполнением (иначе `CheapGateVersion`/`pack.Version()` двигают снапшот стендовых книг и убивают $0-резюм драфта под замер остатка пака-16): Detail-строки чекеров → шаблоны/цитаты из данных (статический 时辰-текст в generic-файле соврёт второй паре) · множитель ×2 `lintTimeUnits` (`checkers.go:174`) → ключ данных. *Полигон:* ~~пакет 5 (факты ToS + 8 пингов)~~ **ОТРАБОТАН ЦЕЛИКОМ И ПРИНЯТ 25.07 (D39.29); все восемь пингов закрыты; полигон-очереди НЕТ.** Исторический список закрытых пингов: (1) `00-provider-quirks.md:13,73` — катовер deepseek-chat 24.07 ПРОШЁЛ, пост-катовер факт (жив ли алиас, цены) не внесён — live-проба `/models` + вендор-дока; (2) `00-provider-quirks.md:69` — оговорка «mistral-editor-арм до guard» superseded D39.20 (mistral вон из редакторов; rate-limit-цифры оставить — нужны переводчику канала B пака-17); (3) `09-pilot-protocol.md:3` — ⚠-указатель застрял на D31/glm-5: продлить до пост-D39.22 (dspro-интерим · планка ≤2 не пройдена rerun2 · авто-QA слепыми метками D39.20 · стайл-каноны D39.21), тело не трогать; (4) `15-segmentation-empirics.md:6-7` — шапка до сих пор «НИ ОДНОГО платного вызова», а эксп залендён REV.2 (D39.7/D39.8, $12.79) — перевернуть статус-блок; (5) `16-bank-mining.md:3` — дописать финал «ИСПОЛНЕН И ЗАЛЕНДЁН (D39.10); gender-тачпойнт §7 закрыт (D39.19); продолжение — Go-майнер парити EXACT»; (6) `experiments/README.md` — индекс без строк exp15/16 (сверка застряла на D38.2); (7) `eval/README.md` — не тронут с 19.07 (пре-rerun2): освежить статус под пост-D39.22 (эксп закрыт · судейский риг-стандарт D39.7 · экстракция только `tmctl export`). +> - **ОТКРЫТЫЕ ПЕТЛИ (норма 25.07, перепись 180 инцидентов: класс NEVER_CLOSED больше не копим — каждая петля обязана получить диспозицию решено/отложено-с-записью/отклонено; ведёт оркестратор).** *Владелец:* ~~вокабуляр content-лейблов~~ **РЕШЕНО 25.07: ПАРА `violence`/`sexually-explicit`** (D39.27 п.5 — сохраняет D14 п.1 и dspro-редактора на violence) ⇒ следствие в работе: assert-only `action` (дельта пака-17) · ~~Q2 редактор под лейблом~~ **ОТЛОЖЕН С ЗАПИСЬЮ** (D39.27 п.7: при паре связывает только `sexually-explicit`-книги, которых нет; возвращается вместе с предусловием D22.7 перед первой explicit-книгой; кандидаты — grok-4.3 интерим, glm-5 только после сверки ToS Z.AI по первоисточнику, mistral отвергнут D39.20) · ~~ToS-факты + три решения по ним~~ **ЗАКРЫТЫ 25.07 (D39.29 факты + D39.30 подписи владельца):** лейбл `violence` НЕ заводится, интерпретация «нейтральное изображение ≠ пропаганда» зафиксирована письменно · риск Z.AI (плоский запрет «violent content»; экспозиция состоялась в rerun2 через glm-арм) ПРИНЯТ сознательно с условиями пересмотра · две строки `accepts_labels: [sexually-explicit]` (`xai`, `mistral`) ПОДПИСАНЫ и ПРИМЕНЕНЫ в `models.yaml`. **Остаток ToS-вопросов ЗАКРЫТ владельцем 25.07 (D39.32):** local = «политик нет, переводим всё» ⇒ строка применена · Gemini-оговорка читается СУЖЕНИЕМ ⇒ вердикт `FORBIDDEN`→`UNCLEAR`, но по доктрине это НЕ разрешение: строку Gemini не получает, **ждёт одного слова, если владелец хочет подписать интерпретацию как разрешение** · причину пустоты в конфиг НЕ выносим (живёт в quirks-доке) · **ре-чек политик стал ПРАВИЛОМ** (триггеры: Google ToS 30.07.2026 · квартал 25.10.2026 · любая правка `accepts_labels`) · дата аккаунта OpenAI — открыта, приоритет низкий (OpenAI вне цепочки). **Продуктовое (Ф3):** Gemini API Additional ToS запрещает клиентов, «likely to be accessed by individuals under 18» — обязательство на конечный продукт, независимо от лейблов · **промпт-слой под лейблом (НОВОЕ, вскрыто вопросом владельца): лейбл сегодня НЕ доезжает до промпта** (чистые данные маршрутизации + load-time гарантия); нужно ли, чтобы переводчик/редактор ЗНАЛИ о свойстве контента — отдельное решение промпт-слоя · мини-каноны сид-дельты — 元-корень (предложение «изначальн-») и 春秋蝉 (подписано «Весенне-осенняя цикада», ждёт подтверждения) · ~~включение платной петли ремонта — по замеру остатка~~ **ЗАМЕР ЕСТЬ (D39.37, 26.07): остаток = 0 в трёх классах, которые петля умеет чинить** (оговорка: узость набора классов, не чистота текста); **рекомендация оркестратора — НЕ включать**, петля остаётся за `enabled:false`; расширение набора классов — прежде повторного замера; ждёт слова владельца · ja→ru-реплика §B5 (тайминг) · старые Ф2.5-блокеры (билингв-якорь — D25 п.9 Q1 · контаминация корпуса — D27 п.4 · судья-дублёр D22.6 · планка запуска · publishable/waiver — D25 п.1 · FN-bound L3 — D25 п.4 · юр-пакет · провенанс 12-*-доков · xAI-ключ off перед продом D27 · residual-тачпойнты exp16, не влитые в сид-дельту: мини-голд алиасов · precision@30, `books/gu-zhenren/exp16/`). *Оркестратор:* ~~ратификация дизайна пака-16~~ РЕШЕНО D39.24 (25.07) · фантом-гард D-ссылок в чек-листе лендинга (действует) · ~~doc-sync pass~~ **ИСПОЛНЕН 25.07** (запись ниже: оба puml перерисованы под пост-пак-16 · баннеры на 01/02/03/04/06/07/08/09 · ресёрч-свип 18/18c/19/20/21/22 · README/POLYGON_PACKAGE4 освежены; residual — пинги полигону в его зоне) · ~~приёмка пака-17 фаза 1~~ **РЕШЕНО D39.26 (25.07: принято в редакции §14, три добора приёмки, фаза 2 размечена)** · **`escalation_model` = ОТВЕТИВШАЯ модель (НОВОЕ, вынесено из пака-17): вердикт-изменение** — golden пинит `esc_model` на ОТКАЗАВШЕМ хопе, маскировка не скрывает ⇒ отдельная ратификация с санкционированным пере-капчером · **`--accept-rebill[=usd]` с порогом `min($0.50, 5%×ProjectedBookUSD)` РАТИФИЦИРОВАН D20.2-Q2, но в коде/CLI ОТСУТСТВУЕТ (НОВОЕ)** — живёт только в спеке `backend/docs/D15.2-*.md`; `--resnapshot` пере-пиннит без суммы · **D22.7 (пер-чанковый L3-скрин) — предусловие первой erotica-книги в проде НЕ снято паком-17 (НОВОЕ, держать отдельным гейтом)** · фантом-гард D-ссылок в чек-листе лендинга (действует). *Бэкенд:* **ХВОСТЫ МИНИ-ПРОГОНА (D39.37, 26.07, по убыванию веса):** (1) **размеченный набор для чекеров** — топ-выбор сессии и оркестратора: без него цифры качества держатся на глазомере · (2) **тест-пин джойна WHAT↔WHICH** (мутация оркестратора выжила — фикс-лист №1 следующего касания) · (3) покрытие WHAT↔WHICH (0/2 и 1/2) + флаговый майнинг-стоп (семантика владельца озвучена, D39.36 п.2; стоп очищается РАУНДАМИ) · (4) `dialogue_dash` мерить только на невиданном тексте (подогнан под 蛊真人) · (5) генеральность нарезки: кана/ханьцзи один класс ⇒ ja `est_out` ~1.7× завышен МОЛЧА; `EstimateTokens` недосчитывает кириллицу (резервации занижены) · (6) golden второй фикстурой С langpack · (7) `prompt_override` — честная формулировка гарантии слоя-2 · (8) майнер/банк не-CJK (едет с ja→ru) · (9) `request_log` скоуп/ретеншен + `first_flag_reason` третье представление истории · (10) флор-16000 без модели · (11) `max_tokens`-упоры вернутся на en · ~~пак-17 (фазы 1–2 + дельта)~~ **ЗАКРЫТ ЦЕЛИКОМ 25.07 (D39.26/27/28)** · ~~ПАК-18 «долги контракта»~~ **ПРИНЯТ И ЗАЛЕНДЕН 25.07 (D39.31) — долгов контракта не осталось**; выдавался (`BACKEND_PACK18_DEBTS_SESSION_PROMPT.md`: `--accept-rebill` на снапшотной гранулярности — `guard_hash` в коде нет и не строим · `escalation_model` = авторитетная модель с САНКЦИОНИРОВАННЫМ пере-капчером golden под этот дифф · два нита общности условно-байт-нейтрально) · **ru-target долг (слой 7, `isRuTarget`×11 прод-сайтов · `TokenizeCyrillic`×6 · ключ «ru» в санитайзере) — ЖДЁТ СВОЕГО ДИЗАЙН-ПАКА** (связывает только не-русский таргет, по текущей карте zh/ja/en→ru не срочен) · **ПАК-19 «голос и состояние» ВЫДАН 25.07** (`BACKEND_PACK19_VOICE_STATE_SESSION_PROMPT.md`, двухфазный; ниты общности едут ЕГО лендингом — D39.31 п.5) · **проекция пере-оплаты в `tmctl status` (НОВОЕ, из отчёта пака-18): спека §9 предлагает заменить булев `ConfigDrift` числом «N чанков, ~$X» — дёшево, `omitempty`, не сделано сознательно** · **не-долги, следующие несущие:** масштаб целой книги (волны/каденс/потолки/ETA на сотнях глав — гоняли максимум 5) · Ф2-механизмы (голос/состояние D21 · native-Gemini судья) · извлечение дискурс-норм/few-shot из ТЕЛ промтов (слой 2, форсинг = ja→ru) · ~~пак-16 ф.2~~ ЗАЛЕНДЕНА 25.07 · per-class исходы ремонта — схемное решение (класс не durable), принимается ВМЕСТЕ с включением петли · ниты общности (владелец 25.07) — **ОТЛОЖЕНЫ ПО ЗАМЕРУ (D39.31 п.5): едут на ближайший ПОЛНЫЙ `--resnapshot`**; для Detail-строк байт-нейтральной формы не существует определительно (дефолт шаблона обязан рендерить текущую строку). Прежняя формулировка: только в байт-нейтральной форме с доказательством исполнением (иначе `CheapGateVersion`/`pack.Version()` двигают снапшот стендовых книг и убивают $0-резюм драфта под замер остатка пака-16): Detail-строки чекеров → шаблоны/цитаты из данных (статический 时辰-текст в generic-файле соврёт второй паре) · множитель ×2 `lintTimeUnits` (`checkers.go:174`) → ключ данных. *Полигон:* ~~пакет 5 (факты ToS + 8 пингов)~~ **ОТРАБОТАН ЦЕЛИКОМ И ПРИНЯТ 25.07 (D39.29); все восемь пингов закрыты; полигон-очереди НЕТ.** Исторический список закрытых пингов: (1) `00-provider-quirks.md:13,73` — катовер deepseek-chat 24.07 ПРОШЁЛ, пост-катовер факт (жив ли алиас, цены) не внесён — live-проба `/models` + вендор-дока; (2) `00-provider-quirks.md:69` — оговорка «mistral-editor-арм до guard» superseded D39.20 (mistral вон из редакторов; rate-limit-цифры оставить — нужны переводчику канала B пака-17); (3) `09-pilot-protocol.md:3` — ⚠-указатель застрял на D31/glm-5: продлить до пост-D39.22 (dspro-интерим · планка ≤2 не пройдена rerun2 · авто-QA слепыми метками D39.20 · стайл-каноны D39.21), тело не трогать; (4) `15-segmentation-empirics.md:6-7` — шапка до сих пор «НИ ОДНОГО платного вызова», а эксп залендён REV.2 (D39.7/D39.8, $12.79) — перевернуть статус-блок; (5) `16-bank-mining.md:3` — дописать финал «ИСПОЛНЕН И ЗАЛЕНДЁН (D39.10); gender-тачпойнт §7 закрыт (D39.19); продолжение — Go-майнер парити EXACT»; (6) `experiments/README.md` — индекс без строк exp15/16 (сверка застряла на D38.2); (7) `eval/README.md` — не тронут с 19.07 (пре-rerun2): освежить статус под пост-D39.22 (эксп закрыт · судейский риг-стандарт D39.7 · экстракция только `tmctl export`). > - **НАЙДЕНО СПЛОШНЫМ АУДИТОМ 25.07 (D39.34) — 23 позиции, что были ратифицированы, но не попали в трекер; каждая с диспозицией.** *(1) Обязательство, НЕ исполненное:* **флип `Gates.RegressionGuard.Enabled` (D38.4 п.5 «ОБЯЗАН включить» при сборке resnapshot) — ключа нет ни в одном шиппинг-конфиге ⇒ гейт false; rerun2 прошёл без построенной наблюдаемости** → едет обязательной строкой в пак, вызывающий ближайший resnapshot. *(2) Флипы гейтов за подписью владельца (остаются открытыми-с-записью):* coverage `enabled:false` (D12-Q4) · `postcheck_gate` в hard-gate требует пере-замера recall (D24.4+D28.1). *(3) Поглощено курсом:* заморозка «этапа B ~300 глав» (D26 п.1) — **снята D39.33** (масштаб последним), отдельной петлёй не считается. *(4) Открыто, вне «идеала»:* этапы Б+В спеки D15.2 (D33 п.5 «промт выдам после пере-прогона» — прогон был 23.07) — они про content-addressed resume/`guard_hash`, который D39.31 сознательно не строил · F3-остаток идемпотентности · `human_override` LOCK (D29 п.1г, в коде 0 вхождений) · морфо-гейты канцелярита и L1-лемматизация (Ф2, python-сайдкар) · полный OpenCC (0 вхождений) · опциональный пре-перевод гейт. *(5) Мелкие хвосты качества (собрать одним паком когда-нибудь):* ledger-очередь LOW/NOTE из D39.4 · остаток фикс-листа D23 п.4 и D28 п.3(б) · `editor-mono.md` DORMANT (ни в одном конфиге). *(6) Деньги/провайдеры (полигон):* вендор-чек префикс-кэша ZAI/GLM (на нём стоит вся кэш-экономика W1/Q2c) · продление `prices_checked` без правки чисел · scheduler-aware волны под peak-valley DeepSeek (прямой COGS-рычаг) · гейтнутые платные армы (edit-единица >3200 и др.) · wire-probe temperature в thinking-канале DeepSeek. *(7) Продукт/Ф3:* action-security gate перед выдачей tools/webfetch (D25 п.5) · планы research/22 (epub-tag-rewrite, Q7-леджер) · Gemini под-18 обязательство на конечный продукт. *(8) Вход в пак-19:* требование НАРРАТИВНОЙ гендер-аннотации (since_ch-смены + перспектива), а не булева поля — вписано в промт. > - Архивы хроники: `archive/PROGRESS-2026-07-04-10.md` (D31) · `archive/PROGRESS-2026-07-10-13.md` (D39.6-гигиена) · `archive/PROGRESS-2026-07-13-25.md` (слайс 25.07: стройка паков 11–16, rerun2). Записи ниже — живой хвост. +## Оркестратор №8 — МИНИ-ПРОГОН ПРИНЯТ ПО СЫРЬЮ (D39.37): операционка живьём до цента, шов замкнут, но не запинён (моя мутация выжила), остаток = 0 в трёх классах петли, 26.07 + +Приёмка по леджерам и БД, не по отчёту. Всё пере-ранено (build/vet · `-race` 13/13 · ценз 475→492/0 · golden: маскированный дифф пуст вне трёх заявленных осей · парити EXACT); **деньги до цента** ($0.234583, включая честно отчитанный брак $0.046668; проекция rebill-гейта совпала со списанием ТОЧНО — первое живое исполнение механизма пака-18); спот-чеки по сырым БД: банкнота 71/9из20, v11, dst «学堂家老 → старейшина школы» в `banknote_detail`, **`RegressionGuard` первое живое срабатывание подтверждено моим ре-раном**. **Находка рубежа-2: моя независимая мутация — отключение джойна WHAT в колл-сайте — ВЫЖИЛА по всей сьюте** ⇒ несущий фикс доказан прогоном, но не запинён; фикс-лист №1. **Открылось решение владельца:** замер для петли ремонта ЕСТЬ — остаток 0 в трёх классах петли (оговорка сессии: узость набора, не чистота текста); моя рекомендация — НЕ включать. Хвосты §5 хендоффа — в трекер (топ: размеченный набор для чекеров · генеральность нарезки кана/ханьцзи с est_out ~1.7× на ja · покрытие WHAT↔WHICH + флаговый стоп). Хендофф-дисциплина «чего числа НЕ доказывают» ратифицирована нормой отчётов; в промты идёт формула «пере-мерь свой вывод ДРУГИМ способом, чем получил». + ## Оркестратор №8 — АКТУАЛИЗАЦИЯ ПО ЗАПРОСУ ВЛАДЕЛЬЦА: закрытые промты 17/18/полигон-5 забаннерены и в архиве, курс в CLAUDE.md переписан под голову D39.36, 25.07 Финальный синк перед мини-прогоном. Карта: **паки 17 и 18 — ЗАКРЫТЫ И ЗАЛЕНДЕНЫ** (не впереди); впереди — мини-прогон → сид-дельта (元-корень ждёт владельца) → пак-19 «голос/состояние» (урезанный скоуп, D39.34) → развилка по шву банкноты ПОСЛЕ пробы (D39.36 записан фактом; решение о починке — по данным пробы, не раньше). Актуализировано: `BACKEND_PACK17_*` (был забаннерен, но лежал в docs/) · `BACKEND_PACK18_*` (баннер-исход D39.31 добавлен) · `POLYGON_PACKAGE5_*` (баннер-исход D39.29/30 добавлен) — все три `git mv` → `archive/prompts/`, ссылки README перенацелены; шапка PROGRESS дотянута до D39.36 (отставала на один); курс-буллет CLAUDE.md переписан с эры «пост-пак-17» на фактическую очередь D39.33–36. Активные промты теперь ровно три: `BACKEND_MINIRUN_SESSION_PROMPT.md` (текущий) · `BACKEND_PACK19_VOICE_STATE_SESSION_PROMPT.md` (после мини-прогона) · `POLYGON_PACKAGE4_SESSION_PROMPT.md` (residual-трекер). @@ -94,6 +98,22 @@ Отчёт: [`archive/reports/PACK18_DEBTS_REPORT_2026-07-25.md`](archive/reports/PACK18_DEBTS_REPORT_2026-07-25.md). **П.1 `--accept-rebill[=usd]` ПОСТРОЕН** на снапшотной гранулярности (`internal/pipeline/rebill.go`): проекция = Σ хранимых `cost_usd` оплаченных строк под устаревшим снапшотом ЕГО ВОЛНЫ, порог `min($0.50, 5%×ProjectedBookUSD)` + флор $0.50 + переопределение книгой `rebill_consent_usd`; гейт стоит ДО денег в `TranslateBook` и ДО деструктивного сброса в `Redrive`; read-only пути ($0-контракт D20.4) не тронуты. `ProjectedBookUSD` вынесен в общий `projectBookUSD` — порог и `status` считают ОДНО число. **Развилка на ратификацию:** оговорка спеки «`--resnapshot` = алиас-синоним согласия» СОЗНАТЕЛЬНО не перенесена — флаги ортогональны (иначе ратифицированный порог = мёртвый код); 4 существующих теста получили строку согласия. Каскад §7.1-бис зафиксирован как ВЫРОЖДЕННЫЙ, ветка `EstimateUSD` — без предмета на этой гранулярности; `guard_hash` не строился. **П.2 `escalation_model` = АВТОРИТЕТНАЯ модель ПОСТРОЕН**; пере-капчер golden `f7641993…` → `211c9013…` проверен машинно: сдвинулись ровно 4 строки `ch3/0 draft`, дельта ТОЛЬКО в колонке `esc_model`, оба волновых снапшота/`brief_hash`/`memory_version` байт-идентичны ⇒ пере-оплаты нет; миграции нет, смешанная семантика записана в доккоммент `store.ChunkStatus`. **П.3 (два нита общности) ОТЛОЖЕН с замером:** одна новая строка в `configs/langpacks/zh-ru/dc-checkers.txt` двигает `pack.Version()` `a7d4be2c0465`→`133d71e8d132` и ОБА волновых снапшота (проба исполнением), а стендовые книги ссылаются ровно на этот каталог (`rerun2/book*.yaml:22-23`) ⇒ байт-нейтральной формы нет; для Detail-строк она нереализуема определительно. Готовый план на ближайший полный `--resnapshot` — в отчёте. **Приёмка:** build/vet чисты · `-race` 12/12 · `-shuffle=on` зелёный · парити EXACT `n=13618 … 0.9655 (56/58)` · ценз диффом ИМЁН 455→475, удалено 0 · **19 мутаций, 19 красных** (две сначала выжили и вскрыли дыры в тестах — обе закрыты, в т.ч. регресс-класс D20.4 «распарсено, но не проведено в раннер»). +### Мини-прогон ~10 глав ИСПОЛНЕН одной сессией (гигиена → главный проход → проба банкноты → верификация), 25.07 + +Отчёт: [`archive/reports/MINIRUN_REPORT_2026-07-25.md`](archive/reports/MINIRUN_REPORT_2026-07-25.md) — пре-регистрация первым разделом (написана ДО первой траты, критерии провала названы заранее). **ШАГ 0 (гигиена, $0-код, НЕ закоммичен):** обязательный флип `Gates.RegressionGuard.Enabled` во всех 5 шиппинг-конфигах (D38.4 п.5, невыполненное обязательство D39.34) + 3 теста, что гейт РЕАЛЬНО стреляет через настоящий драйвер; два нита общности взяты в fail-loud форме БЕЗ Go-дефолта (множитель `dc1_unit_in_hours` и Detail-шаблоны `msg` → пар-данные; возражение D39.31 п.5 снято тем, что дефолта просто нет); проекция пере-оплаты в `tmctl status` (спека D15.2 §9, тем же `projectRebill`, что и гейт). Приёмка: build/vet чисты, `-race` 15/15, **golden `211c9013…089045d6` байт-в-байт равен HEAD**, ценз ИМЁН 475→483 (удалено 0), **5 мутаций — 5 красных**. Сдвинутая версионная ось ровно одна: `langpack_version` `a7d4be2c0465`→`a28ed743c99c`; флип гарда снапшот НЕ двигает (проверено исполнением). **Прогон:** новый проект `guzhenren-minirun` (главы 1–10, 20 драфт-чанков → 14 edit-единиц), **$0.114291** при оценке $0.10–0.19, 14/14 ok. **Семь утверждений подтверждены сырьём:** rebill-гейт отработал ЖИВЬЁМ впервые (проекция `34 единицы ~$0.114291` = списанному ТОЧНО; 3 отказа, деньги/строки побайтно не тронуты; пас-лег на микро-проекте за $0.003) · майнинг-стоп остановил и очистился, термы БЕЗ dst (D39.36 подтверждён живьём) · `escalation_model` = ОТВЕТИВШАЯ модель · лейбл-путь no-op (0 расхождений requested↔actual, полей маршрутизации в read-model НЕТ) · деньги durable (`committed == SUM(checkpoints)` точно, `finish=stop` 35/37) · `RegressionGuard` даёт честный ноль (перепроверен независимо: ratio 0.90–1.06, потеряно 0 чисел). **ГЛАВНОЕ ИЗМЕРЕНИЕ: `repair_candidates` = 0** — на этих 10 главах платная петля не нашла бы НИ ОДНОЙ работы; из 11 стилевых сигналов **11 — ложняки** (8 `dialogue_dash` бьют по законной русской типографике «мысль в «…» против речи в «—»», 2 `percent_scale` называют верный масштаб ошибкой масштаба, 1 `dc2` — гипербола, о которой предупреждает его же комментарий). **Проба банкноты ($0.021688): канал ФИЗИЧЕСКИ РАБОТАЕТ** — 71 строка на 9 из 20 чанков, `truncated` 0, сепаратор из чистого черновика срезан; предложения годятся как основа канона (~40–45% полезных: 智慧蛊 «гу Мудрости» и 古月陈博 «Гуюэ Чэньбо» дословно совпали с подписями rerun2). **Поправка к D39.36: предложенный dst НЕ теряется** — сырой блок лежит в `checkpoints.response_text` первичной попытки (9 строк), выбрасывается только распарсенная структура ⇒ починка шва не требует пере-прогона. **Сработали два пре-регистрационных критерия провала, не переопределяю:** П3 (эхо `cjk_artifact` 1/20 = 5% при thinking-ON; эскалация вылечила) и П8 (`parse_fail` 1 — модель выдала строку без ханьца в src, парсер отработал по контракту). **Найденные дефекты:** Д1 эхо-метрика слепа к эху, вылеченному эскалацией (отчёт говорит 0.0% там, где живьём 5%) · Д2 редакторский `max_tokens` всегда равен флору модели 8000, две единицы не поместились — **15.5% денег прогона ушло в брак** · Д3 `LOG_LLM_BODIES` режет тела на 4096 байт, банкноту в логах не увидеть · Д5 майнинг-стоп очищается РАУНДАМИ (подпись 学堂 расчехлила 学堂家老 — владелец подписывал дважды) · Д6 в сиде нет форм мн. ч. (4 из 9 промахов пост-чека — данные, не перевод) · реальный дрейф канона ровно один: 族长 → «глава рода» вместо «глава клана». Подписи владельца: 学堂 → «школа» (рассмотрел «академию», отклонил по фактуре), 学堂家老 → «старейшина школы». Сессия НЕ коммитила. + +### Фикс-пак по итогам мини-прогона: 4 дефекта починены, 2 кандидата ОТКЛОНЕНЫ исследованием, 25.07 + +Санкция владельца «исследование → правки → ревью». Детали — §14 [`archive/reports/MINIRUN_REPORT_2026-07-25.md`](archive/reports/MINIRUN_REPORT_2026-07-25.md). **Починено:** Д1 эхо-метрика (миграция **v11** `chunk_status.first_flag_reason`; `echo_draft` теперь считает эхо, ВЫЛЕЧЕННОЕ эскалацией, + `echo_draft_recovered` держит число честным в обе стороны) · **Д4 шов банкноты замкнут** (та же v11, `retrieval_state.banknote_detail`; майнинг-стоп джойнит предложения в подписную карту: `dst` = самое частое, альтернативы с весами в note, **`status: auto` не трогается** — trust-gate D39.2 по-прежнему не пускает неподписанное в инъекцию; развилки D39.36 п.2/п.3 НЕ трогались, это продуктовое решение владельца) · Д7a `dialogue_dash` перестал считать смешением стилей внутреннюю речь (данные таргета `inner_speech`, `CheapGateVersion` v4→v5) · Д3 `LOG_LLM_BODIES` кап 4096→32768 и обрезка СЕРЕДИНЫ (блок банкноты живёт в хвосте) · Д2 флор `deepseek-v4-pro` 8000→16000 по замеру (перебор бюджета бесплатен — списание по факту; недобор стоит целой генерации, на прогоне это 15.5% счёта). **Находка, удешевившая Д4:** сырой текст банкноты УЖЕ durable в чекпоинте, resume его пере-парсит ⇒ новой таблицы не понадобилось, хватило четвёртой `*_detail`-колонки. **ОТКЛОНЕНО после написания и прогона (обе правки откачены):** `percent_scale` — «стрелять только на цифровой форме» роняет ратифицированную фикстуру «шесть и шесть десятых»=6.6 (настоящая 100× ошибка тоже прописана словами), нужен разбор числительных таргета в данные · `dc2` 千万 — живой ложняк ШЕЙПОМ ТОЖДЕСТВЕНЕН ратифицированному ИСТИННОМУ (千万生灵), offline неразделим ⇒ это принятая неоднозначность класса, а не баг реализации (правит мой же §6.2 отчёта). **Ревью:** build/vet/gofmt чисты · `-race` 15/15 · **golden пере-капчер санкционирован и ДОКАЗАН — маскированный структурный дифф ПУСТ** (`211c9013…089045d6`→`1165e3a9…2132ea82`; двигались только версии и производные от них хеши, ни один вердикт/текст/стоимость) · ценз ИМЁН 483→491, удалено 0 · **6 мутаций — 6 красных** · **живая проверка на данных прогона**: 9 реальных блоков банкноты прогнаны через новый шов → 71 строка, 67 ключей, 2 терма с конкурирующими вариантами, среди них 学堂家老 — под фиксом подписная карта пришла бы с «старейшина школы», ровно тем вариантом, который владелец выбрал вслепую. **Цена:** две оси снапшота сдвинуты (`style_check_version`, `max_tokens` через флор) ⇒ следующий прогон стендовой книги требует `--resnapshot --accept-rebill`; сегодня это бесплатно (снапшот стенда и так разошёлся, D39.35), а экономия Д2 окупает пере-пин на первом же прогоне. **Моя ошибка в ходе ревью, фиксирую:** проверка миграции v9→v11 собрала конфиг «копия rerun2-БД + источник мини-прогона», движок увидел НОВУЮ книгу и реально перевёл — **$0.046668 (31 вызов) потрачено зря**; оригинал rerun2 и бэкап целы (v9, spend не изменился), трата осталась на выбрасываемой копии. Итог денег сессии: $0.139060 + $0.046668 брака = **$0.185728**. Сессия НЕ коммитила. + +### Сквозная проверка бэкенда после фикс-пака (вопрос владельца «а ты протестировал?»), 26.07 + +Честный ответ был «нет»: после правок гонялись ТЕСТЫ, а не бэкенд. Прогнал два проверочных прогона ($0.049, главы 1–2 и 4–5, банкнота ВКЛЮЧЕНА) — §15 отчёта. **Все узлы отработали:** миграция v11 на свежей БД · ingest/чанкер · драфт с банкнотой (`banknote_detail` пишется живьём) · майнинг-стоп + подпись + резюм за $0 · edit-волна 3/3 · санитайзер поймал РЕАЛЬНЫЙ дефект (гомоглиф «лицó») · **`RegressionGuard` дал ПЕРВОЕ ЖИВОЕ срабатывание** (`number_drift [66]` — «66%» → «шестьдесят шесть процентов»: ровно тот ложняк, что предсказан его же докой) · банк 56+12 · экспорт чист (сепаратор 0 раз) · деньги durable. **Найдено ТОЛЬКО сквозным прогоном:** (1) **поправка к моему вчерашнему утверждению** — банкнота НИКОГДА не предлагала `学堂` отдельно, только `学堂家老`, значит первая подпись всё равно пришла бы голой, фикс помог бы во ВТОРОМ раунде; (2) **структурный разрыв**: на verify2 стоп дал `terms_with_proposed_dst=0` — майнер предложил 花海/陈博, банкнота 古月方源/学堂家老/开窍大典/力量蛊/智慧蛊/灵泉, пересечение НУЛЕВОЕ (механизм исправен, но каналы выбирают разные популяции: майнер — частотные name/place/title, банкнота — новую терминологию) ⇒ следующая работа не «замкнуть шов» (замкнут), а заставить WHAT покрывать кандидатов WHICH; (3) **полярность дискриминатора чекера была неверной** — список глаголов МЫСЛИ безграничен (живьём тут же выдало «понимал», «размышлял»), перевернул на закрытый список глаголов РЕЧИ + вето «про себя»: замер по реальным экспортам по чанкам **8→0** и **1→0**, настоящее смешение речи по-прежнему флагается; (4) **дыра в моих тестах**: мутация «снять nil/no-data гард» ВЫЖИЛА (ветка книги без данных таргета не покрыта, там nil-разыменование) — закрыто тестом, пере-мутация красная с паникой. **Ревью:** `-race` 15/15 · golden пере-капчен второй раз, маскированный дифф снова ПУСТ (`281d7025…`) · ценз 475→492, удалено 0 · **9 мутаций — 9 красных**. Деньги сессии суммарно: $0.139060 прогон + $0.046668 мой брак + $0.048855 проверка = **$0.234583**. Сессия НЕ коммитила. + +### ХЕНДОФФ ОРКЕСТРАТОРУ: сессия бэкенда 25–26.07 закрыта владельцем, 26.07 + +Сводка для выдачи следующего промта (точечная зачистка + повторный прогон): [`archive/reports/MINIRUN_HANDOFF_2026-07-26.md`](archive/reports/MINIRUN_HANDOFF_2026-07-26.md). Содержит то, чего нет в отчёте прогона: **вес каждого числа** (в частности — «остаток дефектов = 0» означает «ноль в ТРЁХ классах, которые петля умеет чинить», а не «текст чистый»; по ней корректно решать «не включать петлю», но некорректно читать её как оценку качества) · **самооценка честности пяти фиксов** (один чистый, два обхода, два подогнанных под корпус; `dialogue_dash` — 29 глаголов, выведенных из отказов ОДНОЙ книги и проверенных НА НЕЙ ЖЕ, «8→0» = замер на обучающей выборке) · **мои методические ошибки** ($0.046668 брака от конфига, собранного sed-ом по чужому book.yaml; рекомендация владельцу раньше фактуры; утверждение о пользе фикса без проверки — банкнота НИКОГДА не предлагала `学堂` отдельно, значит первая подпись всё равно пришла бы голой; пре-рег П8 сформулирован мимо диагноза) · **8 хвостов, увиденных но не чиненных** (главный: генерик-фолбэк нарезки — китайские числа, а `TokenClassCounts` кладёт кану и ханьцзи в один класс: замер по книгам на диске — в японской кана 70% класса ⇒ est_out завышен ~1.7×, вдвое больше вызовов, и всё это МОЛЧА; плюс `EstimateTokens` недосчитывает кириллицу на резервациях, golden покрывает только фолбэк-путь без langpack, `prompt_override` — легальная дыра в гарантии слоя 2) · **что обязан измерить следующий прогон** (эхо-метрика на живом эхо; джойн WHAT↔WHICH при НЕПУСТОМ пересечении; `dialogue_dash` на тексте, которого сессия не видела — любой замер на 蛊真人 теперь загрязнён подгонкой). **Проверено исполнением, $0:** обе не-китайские книги с диска (`Kristoff_Jay_-_Empire_of_the_Vampire_3`, `isekai_majutsushi_jp`) падают ГРОМКО до единого цента, называя ожидаемый путь промпт-пакета; с обойдённым гейтом обе проходят ingest/чанкер/read-model без единой правки Go (EN 352 единицы, JA 138) — норматив общности §0 держится на уровне КОДА, но данные и фолбэки везде китайские. **Рекомендация сессии, если брать один хвост:** размеченный набор для чекеров — без него следующий прогон произведёт ещё один набор чисел, держащихся на глазомере. Дерево НЕ закоммичено (35 позиций, миграция v11, три сдвинутые оси снапшота). + ## Полигон (секция параллельной сессии — записи добавлять сюда) diff --git a/docs/README.md b/docs/README.md index 41c8c1c4..fa051048 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,12 +11,12 @@ ## Структура -- `architecture/` — синтез. **Источник истины по решениям — [`05-decisions-log.md`](architecture/05-decisions-log.md) (D1–D39.36); при конфликте с любым доком он выше.** +- `architecture/` — синтез. **Источник истины по решениям — [`05-decisions-log.md`](architecture/05-decisions-log.md) (D1–D39.37); при конфликте с любым доком он выше.** - `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-ответы. **Исполненные (архив 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) — **читай баннер прежде содержимого**. - `PROGRESS.md` — **журнал** (CURRENT-STATE сверху, ниже хронология; НЕ источник решений). -- **Активные хендофф-промты (пост-пак-18, 25.07):** [`BACKEND_MINIRUN_SESSION_PROMPT.md`](BACKEND_MINIRUN_SESSION_PROMPT.md) (**ТЕКУЩИЙ — мини-прогон ~10 глав ОДНОЙ сессией: гигиена → главный проход → проба банкноты → верификация по сырью; ~$0.12–0.19, судья не зовётся: мини-прогон ~10 глав одной сессией — гигиена (флип `RegressionGuard` + ниты) → сквозной платный прогон $0.10–0.35 → верификация сырьём; судья не зовётся**) · [`BACKEND_PACK19_VOICE_STATE_SESSION_PROMPT.md`](BACKEND_PACK19_VOICE_STATE_SESSION_PROMPT.md) (**ТЕКУЩИЙ бэкендный: механизмы D21 «голос и состояние» — voice-профиль · реестр обращений ты/вы · reveal-окна; двухфазный, ниты общности едут его лендингом**) · [`BACKEND_PACK18_DEBTS_SESSION_PROMPT.md`](archive/prompts/BACKEND_PACK18_DEBTS_SESSION_PROMPT.md) (**ТЕКУЩИЙ бэкендный: долги контракта — `--accept-rebill` (D20.2-Q2) на снапшотной гранулярности · семантика `escalation_model` с санкционированным пере-капчером golden · два нита общности условно; ru-target долг ЯВНО вынесен в свой будущий пак**) · [`BACKEND_PACK17_CHANNEL_B_SESSION_PROMPT.md`](archive/prompts/BACKEND_PACK17_CHANNEL_B_SESSION_PROMPT.md) (**пак-17 канал B (18+) вживую — ОТРАБОТАН ЦЕЛИКОМ 25.07: дизайн D39.26 · стройка D39.27 · дельта `allow` D39.28; вокабуляр = ПАРА `violence`/`sexually-explicit`, остаток — ФАКТЫ ToS в зоне полигона**; отчёты с ревью-шапками — [`archive/reports/PACK17_DESIGN_2026-07-25.md`](archive/reports/PACK17_DESIGN_2026-07-25.md), [`archive/reports/PACK17_CHANNEL_B_REPORT_2026-07-25.md`](archive/reports/PACK17_CHANNEL_B_REPORT_2026-07-25.md), [`archive/reports/PACK17_DELTA_2026-07-25.md`](archive/reports/PACK17_DELTA_2026-07-25.md); L3 — жёсткая линия, live только по санкции владельца) · [`ORCHESTRATOR_SESSION_PROMPT.md`](ORCHESTRATOR_SESSION_PROMPT.md) (хендофф №6, 25.07 — только живое: роль/методология/уроки/нормы §1–11; состояние НЕ дублирует) · [`POLYGON_PACKAGE5_SESSION_PROMPT.md`](archive/prompts/POLYGON_PACKAGE5_SESSION_PROMPT.md) (**ТЕКУЩИЙ полигонный: блок A — ФАКТЫ ToS/AUP провайдер × лейбл под `accepts_labels` пака-17 (вердикты с дословными цитатами первоисточников, UNCLEAR легитимен); блок B — 7 док-синк пингов**) · [`POLYGON_PACKAGE4_SESSION_PROMPT.md`](POLYGON_PACKAGE4_SESSION_PROMPT.md) (residual пилот/18+/echo). **Пак-16 ЗАЛЕНДЁН 25.07** (слой-3 diff-редактор: двухфазный дизайн D39.24 → стройка за `enabled:false`; отчёты с ревью-шапками — [`archive/reports/PACK16_DESIGN_2026-07-25.md`](archive/reports/PACK16_DESIGN_2026-07-25.md), [`archive/reports/PACK16_DIFF_EDITOR_REPORT_2026-07-25.md`](archive/reports/PACK16_DIFF_EDITOR_REPORT_2026-07-25.md)). **Паки 12/13/14/15 ЗАЛЕНДЕНЫ 24–25.07** (приёмка воркфлоу-линзами; отчёты с ревью-шапками — [`archive/reports/PACK14_GENERALITY_REPORT_2026-07-24.md`](archive/reports/PACK14_GENERALITY_REPORT_2026-07-24.md), [`archive/reports/PACK15_STRUCTURE_REPORT_2026-07-25.md`](archive/reports/PACK15_STRUCTURE_REPORT_2026-07-25.md); норматив — [`architecture/12-go-style-notes.md`](architecture/12-go-style-notes.md)). Дальше: **пак-17 = канал B вживую** (батч со следующим платным прогоном: вопросы итерации №2 + замер остатка ремонта → решение о включении петли) → голос/состояние D21 + C2/C3 → пилот Ф2.5. Закрытые — в `archive/prompts/`. +- **Активные хендофф-промты (пост-пак-18, 25.07):** [`BACKEND_MINIRUN_SESSION_PROMPT.md`](archive/prompts/BACKEND_MINIRUN_SESSION_PROMPT.md) (**ТЕКУЩИЙ — мини-прогон ~10 глав ОДНОЙ сессией: гигиена → главный проход → проба банкноты → верификация по сырью; ~$0.12–0.19, судья не зовётся: мини-прогон ~10 глав одной сессией — гигиена (флип `RegressionGuard` + ниты) → сквозной платный прогон $0.10–0.35 → верификация сырьём; судья не зовётся**) · [`BACKEND_PACK19_VOICE_STATE_SESSION_PROMPT.md`](BACKEND_PACK19_VOICE_STATE_SESSION_PROMPT.md) (**ТЕКУЩИЙ бэкендный: механизмы D21 «голос и состояние» — voice-профиль · реестр обращений ты/вы · reveal-окна; двухфазный, ниты общности едут его лендингом**) · [`BACKEND_PACK18_DEBTS_SESSION_PROMPT.md`](archive/prompts/BACKEND_PACK18_DEBTS_SESSION_PROMPT.md) (**ТЕКУЩИЙ бэкендный: долги контракта — `--accept-rebill` (D20.2-Q2) на снапшотной гранулярности · семантика `escalation_model` с санкционированным пере-капчером golden · два нита общности условно; ru-target долг ЯВНО вынесен в свой будущий пак**) · [`BACKEND_PACK17_CHANNEL_B_SESSION_PROMPT.md`](archive/prompts/BACKEND_PACK17_CHANNEL_B_SESSION_PROMPT.md) (**пак-17 канал B (18+) вживую — ОТРАБОТАН ЦЕЛИКОМ 25.07: дизайн D39.26 · стройка D39.27 · дельта `allow` D39.28; вокабуляр = ПАРА `violence`/`sexually-explicit`, остаток — ФАКТЫ ToS в зоне полигона**; отчёты с ревью-шапками — [`archive/reports/PACK17_DESIGN_2026-07-25.md`](archive/reports/PACK17_DESIGN_2026-07-25.md), [`archive/reports/PACK17_CHANNEL_B_REPORT_2026-07-25.md`](archive/reports/PACK17_CHANNEL_B_REPORT_2026-07-25.md), [`archive/reports/PACK17_DELTA_2026-07-25.md`](archive/reports/PACK17_DELTA_2026-07-25.md); L3 — жёсткая линия, live только по санкции владельца) · [`ORCHESTRATOR_SESSION_PROMPT.md`](ORCHESTRATOR_SESSION_PROMPT.md) (хендофф №6, 25.07 — только живое: роль/методология/уроки/нормы §1–11; состояние НЕ дублирует) · [`POLYGON_PACKAGE5_SESSION_PROMPT.md`](archive/prompts/POLYGON_PACKAGE5_SESSION_PROMPT.md) (**ТЕКУЩИЙ полигонный: блок A — ФАКТЫ ToS/AUP провайдер × лейбл под `accepts_labels` пака-17 (вердикты с дословными цитатами первоисточников, UNCLEAR легитимен); блок B — 7 док-синк пингов**) · [`POLYGON_PACKAGE4_SESSION_PROMPT.md`](POLYGON_PACKAGE4_SESSION_PROMPT.md) (residual пилот/18+/echo). **Пак-16 ЗАЛЕНДЁН 25.07** (слой-3 diff-редактор: двухфазный дизайн D39.24 → стройка за `enabled:false`; отчёты с ревью-шапками — [`archive/reports/PACK16_DESIGN_2026-07-25.md`](archive/reports/PACK16_DESIGN_2026-07-25.md), [`archive/reports/PACK16_DIFF_EDITOR_REPORT_2026-07-25.md`](archive/reports/PACK16_DIFF_EDITOR_REPORT_2026-07-25.md)). **Паки 12/13/14/15 ЗАЛЕНДЕНЫ 24–25.07** (приёмка воркфлоу-линзами; отчёты с ревью-шапками — [`archive/reports/PACK14_GENERALITY_REPORT_2026-07-24.md`](archive/reports/PACK14_GENERALITY_REPORT_2026-07-24.md), [`archive/reports/PACK15_STRUCTURE_REPORT_2026-07-25.md`](archive/reports/PACK15_STRUCTURE_REPORT_2026-07-25.md); норматив — [`architecture/12-go-style-notes.md`](architecture/12-go-style-notes.md)). Дальше: **пак-17 = канал B вживую** (батч со следующим платным прогоном: вопросы итерации №2 + замер остатка ремонта → решение о включении петли) → голос/состояние D21 + C2/C3 → пилот Ф2.5. Закрытые — в `archive/prompts/`. - `archive/` — `prompts/`: закрытые сессионные промты (только история, инструкции оттуда не исполнять) · `reports/`: отчёты паков/ресёрч-сессий с ревью-шапками (живая фактура приёмок — на них ссылаются D-лог и активные промты) · `architecture/`: исполненные арх-доки (07 стратревью · 08 синк-ледджер · 11 план пака-11) · три слайса хроники `PROGRESS-2026-07-{04-10,10-13,13-25}.md`. ## Статус (2026-07-25, пак-17 ЗАКРЫТ ЦЕЛИКОМ — механизм content-labels живой, D39.26–D39.28 — механизация выпускного качества) diff --git a/docs/architecture/05-decisions-log.md b/docs/architecture/05-decisions-log.md index 8476c0a6..3dea19e6 100644 --- a/docs/architecture/05-decisions-log.md +++ b/docs/architecture/05-decisions-log.md @@ -1032,3 +1032,25 @@ API-529-долг закрыт: 8-осевой refute-by-default воркфлоу **Третья, несущая связка, которую надо решить ЯВНО:** сегодня в инъекцию идёт **только CONFIRMED** (trust-gate банка, D39.2; D21 п.5). Значит «идти дальше на предложенном банке без оков» **не эквивалентно** «просто не останавливаться»: если mined-термы остаются `auto`, они в инъекцию НЕ попадают, и продолжение без подписи даёт банк, который для этих терминов не работает. Либо trust-gate ослабляется для mined+dst (и тогда в перевод едет НЕподписанный канон — с риском закрепить ошибку на всю книгу), либо остаётся, и «без оков» означает лишь «не блокировать прогон», а не «использовать предложенное». **Диспозиция:** дефект №1 (выброшенный dst) — чинить безусловно, он противоречит собственному замыслу и обесценивает канал WS4. Пункты 2 и 3 — **вопрос владельцу**, потому что это его продуктовый выбор между скоростью и риском закрепления неподписанного канона. До ответа мини-прогон ЗАМОРОЖЕН: он упрётся ровно в этот стоп и попросит подписать голые термины — то есть воспроизведёт трение, которое владелец только что назвал. + +## D39.37 — Мини-прогон + фикс-пак + сквозная проверка ПРИНЯТЫ и залендены: операционка подтверждена живьём до цента, шов банкноты замкнут (но не запинён — фикс-лист), остаток = 0 в трёх классах петли; слабое место проекта — измерительный слой (26.07, оркестратор №8). ✅ + +**Отчёты:** `MINIRUN_REPORT_2026-07-25.md` (фактура, §0 пре-регистрация написана ДО первой траты с named-критериями ПРОВАЛА) · `MINIRUN_HANDOFF_2026-07-26.md` (веса чисел, самооценка фиксов, 8 хвостов) · `MINIRUN_NOTE_2026-07-26.md`. Сессия шла под живым надзором владельца (две подписи майнинг-стопа, диалог «школа/академия»); фикс-пак — с его санкцией в сессии («исследование → правки → ревью»). Сессия не коммитила; лендинг мой. + +**Моя приёмка — execute-first, всё пере-ранено:** build/vet/gofmt чисты (`internal/llm/llm.go` — HEAD-состояние) · **`-race` 13 пакетов ok, 0 FAIL** (в дереве появился `internal/obs` с тестами) · **ценз диффом ИМЁН 475→492, удалено 0** · **golden `281d7025…dd9b99b4`: мой маскированный структурный дифф против HEAD ПУСТ** вне трёх заявленных осей (`CheapGateVersion` v4→v5 · `min_max_tokens` dspro 8000→16000 · `langpack_version` `a7d4be2c0465`→`a28ed743c99c`) · парити EXACT `n=13618 … 0.9655`. + +**Деньги сверены по СЫРЫМ ЛЕДЖЕРАМ до цента:** главный прогон `spend.committed_usd = $0.114291` (совпал с проекцией rebill-гейта ТОЧНО — она суммирует хранимые `cost_usd`); проба банкноты $0.021688; микро-проект гейта $0.003081 (леджер несёт ОБЕ оплаты; my first sum читала chunk_status — текущее состояние — и потому недосчитывала); сквозная проверка $0.048855; **брак сессии $0.046668** (sed-сборка конфига по чужому `book.yaml` — честно отчитан, оригиналы целы, бэкап rerun2 проверен: схема v9, spend неизменен). Итог **$0.234583** — арифметика сходится побайтно с отчётом. + +**Спот-чеки по сырью (мои):** банкнота **71 строка / 9 из 20 чанков / truncated 0** — совпало с БД; миграция **v11** реальна (`first_flag_reason`, `banknote_detail`); **шов замкнут вживую** — `banknote_detail` несёт пары src→dst, включая «学堂家老 → старейшина школы», тот самый терм второй подписи; **`RegressionGuard` — первое живое срабатывание за историю проекта ПОДТВЕРЖДЕНО МОИМ ре-раном** `tmctl report` на verify2: `number_drift:1`, «draft numbers missing from the final: [66]»; главный прогон 34/34 строки ok. БД главного прогона на v10 — скан остатка принят по записанному выводу команды (артефакт эпохи до миграции; форсить миграцию фактуры не стал). + +**НАХОДКА РУБЕЖА-2 (моя независимая мутация, ВЫЖИЛА):** отключение джойна WHAT в точке вызова (`miner.DeltaYAML(mined, пустая_карта)` вместо `proposals`) оставляет **всю сьюту зелёной** — несущий продуктовый фикс сессии доказан живым прогоном, но НЕ запинён ни одним тестом: регрессия была бы молчаливой. 9 мутаций сессии были красными, но ни одна не целила в этот колл-сайт. **Фикс-лист №1 следующего касания бэкенда: сквозной тест «намайненный терм с предложением банкноты → подписная карта несёт dst».** + +**РАТИФИЦИРОВАНО:** +1. Гигиена: флип `RegressionGuard` в 5 конфигах (обязательство D38.4 п.5 ПОГАШЕНО) · оба нита общности в **fail-loud форме без Go-дефолта** — снимает моё возражение D39.31 п.5 (дефолта, обязанного рендерить сегодняшнюю строку, не существует) · проекция пере-оплаты в `status` тем же `projectRebill`. +2. Фикс-пак (санкция владельца в сессии): v11 · эхо-метрика через `first_flag_reason` · шов банкноты (dst как EVIDENCE, `status:auto` не трогается — подпись остаётся владельцу) · `dialogue_dash` · обрезка лога · флор 16000. **Две откаченные правки (`percent_scale`, `dc2`) фиксирую как ОБРАЗЕЦ:** обе маскировали бы настоящие ошибки; «написано → прогнано → откачено с причиной» — правильный исход, не потерянная работа. +3. **Хендофф-дисциплина «чего числа НЕ доказывают» — принимается как НОРМА отчётов** (вес каждого числа: «0 в трёх классах ≠ текст чистый» · «замер на обучающей выборке» · «доказано тестами, не прогоном»). +4. **Методические ошибки сессии №2/№3/№5** («рекомендация раньше фактуры», «польза фикса без проверки», «вывод быстрее обеспечения») — тот же класс, что мои D39.34-находки; в следующие промты идёт формула сессии: **«пере-мерь свой вывод ДРУГИМ способом, чем получил»**, вместо голого «самопроверься». + +**НА ВЛАДЕЛЬЦЕ (открылось решение):** **включение платной петли ремонта (D39.24) — замер ЕСТЬ: остаток 0 в трёх классах, которые петля умеет чинить.** По этой цифре корректно решить «НЕ включать» (чинить нечего) — с оговоркой сессии: это узость набора классов, не чистота текста; расширение набора — прежде повторного замера. Рекомендация моя: не включать, петля остаётся построенной за `enabled:false`. + +**ХВОСТЫ (в трекер, по убыванию веса):** (1) **размеченный набор для чекеров** — топ-выбор сессии и мой: без него каждая цифра качества держится на глазомере очередной сессии; (2) тест-пин джойна WHAT↔WHICH (моя выжившая мутация); (3) покрытие WHAT↔WHICH (замер: 0/2 и 1/2 — следующая работа шва не «замкнуть», а «покрыть») + майнинг-стоп очищается РАУНДАМИ (вход в развилку D39.36 п.2 — флаговый стоп, семантика владельцем озвучена, реализация открыта); (4) `dialogue_dash` подогнан под 蛊真人 — честный замер только на невиданном тексте; (5) **генеральность нарезки: кана и ханьцзи в одном классе** (`TokenizeClassCounts`) — на ja-книге `est_out` завышен ~1.7×, чанки вдвое мельче, вызовов вдвое больше, МОЛЧА; + `EstimateTokens` недосчитывает кириллицу → резервации занижены на русско-тяжёлых вызовах; (6) golden пинит только фолбэк-путь (без langpack) — нужна вторая фикстура; (7) `prompt_override` — легальный обход гарантии слоя-2, сформулировать честно; (8) майнер/банк не-CJK конструктивно не работают (манифест = китайская ономастика) — граница признана, едет с ja→ru; (9) `request_log` без скоупа/ретеншена + `first_flag_reason` = ТРЕТЬЕ представление истории попыток (корень — неразведённые скоупы) ; (10) флор-16000 — калиброванная константа без модели «thinking в completion»; (11) две единицы упёрлись в `max_tokens` — вернётся на en-книге. **Проверено исполнением сессии ($0): ja/en книги падают громко ДО цента; с обойдённым гейтом проходят ingest/чанкер/read-model без правки Go** — §0 держится на уровне кода, но калибровки фактически китайские. diff --git a/docs/BACKEND_MINIRUN_SESSION_PROMPT.md b/docs/archive/prompts/BACKEND_MINIRUN_SESSION_PROMPT.md similarity index 97% rename from docs/BACKEND_MINIRUN_SESSION_PROMPT.md rename to docs/archive/prompts/BACKEND_MINIRUN_SESSION_PROMPT.md index c7f29400..76f2cd48 100644 --- a/docs/BACKEND_MINIRUN_SESSION_PROMPT.md +++ b/docs/archive/prompts/BACKEND_MINIRUN_SESSION_PROMPT.md @@ -1,6 +1,8 @@ # Промт бэкенд-сессии: МИНИ-ПРОГОН ~10 глав — сквозной тест всего построенного (гигиена → прогон → верификация в ОДНОЙ сессии) -**СТАТУС: РАЗМОРОЖЕН, ЗАПУСК САНКЦИОНИРОВАН ВЛАДЕЛЬЦЕМ (25.07).** Цель — доказать, что построенное РАБОТАЕТ, поэтому проверяется и канал банкноты, который до сих пор ни разу не стрелял (D39.36: гейт выключен, в промпте переводчика блока нет, а распарсенные записи код выбрасывает). **Но проверяется ОТДЕЛЬНЫМ дешёвым проходом (ШАГ 3), а не включением в главный прогон** — правка промпта переводчика меняет сам черновик, и главное измерение (остаток дефектов) перестало бы отвечать на свой вопрос. Два прохода, два ответа, ничего не смешано. +**Статус: ✅ ИСПОЛНЕН И ПРИНЯТ (`D39.37`, 26.07) — промт архивный.** Итог: $0.234583 · все 7 утверждений подтверждены сырьём · шов банкноты замкнут фикс-паком в сессии · остаток = 0 в трёх классах петли · хвосты — в трекере. Отчёты: `archive/reports/MINIRUN_{REPORT_2026-07-25,HANDOFF_2026-07-26,NOTE_2026-07-26}.md`. + +**Статус выдачи (история):** Цель — доказать, что построенное РАБОТАЕТ, поэтому проверяется и канал банкноты, который до сих пор ни разу не стрелял (D39.36: гейт выключен, в промпте переводчика блока нет, а распарсенные записи код выбрасывает). **Но проверяется ОТДЕЛЬНЫМ дешёвым проходом (ШАГ 3), а не включением в главный прогон** — правка промпта переводчика меняет сам черновик, и главное измерение (остаток дефектов) перестало бы отвечать на свой вопрос. Два прохода, два ответа, ничего не смешано. **ОДНА СЕССИЯ ДЕЛАЕТ ВСЁ (решение владельца):** гонит, проверяет, отчитывается. Цена решения названа честно: наш принцип author ≠ reviewer снят, а по переписи инцидентов проекта самоотчёт — доминирующий канал ошибок (62 из 180). **Три компенсации обязательны и не обсуждаются:** (1) **пре-регистрация ДО первой траты** — что меряем, пороги, и ЧТО МЫ ЗАРАНЕЕ НАЗОВЁМ ПРОВАЛОМ, пишется первым разделом отчёта и потом НЕ переписывается (прогон, у которого критерий успеха дописан после результата, ничего не доказывает); (2) **заявление = команда** — каждое число и каждое «отработало» сопровождается командой/артефактом, которым получено; (3) **приёмка оркестратора идёт по СЫРЬЮ** — БД, логи, экспорт — и ре-ранит твой манифест; твои выводы принимаются только вместе с фактурой. diff --git a/docs/archive/reports/MINIRUN_HANDOFF_2026-07-26.md b/docs/archive/reports/MINIRUN_HANDOFF_2026-07-26.md new file mode 100644 index 00000000..1e57e7be --- /dev/null +++ b/docs/archive/reports/MINIRUN_HANDOFF_2026-07-26.md @@ -0,0 +1,102 @@ +# Хендофф оркестратору: мини-прогон + фикс-пак + сквозная проверка (сессия бэкенда 25–26.07) + +**Кому:** оркестратор, для выдачи следующего промта (точечная зачистка хвостов + повторный прогон книги). +**Статус сессии:** закрыта владельцем. Дерево НЕ закоммичено. +**Подробности по фактам:** [`MINIRUN_REPORT_2026-07-25.md`](MINIRUN_REPORT_2026-07-25.md) — §0 пре-регистрация, §1–13 прогон, §14 фикс-пак, §15 сквозная проверка. Здесь — сводка, вес каждого утверждения и то, чего в отчёте нет. + +--- + +## 1. Что реально сделано + +**Гигиена ($0-код).** Флип `Gates.RegressionGuard.Enabled` в 5 шиппинг-конфигах — невыполненное обязательство D38.4 п.5. Два нита общности взяты в **fail-loud форме без Go-дефолта** (множитель `dc1_unit_in_hours` и Detail-шаблоны `msg` → пар-данные), чем снято возражение D39.31 п.5: дефолта, обязанного рендерить сегодняшнюю строку, просто нет. Проекция пере-оплаты в `tmctl status` тем же `projectRebill`, на котором отказывает гейт. + +**Прогон.** Новый проект, главы 1–10, **$0.114291** при оценке $0.10–0.19, 14/14 единиц ok. Все семь утверждений промта подтверждены сырьём. Первое живое исполнение rebill-гейта: проекция `34 единицы ~$0.114291` совпала со списанным **точно**, три отказа не тронули ни строки, пас-лег сделан на микро-проекте за $0.003. + +**Проба банкноты ($0.021688).** Канал физически работает: 71 строка на 9 из 20 чанков, `truncated` 0, сепаратор из чистого черновика срезан. + +**Фикс-пак.** Миграция **v11** (`chunk_status.first_flag_reason`, `retrieval_state.banknote_detail`); эхо-метрика видит эхо, вылеченное эскалацией; шов банкноты доведён до подписной карты (`status: auto` не трогается); `dialogue_dash` перестал бить по внутренней речи; лог режет середину; флор `deepseek-v4-pro` 8000→16000. + +**Сквозная проверка после фиксов ($0.048855).** Два прогона (главы 1–2 и 4–5, банкнота ВКЛ). Все узлы отработали; `RegressionGuard` дал **первое живое срабатывание за историю проекта**; санитайзер поймал реальный гомоглиф «лицó». + +**Деньги сессии: $0.234583** = $0.139060 прогон и пробы + **$0.046668 мой брак** (см. §4) + $0.048855 сквозная проверка. + +--- + +## 2. Чего эти числа НЕ доказывают + +Это главное, что я прошу прочитать до того, как какое-то из них попадёт в решение. + +**«Остаток дефектов = 0» — самая опасная цифра отчёта.** Она означает «ноль в трёх классах, которые петля ремонта умеет чинить» (DC1-время, латиница-остаток, битая словоформа), причём два из трёх — таргет-общие. Это не «текст чистый», это «набор классов петли узкий». Владелец принимает по ней решение о включении платной петли — и по ней корректно решать «не включать», но некорректно читать её как оценку качества. + +**«11 сигналов — 11 ложняков» (§6.2) я отчитал увереннее, чем имел право.** Последующие проверки частично это перевернули: dc2-срабатывание оказалось не дефектом реализации, а **принятой неоднозначностью класса**, о которой предупреждает его собственный комментарий; а мой же фикс `dialogue_dash` был сделан с **неверной полярностью** и переделан только после живого прогона. То есть мой разбор класса был мельче, чем выглядел. + +**Планка качества не измерялась вообще.** Судья не звался. Всё, что сказано про текст, — детерминированные сигналы и мои глаза на выборке. + +**Выборка — 14 единиц одной книги одной пары одним армом.** Любая экстраполяция на 2284 раздела не обеспечена. + +--- + +## 3. Насколько честны мои фиксы (самооценка, по которой стоит планировать зачистку) + +| фикс | вердикт | что осталось нерешённым | +|---|---|---| +| Д3 лог: обрезка середины | **чистый** | — | +| Д4 шов банкноты | провод настоящий, **вывод перепродан** | замер: пересечение каналов **0/2** и **1/2**. Продуктовая проблема (владелец подписывает голое) решена частично | +| Д1 `first_flag_reason` | **обход** | помнит только попытку 0 (эхо на попытке 1 теряется); заведено ТРЕТЬЕ представление истории попыток. Корень — неразведённые скоупы `request_log` (всё, что было) и `chunk_status` (текущее состояние) — не тронут | +| Д2 флор 16000 | **калиброванная константа** | модель «thinking внутри completion» не построена; поднимется `edit_ceiling_out` или придёт другая subset-модель — дыра открыта | +| Д7 `dialogue_dash` | **подгонка под корпус** | 29 глаголов + 5 вето выведены из отказов ОДНОЙ книги и проверены НА НЕЙ ЖЕ. «8→0» — замер на обучающей выборке. Сколько настоящих смешений теперь пропускается — **неизвестно, размеченного набора нет** | + +Положительная часть записи: **две правки написаны, прогнаны и откачены** — `percent_scale` (уронила ратифицированную фикстуру «шесть и шесть десятых»=6.6, где ошибка тоже прописана словами) и `dc2` (живой «ложняк» шейпом тождествен ратифицированному истинному). Обе маскировали бы настоящие ошибки. + +--- + +## 4. Мои ошибки в этой сессии (методические, а не только денежные) + +Записываю, потому что они системные и следующей сессии стоит защититься. + +1. **$0.046668 потрачено зря.** Проверяя миграцию, я собрал конфиг «копия чужой БД + источник мини-прогона» — движок увидел новую книгу и честно перевёл её, пока не сработал таймаут. Оригиналы целы, трата осталась на выбрасываемой копии. **Корень:** я трижды за сессию собирал проект через `sed` по чужому `book.yaml`; один раз это выстрелило. Конфиг прогона надо собирать с нуля и проверять `status` до `translate` — я так делал в трёх случаях из четырёх. +2. **Рекомендация раньше фактуры.** На первой подписи я предложил владельцу «школа» до того, как посмотрел контексты в исходнике. Владелец возразил («это разве не академия?») — и только тогда я пошёл в текст. Вывод оказался тот же, метод — нет. Это ровно тот класс, который норма проекта называет «вывод, совпавший с репликой собеседника, обязан проверяться против документов ДО выдачи». +3. **Утверждение о пользе фикса без проверки.** Я сказал владельцу «под фиксом карта пришла бы с dst «старейшина школы» — ровно тем, что вы выбрали». Проверка по сырью (уже после вопроса «а ты протестировал?») показала: банкнота **никогда не предлагала `学堂` отдельно**, только составной `学堂家老`. Первая подпись всё равно пришла бы голой; фикс помог бы во ВТОРОМ раунде. +4. **Пре-регистрация написана мной и частично мимо.** П8 сформулирован как «парсер не принимает то, что модель выдаёт» — предполагал вину парсера. Сработал, а диагноз оказался обратным (строка без ханьца от модели, парсер отработал по контракту). Критерий, который потом приходится объяснять, даёт ложное спокойствие. Следующей сессии критерии провала стоит давать **в промте**, а не оставлять на самоописание. +5. **Общий паттерн:** я перехожу от наблюдения к выводу быстрее, чем обеспечиваю его, и **каждая поправка в этой сессии пришла от исполнения, ни одна — от моего перечитывания.** Практический вывод для промта: требовать не «самопроверку», а конкретно «пере-мерь свой вывод другим способом, чем получил». + +--- + +## 5. Что я видел, но не чинил (кандидаты в зачистку, по убыванию веса) + +1. **Калибровка нарезки для не-zh пары — китайская, и молча.** Генерик-фолбэк `LoadPipeline` — это ратифицированные zh-ru числа (1797/3200/1.1978/0.3852), а счётчик `TokenClassCounts` (`chunker.go:421`) кладёт **кану и ханьцзи в один класс с одной плотностью**. Замерил обе книги на диске: в `isekai_majutsushi_jp` кана — **70%** класса (138 831 против 59 322 ханьцзи) ⇒ `est_out` завышен грубо в ~1.7×, чанки вдвое мельче нужного, вдвое больше вызовов. В английской книге все 1.31 млн символов идут в «other» с коэффициентом, выведенным как остаточный член на китайской пунктуации. **Ничего не падает.** `configs/pairs/ja-ru.yaml` это и признаёт: «калибровка нарезки НЕ выведена — прогонов не было». +2. **`EstimateTokens` (`cjk + other/3`) недосчитывает кириллицу.** Он же считает промпт для РЕЗЕРВАЦИИ, а вход редактора — русский черновик ⇒ резервации систематически занижены на русско-тяжёлых вызовах. Никем не измерено; на потолок влияет, на списание нет. +3. **Golden покрывает только фолбэк-путь.** Фикстура — ja→ru **без langpack**, то есть детерминизм-гард никогда не пинит путь с загруженным пакетом байт-в-байт. Дыра вскрылась, когда я двигал `langpack_version`: стендовые книги поехали, golden — нет. +4. **`prompt_override` — легальная дыра в гарантии слоя 2.** Fail-loud «никогда не подставляй конвенции другой пары» обходится оверрайдом на что угодно; именно так я и прогнал en/ja книги. Это by design, но гарантию стоит сформулировать честно. +5. **Майнер и банк для не-CJK пары не работают конструктивно:** обязательный манифест пакета — схема китайской ономастики (百家姓, 甲乙丙, 等/转/阶), обязательные пар-файлы называются `palladius.txt`. Комментарий в коде это признаёт. Для en-книги `langpack_root` просто не задаётся — вместе с ним отваливаются майнинг и DC-чекеры. +6. **Майнинг-стоп очищается РАУНДАМИ.** Промоушен `学堂` расчехлил `学堂家老` (владелец подписывал дважды). На книге число раундов заранее неизвестно — это аргумент в развилку D39.36 п.2 сильнее, чем «одна пауза». +7. **`request_log` растёт без определённого скоупа** (77 строк на 37 вызовов после трёх запусков) — ни ретеншена, ни ответа «за какой прогон». Связано с корнем Д1. +8. **Две единицы упёрлись в `max_tokens` даже под фиксом-константой** — на английской книге, где чанки крупнее нужного, этот класс вернётся. + +--- + +## 6. Что должен измерить следующий прогон + +- **Эхо-метрика после Д1 на живом эхо.** В сквозной проверке эха не случилось ⇒ фикс доказан тестами, но не прогоном. +- **Джойн WHAT↔WHICH при непустом пересечении.** Сегодня доказано только, что он не ломается на пустом. Нужен срез, где банкнота и майнер называют один терм (на 10 главах это `学堂家老`). +- **`dialogue_dash` на тексте, которого я не видел.** Любой замер на 蛊真人 теперь загрязнён: правило подогнано под него. Честная проверка — другая книга или размеченный срез. +- **Остаток дефектов при расширенном наборе классов петли** — иначе цифра будет снова «0 в трёх классах». +- **Стоимость под флором 16000** — упала ли доля брака с измеренных 15.5%. + +--- + +## 7. Операционное состояние + +- **Дерево не закоммичено**, 35 позиций. Ключевое: миграция **v11**; `CheapGateVersion` v4→**v5** (`cheapgate-v5-chevron-speech-attribution`); `min_max_tokens` dspro 8000→16000; `langpack_version` `a7d4be2c0465`→`a28ed743c99c`. +- **Сдвинуты три оси снапшота** ⇒ следующий прогон стендовой книги требует `--resnapshot --accept-rebill`. Сегодня это ничего не стоит (снапшот стенда и так разошёлся, D39.35). +- **Golden пере-капчен дважды**, оба раза маскированный структурный дифф **пуст**; текущий `281d7025…dd9b99b4`. +- **Манифест приёмки:** build/vet/gofmt чисты (жалоба `internal/llm/llm.go` воспроизводится на HEAD) · `-race` 15/15 · ценз ИМЁН **475 → 492, удалено 0** · **9 мутаций — 9 красных** (одна выжила, вскрыла непокрытую ветку «книга без данных таргета» с nil-разыменованием, ветка закрыта тестом, пере-мутация красная). +- **Артефакты прогонов** — вне git: `minirun/`, `minirun-banknote/`, `minirun-rebill/`, `minirun-verify/`, `minirun-verify2/` в `/home/ubuntu/books/gu-zhenren/`; бэкап фактуры rerun2 — `backup-pre-minirun-2026-07-25/` (проверен: цел, схема v9, spend не изменился). + +--- + +## 8. Одно суждение напоследок + +Прогон показал, что **операционка построена хорошо**: деньги durable до цента, гейты отказывают до трат, резюм за $0 работает, fail-loud на чужой паре срабатывает до единого цента. Слабое место не там — оно в **измерительном слое**. Чекеры не имеют размеченного набора, поэтому их цифры (включая ту, по которой принимается решение о платной петле) держатся на моём глазомере; калибровки объявлены пар-данными, но фактически везде китайские; а метрики качества измеряют то, что удобно достать, а не то, что решает. + +Если выбирать один хвост из всех перечисленных — я бы взял **размеченный набор для чекеров**. Без него следующий прогон произведёт ещё один набор чисел, которые снова придётся проверять глазами, и снова моими. diff --git a/docs/archive/reports/MINIRUN_NOTE_2026-07-26.md b/docs/archive/reports/MINIRUN_NOTE_2026-07-26.md new file mode 100644 index 00000000..3cf5876b --- /dev/null +++ b/docs/archive/reports/MINIRUN_NOTE_2026-07-26.md @@ -0,0 +1,17 @@ +# Записка оркестратору (сессия бэкенда 25–26.07, закрыта) + +**Сделано:** гигиена ($0-код: обязательный флип `RegressionGuard` + два нита общности + проекция пере-оплаты в `status`) → мини-прогон 10 глав → проба банкноты → фикс-пак по найденным дефектам → сквозная проверка бэкенда после фиксов. **Деньги: $0.234583**, из них **$0.046668 — мой брак** (конфиг, собранный по чужому `book.yaml`, ушёл в реальный перевод). + +**Состояние дерева:** НЕ закоммичено, 36 позиций. Миграция **v11**; сдвинуты три оси снапшота (`CheapGateVersion` v4→v5, `min_max_tokens` dspro 8000→16000, `langpack_version`) ⇒ следующий прогон стендовой книги требует `--resnapshot --accept-rebill` (сегодня бесплатно — снапшот стенда и так разошёлся, D39.35). + +**Приёмка:** build/vet/gofmt чисты · `-race` 15/15 · golden пере-капчен дважды, **маскированный структурный дифф оба раза пуст** · ценз ИМЁН **475 → 492, удалено 0** · **9 мутаций — 9 красных**. + +**Три вещи, которые прошу не потерять при выдаче следующего промта:** + +1. **«Остаток дефектов = 0» — это ноль в ТРЁХ классах, которые петля ремонта умеет чинить, а не «текст чистый».** По ней корректно решать «петлю не включать» и некорректно читать её как оценку качества. +2. **Фикс `dialogue_dash` подогнан под 蛊真人 и проверен на нём же** («8→0» — замер на обучающей выборке). Любой будущий замер этого класса на той же книге загрязнён. Корень — отсутствие размеченного набора для чекеров; **если брать один хвост, я бы взял этот**. +3. **Шов банкноты замкнут, но каналы почти не пересекаются** (замер: 0/2 и 1/2 подписных раунда). Следующая работа — не «замкнуть шов», а заставить WHAT покрывать кандидатов WHICH. + +**Проверено исполнением, $0:** японская и английская книги с диска падают **громко до единого цента** (нет промпт-пакета пары); с обойдённым гейтом обе проходят ingest/чанкер/read-model **без единой правки Go**. Норматив общности §0 держится на уровне кода — но генерик-фолбэк нарезки китайский, а счётчик кладёт кану и ханьцзи в один класс (в японской книге кана — 70%, `est_out` завышен ~1.7×). Молча. + +**Читать:** [`MINIRUN_HANDOFF_2026-07-26.md`](MINIRUN_HANDOFF_2026-07-26.md) — вес каждого числа, самооценка честности пяти фиксов, мои методические ошибки, 8 хвостов с рангами, что обязан измерить следующий прогон. Фактура прогона — [`MINIRUN_REPORT_2026-07-25.md`](MINIRUN_REPORT_2026-07-25.md). diff --git a/docs/archive/reports/MINIRUN_REPORT_2026-07-25.md b/docs/archive/reports/MINIRUN_REPORT_2026-07-25.md new file mode 100644 index 00000000..884038a4 --- /dev/null +++ b/docs/archive/reports/MINIRUN_REPORT_2026-07-25.md @@ -0,0 +1,528 @@ +# Мини-прогон ~10 глав: сквозной тест построенного (гигиена → главный проход → проба банкноты → верификация) + +> **Ревью-шапка оркестратора №8 (26.07, приёмка execute-first по СЫРЬЮ; ПРИНЯТ — `D39.37`). 0 блокеров.** Ре-ранено: build/vet/gofmt · `-race` 13/13 · ценз ИМЁН 475→492 (удалено 0) · парити EXACT · **маскированный дифф golden против HEAD пуст** вне трёх заявленных осей. **Деньги сверены по сырым леджерам до цента:** $0.114291 (проекция = списание ТОЧНО) + $0.021688 + $0.003081 (обе оплаты в `spend`) + $0.048855 + $0.046668 брака = $0.234583. Спот-чеки: банкнота 71/9из20/0 по БД · v11 реальна · `banknote_detail` несёт «学堂家老 → старейшина школы» · **`RegressionGuard` live-срабатывание подтверждено МОИМ ре-раном** (`number_drift:[66]`). **Находка рубежа-2: моя мутация «отключить джойн WHAT в колл-сайте» ВЫЖИЛА по всей сьюте** — фикс доказан прогоном, но не запинён тестом; фикс-лист №1 следующего касания. Оговорки хендоффа §2 ратифицированы как норма чтения этих чисел; хвосты §5 — в трекере с диспозициями (D39.37). + +**Сессия:** бэкенд, одна сессия делает всё (решение владельца). Промт — `docs/archive/prompts/BACKEND_MINIRUN_SESSION_PROMPT.md`. +**Дата:** 2026-07-25. **Книга:** 蛊真人, главы 1–10, новый проект `guzhenren-minirun` (вне git). + +> **⟶ ЧИТАТЬ ВМЕСТЕ С ХЕНДОФФОМ [`MINIRUN_HANDOFF_2026-07-26.md`](MINIRUN_HANDOFF_2026-07-26.md).** Там — вес каждого числа этого отчёта (что оно доказывает и чего НЕ доказывает), самооценка честности фиксов, методические ошибки сессии и 8 хвостов, увиденных но не чиненных. Отдельные выводы §6.2 хендофф уточняет: срабатывание dc2 — не дефект реализации, а принятая неоднозначность класса. + +--- + +## §0. ПРЕ-РЕГИСТРАЦИЯ (написана ДО первой траты; ниже НЕ переписывается) + +> Это первый раздел отчёта и он зафиксирован до единого платного вызова. Причина названа в промте +> прямо: принцип author ≠ reviewer снят, а по переписи инцидентов проекта самоотчёт — доминирующий +> канал ошибок (62 из 180). Прогон, у которого критерий успеха дописан после результата, ничего не +> доказывает, поэтому критерий провала назван здесь заранее. + +### 0.1 Что меряем (M1–M8) + +| # | Утверждение, которое прогон обязан подтвердить СЫРЬЁМ | Артефакт-носитель | +|---|---|---| +| M1 | Гейт согласия на пере-оплату (пак-18) отработал ЖИВЬЁМ: показал проекцию (N единиц, ~$X), отказал без флага ДО первой резервации, прошёл с флагом | лог отказа, `chunk_status`, `SpentUSD` до/после | +| M2 | Майнинг-стоп остановил прогон перед волной редактуры, записал подписную карту, очистился после подписи (промоушен + reject) | `*.db.mined-signature.yaml`, `mined-delta.yaml`, `mined-rejects.yaml`, логи | +| M3 | Чекеры и `QualityReport` дали ОСТАТОК ДЕФЕКТОВ по классам ($0 read-only скан) — главное измерение прогона | `tmctl report`, `repair_candidates` | +| M4 | `RegressionGuard` ПОСЛЕ флипа даёт наблюдаемость (длина/числовой дрейф видны) | `retrieval_state.n_style_flags` + `style_detail` | +| M5 | Лейбл-путь = no-op: резолвнутые модели тождественны конфиговым, ноль строк проблем маршрутизации | `tmctl report`/`status --json`, `chunk_status.model_*` | +| M6 | Деньги durable: доля `finish=stop`, эхо-класс, reserve→settle одной транзакцией, итог против потолка и против оценки | `request_log`, `checkpoints`, `ledger` | +| M7 | `escalation_model` держит ОТВЕТИВШУЮ модель, а не попробованную | `chunk_status.escalation_model` | +| M8 | Канал WHAT (банкнота) физически работает от промпта до парсера | сырые тела ответов (`LOG_LLM_BODIES`), `retrieval_state` банкнота-телеметрия | + +### 0.2 Пороги и ожидания (названы заранее) + +- **Деньги главного прохода:** ожидание **$0.10–0.19** (драфт ≈$0.022 + редактура ≈$0.08–0.15; экстраполяция фактических цифр rerun2 на срез ровно вдвое больший). Потолки книги/дня — **$1.00/$1.00**, выставлены с 5–6× запасом, но конечны: гейт потолка обязан быть способен выстрелить. +- **Проба банкноты:** ожидание **≈$0.02** (только волна черновика). +- **Проба гейта пере-оплаты (пас-лег):** ожидание **≤$0.01** (отдельный микро-проект в одну главу, чтобы «проход с флагом» стоил центы, а не второй прогон). +- **Судья НЕ вызывается.** Петля ремонта остаётся `enabled: false`. Второй арм (glm-5) НЕ гоняется без отдельного слова владельца. + +### 0.3 ЧТО МЫ ЗАРАНЕЕ НАЗЫВАЕМ ПРОВАЛОМ + +| # | Провал (заранее) | +|---|---| +| П1 | Фактическая стоимость главного прохода **> $0.40** (вдвое выше верха оценки) — модель денег неверна | +| П2 | Пробит потолок книги/дня ИЛИ `committed ≠ SUM(checkpoints)` вне ратифицированного исключения `redrive` | +| П3 | Эхо-класс (`cjk_artifact`) на черновике **> 0** — эхо-мина DeepSeek жива при thinking-ON | +| П4 | Гейт пере-оплаты НЕ отказал при проекции выше порога, ИЛИ отказал ПОСЛЕ резервации денег (сырьё: любая новая строка ledger/checkpoint на отказе) | +| П5 | Майнинг-стоп НЕ остановил прогон при непустой дельте, ИЛИ не очистился после того, как каждый терм промоутнут/отклонён | +| П6 | `RegressionGuard` после флипа молчит, **а ручная проверка находит реальный обвал длины/числовой дрейф** в отгруженном тексте. (Честный ноль при отсутствии дрейфа — НЕ провал.) | +| П7 | Хоть одна резолвнутая модель ≠ конфиговой ИЛИ есть хоть одна строка проблем маршрутизации на книге без лейблов | +| П8 | Банкнота: `n_banknote_lines = 0` по ВСЕМ чанкам (модель не выдаёт блок) ИЛИ `parse_fail > 0` (парсер не принимает то, что модель выдаёт) | +| П9 | Остаток дефектов отчитан СЫРЫМ счётчиком без ручного отделения ложняков — решение владельца о платной петле принимается по ЧИСТОМУ остатку | +| П10 | Потеря/порча данных: битая БД, рваное состояние после прерывания, утрата фактуры rerun2 | + +### 0.4 Что заранее объявлено НЕ провалом (чтобы задним числом не переписать) + +- **Майнинг-стоп с нулём кандидатов** — законный исход (сид уже покрывает главы 1–10). Тогда M2 остаётся НЕДОКАЗАННЫМ и отчитывается именно так, а не как успех. +- **Эскалация не случилась** — законный исход; M7 остаётся НЕДОКАЗАННЫМ. +- **Термины приходят на подпись БЕЗ dst** — это сегодняшнее ОЖИДАЕМОЕ поведение (D39.36), фиксируется как факт, а не как авария. +- **Разрыв канона 元-семейства** (元海/真元/元石 расщеплены, единый корень ждёт владельца), отсутствие `学堂家老` в сиде, `赤城` только внутри `古月赤城` — ИЗВЕСТНЫЕ разрывы, объявленные ДО прогона; чтение результата не имеет права приписывать их качеству перевода. +- **Качество перевода как таковое НЕ измеряется** (судья не зовётся); прогон отвечает на вопрос «работают ли узлы», а не «хорош ли текст». + +### 0.5 Пре-регистрационные факты стенда (зафиксированы до трат) + +- Бэкап `rerun2/*.db` сделан ДО первой write-команды: `/home/ubuntu/books/gu-zhenren/backup-pre-minirun-2026-07-25/` (3 БД + shm/wal). +- Срез: строки 93–837 исходного `guzhenren-gb18030.txt` = 10 глав, **57 838 байт GB18030**. Метод среза проверен воспроизведением: строки 93–469 дают файл, **байт-в-байт равный** `rerun2/guzhenren-ch1-5.gb18030.txt` (sha256 `8755afd3…d3d76f64`). +- Манифест до прогона: **10 глав, 14 edit-единиц** (`tmctl status`, $0). +- Слаги живьём (правило двух направлений, до платных вызовов): `GET /v1/models` DeepSeek → `['deepseek-v4-flash', 'deepseek-v4-pro']`, оба слага конфига живы. `prices_checked: 2026-07-10` (15 дней, порог 120). +- **Ре-чек ToS (D39.32 п.4): проверено, изменений нет.** Триггеры не наступили: головной Google ToS — 30.07.2026 (впереди), правок `accepts_labels` в этой сессии не было, книга лейблов не несёт, Gemini в цепочке этого прогона не участвует. + +--- + +## §1. ШАГ 0 — ГИГИЕНА (исполнена ДО прогона, $0-код) + +### 1.1 Обязательный флип `Gates.RegressionGuard.Enabled` (D38.4 п.5, невыполненное обязательство D39.34) + +Включён во **всех пяти** шиппинг-конфигах: `pipeline-c1.yaml` · `pipeline-c2.yaml` · `pipeline-arm-deepseek-pro.yaml` · `pipeline-arm-glm.yaml` · `pipeline-arm-mistral.yaml` (`grep -c regression_guard configs/*.yaml` = 5). C2 включён осознанно: у него ЕСТЬ стадия `edit`, значит переход черновик→финал существует и гард не вырожден. + +**Флип НЕ двигает снапшот** — проверено исполнением, а не рассуждением: в логах одного и того же прогона с гейтом и без него обе волны рендерят те же id (`snapshot=2ab1b7578d40` драфт, `46e9bc2bcf5a` эдит). Пороги гарда — код-консты, а его версия уже сидит в `CheapGateVersion` ⇒ **пере-оплаты флип не стоит**. + +**Покрыт тестом, что гейт РЕАЛЬНО стреляет** — `internal/pipeline/regressionguard_live_test.go`, три теста через настоящий драйвер (норма 11): +- `TestRegressionGuardFiresLiveWhenEnabled` — редактор возвращает текст вчетверо короче черновика и без двух многозначных чисел ⇒ `n_style_flags=3`, в `style_detail` `"length_collapse":1`, `"number_drift":2` и сами человекочитаемые строки; +- `TestRegressionGuardSilentWhenDisabled` — тот же прогон без гейта ⇒ 0 (гейт opt-in, «выключён = байт-идентично»); +- `TestRegressionGuardComparesDraftAgainstFinal` — редактор возвращает черновик БЕЗ изменений ⇒ 0. Пинит, ЧТО именно волна подаёт гарду: разводка «финал против финала» (законная форма драфт-волны) отчитывалась бы чистой книгой вечно. + +### 1.2 Два нита общности (условность снята D39.35) + +| нит | было | стало | +|---|---|---| +| множитель DC1 | `expectedHours := n * 2` в `internal/checks/checkers.go` | `n * c.dc1UnitHours`, значение из `dc_ratio dc1_unit_in_hours 2` в `configs/langpacks/zh-ru/dc-checkers.txt` | +| Detail-строки | Go-литералы с 时辰/千万/数十万/成 в generic-файле | шаблоны `msgkeytemplate` в пар-паке, подстановка `{name}` | + +Форма выбрана **fail-loud, без Go-дефолта**: пак, который несёт детекцию, но не несёт шаблон/отношение, — корраптный, и `CompileCheckers` падает так же, как на битом регексе (`mustPairRatio`/`mustPairMessages`). Это снимает возражение D39.31 п.5 («дефолт шаблона обязан рендерить текущую строку, значит 时辰-литерал остаётся в Go») — дефолта просто нет. + +Заодно из engine-файлов убраны ЦЕЛЕВЫЕ литералы, которые соврали бы второй паре/второму таргету, без всякой новой данных-механики: `«万/億»` и пример `三万→«три миллиона»` из сообщения магнитудного гейта, «no valid **Russian** word» → «the target has no well-formed word», «Latin word … in the **Russian** output» → «in the target output». + +Тесты (`internal/checks/checkers_pairdata_test.go`): синтетический «второй пар-пак» с отношением 1:1 МОЛЧИТ там, где 2:1 флагает (`TestDC1UnitRatioIsPairData`); каждая пар-строка рендерится из пакета, а не из Go (`TestCheckerDetailTextComesFromPairData`); корраптный пак падает с именем недостающего ключа (`TestCorruptPairPackFailsLoud`, 4 кейса); книга без пакета остаётся инертной (`TestNoPackStaysInert`). + +**Сдвинутая версионная ось — ровно одна, называю строкой (как требует промт):** `langpack_version` `langpack-v2-a7d4be2c0465` → `langpack-v2-a28ed743c99c` (замер: тот же пробный тест на HEAD-дереве и на рабочем). Двигает ОБА волновых снапшота для книг, ссылающихся на репозиторный каталог langpack. Для мини-прогона это бесплатно (новый проект), для стендовых книг rerun2 — часть уже состоявшегося расхождения D39.35. `CheapGateVersion` НЕ бампался осознанно: изменились ТЕКСТЫ строк, а не счётчики и не вердикты; пар-сообщения теперь данные, и их версия — это и есть хеш пакета. + +### 1.3 Опциональное: проекция пере-оплаты в `tmctl status` (спека D15.2 §9) + +Добавлены `rebill_units`/`rebill_usd` (оба `omitempty`) + строка в человекочитаемом рендере. Считает **тот же `projectRebill`**, на котором отказывает гейт, — одно определение числа, а не второе, дрейфующее. Булев `ConfigDrift` СОХРАНЁН (не заменён): его читает полигонная экстракция, а добавление чисел аддитивно. Тест `TestStatusPricesTheDriftWithTheGatesOwnNumber` пинит, что цифра статуса равна и фактически списанному, и той, что называет отказ гейта. + +### 1.4 Приёмка гигиены исполнением + +| проверка | результат | +|---|---| +| `go build ./... && go vet ./...` | чисто | +| `go test ./... -race` | 15 пакетов **ok, 0 FAIL** | +| golden | `211c9013f5cfa31562857cc9518488393a647cce489d37113402b159089045d6` — **байт-в-байт равен HEAD** (`git show HEAD:…\| sha256sum`), пере-капчера не было | +| ценз диффом ИМЁН тестов | 475 → 483, **удалено 0** (8 добавленных перечислены выше) | +| мутации (самопроверка исполнением) | **5 мутаций — 5 красных**: `RegressionEnabled:false` · `RunCheapGates(…, finalText, finalText, …)` · `n * 2` вместо данных · `renderMsg` без подстановки · `status` не пишет проекцию | + +--- + +## §2. ГЛАВНЫЙ ПРОХОД — что произошло + +| | | +|---|---| +| манифест | 10 глав → **20 драфт-чанков** → **14 edit-единиц** | +| прогон 1 | драфт-волна целиком, **STOP** на майнинг-стопе (exit 3), $0.022205 | +| подпись, раунд 1 | 学堂 → «школа» (владелец рассмотрел «академия», подписал «школа») | +| прогон 2 | драфт резюмнулся за **$0.00**, стоп СНОВА (exit 3) — расчехлился 学堂家老 | +| подпись, раунд 2 | 学堂家老 → «старейшина школы» | +| прогон 3 | «bank-mining: empty delta, auto-continuing», edit-волна 14/14 **ok**, $0.092086 | +| итог | **14/14 units done, 0 flagged**, committed **$0.114291**, потолок $1.00 (11.4%) | + +Эскалация случилась **одна**: ch3/chunk0 драфт flagged `cjk_artifact` → хоп `deepseek-v4-pro` → ok. +Два edit-юнита (ch5/0, ch6/0) упёрлись в `max_tokens=8000`, ретрай на 16000 вытащил оба. + +--- + +## §3. ПРОБА БАНКНОТЫ (отдельный проект, только волна черновика) + +Проект `minirun-banknote/`: своя БД, свой `book.yaml`, КОПИЯ промпта переводчика с блоком `⟦TM-BANK-v1⟧` (шиппинг-файл не тронут), `gates.banknote.enabled: true`, `LOG_LLM_BODIES=1 LOG_LEVEL=debug`. **$0.021688**, 20/20 чанков ok. + +| метрика | значение | +|---|---| +| `n_banknote_lines` (сумма) | **71** строка на 9 из 20 чанков | +| `banknote_parse_fail` | **1 чанк** (ch3/chunk0) | +| `banknote_truncated` | **0** | +| производных чекпоинтов `banknote_export` | 9 | +| сепаратор в чистом экспорте | **0 вхождений** (`grep -c` по `export-banknote.txt`), табличных строк-остатков — 0 | + +--- + +## §4. ПРОБА ГЕЙТА СОГЛАСИЯ НА ПЕРЕ-ОПЛАТУ + +Гейт стрелял **живьём дважды**: на микро-проекте `minirun-rebill/` (одна глава, чтобы «проход с флагом» стоил центы) и на ГЛАВНОМ проекте (только отказ — он бесплатен). + +| лег | команда | исход | +|---|---|---| +| проекция | `status` | `⚠ RE-PAYMENT: 2 chunk×stage unit(s) … ~$0.001629` | +| отказ без флага | `translate` | exit 1, «NOTHING was reserved and no row was touched» | +| отказ на `--resnapshot` БЕЗ согласия | `translate --resnapshot` | exit 1 — ре-пин ≠ согласие на КОНКРЕТНУЮ сумму (развилка пака-18 подтверждена живьём) | +| отказ по потолку | `--accept-rebill=0.0001` | exit 1: «caps consent at $0.000100 — refusing» | +| **проход с флагом** | `--resnapshot --accept-rebill` | WARN `rebill_units=2 rebill_usd=0.001629 threshold_usd=0.000081`, прогон пере-оплатил $0.001452 | +| главный проект (обе волны сдвинуты) | `status` + `translate` | `34 chunk×stage unit(s) … ~$0.114291`, отказ; порог $0.005715 (5% от $0.114291) | + +**Деньги ДО и ПОСЛЕ трёх отказов побайтно те же:** `spend` $0.0016289784 → $0.0016289784, `checkpoints` 2 → 2, `request_log` 2 → 2, `snapshot_id` в `chunk_status` не сдвинулся. Отказ происходит **до резервации и до провайдерского вызова**, а не после. + +После возврата конфига в исходное состояние снапшот вернулся (`2299964c3e56`) и проекция замолчала — то есть отказ не оставил следа, а гейт есть чистая функция конфига. + +--- + +## §5. ВЕРДИКТЫ ПО ШАГУ 4 (каждый — с артефактом; проверял так, будто отчёт писал не я) + +### 4.1 Гейт согласия на пере-оплату — ✅ ПОДТВЕРЖДЁН ЖИВЬЁМ (первое исполнение механизма) + +Фактура — §4. **Отказ ДО денег доказан сырьём, а не текстом сообщения:** `spend`/`checkpoints`/`request_log`/`snapshot_id` побайтно те же после трёх отказов. **Сверка проекции с фактической тратой:** проекция главного проекта `34 единицы, ~$0.114291` **точно равна** `spend.committed_usd = $0.11429117`. Расхождение отсутствует, потому что проекция суммирует хранимые `chunk_status.cost_usd` — то есть буквально то, что уже было списано. На микро-проекте проекция $0.001629, а фактическая пере-оплата $0.001452 (−11%): **это не ошибка проекции, а её семантика** — она отвечает «сколько стоила эта работа В ПЕРВЫЙ РАЗ», а второй раз провайдер отдал часть промпта из кэша. Направление безопасное (перед фактом называется не-заниженное число). + +### 4.2 Майнинг-стоп — ✅ ПОДТВЕРЖДЁН, с новым фактом о его форме + +Остановился перед edit-волной (exit 3, WARN `terms=1`), записал подписную карту, после подписи дельта опустела («empty delta, auto-continuing to the edit wave») и стоп не повторился. Драфт-волна при этом резюмнулась **за $0.00** (в `request_log` 40 строк `tm_hit=1`, cost 0) — «пере-оплачено ОДИН раз» держится. + +**Термы приходят БЕЗ dst — подтверждено, как ожидаемое поведение (D39.36), не как авария:** в обеих подписных картах `dst: ""`. + +**НОВОЕ (не предсказано ни промтом, ни D39.36): стоп очищается РАУНДАМИ, а не одним заходом.** Промоушен 学堂 расчехлил 学堂家老, которого в первом раунде в карте не было (его перекрывала эмиссия). Владелец на первом стопе видит «1 терм» и не может знать, что подписей будет 2 (а на книге — неизвестно сколько). Это не дефект кода, но это свойство контракта, которое стоит назвать вслух рядом с «опциональным ОК» из D39.36. + +### 4.3 Банк памяти — ✅ ПОДТВЕРЖДЁН, с независимой сверкой глазами + +`injections(exact)=268 · sticky=64 · ambiguous=5 · spoiler-blocked=0 · evicted=0 · post-check-misses=9` на 20 чанках (ориентир прошлого прогона — 107/3 на 10 чанков; здесь вдвое больше чанков и вдвое больше попаданий). + +**Независимая проверка (взял термины из глоссария, нашёл в исходнике, посмотрел финальный текст):** + +| терм (канон) | что в отгруженном тексте | вердикт | +|---|---|---| +| 蛊虫 «гу-червь» ×3 | «гу-червей», «гу-червям» | перевод ВЕРЕН; в `decl.forms` сида только ЕДИНСТВЕННОЕ число ⇒ промах записан честно, но это **дыра в данных сида**, не дефект перевода | +| 月兰花 «лунная орхидея» | «лунных орхидей» | то же — множественного числа в формах нет | +| 甲等/丙等 «класс А»/«класс В» | «талантом класса «А»», «класса «В»» | смысл верен, канон-форма не воспроизведена буквально: редактор добавил кавычки вокруг литеры | +| 开窍 «открытие апертуры» ×2 | «Церемония открытия апертур» | форма-дрейф (мн. ч. «апертур» вместо «апертуры») | +| 族长 «глава клана» | «Первый **глава рода**», «в нашем **роду** Гуюэ» | **РЕАЛЬНЫЙ ДРЕЙФ КАНОНА** — ровно тот, о котором предупреждает заметка самого сида («⚠ консистентность род/клан») | + +Итого 9 промахов: **1 реальный дрейф канона · 4 формальных/косметических · 4 — отсутствие мн. ч. в сиде.** + +### 4.4 Чекеры и остаток дефектов — ГЛАВНОЕ ИЗМЕРЕНИЕ ⚠ см. §6 + +### 4.5 `RegressionGuard` после включения — ✅ ЧЕСТНЫЙ НОЛЬ (проверен независимо) + +Гард зафиксировал 0 обвалов длины и 0 числовых дрейфов. Пре-регистрация требовала не поверить этому нулю на слово (П6), поэтому я пересчитал переход черновик→финал **сам, по чекпоинтам и экспорту**: + +- отношение непробельных символов финал/черновик по 14 единицам: **0.90 … 1.06** (порог гарда — падение ниже 0.60); +- многозначных чисел ПОТЕРЯНО: **0** во всех 14 единицах; +- единственное «появившееся» число — `10` в ch10/0, и это артефакт МОЕЙ сверки: `ApplyHeading` дописывает «Глава 10» в экспорт ПОСЛЕ того, как гейты отработали (у глав 1–9 номера однозначные и правилом «≥2 цифр» отсекаются). + +⇒ ноль гарда — правда, а не молчание. Наблюдаемость при этом работает: соседние классы в том же канале дали 11 флагов с деталями. + +### 4.6 Путь content-лейблов = no-op — ✅ ПОДТВЕРЖДЁН + +`SELECT count(*) FROM checkpoints WHERE model_requested <> model_actual` = **0**. Резолвнутые модели тождественны конфиговым: `draft/deepseek-v4-flash ×20`, `draft/deepseek-v4-pro ×1` (это сконфигурированный `escalate_to`), `edit/deepseek-v4-pro ×16`. В `status --json` полей маршрутизации/лейблов НЕТ ВООБЩЕ (все `omitempty`) ⇒ read-model книги без лейблов байт-идентичен до-паковому, строк проблем маршрутизации ноль. + +### 4.7 Деньги durable — ✅ ПОДТВЕРЖДЁН (по строкам БД) + +| проверка | значение | +|---|---| +| `spend.committed_usd` | 0.11429117 | +| `SUM(checkpoints.cost_usd)` | 0.11429117 (37 строк) | +| `SUM(chunk_status.cost_usd)` | 0.11429117 | +| `reserved` в покое | 0.0 | +| `jobs` | 20/20 `done` | +| доля `finish=stop` | **35/37 = 94.6%** (2 × `length`, оба вытащены ретраем) | +| потолок | $0.114291 из $1.00 = 11.4% | + +Инвариант `committed == SUM(checkpoints)` держится ТОЧНО, резервов в покое нет ⇒ на этом прогоне не осталось ни одной незакрытой резервации. **Честная оговорка о границе доказательства:** «reserve→settle одной транзакцией» этот прогон подтверждает КОСВЕННО (нет рваных состояний после 6 запусков процесса); прямое доказательство атомарности — `kill9_test.go` в CI, живого kill -9 в этом прогоне не делалось. + +**Эхо-класс — см. §7-Д1: сырой факт «1 эхо из 20», отчётная метрика — 0.0%.** + +### 4.8 Слаги и цены живые — ✅ + +`GET /v1/models` DeepSeek до платных вызовов: `['deepseek-v4-flash','deepseek-v4-pro']` — оба слага конфига живы, катовер `deepseek-chat` (24.07) нас не касается. `prices_checked: 2026-07-10` (15 дней при пороге 120). **Ре-чек ToS (D39.32): проверено, изменений нет** (триггеры не наступили). + +### 4.9 Канал WHAT — ✅ ФИЗИЧЕСКИ РАБОТАЕТ, с двумя поправками к тому, как его смотреть + +- `n_banknote_lines` = **71 > 0** ✔ · `truncated` = **0** ✔ · сепаратор в чистом черновике **отсутствует** (срезался) ✔ · `parse_fail` = **1** ✘ (диагноз ниже). +- **Диагноз единственного `parse_fail`:** модель выдала строку `Бам, бам-бам → Тук, тук-тук` — звукоподражание, в `src` НЕТ ни одного ханьца. Парсер отверг её по правилу `hasBankSrcHan` — то есть **отработал КАК ЗАДУМАН**, а негодную строку произвела модель. Флаг сделал свою работу; чинить парсер не надо. + +**Оценка предложений ГЛАЗАМИ (выборка — все 71 строка, ниже 15 характерных):** + +*Годятся как основа канона:* 力量蛊 «гу Силы» · 智慧蛊 «гу Мудрости» (обе — дословно то, что владелец подписал в rerun2 своими руками) · 古月方源 «Гуюэ Фан Юань» · 古月陈博 «Гуюэ Чэньбо» (совпало с подписью rerun2) · 沈嬷嬷 «матушка Шэнь» · 开窍大典 «Церемония открытия апертуры» (дважды одинаково) · 紫府 «Пурпурный дворец» · 华池 «Нефритовый пруд» · 青铜海 «Бронзовое море» · 元石碎块 «осколок первобытного камня» (согласовано с сидовым 元石) · 学堂家老 (три варианта — и это ПОЛЕЗНЫЙ сигнал: терм дрейфует, нужен канон; именно его владелец сегодня и подписал). + +*Шум, который пришлось бы отклонять:* 少年/中年/老年 «юность/средний возраст/старость» · 无 «нет» · 十二分之一 «одна двенадцатая» · 雨声 «звук дождя» · 停息 «прекратиться» · строка-звукоподражание без ханьца. + +**Прямой ответ на вопрос промта:** предложения **годятся как основа канона**, но не как автоматический канон. Полезная доля — примерно 40–45% строк (имена, титулы, гу-термины, топонимы); остальное — обиходная лексика и числовые выражения, которые владелец отклонял бы. Ценность в том, что предложение приходит ВМЕСТЕ с исходником, а альтернатива сегодня — придумывать русский вариант с нуля на голом терме. Починка шва работы стоит. + +--- + +## §6. ГЛАВНОЕ ИЗМЕРЕНИЕ: ОСТАТОК ДЕФЕКТОВ, ЛОЖНЯКИ ОТДЕЛЕНЫ + +Read-only скан ($0) по отгруженному тексту 10 глав. + +### 6.1 Остаток в классах, которые петля ремонта УМЕЕТ чинить + +**`repair_candidates` = 0.** По классам — пусто: `DC1 время` 0 · `латиница-остаток` 0 · `битая словоформа` 0. (Строка в рендере печатается только при >0; проверено по коду `render.go:277` и по `--json`-полю `repair_candidates,omitempty`.) + +> **Вход в решение владельца о включении платной петли (D39.24): на этих 10 главах петля не нашла бы НИ ОДНОЙ работы.** Она бы отработала вхолостую и стоила бы только денег на скан. Это аргумент НЕ включать её сейчас — но аргумент слабый по объёму выборки: 14 единиц, один арм, одна книга. + +### 6.2 Сырой счётчик наблюдаемости — 11 флагов. Каждый разобран ГЛАЗАМИ + +| класс | сырых | реальных | ложняков | почему | +|---|---|---|---|---| +| `dialogue_dash` (смешение стилей речи) | **8** | **0** | **8** | «…»-строки — это ВНУТРЕННИЙ МОНОЛОГ («Становится всё интереснее», — усмехнулся про себя Фан Юань), а «—» — звучащая речь. Это НОРМА русской типографики, а не смешение стилей. Чекер бьёт по законному приёму | +| `percent_scale` (成 как дробь) | **2** | **0** | **2** | «три десятых» = 30%, «сорок четыре сотых» = 44% — **масштаб верен**, отличается только ФОРМА. Заявленный класс («~100× ошибка масштаба») не реализовался | +| `dc2_magnitude` (千万 как «тысячи») | **1** | **0** | **1** | 杀了千万人 — стоковая ГИПЕРБОЛА («погубил тысячи и тысячи людей»), о чём предупреждает комментарий самого чекера (§5-A4) | +| **ИТОГО** | **11** | **0** | **11** | | + +### 6.3 Остаток по каналу памяти (пост-чек) + +9 подтверждённых промахов → после разбора глазами: **1 реальный дрейф канона** (族长 «глава рода» вместо «глава клана») · **4 формальных** (кавычки вокруг литеры класса ×2, «апертур» вместо «апертуры» ×2) · **4 — дыра в данных сида** (нет форм мн. ч. у 蛊虫 и 月兰花). + +### 6.4 ЧИСТЫЙ ОСТАТОК — то, по чему принимается решение + +| | | +|---|---| +| адресуемых петлёй ремонта дефектов | **0** | +| реальных дефектов, найденных детерминированными чекерами | **0 из 11 сигналов** (все 11 — ложняки) | +| реальных дефектов канона, найденных банком | **1** (族长 → род/клан) | +| дефектов, вызванных дырой в ДАННЫХ, а не кодом | **4** (мн. ч. в `decl.forms` сида) | +| структурный KPI | 3.20 предложения на нарративный абзац (обвала в «одно предложение — абзац» нет) | +| эхо / косметический стрип / вырожденные циклы | 0 / 0 / 0 | + +**Честный вывод:** на этой выборке платить за петлю ремонта не за что, а деньги, которые стоило бы потратить, лежат в двух других местах — в чистке ЛОЖНЯКОВ чекеров (иначе цифра «11 флагов» будет вводить в заблуждение на каждом прогоне) и в ДАННЫХ сида (формы мн. ч.). Обе работы $0. + +--- + +## §7. ДЕНЬГИ: ФАКТ ПРОТИВ ОЦЕНКИ + +| статья | оценка промта | факт | вердикт | +|---|---|---|---| +| главный проход (один арм) | $0.10–0.19 | **$0.114291** | попал в вилку | +| — драфт | ≈$0.022 | $0.022205 | попал почти точно | +| — редактура | $0.08–0.15 | $0.092086 | попал | +| проба банкноты | ≈$0.02 | **$0.021688** | попал | +| проба гейта пере-оплаты | ≤$0.01 (моя пре-рег) | **$0.003081** | попал | +| **ВСЕГО за сессию** | — | **$0.139060** | | + +**Где оценка промта оказалась неточна (прямой ответ на вопрос «скажи, где я ошибся»):** нигде по сумме — но вилка была построена как «удвоенный rerun2» и случайно совпала, погасив внутри себя **16% брака**. Из $0.114291 главного прохода **$0.018337 ушло в ПРОВАЛЬНЫЕ попытки**: $0.000638 — эхо-черновик, **$0.017699 — две обрезанные по `max_tokens` попытки редактора** (см. §8-Д2). Без этого брака проход стоил бы ≈$0.096. То есть модель «драфт + редактура» верна, а вот статьи «брак» в ней нет вообще, и на этом прогоне она составила шестую часть счёта. + +--- + +## §8. НАЙДЕННЫЕ ДЕФЕКТЫ (то, ради чего прогон и затевался) + +### Д1 — MEDIUM, наблюдаемость: эхо, вылеченное эскалацией, отчитывается как «эха не было» + +**Сырьё:** `request_log` 18:53:02 — ch3/chunk0 draft, `ok=0`, `err=cjk_artifact`, $0.000638. `chunk_status` той же строки: `disposition=ok`, `escalated=1`, `escalation_model=deepseek-v4-pro`. **`tmctl report`: `echo draft=0 (0.0%)`.** + +**Корень (`internal/pipeline/quality.go:245`):** `draftEcho` считает строки, у которых `chunk_status.flag_reason == cjk_artifact`. Успешная эскалация переписывает строку в `ok` с пустым `flag_reason` ⇒ эхо исчезает из метрики. Доккоммент честно перечисляет случай c-lite (упавший мембер), но случай «эскалация вылечила» в него не попал. + +**Почему это важно:** эхо-рейт — единственный числовой сторож эхо-мины DeepSeek (D18/D19), и он показывает 0.0% там, где живьём было 5% (1 из 20). На книге в 2284 раздела так теряется весь класс. + +**Предлагаемый фикс (в пак-19 или отдельной мелочью):** считать `echo_draft` по `request_log` (attempt-уровень, `err=cjk_artifact`), а не по финальной строке `chunk_status`; либо завести на `chunk_status` durable-флаг «первичная попытка эхнула». Первое дешевле и не трогает схему. + +### Д2 — MEDIUM, деньги: редактор упирается в `max_tokens` = флор модели, и это 15.5% счёта + +**Сырьё:** ch5/0 (`finish=length`, классифицировано `empty`, $0.008706) и ch6/0 (`finish=length`, $0.008993) — оба на `max_tokens=8000`; WARN «stage flagged, regenerating with a larger budget … next_max_tokens=16000»; вторые попытки прошли (ch6/0 отдал 10 951 completion-токен). + +**Корень:** у `deepseek-v4-pro` `capabilities.min_max_tokens: 8000` (флор из D24.3, калиброванный под ЧЕРНОВИК), а `max_output_ratio: 2.2` на edit-единице в 3200 ru-токенов даёт меньше флора ⇒ бюджет ВСЕГДА ровно 8000. При этом у DeepSeek thinking-токены входят в `completion` (`reasoning: subset`), то есть 8000 делятся между размышлением и текстом. Крупная единица не помещается — платим дважды. + +**Предлагаемый фикс:** считать потолок редактора от РАЗМЕРА ЧЕРНОВИКА единицы (вход редактора), а не от исходника, и/или поднять флор редакторской роли. Дешёвая мера до дизайна — поднять `min_max_tokens` в конфиге арма. **Не делал: это правка поведения вне скоупа промта, и она двигает снапшот.** + +### Д3 — LOW, инструментальный: `LOG_LLM_BODIES` обрезает тела на 4096 БАЙТ — банкноту в логах не увидеть + +`internal/obs/logging.go:88`: `llmBodyLogMax = 4096`. Кириллица — 2 байта на символ, перевод чанка — 4–8 КБ, а блок банкноты идёт В КОНЦЕ ответа ⇒ он гарантированно за отсечкой. Способ, предписанный промтом («единственный способ увидеть сами предложения — сырые тела»), **физически не работает**. + +### Д4 — ПОПРАВКА К D39.36 (хорошая новость, меняет цену починки шва) + +D39.36 утверждает: «предложенный перевод не остаётся и в артефактах прогона». **Это не так.** Проверено: `SELECT count(*) FROM checkpoints WHERE response_text LIKE '%⟦TM-BANK-v1⟧%'` = **9** — первичный чекпоинт попытки хранит СЫРОЙ текст ответа вместе с блоком; чистится только ПРОИЗВОДНЫЙ чекпоинт `banknote_export` (9 строк, во всех блока нет), который и читают редактор с экспортом. + +⇒ выбрасывается не текст, а **распарсенная структура** (`_, flags = parseBanknote(...)`, `banknote.go:177`). Практическое следствие: чинить шов можно, **не пере-прогоняя ничего** — dst за любой уже сделанный прогон с включённым каналом восстановим из БД. Именно так я и достал 71 строку для оценки в §5-4.9. + +### Д5 — свойство контракта (не баг): майнинг-стоп очищается РАУНДАМИ + +Подпись 学堂 расчехлила 学堂家老 (второй стоп, второй заход владельца). На книге число раундов заранее неизвестно. Ложится прямо в развилку D39.36 («опциональный ОК вместо жёсткого стопа») как дополнительный аргумент: сегодня цена стопа — не одна пауза, а неизвестное их число. + +### Д6 — данные, не код: в сиде нет форм МНОЖЕСТВЕННОГО числа + +4 из 9 промахов пост-чека — «гу-червей», «гу-червям», «лунных орхидей»: перевод верен, а `decl.forms` перечисляет только единственное число. Дешёвая правка данных, которая уберёт треть шума пост-чека. **Не делал: сид — зона владельца.** + +### Д7 — качество чекеров: 11 сигналов из 11 — ложные (см. §6.2) + +Самый крупный класс (`dialogue_dash`, 8 из 11) systematically бьёт по ЗАКОННОЙ русской типографике «мысль в «…» против речи в «—»». Пока это не исправлено, книжная цифра «N стилевых флагов» не является входом ни в какое решение. + +--- + +## §9. ПРЕ-РЕГИСТРАЦИОННЫЕ КРИТЕРИИ: ЧТО СРАБОТАЛО + +Критерии из §0.3 применяю дословно, БЕЗ переопределения задним числом. + +| # | статус | по факту | +|---|---|---| +| П1 стоимость > $0.40 | **не сработал** | $0.114291 | +| П2 потолок / `committed ≠ SUM(checkpoints)` | **не сработал** | равенство точное, потолок 11.4% | +| **П3 эхо-класс на черновике > 0** | **СРАБОТАЛ** | 1 из 20 драфт-чанков (5%) отдал `cjk_artifact` при thinking-ON | +| П4 гейт не отказал / отказал после резервации | **не сработал** | отказал трижды, ничего не тронув | +| П5 стоп не остановил / не очистился | **не сработал** | остановился и очистился (за два раунда) | +| П6 гард молчит при реальном дрейфе | **не сработал** | ноль перепроверен независимо: дрейфа нет | +| П7 модель ≠ конфиговой / строки маршрутизации | **не сработал** | 0 расхождений, 0 строк | +| **П8 `parse_fail > 0`** | **СРАБОТАЛ** | 1 чанк — строка без ханьца в `src` | +| П9 остаток без отделения ложняков | **не сработал** | ложняки отделены поимённо (§6.2) | +| П10 потеря/порча данных | **не сработал** | бэкап цел, БД мигрировала v9→v10 штатно, рваных состояний нет | + +**Разбор двух сработавших — без смягчения формулировки:** + +- **П3 сработал, и я не переопределяю его задним числом.** Эхо DeepSeek живо при thinking-ON: 5% на этом срезе (D18 мерил 25% на приёмочной книге — порядок тот же). Механизм ответа отработал ровно как ратифицирован: класс распознан, single-hop эскалация на `deepseek-v4-pro` вылечила чанк, книга не пострадала. Но факт остаётся фактом: **эхо-мина не «закрыта», она компенсируется деньгами** — и, как показывает Д1, компенсируется НЕВИДИМО для отчёта. +- **П8 сработал, и его диагноз меняет адресата, а не статус.** Парсер отверг строку `Бам, бам-бам→Тук, тук-тук`, у которой в `src` нет ни одного ханьца, — то есть исполнил свой контракт. Негодную строку выдала МОДЕЛЬ. Формулировка моего же критерия («парсер не принимает то, что модель выдаёт») оказалась неточной: она предполагала вину парсера. Критерий сработал, дефект — на стороне промпта/модели, и лечится строкой инструкции («src обязан быть исходным написанием»). + +--- + +## §10. ДЕВИАЦИИ ОТ ПРОМТА (отдельным разделом, как требует приёмка) + +1. **Промт: «LOG_LLM_BODIES включить — записи парсера код выбрасывает, поэтому единственный способ увидеть САМИ предложения — сырые тела».** Включил (`LOG_LLM_BODIES=1 LOG_LEVEL=debug`), но метод не сработал: тела режутся на 4096 байт и банкнота в хвосте не попадает в лог (Д3). **Девиация:** предложения извлечены из `checkpoints.response_text` первичных попыток — сырьё более прямое, чем лог. Предпосылка промта заодно опровергнута (Д4). +2. **Промт: «проба банкноты — ОТДЕЛЬНЫЙ проект, только волна черновика».** Исполнено буквально. Дополнительно завёл **третий** микро-проект (`minirun-rebill/`, одна глава, только драфт) — промт этого не предусматривал, но требовал «проход с флагом» у rebill-гейта, а на главном проекте это стоило бы второй оплаты всего прогона ($0.114). Микро-проект дал тот же лег за $0.003. +3. **Пар-слой и промпты.** rerun2 прописывал `prompt_override` и inline-`segmentation`; я вместо этого положил в проект КОПИЮ ратифицированного `pairs/zh-ru.yaml` с абсолютным `prompts_root` и гоню промпты КОНВЕНЦИЕЙ (`prompts/<пара>/<роль>.md`). Пар-данные сверены с репозиторным файлом: не-комментарные строки идентичны. Мотив — проверить штатный шов слоя 2, а не обходить его. +4. **`escalation.budget_usd: 0.30`** (rerun2 держал 3.0). Осознанно: на 10 главах бюджет должен позволять эскалации, но не финансировать их десятками. +5. **Второй арм (glm-5) НЕ гонялся** — промт разрешает только по отдельному слову владельца. Не спрашивал. +6. **Владельцу задано ДВА вопроса вместо одного** — майнинг-стоп очистился раундами (Д5). Первый: 学堂 (владелец рассмотрел «академию», подписал «школа» после того, как я принёс фактуру из исходника). Второй: 学堂家老 → «старейшина школы». +7. **Сессия НЕ коммитила** — ни код, ни артефакты прогона (промт). Рабочее дерево бэкенда содержит гигиену ШАГА 0 незакоммиченной; книга и производные — вне git. + +--- + +## §11. ЧТО ПРОГОН НЕ ПРОВЕРИЛ (честнее списка успехов) + +- **Качество перевода.** Судья не звался (D39.32). Всё, что сказано выше про текст, — детерминированные сигналы и мои глаза на выборке, а не вердикт о читаемости. Планка «≤2 претензии владельца» этим прогоном НЕ адресовалась. +- **Масштаб.** 10 глав из 2284 разделов. Ни каденс волн, ни поведение потолков/ETA на сотнях глав, ни деградация банка на длинной книге не проверялись. Цифра «остаток = 0» — про 14 единиц, а не про книгу. +- **Второй арм и итерация №2.** dspro-vs-glm не сравнивались; вопрос «кто редактор» этим прогоном не двигался. +- **Петля ремонта.** Осталась `enabled: false`. Мы измерили, СКОЛЬКО работы она нашла бы (ноль), но не проверили, что она делает эту работу правильно. +- **Атомарность reserve→settle под падением.** Живого `kill -9` не делал; инвариант держится косвенно (нет рваных состояний) и прямо — только тестом в CI. +- **Лейбл-путь под ЛЕЙБЛОМ.** Доказано, что книга БЕЗ лейблов идёт байт-идентично; путь помеченной книги (маршрутизация, `label_models`, fail-closed) живьём не гонялся — помеченных книг нет. +- **Канал B / 18+.** Не трогались (промт запрещает). +- **Эскалация дальше первого хопа.** Случился ровно один хоп и он ответил; поведение цепочки при отказе ПЕРВОГО хопа (`escalation_model` на отказавшем хопе — кейс, ради которого пере-капчерился golden в паке-18) живьём не воспроизводилось. +- **Резюм после падения посреди волны.** Все три остановки были штатными (майнинг-стоп, отказ гейта). Аварийного прерывания не было. +- **ja/en-пары.** Инвариант общности §0 проверялся кодом и тестами (синтетический «второй пак»), но не живой книгой на другой паре. +- **Банкнота в шиппинг-форме.** Проверен канал, а не продукт: dst по-прежнему выбрасывается кодом, майнинг-стоп по-прежнему отдаёт голые термы. Проба доказала, что чинить есть что и есть из чего. + +--- + +## §12. ЧТО ЛОЖИТСЯ НА ВЛАДЕЛЬЦА / В ОЧЕРЕДЬ + +1. **Решение о платной петле ремонта (D39.24):** чистый остаток в её классах — **0**. Моя рекомендация — НЕ включать сейчас; вернуться к вопросу, когда появится выборка больше 14 единиц или когда классы петли расширятся. +2. **Шов банкноты (развилка D39.36):** проба говорит «канал работает, предложения годятся как основа канона (~40–45% полезных), и dst НЕ потерян — он в БД». Починка дешевле, чем считалось: пере-прогон не нужен. +3. **Д1 (эхо-метрика слепа к вылеченному эскалацией эху)** — фикс дешёвый, но это правка кода: в пак-19 или отдельной мелочью. +4. **Д2 (редакторский `max_tokens` = флор модели, 15.5% денег в брак)** — требует решения о том, откуда считать потолок редактора; двигает снапшот, поэтому едет с ближайшим resnapshot. +5. **Д6 (нет форм мн. ч. в сиде)** и **族长 → род/клан** — правки ДАННЫХ сида, зона владельца, $0. +6. **Д7 (11 ложняков из 11)** — чистка чекеров: `dialogue_dash` не должен считать «мысль в «…»» смешением стилей; `percent_scale` не должен называть верный масштаб ошибкой масштаба. +7. **Сдвинутая ось `langpack_version`** (`a7d4be2c0465` → `a28ed743c99c`) — при следующем прогоне СТЕНДОВЫХ книг это часть уже состоявшегося расхождения D39.35, отдельной оплаты не создаёт. + +--- + +## §13. АРТЕФАКТЫ (для приёмки по сырью и ре-рана манифеста) + +| артефакт | путь | +|---|---| +| главный проект | `/home/ubuntu/books/gu-zhenren/minirun/` — `book.yaml`, `pipeline.yaml`, `pairs/zh-ru.yaml`, `guzhenren-minirun.db` | +| логи главного прохода | `run-01-draft.log` (стоп 1) · `run-02-edit.log` (стоп 2) · `run-03-edit.log` (edit-волна) · `run-04-rebill-refusal.log` | +| подписи | `mined-delta.yaml` (2 терма) · `mined-rejects.yaml` (пусто) · `guzhenren-minirun.db.mined-signature.yaml` | +| проекции | `status-final.txt` · `status-final.json` · `report-final.txt` · `export.json` · `export.txt` | +| проба банкноты | `/home/ubuntu/books/gu-zhenren/minirun-banknote/` — `run-banknote.log`, `guzhenren-banknote.db`, `export-banknote.txt`, `prompts/zh-ru/translator.md` (копия с блоком) | +| проба rebill-гейта | `/home/ubuntu/books/gu-zhenren/minirun-rebill/` — `run-01-baseline.log` … `run-05-accept.log`, `guzhenren-rebill.db` | +| бэкап фактуры rerun2 | `/home/ubuntu/books/gu-zhenren/backup-pre-minirun-2026-07-25/` | +| код гигиены (НЕ закоммичен) | `backend/`: `configs/pipeline-*.yaml`, `configs/langpacks/zh-ru/dc-checkers.txt`, `internal/lang/langpack.go`, `internal/checks/{checkers.go,cheapgates.go}`, `internal/pipeline/status.go`, `cmd/tmctl/render.go` + 3 новых тест-файла | + +**Ре-ран манифеста приёмки (всё $0):** +```bash +cd backend && go build ./... && go vet ./... && go test ./... -race +sha256sum internal/pipeline/testdata/golden/capture.golden # 211c9013…089045d6 == HEAD +grep -rh '^func Test' --include=*_test.go . | wc -l # 483 (было 475, удалено 0) +tmctl status --config /home/ubuntu/books/gu-zhenren/minirun/book.yaml +tmctl report --config /home/ubuntu/books/gu-zhenren/minirun/book.yaml +tmctl export --config /home/ubuntu/books/gu-zhenren/minirun/book.yaml --plaintext +``` + +--- + +## §14. ФИКС-ПАК ПО ИТОГАМ ПРОГОНА (та же сессия, санкция владельца: «исследование → правки → ревью») + +Четыре дефекта починены, **два кандидата на починку сознательно ОТКЛОНЕНЫ по итогам исследования** — и это главный результат раздела: правка, умершая о ратифицированную фикстуру, дешевле правки, которая её тихо переписала. + +### 14.1 Что построено + +| # | Фикс | Форма | Сдвигает снапшот? | +|---|---|---|---| +| Д1 | `chunk_status.first_flag_reason` (миграция **v11**) + `echo_draft` считает эхо, **вылеченное эскалацией**; новое поле `echo_draft_recovered` держит число честным в обе стороны | код + схема | нет | +| Д4 | `retrieval_state.banknote_detail` (та же v11) — распарсенные записи банкноты durable; майнинг-стоп джойнит их в подписную карту: `dst` = самое частое предложение, альтернативы с весами в note, **`status: auto` НЕ трогается** | код + схема | нет | +| Д7a | `dialogue_dash` больше не считает смешением стилей строку, чья атрибуция — глагол ВНУТРЕННЕЙ речи (данные таргета `inner_speech`) | код + target-данные | **да** (`CheapGateVersion` v4→v5) | +| Д3 | `LOG_LLM_BODIES`: кап 4096→32768 байт и обрезка **СЕРЕДИНЫ**, а не хвоста — блок банкноты живёт в хвосте ответа | код | нет | +| Д2 | флор `deepseek-v4-pro` 8000→**16000** по замеру (перебор бюджета бесплатен, недобор стоит целой генерации) | данные `models.yaml` | **да** (`max_tokens` ∈ request_hash) | + +**Ключевая находка исследования, удешевившая Д4:** сырой текст банкноты УЖЕ durable в чекпоинте, а resume его пере-парсит (`stagerun.go` resume-path) — поэтому новая таблица не нужна, хватило четвёртой `*_detail`-колонки рядом с тремя такими же. + +**Граница безопасности Д4 названа явно:** предложенный dst приезжает как ЕВИДЕНС на терме `status: auto`. Trust-gate (D39.2) в инъекцию пускает только CONFIRMED, значит «модель предложила» не может само стать «книга использует». Развилки D39.36 п.2/п.3 (опциональный ОК, ослабление trust-gate) НЕ трогались — это продуктовое решение владельца. + +### 14.2 Что ОТКЛОНЕНО и почему (обе правки были написаны, прогнаны и откачены) + +- **`percent_scale`.** Мой отчёт назвал 2 живых срабатывания ложняками (масштаб верен, форма — дробь). Правка «стрелять только на ЦИФРОВОЙ форме» уронила ратифицированную фикстуру `«шесть и шесть десятых»` = 6.6 для 六成六 — это НАСТОЯЩАЯ 100× ошибка, и она тоже прописана словами. То есть цифро-словесная граница не разделяет классы; разделяет ЗНАЧЕНИЕ, а для него нужен разбор числительных таргета в данные. Правка откачена, дизайн записан. +- **`dc2` 千万.** Живое срабатывание (杀了千万人 → «тысячи и тысячи людей») **шейпом тождественно ратифицированному ИСТИННОМУ** (千万生灵 → «тысячи жизней»). Вето по коэффициенту убивало оба. Вывод, который правит мой же §6.2: это не дефект реализации, а **принятая неоднозначность класса**, о которой предупреждает его собственный комментарий (§5-A4). Правка откачена. + +⇒ чистый остаток ложняков после фикс-пака: **8 из 11 закрыты (dialogue_dash), 3 остаются осознанно** — 2 percent_scale (ждут числительных в данных) и 1 dc2 (offline неразделим). + +### 14.3 Ревью исполнением + +| проверка | результат | +|---|---| +| build / vet / gofmt | чисто (единственная жалоба `internal/llm/llm.go` воспроизводится на HEAD — файл не трогался) | +| `go test ./... -race` | 15 пакетов green, 0 FAIL | +| **golden** | пере-капчер **САНКЦИОНИРОВАННЫЙ и доказанный**: 172 сырых строки сдвинулись, но **МАСКИРОВАННЫЙ структурный дифф (хеши→``, версия→``) ПУСТ** ⇒ двигались только версии и производные от них хеши, ни один вердикт/текст/стоимость не изменились. `211c9013…089045d6` → `1165e3a9…2132ea82` | +| ценз ИМЁН тестов | 483 → **491, удалено 0** | +| **мутации** | **6 мутаций — 6 красных**: echo-метрика читает только вердикт · `first_flag_reason` пишется и когда он И ЕСТЬ вердикт (двойной счёт) · записи банкноты снова выбрасываются · подписная карта перестаёт нести dst · дискриминатор внутренней речи выключен · обрезка лога снова хвостовая | +| **проверка на ЖИВЫХ данных прогона** | реальные 9 блоков банкноты из БД пробы прогнаны через НОВЫЙ шов: 71 строка, **67 различных ключей**, 2 терма с конкурирующими вариантами. Среди них — **学堂家老**, тот самый, который владелец сегодня подписывал вслепую: под фиксом карта пришла бы с `dst: «старейшина школы»` (+ «старейшина-наставник», «учитель-старейшина» в note) — **ровно тем вариантом, который владелец и выбрал** | + +**Честная граница этой проверки:** $0-реплей ПОЛНОГО прогона через новый код невозможен — правки Д7a/Д2 двигают снапшот, и job-пиннинг корректно отказал («already-paid checkpoints become invalid»). Это не дефект, а работающая дисциплина; поэтому живьём проверены ПУРЕ-функции нового шва на реальных ответах, а сквозняк — тремя тестами через настоящий драйвер. + +### 14.3-bis МОЯ ОШИБКА В ХОДЕ РЕВЬЮ: непреднамеренная трата $0.046668 + +Проверяя, что миграция **v9→v11** чисто ложится на БД стендовой схемы, я собрал `book.yaml`, указывающий на КОПИЮ `rerun2`-БД, но оставил в нём источник и пайплайн мини-прогона. Для движка это была НОВАЯ книга (строк `guzhenren-minirun` в той БД нет) — и он честно начал переводить. Прогон шёл ~5 минут до моего таймаута. + +**Точный счёт (по строкам БД-копии):** `guzhenren-minirun / 2026-07-25 → $0.046668`, 31 живой вызов. Деньги потрачены зря. + +**Что НЕ пострадало (проверено):** оригинальная `rerun2/guzhenren-rerun2.db` — схема по-прежнему **v9**, единственный `book_id` = `guzhenren-rerun2`, spend не изменился · бэкап 25.07 цел · БД мини-прогона и пробы банкноты нетронуты (v10, свои суммы). Трата целиком осталась на выбрасываемой копии в скретчпаде. + +**Что проверка всё же дала:** миграция v9→v11 применилась чисто (`schema_version` 1…11, обе колонки на месте, старые строки целы) — ради этого она и затевалась; и в логе видно `max_tokens=16000` на живом вызове редактора, то есть фикс Д2 подтверждён исполнением. + +**Правильный способ, которым надо было это делать:** открывать стор на запись командой, которая гарантированно не доходит до провайдера (или на БД, где книга уже завершена), а не собирать конфиг «чужая БД + чужой источник». + +**Итог по деньгам сессии:** $0.139060 (прогон + проба + гейт) **+ $0.046668 брака ревью = $0.185728.** + +### 14.4 Цена следующего прогона + +Две оси версий сдвинуты осознанно: `style_check_version` (v4→v5) и `max_tokens` через флор dspro. Обе входят в снапшот ⇒ **следующий платный прогон стендовой книги пере-оплачивается целиком** и потребует `--resnapshot --accept-rebill`. На сегодня это ничего не стоит: мини-прогон закрыт, а стендовый снапшот и так разошёлся (D39.35). Экономия от Д2 (≈15% счёта на браке) окупает пере-пин на первом же прогоне. + +--- + +## §15. СКВОЗНАЯ ПРОВЕРКА БЭКЕНДА ПОСЛЕ ФИКС-ПАКА (26.07, по вопросу владельца «а ты протестировал?») + +Вопрос был законный: после фиксов я прогнал ТЕСТЫ, но не гонял БЭКЕНД. Прогнал — два проверочных прогона, **$0.049**, и они нашли то, чего тесты не могли. + +### 15.1 Что проверено живьём + +Проект `minirun-verify` (главы 1–2) и `minirun-verify2` (главы 4–5, выбраны потому, что там ЕСТЬ майнинг-кандидат), оба с **включённой банкнотой** — иначе главный фикс не отрабатывает. + +| узел | результат | +|---|---| +| миграция **v11** на свежей БД | схема 1…11, обе новые колонки на месте | +| ingest → чанкер | 2 главы → 4 драфт-чанка → 3 edit-единицы | +| драфт-волна с банкнотой | `n_banknote_lines` 7 и 3, **`banknote_detail` записан живьём** (396 и 193 байта) | +| **майнинг-стоп** | выстрелил, 2 терма, подписная карта записана | +| подпись → резюм | стоп очистился, драфт резюмнулся за $0, edit-волна прошла 3/3 | +| санитайзер | **поймал РЕАЛЬНЫЙ дефект**: `mixed Cyrillic and Latin in one word (homoglyph): лицó` | +| **`RegressionGuard`** | **ПЕРВОЕ ЖИВОЕ СРАБАТЫВАНИЕ**: `number_drift [66]` — черновик писал «66%», редактор «шестьдесят шесть процентов». Значение цело, цифра ушла ⇒ это ровно тот ложняк, который предсказан в его собственной доке. Гейт, невключённый до 25.07, теперь даёт сигнал | +| банк памяти | 56 попаданий + 12 sticky, 2 промаха пост-чека | +| экспорт | 31 715 байт, сепаратор банкноты в отгруженном тексте **0 раз** | +| деньги | $0.032764 из потолка $0.30, `finish=stop` на всех вызовах | + +### 15.2 Что нашёл ТОЛЬКО сквозной прогон + +**(1) Поправка к тому, что я сказал владельцу вчера.** Я утверждал: «под фиксом подписная карта пришла бы с dst «старейшина школы» — ровно тем, что владелец выбрал». **Верно только для ВТОРОЙ подписи.** Проверка по сырью пробы: банкнота НИКОГДА не предлагала `学堂` отдельно — только составной `学堂家老`. Значит раунд 1 всё равно пришёл бы голым. + +**(2) Структурный разрыв, из-за которого фикс даёт меньше, чем кажется.** На `verify2` стоп выстрелил с `terms_with_proposed_dst=0`: майнер предложил 花海 и 陈博, банкнота — 古月方源, 学堂家老, 开窍大典, 力量蛊, 智慧蛊, 灵泉. **Пересечение НУЛЕВОЕ.** Механизм джойна исправен (ключи нормализуются верно), но два канала систематически выбирают РАЗНЫЕ популяции: майнер — частотные name/place/title (freq ≥ 5), банкнота — то, что модель считает новой терминологией. Итог: на 10 главах шов помог бы в 1 подписном раунде из 2, на 2 главах — в 0 из 1. **Следующая работа по банкноте — не «замкнуть шов» (замкнут), а заставить WHAT покрывать кандидатов WHICH** (например, инъекция списка mined-кандидатов в промпт переводчика). + +**(3) Полярность дискриминатора чекера была выбрана неверно — исправлено по живым данным.** Первая версия спрашивала «это МЫСЛЬ?» по списку глаголов мысли. Живой прогон за минуты выдал «понимал» и «размышлял», которых в списке нет: список мыслей не имеет естественной границы, и каждый промах = ложный флаг на хорошей прозе. Перевернул: «это РЕЧЬ?» по закрытому списку речевых глаголов + вето `про себя`/`мысленно` (нашлось на «пробормотал про себя»). Промах теперь = МОЛЧАНИЕ, а не флаг — та ошибка, которую и требует ратифицированный уклон precision-over-recall. +**Замер по реальным экспортам, по чанкам:** мини-прогон 10 глав **8 → 0**, проверка 2 глав **1 → 0**, при этом настоящее смешение речи («…», — сказал он) по-прежнему флагается. + +**(4) Дыра в моих же тестах, вскрытая мутацией.** Мутация «снять nil/no-data гард» ВЫЖИЛА: ветка «книга без данных таргета» была не покрыта, а без гарда там nil-разыменование. Закрыто тестом `TestLintDialogueDashInertWithoutTargetData` (nil-спека и спека без данных: правило молчит, но его data-independent половина — дефис вместо тире — продолжает работать). Пере-мутация после этого — красная с паникой. + +### 15.3 Ревью финального состояния + +build/vet/gofmt чисты · `-race` 15/15 · **golden пере-капчен ВТОРОЙ раз** (точное имя версии `cheapgate-v5-chevron-speech-attribution`), маскированный структурный дифф снова **ПУСТ** → `281d7025…dd9b99b4` · ценз ИМЁН **475 → 492, удалено 0** · **9 мутаций суммарно — 9 красных** (три последние по новому правилу; одна выжила, вскрыла дыру, дыра закрыта, пере-мутация красная). + +**Деньги проверки: $0.032764 + $0.016091 = $0.048855.** Итог сессии: $0.139060 (прогон) + $0.046668 (мой брак) + $0.048855 (сквозная проверка) = **$0.234583.** +