Land the ten-chapter mini-run with fix pack as D39.37: money verified to the cent from raw ledgers, banknote seam closed live but unpinned, remnant zero in three repair classes
This commit is contained in:
parent
683d0ede34
commit
ad3633af21
41 changed files with 2199 additions and 181 deletions
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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_ratio<TAB>key<TAB>int`). 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 (`msg<TAB>key<TAB>template`, 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}%)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -50,6 +50,10 @@ gates:
|
|||
min_chunk_chars: 500
|
||||
sanitizer:
|
||||
enabled: true
|
||||
regression_guard:
|
||||
# D38.4 п.5 (обязательство, не исполненное к rerun2 — D39.34): наблюдаемость обвала длины
|
||||
# и числового дрейфа на переходе черновик→финал. Никогда не вердикт; флип не двигает снапшот.
|
||||
enabled: true
|
||||
|
||||
escalation:
|
||||
chains:
|
||||
|
|
|
|||
|
|
@ -49,6 +49,10 @@ gates:
|
|||
min_chunk_chars: 500
|
||||
sanitizer:
|
||||
enabled: true
|
||||
regression_guard:
|
||||
# D38.4 п.5 (обязательство, не исполненное к rerun2 — D39.34): наблюдаемость обвала длины
|
||||
# и числового дрейфа на переходе черновик→финал. Никогда не вердикт; флип не двигает снапшот.
|
||||
enabled: true
|
||||
|
||||
escalation:
|
||||
chains:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
155
backend/internal/checks/checkers_pairdata_test.go
Normal file
155
backend/internal/checks/checkers_pairdata_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 все
|
||||
|
|
|
|||
|
|
@ -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_numeral<TAB>rune<TAB>value | ru_hour<TAB>word<TAB>value | register_neg<TAB>lexeme
|
||||
// pattern<TAB>key<TAB>value (VERBATIM) | msg<TAB>key<TAB>template (VERBATIM) | dc_ratio<TAB>key<TAB>int
|
||||
//
|
||||
// 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 `pattern<TAB>key<TAB>value`, got %q", i+1, line)
|
||||
return nil, fmt.Errorf("line %d: %s wants `%s<TAB>key<TAB>value`, 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_neg<TAB>lexeme`, 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_ratio<TAB>key<TAB>value`, 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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
56
backend/internal/obs/logging_truncate_test.go
Normal file
56
backend/internal/obs/logging_truncate_test.go
Normal file
|
|
@ -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 == '<27>' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
225
backend/internal/pipeline/minirun_fixes_test.go
Normal file
225
backend/internal/pipeline/minirun_fixes_test.go
Normal file
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
139
backend/internal/pipeline/regressionguard_live_test.go
Normal file
139
backend/internal/pipeline/regressionguard_live_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
77
backend/internal/pipeline/status_rebill_projection_test.go
Normal file
77
backend/internal/pipeline/status_rebill_projection_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -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 — механизация выпускного качества)
|
||||
|
|
|
|||
|
|
@ -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 держится на уровне кода, но калибровки фактически китайские.
|
||||
|
|
|
|||
|
|
@ -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) **приёмка оркестратора идёт по СЫРЬЮ** — БД, логи, экспорт — и ре-ранит твой манифест; твои выводы принимаются только вместе с фактурой.
|
||||
|
||||
102
docs/archive/reports/MINIRUN_HANDOFF_2026-07-26.md
Normal file
102
docs/archive/reports/MINIRUN_HANDOFF_2026-07-26.md
Normal file
|
|
@ -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 на чужой паре срабатывает до единого цента. Слабое место не там — оно в **измерительном слое**. Чекеры не имеют размеченного набора, поэтому их цифры (включая ту, по которой принимается решение о платной петле) держатся на моём глазомере; калибровки объявлены пар-данными, но фактически везде китайские; а метрики качества измеряют то, что удобно достать, а не то, что решает.
|
||||
|
||||
Если выбирать один хвост из всех перечисленных — я бы взял **размеченный набор для чекеров**. Без него следующий прогон произведёт ещё один набор чисел, которые снова придётся проверять глазами, и снова моими.
|
||||
17
docs/archive/reports/MINIRUN_NOTE_2026-07-26.md
Normal file
17
docs/archive/reports/MINIRUN_NOTE_2026-07-26.md
Normal file
|
|
@ -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).
|
||||
528
docs/archive/reports/MINIRUN_REPORT_2026-07-25.md
Normal file
528
docs/archive/reports/MINIRUN_REPORT_2026-07-25.md
Normal file
|
|
@ -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-файле | шаблоны `msg<TAB>key<TAB>template` в пар-паке, подстановка `{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 сырых строки сдвинулись, но **МАСКИРОВАННЫЙ структурный дифф (хеши→`<HASH>`, версия→`<CHEAPGATE_VERSION>`) ПУСТ** ⇒ двигались только версии и производные от них хеши, ни один вердикт/текст/стоимость не изменились. `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.**
|
||||
|
||||
Loading…
Add table
Reference in a new issue