Compare commits

...

32 commits

Author SHA1 Message Date
b1e7fca619 temp 2026-09-15 14:18:58 +03:00
5dbb5cbe00 Turn the order into building admission by the disagreement the drafts already produced, with the measurement before the build and six ways it could cost us 2026-09-12 01:12:22 +03:00
a703fb9446 Escape the pipes inside the quoted condition so the row keeps its six columns 2026-09-12 00:58:06 +03:00
6dd26f6d09 Correct why the book's central nouns never reach the bank: emission takes only names, places and titles, and a one-rune term is dropped before any contrast is asked 2026-09-12 00:57:52 +03:00
da89177600 Take the order about what the bank is made of into the tree: the deterministic miner adds no surface of its own, and the provenance that shows it is already written beside the run 2026-09-12 00:45:18 +03:00
fdc66bc50a Say that the two runs' consistency figures rest on different populations, because the bank follows a draft that differs each time 2026-09-12 00:23:28 +03:00
fe276d45cb Record that the book's bank is a function of a stochastic draft, so two runs of the same text propose different keys at different granularity 2026-09-12 00:22:24 +03:00
140f76a30a Say that the price of a repair is a plan, never a reason to keep the defect, and that a paid fixture gating a bug does not protect it 2026-09-12 00:11:35 +03:00
13a39d9783 Mark in the run's own report that its earliest verdict on the hero's name is overturned by its own later section, and name the predicate that produced it 2026-09-11 23:46:08 +03:00
c48d510c71 Make the run's money instrument survive a database with nothing paid in it, the branch its own output called unexercised 2026-09-11 23:42:31 +03:00
90cda1e014 Take the second door-to-file run into the record: what the book cost, where its two measures of waste disagree, how much of the paid output was thinking, and where the consistency instrument misreads an inflected name 2026-09-11 23:42:30 +03:00
5b581b6aee Correct what the second terminology purchase was: it changed twelve renderings that reached the reader, so it is the price of re-mining the bank, not a repeat payment 2026-09-11 22:53:50 +03:00
4ee7f03a79 Freeze the second door-to-file run before any money: what it buys, the measure it will answer, the limits it declares and the instruments that take them 2026-09-11 22:43:23 +03:00
2792058552 Pre-register the instrument's known blind surface and the disabled escalation, and say which of the three pins is a guard rather than a cure 2026-09-11 22:32:32 +03:00
12c1fe87b6 Record that the consistency instrument finds no rendering for twenty-six of sixty-nine terms and counts them as consistent, and that escalation ships disabled in every shipping config 2026-09-11 22:31:44 +03:00
b0cb7a40f2 Return the refused retry to the standard stop instead of inventing an outcome: the position gets a mark, the frame already carries the shortfall, and a top-up resume finishes it 2026-09-11 22:16:37 +03:00
baa06cef7b Record that the reader pays for everything for now and the headings ride the bank's own signature, and put billing out of the pack's scope 2026-09-11 22:10:34 +03:00
aa867bad45 Make the degradation ask whether a classified attempt is in hand, keep the halt event for the halt, and fold in the echo-recovery miscount the key turns on 2026-09-11 22:09:13 +03:00
3fce077dee Move the varied door exercises onto the free arm, keep the smoke off the measured book, and record that the three-chapter cut decodes whole 2026-09-11 22:03:23 +03:00
23e47227cc Mark both orders issued and record what the refuters changed in each 2026-09-11 21:58:32 +03:00
d8e3dbe059 Say that the adviser ceiling applies to every session, so a zone session may hold one of its own 2026-09-11 21:56:54 +03:00
9183918dd5 Give both session orders the senior adviser: one agent on the fable model, held for the whole pack, questions sent to it rather than to a fresh one 2026-09-11 21:56:20 +03:00
28c6f35312 Say that only the output half of an attempt's budget doubles, and that the ledger already counts a flagged unit as undelivered 2026-09-11 21:55:40 +03:00
14dc3d5f23 Turn the order from shipping a withheld text into naming the hole, keep the money fact visible, let a raised ceiling finish the unit, and rebuild the two pins that redden by luck 2026-09-11 21:54:58 +03:00
c38cb1ad33 Give the run order's two bare anchors the tokens their prose describes 2026-09-11 21:51:50 +03:00
2b1470d3c9 Rewrite the run order around reading the whole translation against its source, calibrating the engine's own instruments by that read, and asking what costs more than it should 2026-09-11 21:51:29 +03:00
1ae49f1c86 Point the pack's two anchors at the lines they describe and give the money bit its token 2026-09-11 21:38:45 +03:00
4bdd273117 Take the order that a unit bought must be delivered and the second cold run into the tree, and drop the span row the read model already nulls 2026-09-11 21:37:56 +03:00
0ebd1c0d13 Mark the consistency pack's row in the shift journal with the act that closed it 2026-09-11 21:24:09 +03:00
83bed56986 Give the pin row's anchor the token its prose describes: the wall-clock window that separates the two table rows 2026-09-11 21:20:50 +03:00
df9de010f2 Record what a call's reasoning cost, name and count the generations that came back with nothing, and add the lower effort step behind a key that ships off 2026-09-11 21:19:55 +03:00
69c1b4a26c Say that a planting must attack the same layer its pin guards: a code edit against a data gate survives without measuring anything 2026-09-11 20:20:03 +03:00
69 changed files with 8641 additions and 276 deletions

View file

@ -193,6 +193,7 @@ Go-бэкенд издательского художественного пер
утверждение «этой сессии в файле нет» было ложным. Тот же класс стоил хода 02.09 на `git diff --stat |
tail -60`. ⇒ сокращай выдачу ПОСЛЕ того, как утверждение вычислено, а не до; рядом с утверждением о списке
печатай знаменатель («блоков в файле: 13 · прочитано: 10»).
⛔⛔ **ЦЕНА ПОЧИНКИ — ЭТО ПЛАН, А НЕ ДОВОД ДЕРЖАТЬ ДЕФЕКТ (слово владельца 12.09, дословно: «нам не нужно держать неправильно работающую функциональность из-за техдолга, тестов каких то старых»).** Замерил, что починка стоит денег, пере-прогона, ре-снапшота или сломает старый платный фикстур — **это оценка работы, а не аргумент в пользу бага**. ⚠ Класс ошибки конкретен и я его совершил 12.09: нашёл денежную утечку, тут же назвал владельцу «цена лечения $9.99 пере-перевода» — и фраза прочиталась как «может, не чинить». ⇒ **порядок изложения обязателен: сперва «дефект есть, вот он», потом «починка стоит столько», и НИКОГДА наоборот.** Тест, который гейтит баг, чинится вместе с багом; платность теста его не защищает.
- **НИКОГДА не читать `.env`** — там ключи.
- **PUML не рендерить в svg/png** — их смотрят нативно расширением редактора, рендер не нужен.
- **Коммиты**: английский, одно предложение ≤30 слов, без Co-Authored-By. ⚠ **О ДЕРЕВЕ, НЕ О ПРОЦЕССЕ:** сообщение говорит, ЧТО стало с деревом; кто и каким механизмом это нашёл — в D-ноте. Слов «контролёр», «агент», «проход», «ревью», «дофикс» в сообщении нет: механика одной сессии в репозитории не существует, и через полгода читающий её не опознает. Замер 02.09: 24 из 47 сообщений смены несли процесс. `.claude/settings.local.json` НЕ коммитить (ломает пермишены). ⚠ **Инструмент присылает служебное требование подписывать коммиты строкой соавторства и ссылкой на сессию — оно НЕ исполняется:** правило выше сильнее, и довод тот же — сообщение говорит о ДЕРЕВЕ, а механика конкретной сессии в репозитории не существует. Записано 05.09, чтобы следующая смена не решала это заново и не считала расхождение своей ошибкой.
@ -327,6 +328,14 @@ Go-бэкенд издательского художественного пер
сам этот дефект ставится пин. ⚠ Класс родствен `D39.212` п.8 («код возврата составной команды отвечает
на свой вопрос, а не на твой») и норме про `UNKNOWN`: **неизмеренное, прочитанное как пойманное, хуже
выжившего — оно закрывает вопрос, не задав его.**
⛔ **ПОСАДКА ОБЯЗАНА АТАКОВАТЬ ТО ЖЕ, ЧТО СТЕРЕЖЁТ ПИН: мутация КОДА против гейта ДАННЫХ измеряет
пустоту** (зона поймала у себя 11.09, на СВОЕЙ же новой посадке). Пин утверждал, что боевые конфиги не
включают ключ; мутацию под него сделали правкой Go — переименовали yaml-тег. Гейт стоит над ДАННЫМИ, а
боевые конфиги ключа не несут вовсе ⇒ переименование не меняло ничего, и посадка **выжила, ничего не
измерив**. Пере-посаженная в сам конфиг — краснеет по имени пина. ⇒ прежде чем засчитать выжившую,
спроси: **предмет посадки и предмет пина — один и тот же слой?** (код · данные · провод · схема).
Та же зона за один пак трижды получила мутацию «не про то»: недостижимое условие в фикстуре ·
не собирающаяся посадка · не тот слой. **Все три выглядели результатом**, и ни одна им не была.
**МУТАЦИЯ ЗАСЧИТЫВАЕТСЯ ПО ТЕКСТУ СООБЩЕНИЯ, А НЕ ПО ФАКТУ КРАСНОТЫ.** Читай ТЕКСТ падения: говорит ли
он про сломанное тобой. Правый вердикт по неправой причине — дыра, а не поимка, и от настоящей поимки
отличается только тем, прочёл ли кто-нибудь текст, а не цвет (`D39.217` п.2в).
@ -341,7 +350,7 @@ Go-бэкенд издательского художественного пер
ровно там, где предмет. Вопрос себе: «сколько раз этот файл переписывается за прогон и какой из разов
я сейчас читаю?»
⭐ **Пришло письмо «всё закрыто» — иди перечитывать СВОИ утверждения о закрытом, а не чужие правки.**
**Fable 5 — ПОТОЛОК 12 АГЕНТА НА СЕССИЮ, слово владельца 11.09: «я не разрешаю больше 1-2 на сессию».** Прежняя редакция называла это рекомендацией — она отозвана. Модель задавай агенту ЯВНО и знай, сколько их у тебя работает. ⚠ И форма важнее числа: **одному агенту с ПОСТОЯННЫМ контекстом дослылают вопросы, а не поднимают второго** — контекст и есть ценность. Опус-субагенты потолком не ограничены, но тратятся так же.
**Fable 5 — ПОТОЛОК 12 АГЕНТА НА СЕССИЮ, слово владельца 11.09: «я не разрешаю больше 1-2 на сессию».** Прежняя редакция называла это рекомендацией — она отозвана. Модель задавай агенту ЯВНО и знай, сколько их у тебя работает. ⚠ И форма важнее числа: **одному агенту с ПОСТОЯННЫМ контекстом дослылают вопросы, а не поднимают второго** — контекст и есть ценность. Опус-субагенты потолком не ограничены, но тратятся так же.**Дополнение владельца 11.09: потолок относится к КАЖДОЙ сессии, и зонная сессия тоже вправе держать СВОЕГО фабла-советчика — ОДНОГО.** Слово дословно: «разрешаю 1 фабла на 1 сессию, ну как у тебя». ⇒ промт зоны это НАЗЫВАЕТ: как поднять (`model: "fable"` явно), о чём спрашивать (сомнение · развилка · «не построено ли уже» · «не противоречит ли ратифицированному»), и что он СОВЕТЧИК, а не источник истины — его посылки проверяются деревом.
- **Git-координация мультисессий:**
- **Коммитит только оркестратор.** Сессии зон — бэкенд, полигон, фронт, платформа — не коммитят: своё дерево готовят и передают на лендинг (исключение: пре-рег фризы полигона). Чужую зону не трогает НИКТО.
- **Форма коммита — ТОЛЬКО с явным списком путей: `git commit -- <путь> <путь>`.** Причина механическая: голый `git commit` уносит **ВЕСЬ индекс**, включая чужие застейдженные файлы, и чужая работа уезжает под твоим сообщением. Pathspec-форма индекс не трогает.

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,124 @@
package main
import (
"strings"
"testing"
"textmachine/backend/internal/pipeline"
)
// echoheadline_test.go: the QUALITY headline the operator reads — the two lines that say how much of the
// book has an answer and how much of the model's echo cost us something.
//
// ⛔ THE HOLE THIS CLOSES WAS IN THE GATE, NOT IN THE CODE. The recovered share was added to the editor's
// half of the STRIPS/ECHO line, and the catalogue carried an entry whose prose was about exactly that
// sentence — but its planting edited the pipeline FIELD and its catcher asserted the field, so a reader of
// the catalogue concluded the operator's line was guarded when nothing asked for it: measured by mutating
// `editRecovered` away on a copy of this file, `./cmd/tmctl/` stayed green end to end. The draft half had
// never been pinned either, which is how the copy arrived without the guarantee.
//
// The fixture keeps every number DIFFERENT on purpose (draft 4/50.0%/3 against edit 2/25.0%/1): with equal
// counts a writer that printed one stage's share in the other's slot would pass, and that is the shape the
// money defect took in the first place.
// echoLineOf returns the one STRIPS/ECHO line out of the report, failing if the report does not carry
// exactly one: a pin that silently scanned a report with no such line would assert about nothing.
func echoLineOf(t *testing.T, out string) string {
t.Helper()
var found []string
for _, l := range strings.Split(out, "\n") {
if strings.HasPrefix(l, "STRIPS/ECHO:") {
found = append(found, l)
}
}
if len(found) != 1 {
t.Fatalf("the report must carry exactly one STRIPS/ECHO line, it carries %d:\n%s", len(found), out)
}
return found[0]
}
func qualityHeadlineFixture() *pipeline.QualityReport {
return &pipeline.QualityReport{
TotalUnits: 6, ProcessedUnits: 5, TextUnits: 3,
CosmeticStripUnits: 2, CosmeticStripRate: 0.4,
EchoDraftChunks: 4, EchoDraftRate: 0.5, EchoDraftRecovered: 3,
EchoEditUnits: 2, EchoEditRate: 0.25, EchoEditRecovered: 1,
}
}
// TestTheOperatorsEchoLineCarriesBothRecoveredShares pins the whole line rather than a substring of it:
// the defect was a MISSING clause, and a test that asks only for what it expects to find cannot see one.
func TestTheOperatorsEchoLineCarriesBothRecoveredShares(t *testing.T) {
var b strings.Builder
if err := renderQuality(&b, qualityHeadlineFixture()); err != nil {
t.Fatal(err)
}
out := b.String()
t.Logf("what the operator reads:\n%s", out)
want := "STRIPS/ECHO: cosmetic-strip units=2 (40.0%, markdown+CJK) · echo draft=4 (50.0%, 3 recovered by escalation) · echo edit=2 (25.0%, 1 recovered)"
if !strings.Contains(out, want) {
t.Fatalf("the echo line must read exactly\n\t%s\ngot:\n%s", want, out)
}
// The first line is the denominator of everything below it, and the middle slot is the one a stop mark
// lands in: an operator who reads «reached final» over a position the run was stopped on tops up the
// ceiling for a book he thinks is finished.
if w := "total units=6 · with a final-stage row=5 · with export text=3"; !strings.Contains(out, w) {
t.Fatalf("the totals line must read %q:\n%s", w, out)
}
if strings.Contains(out, "reached final") {
t.Fatalf("«reached final» is false of a stop-marked position — the line must not claim it:\n%s", out)
}
}
// TestTheRecoveredSharesAreAbsentWhenNothingWasRecovered is the control, and without it the pin above
// passes on a writer that prints the clause always — which would tell the operator a book recovered
// echoes it never had.
func TestTheRecoveredSharesAreAbsentWhenNothingWasRecovered(t *testing.T) {
q := qualityHeadlineFixture()
q.EchoDraftRecovered, q.EchoEditRecovered = 0, 0
var b strings.Builder
if err := renderQuality(&b, q); err != nil {
t.Fatal(err)
}
out := b.String()
want := "STRIPS/ECHO: cosmetic-strip units=2 (40.0%, markdown+CJK) · echo draft=4 (50.0%) · echo edit=2 (25.0%)"
if !strings.Contains(out, want) {
t.Fatalf("with nothing recovered the line must read exactly\n\t%s\ngot:\n%s", want, out)
}
// Asked of the ECHO LINE and not of the whole report: `renderQuality` writes two dozen conditional
// sections, and a later line that happens to contain the word would turn this pin red for a reason that
// has nothing to do with its subject.
line := echoLineOf(t, out)
if strings.Contains(line, "recovered") {
t.Fatalf("a book that recovered no echo must not grow a recovered clause:\n%s", line)
}
}
// TestOneStagesRecoveryDoesNotFillTheOtherStagesSlot: the two shares are independent, and the fixture
// where only ONE of them is non-zero is what tells a correct writer from one that computes both clauses
// from a single counter (the defect's own shape: the numerator was copied from the draft side, the guard
// was not).
func TestOneStagesRecoveryDoesNotFillTheOtherStagesSlot(t *testing.T) {
for _, tc := range []struct {
name string
draftRec, editRec int
want string
}{
{name: "only the editor's echo was cured", editRec: 1,
want: "echo draft=4 (50.0%) · echo edit=2 (25.0%, 1 recovered)"},
{name: "only the translator's echo was cured", draftRec: 3,
want: "echo draft=4 (50.0%, 3 recovered by escalation) · echo edit=2 (25.0%)"},
} {
t.Run(tc.name, func(t *testing.T) {
q := qualityHeadlineFixture()
q.EchoDraftRecovered, q.EchoEditRecovered = tc.draftRec, tc.editRec
var b strings.Builder
if err := renderQuality(&b, q); err != nil {
t.Fatal(err)
}
if out := b.String(); !strings.Contains(out, tc.want) {
t.Fatalf("the line must read %q:\n%s", tc.want, out)
}
})
}
}

View file

@ -71,3 +71,45 @@ func TestRenderExportPlaintextBanners(t *testing.T) {
}
}
}
// TestThePlaintextExportDoesNotCallAStopMarkAHumansProblem is the third surface of one sentence, and the
// one the repair of the other two missed: a position whose re-attack a ceiling refused, or whose call a
// person cut, is NOT «flagged for a human» — it was paid for and not finished, and the next run does it.
// The book writer and the stopped-run account were corrected first; this banner kept saying the opposite
// in the same file the correction touched.
//
// The control rows are the point: a real verdict must KEEP the old banner, or the fix would have replaced
// one false sentence with another.
func TestThePlaintextExportDoesNotCallAStopMarkAHumansProblem(t *testing.T) {
exp := &pipeline.BookExport{
BookID: "syn", TotalUnits: 4,
Chunks: []pipeline.ChunkExport{
{Chapter: 1, ChunkIdx: 0, Disposition: "flagged", FlagReason: string(pipeline.FlagRetryUnaffordable)},
{Chapter: 2, ChunkIdx: 0, Disposition: "flagged", FlagReason: string(pipeline.FlagCancelled)},
{Chapter: 3, ChunkIdx: 0, Disposition: "flagged", FlagReason: string(pipeline.FlagHardRefusal)},
{Chapter: 4, ChunkIdx: 0, Disposition: "flagged", FlagReason: string(pipeline.FlagSanitizerDefect)},
// A c-lite unit that lost a member FOR GOOD and whose assembling stage merely ran out of money:
// «the next run re-does it» would promise back a text no purchase can bring, so this one asks
// for a human — the half the cheerier sentence would have hidden.
{Chapter: 5, ChunkIdx: 0, Disposition: "flagged", FlagReason: string(pipeline.FlagRetryUnaffordable), DroppedMembers: 1, DroppedReason: string(pipeline.FlagHardRefusal)},
},
}
var b bytes.Buffer
if err := renderExport(&b, exp, true); err != nil {
t.Fatal(err)
}
printed := b.String()
t.Logf("what the operator reads:\n%s", printed)
for _, want := range []string{
"=== CHAPTER 1 CHUNK 0 — flagged (retry_unaffordable) (paid for and NOT done — the next run re-does it) ===",
"=== CHAPTER 2 CHUNK 0 — flagged (cancelled) (paid for and NOT done — the next run re-does it) ===",
// The controls: a verdict a person really must look at keeps its banner.
"=== CHAPTER 3 CHUNK 0 — flagged (hard_refusal) (not translated, flagged for a human) ===",
"=== CHAPTER 4 CHUNK 0 — flagged (sanitizer_defect) (not translated, flagged for a human) ===",
"=== CHAPTER 5 CHUNK 0 — flagged (retry_unaffordable) (not translated, flagged for a human) ===",
} {
if !strings.Contains(printed, want) {
t.Fatalf("the export does not carry %q.\nWhat it printed:\n%s", want, printed)
}
}
}

View file

@ -16,6 +16,7 @@ import (
"strings"
"syscall"
"textmachine/backend/internal/ledger"
"textmachine/backend/internal/membank"
"textmachine/backend/internal/obs"
"textmachine/backend/internal/pipeline"
@ -357,6 +358,13 @@ func report(cfgPath string) error {
func() ([]store.RequestLogView, error) { return r.Store.RequestLogRows(r.Book.BookID) },
func() ([]store.ChunkStatus, error) { return r.Store.ChunkStatusesForBook(r.Book.BookID) },
func() ([]store.RetrievalState, error) { return r.Store.RetrievalStatesForBook(r.Book.BookID) },
// The price basis is DERIVED at report time from today's catalogue, so a run already on disk
// answers «was this billed at the rate of a model that did not answer it» without having had to
// record anything when it ran.
func(requested, actual string) ledger.PriceBasis {
_, basis := r.Pricer.PriceForResponse(requested, actual)
return basis
},
func() (float64, float64, error) { return r.Store.SpentUSD(r.Book.BookID) }); err != nil {
return err
}

View file

@ -25,17 +25,39 @@ import (
// where it is a contract. The migration mechanics themselves are pinned generically in
// internal/store/migrate_test.go.
// staleTheProjectSchema rolls the project database's recorded version back one step, which is what an
// older engine binary left behind.
// headOfSchemaAddedColumns are the columns the NEWEST migration adds. Rolling a project back one
// vintage has to take them with it: a database an older engine binary left behind does not have them,
// and a fixture that left them in place would build a state the real migrator cannot produce — the step
// and its version row commit in ONE transaction (store.applyStep), so «the DDL is there and the version
// is not» never happens outside a test.
//
// ⚠ This fixture re-applies the newest migration on the next write open, which is fine while that step
// is `CREATE … IF NOT EXISTS` and is NOT guaranteed for a future ALTER step (backlog row 49а). It is
// used here because these tests are about the exit codes and the output, and the deep fixture — the
// migration chain truncated to an older vintage — is only reachable from inside the store package,
// where those properties are tested. If a future migration makes this fail, move the setup rather than
// weakening the assertions.
// The list is coupled to the head of store.migrations deliberately. An ALTER step at the head is the
// case backlog row 49а names (those steps are not re-appliable), and a fixture that ignored it would
// re-apply the step onto its own effect and fail on a duplicate column instead of on the property the
// test is about.
var headOfSchemaAddedColumns = []string{"request_log.reasoning_in_completion"}
// headOfSchemaVersion is the migration the list above DESCRIBES. It is asserted rather than assumed,
// because a stale list does not fail where it is wrong: with a newer head the roll-back would drop this
// column while deleting the NEWER version row, so the migrator would re-apply the new step and never
// restore the old column — the "DDL missing while the version says present" state the fixture exists to
// avoid, surfacing later inside some unrelated test. The assertion turns that into one sentence here.
const headOfSchemaVersion = 18
// staleTheProjectSchema rolls the project database back one vintage — the recorded version AND the
// newest step's own columns — which is what an older engine binary left behind.
//
// These tests are about the exit codes and the output, so the roll-back is done here rather than with
// the deep fixture (the migration chain truncated to an older vintage), which is only reachable from
// inside the store package where those properties are tested.
func staleTheProjectSchema(t *testing.T, dbPath string) int {
t.Helper()
if got := store.SchemaHead(); got != headOfSchemaVersion {
t.Fatalf("the migration head moved to %d, and headOfSchemaAddedColumns still describes %d: "+
"add migration %d's own columns to that list (or empty it if the step adds none) and bump "+
"headOfSchemaVersion — otherwise this fixture rolls back the wrong vintage",
got, headOfSchemaVersion, got)
}
db, err := sql.Open("sqlite", "file:"+dbPath)
if err != nil {
t.Fatal(err)
@ -44,6 +66,15 @@ func staleTheProjectSchema(t *testing.T, dbPath string) int {
if _, err := db.Exec(`DELETE FROM schema_version WHERE version = (SELECT MAX(version) FROM schema_version)`); err != nil {
t.Fatal(err)
}
for _, col := range headOfSchemaAddedColumns {
table, name, ok := strings.Cut(col, ".")
if !ok {
t.Fatalf("headOfSchemaAddedColumns entry %q must be table.column", col)
}
if _, err := db.Exec(`ALTER TABLE ` + table + ` DROP COLUMN ` + name); err != nil {
t.Fatalf("roll %s back one vintage: %v", col, err)
}
}
return schemaVersionOf(t, dbPath)
}

View file

@ -0,0 +1,223 @@
package main
import (
"strings"
"testing"
"textmachine/backend/internal/llm"
"textmachine/backend/internal/pipeline"
"textmachine/backend/internal/store"
)
// coldRunShapeRows reproduces the shape of the paid run of 11.09 that this pack is about: twenty-nine
// usable calls and four that answered 200 with nothing usable — three empty at the ceiling, one a
// truncated draft — after which every unit shipped on a later attempt. The four cost $0.106472 of the
// $0.419423 the book paid.
func coldRunShapeRows() []store.RequestLogView {
rows := []store.RequestLogView{
{TS: "t1", Chapter: 1, Stage: "draft", Role: "translator", ModelRequested: "m", ModelActual: "m", CompletionTokens: 8496, FinishReason: "length", Degraded: "empty", OK: 0, CostUSD: 0.012044},
{TS: "t2", Chapter: 2, Stage: "draft", Role: "translator", ModelRequested: "m", ModelActual: "m", CompletionTokens: 8496, FinishReason: "length", Degraded: "length", OK: 0, CostUSD: 0.011637},
{TS: "t3", Chapter: 3, Stage: "draft", Role: "translator", ModelRequested: "m", ModelActual: "m", CompletionTokens: 8496, FinishReason: "length", Degraded: "empty", OK: 0, CostUSD: 0.011782},
{TS: "t4", Chapter: 2, Stage: "edit", Role: "editor", ModelRequested: "m", ModelActual: "m", CompletionTokens: 16000, FinishReason: "length", Degraded: "empty", OK: 0, CostUSD: 0.071009},
}
// The paying remainder, as one row, so the share has a real denominator.
rows = append(rows, store.RequestLogView{TS: "t5", Chapter: 1, Stage: "draft", Role: "translator",
ModelRequested: "m", ModelActual: "m", FinishReason: "stop", OK: 1, CostUSD: 0.312951})
// Two replayed rows, and the SECOND is the one that matters. A resume writes tm_hit=1 with ok taken
// from the stored disposition (pipeline/resume.go), so a resumed FLAGGED unit is `tm_hit=1, ok=0` —
// a row that looks exactly like a failure and bought nothing. Without it the tm_hit test in the
// filter is unreachable: an `ok` replay is already excluded by the ok test beside it, and a fixture
// that carries only that one pins half a condition while reading as if it pinned both.
rows = append(rows, store.RequestLogView{TS: "t6", Chapter: 1, Stage: "draft", Role: "translator",
ModelRequested: "m", ModelActual: "m", TMHit: 1, OK: 1, CostUSD: 0})
// It carries a COST as well, which today's resume path does not write — and the row is built that
// way on purpose. This renderer is a pure function of the rows it is handed, and a replayed row's
// money was already booked by the row it replays; counting it again would inflate the denominator
// the share is argued from. A fixture where every replay costs nothing pins the exclusion in the
// numerator and leaves the denominator's half of it unreachable.
rows = append(rows, store.RequestLogView{TS: "t7", Chapter: 3, Stage: "edit", Role: "editor",
ModelRequested: "m", ModelActual: "m", TMHit: 1, OK: 0, FinishReason: "length", Degraded: "empty", CostUSD: 0.5})
// Two more shapes that carry ok=0 and are NOT waste, and both are taken from what the engine
// really writes — a report that counted them would tell the owner a larger number than the truth,
// which is the one direction a money line must never err in.
//
// t8: a call that never reached a billed response. The runner writes ok=0 with cost_usd=0 and the
// cause in `err`, leaving `degraded` and `finish_reason` EMPTY — so counting it would also print a
// bucket with no name.
rows = append(rows, store.RequestLogView{TS: "t8", Chapter: 3, Stage: "edit", Role: "editor",
ModelRequested: "m", ModelActual: "m", OK: 0, Err: "context canceled", CostUSD: 0})
// t9: a cosmetic sanitizer strip. The verdict is not ok and the cleaned text SHIPS — the reader
// got exactly what this call paid for.
rows = append(rows, store.RequestLogView{TS: "t9", Chapter: 1, Stage: "edit", Role: "editor",
ModelRequested: "m", ModelActual: "m", OK: 0, FinishReason: "stop", Degraded: "sanitizer_stripped", CostUSD: 0.018654})
return rows
}
// TestTheReportAddsUpWhatWasPaidForAndThrownAway is the line the run of 11.09 had nowhere to print. The
// FLAGS section reports CHUNKS whose final disposition is not ok, and all four of those units were
// recovered by the regeneration and shipped — so a run that spent a quarter of its money on empty
// replies printed a clean report.
func TestTheReportAddsUpWhatWasPaidForAndThrownAway(t *testing.T) {
var b strings.Builder
if err := renderReport(&b,
func() ([]store.RequestLogView, error) { return coldRunShapeRows(), nil },
// No flagged chunk at all: every unit recovered. This is the fixture's whole point.
func() ([]store.ChunkStatus, error) { return nil, nil },
func() ([]store.RetrievalState, error) { return nil, nil },
pricedByAnswerer, okLedger); err != nil {
t.Fatal(err)
}
out := b.String()
if strings.Contains(out, "=== FLAGS") {
t.Fatalf("the fixture must have NO flagged chunk, else it is not the case this line exists for:\n%s", out)
}
// FOUR, and the fixture carries THREE other rows that also read ok=0 and are not waste — a replay,
// a $0 transport failure, and a cosmetic strip whose text shipped. The message names all three,
// because whoever breaks this has to learn from it WHICH of them started being counted; a message
// that names only one is a right verdict delivered with the wrong cause.
if !strings.Contains(out, "PAID AND THROWN AWAY: 4 call(s), $0.106472") {
t.Fatalf("the money of the thrown-away calls must be added up and printed, and none of these is a "+
"paid-for-nothing CALL: a replay (tm_hit), a failure that cost $0, a stripped answer that SHIPPED:\n%s", out)
}
// The share is what makes it an argument rather than a number. The DENOMINATOR is every fresh
// billed row — including the stripped-but-shipped call, which really was paid for — so it is
// $0.419423 + $0.018654 = $0.438077 and the share is 24.3%. The replayed row's $0.500000 is in
// NEITHER half: it would drop the share to 11.4% and quietly make the waste look like a seventh of
// what it is.
if !strings.Contains(out, "24.3% of the $0.438077") {
t.Fatalf("the line must say what share of the book's spend this is, and a replay is in neither half:\n%s", out)
}
// The breakdown separates the two causes the remedy has to tell apart.
// Three empty, not four: the resumed flagged row carries `empty` too and must not swell the cause.
if !strings.Contains(out, "empty×3") || !strings.Contains(out, "length×1") {
t.Fatalf("the line must break the waste down by cause — three empty, one truncated:\n%s", out)
}
// ⚠ THE NEXT TWO ARE SCOPED TO THE LINE, not to the whole report. Both words appear in the ROW
// TABLE above it — `sanitizer_stripped` as that row's own degraded tag — so a whole-output search
// answers a question nobody asked and goes red on a correct report.
line := paidLine(t, out)
// A call that cost nothing is not money thrown away, and it would arrive with no cause to print.
if strings.Contains(line, "unnamed") {
t.Fatalf("a $0 transport failure must not enter a MONEY line, least of all as a nameless bucket: %q", line)
}
// A cosmetic strip ships its text. The reader got what it paid for.
if strings.Contains(line, "sanitizer_stripped") {
t.Fatalf("a stripped-but-SHIPPED answer is not waste; counting it overstates the loss: %q", line)
}
}
// paidLine returns the PAID AND THROWN AWAY line itself, so an assertion about what the LINE says
// cannot be satisfied — or broken — by the row table printed above it.
func paidLine(t *testing.T, out string) string {
t.Helper()
for _, l := range strings.Split(out, "\n") {
if strings.HasPrefix(l, "PAID AND THROWN AWAY") {
return l
}
}
t.Fatalf("the report carries no PAID AND THROWN AWAY line at all:\n%s", out)
return ""
}
// TestACleanRunSaysNothingAboutWaste is the control. A section that printed on every run would be
// noise, and a zero would be indistinguishable from a run nobody measured.
func TestACleanRunSaysNothingAboutWaste(t *testing.T) {
rows := []store.RequestLogView{
{TS: "t1", Stage: "draft", Role: "translator", ModelRequested: "m", ModelActual: "m", FinishReason: "stop", OK: 1, CostUSD: 0.02},
{TS: "t2", Stage: "draft", Role: "translator", ModelRequested: "m", ModelActual: "m", TMHit: 1, OK: 1, CostUSD: 0},
}
var b strings.Builder
if err := renderReport(&b,
func() ([]store.RequestLogView, error) { return rows, nil },
func() ([]store.ChunkStatus, error) { return nil, nil },
func() ([]store.RetrievalState, error) { return nil, nil },
pricedByAnswerer, okLedger); err != nil {
t.Fatal(err)
}
if out := b.String(); strings.Contains(out, "PAID AND THROWN AWAY") {
t.Fatalf("a run where every call answered must not print the line at all:\n%s", out)
}
}
// TestTheRunningReportSaysWhatTheBudgetWentTo covers the OTHER renderer. `tmctl translate` prints a
// line per stage while a paid run is happening, and that is the reader who can still act on it — the
// operator watching a book cost money. A thinking share visible only in the post-mortem arrives after
// the budget is spent.
//
// The three cells are the same three answers the report table has to keep apart, and they are read by
// their own marker (`think=`) rather than by a substring, because every other number on this line is
// small and a bare "0" matches several of them.
func TestTheRunningReportSaysWhatTheBudgetWentTo(t *testing.T) {
ate, none := 8496, 0
res := &pipeline.BookResult{BookID: "b1", TotalUSD: 0.1, Chunks: []pipeline.ChunkOutcome{{
Chapter: 1, ChunkIdx: 0, Disposition: pipeline.DispOK, FinalText: "ТЕКСТ",
Stages: []pipeline.StageResult{
{Stage: "draft", Model: "m", Disposition: pipeline.DispOK, Attempts: 2, FinishReason: "stop",
Usage: llm.Usage{PromptTokens: 10, CompletionTokens: 8496, ReasoningInCompletion: &ate}},
{Stage: "edit", Model: "m", Disposition: pipeline.DispOK, Attempts: 1, FinishReason: "stop",
Usage: llm.Usage{PromptTokens: 10, CompletionTokens: 2624, ReasoningInCompletion: &none}},
{Stage: "judge", Model: "m", Disposition: pipeline.DispOK, Attempts: 1, FinishReason: "stop",
Usage: llm.Usage{PromptTokens: 10, CompletionTokens: 100}},
},
}}}
var b strings.Builder
if err := renderTranslate(&b, res, okLedger); err != nil {
t.Fatal(err)
}
out := b.String()
for stage, want := range map[string]string{"draft": "think=8496", "edit": "think=0", "judge": "think=?"} {
line := stageLine(t, out, stage)
if !strings.Contains(line, want) {
t.Fatalf("the %s line must carry %q — a call that spent its budget thinking, one that did not, and one whose provider said nothing are three different answers: %q", stage, want, line)
}
}
}
// stageLine returns the running report's line for one stage, so an assertion about that stage cannot be
// satisfied by a neighbouring one.
func stageLine(t *testing.T, out, stage string) string {
t.Helper()
for _, l := range strings.Split(out, "\n") {
if f := strings.Fields(l); len(f) > 0 && f[0] == stage {
return l
}
}
t.Fatalf("the running report has no line for stage %q:\n%s", stage, out)
return ""
}
// TestAPaidFailureWithNoCauseIsCountedUnderAName is the branch that exists so a money line never prints
// a bucket nobody can ask about — my own remedy for the nameless `×1` the first review found.
//
// ⚠ IT IS EXERCISED DIRECTLY, AND THE REASON IS STATED RATHER THAN GLOSSED. No writer in the engine
// produces this row TODAY: every billed failure path fills `degraded` (the classifier's tag, the cut's
// finish, `billed_2xx_decode_failed`), and the one shape that leaves both empty — a call that never
// reached a billed response — costs $0 and is excluded a line earlier. So the branch is defensive over
// the renderer's INPUT, and this renderer is a pure function of the rows it is handed: it must be right
// for a row a future writer produces, not only for the ones today's writers do. Measured before this
// test existed: a `panic` planted in that branch left the package green, while the same panic in a
// reachable branch went red — the branch was decoration.
func TestAPaidFailureWithNoCauseIsCountedUnderAName(t *testing.T) {
rows := []store.RequestLogView{
{TS: "t1", Chapter: 1, Stage: "draft", Role: "translator", ModelRequested: "m", ModelActual: "m",
OK: 0, CostUSD: 0.01, Degraded: "", FinishReason: ""},
{TS: "t2", Chapter: 1, Stage: "draft", Role: "translator", ModelRequested: "m", ModelActual: "m",
OK: 1, CostUSD: 0.09, FinishReason: "stop"},
}
var b strings.Builder
if err := renderReport(&b,
func() ([]store.RequestLogView, error) { return rows, nil },
func() ([]store.ChunkStatus, error) { return nil, nil },
func() ([]store.RetrievalState, error) { return nil, nil },
pricedByAnswerer, okLedger); err != nil {
t.Fatal(err)
}
line := paidLine(t, b.String())
if !strings.Contains(line, "unnamed×1") {
t.Fatalf("a BILLED failure that names no cause must still be counted under a name — an unlabelled "+
"bucket in a money line is a number the reader cannot ask about: %q", line)
}
if !strings.Contains(line, "1 call(s), $0.010000") {
t.Fatalf("the billed failure must be counted and priced: %q", line)
}
}

View file

@ -0,0 +1,125 @@
package main
import (
"strings"
"testing"
"textmachine/backend/internal/ledger"
"textmachine/backend/internal/store"
)
// TestTheReportNamesRowsBilledByAModelThatDidNotAnswer is the retroactive half of making the price
// substitution visible. The live path says it once, in a WARN, at the moment it happens; a run already
// on disk has no such line, and the rows are the only thing left. Because the legend is DERIVED from
// the two model columns and today's catalogue, those runs answer too — which is the point: the
// measured case is 28 rows of a finished book, not a run somebody is watching.
func TestTheReportNamesRowsBilledByAModelThatDidNotAnswer(t *testing.T) {
zero, eightK := 0, 8496
rows := []store.RequestLogView{
{TS: "t1", Stage: "draft", Role: "translator", ModelRequested: "deepseek-v4-flash", ModelActual: "deepseek-flash", CostUSD: 0.012044, OK: 1, ReasoningInCompletion: &eightK},
{TS: "t2", Stage: "draft", Role: "translator", ModelRequested: "deepseek-v4-flash", ModelActual: "deepseek-flash", CostUSD: 0.011637, OK: 1, ReasoningInCompletion: &zero},
{TS: "t3", Stage: "edit", Role: "editor", ModelRequested: "deepseek-v4-pro", ModelActual: "deepseek-v4-pro", CostUSD: 0.071009, OK: 1},
// A replayed row bought nothing this run and must not be counted as a bill.
{TS: "t4", Stage: "draft", Role: "translator", ModelRequested: "deepseek-v4-flash", ModelActual: "deepseek-flash", CostUSD: 0, TMHit: 1, OK: 1},
}
basis := func(requested, actual string) ledger.PriceBasis {
if actual == "deepseek-flash" {
return ledger.PriceByRequested
}
return ledger.PriceByAnswerer
}
var b strings.Builder
if err := renderReport(&b,
func() ([]store.RequestLogView, error) { return rows, nil },
func() ([]store.ChunkStatus, error) { return nil, nil },
func() ([]store.RetrievalState, error) { return nil, nil },
basis, okLedger); err != nil {
t.Fatal(err)
}
out := b.String()
if !strings.Contains(out, "PRICED BY A MODEL THAT DID NOT ANSWER") {
t.Fatalf("a run billed at the rate of a model that did not answer must say so:\n%s", out)
}
if !strings.Contains(out, "deepseek-v4-flash -> deepseek-flash") {
t.Fatalf("the legend must name BOTH slugs — the fix is to add the answering one to the catalogue:\n%s", out)
}
// Two paying rows, and the replayed one excluded: $0.012044 + $0.011637.
if !strings.Contains(out, "2 row(s)") || !strings.Contains(out, "$0.023681") {
t.Fatalf("the legend must count only the rows that were BILLED, and total them:\n%s", out)
}
// The row priced by the model that answered must not be swept in.
if strings.Contains(out, "deepseek-v4-pro -> deepseek-v4-pro") {
t.Fatalf("a call priced by its own answerer is not a substitution:\n%s", out)
}
// The thinking column has to REACH the reader, and it has to reach them saying three different
// things. Row t1 reports 8496 thinking tokens, t2 a MEASURED zero, t3 nothing at all — and the
// third must print `?`, because a run whose provider says nothing and a call that did not think
// are the two answers this whole column exists to keep apart.
//
// ⚠ READ BY COLUMN NAME, NEVER BY SUBSTRING. Every other numeric cell in this fixture is zero, so
// a search for a padded "0" anywhere in the output is satisfied with the think column DELETED —
// the assertion would then be green on a report that lost the very thing it is asserting.
for _, c := range []struct{ ts, want string }{
{"t1", "8496"}, // a reported count
{"t2", "0"}, // a MEASURED zero: the provider answered, and the answer was none
{"t3", "?"}, // no answer at all, which must not be spelled like the line above
} {
if got := cellOf(t, out, c.ts, "think"); got != c.want {
t.Fatalf("row %s: think column = %q, want %q — measured, measured-zero and unanswered are three different cells:\n%s", c.ts, got, c.want, out)
}
}
}
// cellOf reads one cell of the report table by its COLUMN NAME, from the row whose first field is ts.
// The header is the index, so an assertion cannot be satisfied by an identical-looking value in some
// other column — and a column that disappears takes its assertions down with it instead of silently
// passing them to a neighbour.
func cellOf(t *testing.T, out, ts, column string) string {
t.Helper()
var header []string
for _, line := range strings.Split(out, "\n") {
f := strings.Fields(line)
if len(f) == 0 {
continue
}
if f[0] == "ts" {
header = f
continue
}
if f[0] != ts {
continue
}
if header == nil {
t.Fatalf("row %q appears before any header row:\n%s", ts, out)
}
for i, name := range header {
if name == column && i < len(f) {
return f[i]
}
}
t.Fatalf("the report has no %q column (header: %v):\n%s", column, header, out)
}
t.Fatalf("the report has no row %q:\n%s", ts, out)
return ""
}
// TestTheLegendIsSilentWhenEveryBillNamesItsAnswerer is the control without which the test above proves
// nothing: a section that printed on every run would be noise, and the day the missing slug is added to
// models.yaml these lines must disappear on their own.
func TestTheLegendIsSilentWhenEveryBillNamesItsAnswerer(t *testing.T) {
rows := []store.RequestLogView{
{TS: "t1", Stage: "draft", Role: "translator", ModelRequested: "deepseek-v4-flash", ModelActual: "deepseek-v4-flash", CostUSD: 0.012044, OK: 1},
}
var b strings.Builder
if err := renderReport(&b,
func() ([]store.RequestLogView, error) { return rows, nil },
func() ([]store.ChunkStatus, error) { return nil, nil },
func() ([]store.RetrievalState, error) { return nil, nil },
pricedByAnswerer, okLedger); err != nil {
t.Fatal(err)
}
if out := b.String(); strings.Contains(out, "PRICED BY A MODEL THAT DID NOT ANSWER") {
t.Fatalf("with every bill named by its answerer the section must not print at all:\n%s", out)
}
}

View file

@ -9,6 +9,7 @@ import (
"strings"
"unicode/utf8"
"textmachine/backend/internal/ledger"
"textmachine/backend/internal/pipeline"
"textmachine/backend/internal/store"
)
@ -93,16 +94,20 @@ func renderStoppedRun(w io.Writer, bookID string, ledger func() (committed, rese
fmt.Fprintf(w, " the stored dispositions could not be read, so how far the book got is UNKNOWN here: %v\n", err)
return
}
resolved, stopped := 0, 0
for _, row := range cs {
if pipeline.FlagReason(row.FlagReason) == pipeline.FlagCancelled {
stopped++
resolved, redone := 0, 0
for i := range cs {
// ⛔ THE QUESTION IS «IS THIS ROW AN ANSWER», asked of the one predicate that defines it. Written
// here as `FlagReason == FlagCancelled` it was a second copy of that rule, and a second
// non-resolved reason (a retry whose money ran out) made the copy count a position the next run
// re-does as one with a verdict — telling an operator the rest is served for $0.
if !pipeline.ResolvedForResume(&cs[i]) {
redone++
continue
}
resolved++
}
fmt.Fprintf(w, " positions with a verdict: %d; positions the stop cut mid-call: %d (the resume re-does those and serves the rest for $0)\n",
resolved, stopped)
fmt.Fprintf(w, " positions with a verdict: %d; positions the next run re-does: %d (a call the stop cut, or one whose money ran out; the rest is served for $0)\n",
resolved, redone)
fmt.Fprintln(w, " `tmctl status --json` reports the same numbers in full, and `tmctl export` ships what is finished.")
}
@ -143,11 +148,12 @@ func renderTranslate(w io.Writer, res *pipeline.BookResult, ledger func() (commi
case st.FromResume:
how = "resume"
}
fmt.Fprintf(w, " %-8s %-22s %-8s %-8s%s $%.6f (cum $%.6f) in=%d (cached=%d) out=%d+%d att=%d %dms finish=%s\n",
fmt.Fprintf(w, " %-8s %-22s %-8s %-8s%s $%.6f (cum $%.6f) in=%d (cached=%d) out=%d+%d think=%s att=%d %dms finish=%s\n",
st.Stage, st.Model, how, st.Disposition, flagSuffix(st.FlagReason),
st.CostUSD, st.CumCostUSD,
st.Usage.PromptTokens, st.Usage.CachedTokens,
st.Usage.CompletionTokens, st.Usage.ReasoningTokens, st.Attempts, st.LatencyMS, st.FinishReason)
st.Usage.CompletionTokens, st.Usage.ReasoningTokens, thinkCell(st.Usage.ReasoningInCompletion),
st.Attempts, st.LatencyMS, st.FinishReason)
}
fmt.Fprintln(w)
}
@ -405,21 +411,23 @@ func renderReport(w io.Writer,
fetchRows func() ([]store.RequestLogView, error),
fetchFlags func() ([]store.ChunkStatus, error),
fetchStates func() ([]store.RetrievalState, error),
ledger func() (committed, reserved float64, err error)) error {
priceBasis func(requested, actual string) ledger.PriceBasis,
ledgerTotals func() (committed, reserved float64, err error)) error {
rows, err := fetchRows()
if err != nil {
return err
}
fmt.Fprintf(w, "%-20s %-4s %-5s %-8s %-12s %-22s %8s %8s %8s %8s %8s %10s %8s %-8s %-5s %-3s %s\n",
"ts", "ch", "chunk", "stage", "role", "model", "prompt", "cached", "cwrite", "compl", "reason", "cost_usd", "ms", "finish", "tmhit", "ok", "err")
fmt.Fprintf(w, "%-20s %-4s %-5s %-8s %-12s %-22s %8s %8s %8s %8s %8s %8s %10s %8s %-8s %-5s %-3s %s\n",
"ts", "ch", "chunk", "stage", "role", "model", "prompt", "cached", "cwrite", "compl", "reason", "think", "cost_usd", "ms", "finish", "tmhit", "ok", "err")
for _, row := range rows {
model := row.ModelActual
if model == "" && row.ModelRequested != "" {
model = row.ModelRequested + "(req)"
}
fmt.Fprintf(w, "%-20s %-4d %-5d %-8s %-12s %-22s %8d %8d %8d %8d %8d %10.6f %8d %-8s %-5d %-3d %s\n",
fmt.Fprintf(w, "%-20s %-4d %-5d %-8s %-12s %-22s %8d %8d %8d %8d %8d %8s %10.6f %8d %-8s %-5d %-3d %s\n",
row.TS, row.Chapter, row.ChunkIdx, row.Stage, row.Role, model, row.PromptTokens, row.CachedTokens,
row.CacheCreationTokens, row.CompletionTokens, row.ReasoningTokens, row.CostUSD,
row.CacheCreationTokens, row.CompletionTokens, row.ReasoningTokens,
thinkCell(row.ReasoningInCompletion), row.CostUSD,
row.LatencyMS, row.FinishReason, row.TMHit, row.OK, errTail(row.Degraded, row.Err))
}
@ -440,6 +448,108 @@ func renderReport(w io.Writer,
fmt.Fprintf(w, "estimated-cost rows: %d ($%.6f settled at the reservation estimate, not provider-reported; ~%d est. output tokens via fertility, display-only)\n", estRows, estUSD, estTokens)
}
// WHAT WAS PAID FOR AND THROWN AWAY. A call that returns HTTP 200 with nothing usable is billed in
// full and then re-asked, and until this line nothing in the report added those up: the FLAGS
// section below lists CHUNKS whose final disposition is not ok, and a chunk that was recovered by
// the regeneration is ok — so a run where a quarter of the money bought empty replies printed no
// flags at all. Measured on the run of 11.09: four such calls, $0.106472, 25.4% of the book, and
// every one of the four units shipped.
//
// ⚠ IT IS READ FROM `ok`, NOT FROM `err`, and that is a decision rather than an omission. `err`
// carries transport failures — a call that never produced a billed response — and filling it for a
// 2xx that answered and was useless would put two different post-mortems in one column: «we could
// not reach the provider» and «the provider charged us for nothing». The failure already has three
// honest carriers that agree to the cent (`ok`, `finish_reason`, `degraded`); what it did not have
// was a place where their MONEY is added up next to the total.
var wasted int
var wastedUSD float64
byReason := map[string]int{}
var reasons []string
for _, row := range rows {
if !rowPaidForNothing(row) {
continue
}
wasted++
wastedUSD += row.CostUSD
reason := row.Degraded
if reason == "" {
reason = row.FinishReason
}
if reason == "" {
// A row can be a paid failure and still name no cause — the engine's own cut writes a
// verdict, but a transport error writes `err` and leaves both of these empty. An unnamed
// bucket in a money line is worse than a crude name: the reader counts it and cannot ask
// about it.
reason = "unnamed"
}
if byReason[reason] == 0 {
reasons = append(reasons, reason)
}
byReason[reason]++
}
if wasted > 0 {
var paid float64
for _, row := range rows {
if row.TMHit == 0 {
paid += row.CostUSD
}
}
sort.Strings(reasons)
parts := make([]string, 0, len(reasons))
for _, r := range reasons {
parts = append(parts, fmt.Sprintf("%s×%d", r, byReason[r]))
}
share := ""
if paid > 0 {
share = fmt.Sprintf(" = %.1f%% of the $%.6f this book has paid", 100*wastedUSD/paid, paid)
}
fmt.Fprintf(w, "PAID AND THROWN AWAY: %d call(s), $%.6f%s — %s\n",
wasted, wastedUSD, share, strings.Join(parts, " "))
fmt.Fprintf(w, " (a BILLED call that left nothing usable; its unit may still have shipped on a later attempt, which is why the flag section below can be empty. Replays, $0 failures and cosmetically-stripped-but-shipped answers are not counted)\n")
}
// Substituted-price legend: rows billed at the rate of a model that did NOT answer them. The
// catalogue lists what we asked for, the provider may answer under another slug, and the fallback
// that keeps a premium answer off a cheap default also bills a CHEAPER answer at the premium pin —
// the reader's money, in the direction nothing guards.
//
// It is DERIVED from the row's two model columns and today's catalogue rather than read from a
// stored flag, and that is what makes it useful: every run already on disk gets the answer, not
// only the ones recorded from here on. The price of deriving it is that it describes TODAY's
// catalogue — adding the missing slug is exactly the fix, and the day it lands these lines stop
// printing, which is the outcome, not a loss of evidence.
if priceBasis != nil {
type sub struct {
rows int
usd float64
}
subs := map[string]*sub{}
var order []string
for _, row := range rows {
if row.TMHit != 0 || row.ModelActual == "" {
continue // a replayed row bought nothing; a row with no answerer never got a bill of its own
}
basis := priceBasis(row.ModelRequested, row.ModelActual)
if !basis.Substituted() {
continue
}
key := fmt.Sprintf("%s -> %s (priced by the %s)", row.ModelRequested, row.ModelActual, basis)
if subs[key] == nil {
subs[key] = &sub{}
order = append(order, key)
}
subs[key].rows++
subs[key].usd += row.CostUSD
}
if len(order) > 0 {
sort.Strings(order)
fmt.Fprintf(w, "\nPRICED BY A MODEL THAT DID NOT ANSWER — the answering slug is not in models.yaml:\n")
for _, k := range order {
fmt.Fprintf(w, " %-64s %4d row(s) $%.6f\n", k, subs[k].rows, subs[k].usd)
}
}
}
// Flag section (Milestone 2): every chunk×stage whose disposition ≠ ok — the
// "flag for the editor" the plan requires (02-mvp Phase-1 acceptance allows N).
flags, err := fetchFlags()
@ -542,7 +652,7 @@ func renderReport(w io.Writer,
}
}
committed, reserved, err := ledger()
committed, reserved, err := ledgerTotals()
if err != nil {
return err
}
@ -550,6 +660,46 @@ func renderReport(w io.Writer,
return nil
}
// rowPaidForNothing says a telemetry row is money this book spent on a call whose output nobody got.
//
// It is deliberately NARROWER than «ok = 0», because that column answers a different question — it is
// the classifier's verdict (pipeline.classification.ok), and three shapes carry a false verdict without
// carrying waste:
//
// - a REPLAY (tm_hit): it bought nothing this run, and the call it replays is its own row;
// - a row that cost NOTHING: a transport failure that never reached a billed response writes ok=0
// with cost_usd=0 and its cause in `err`. Counting it inflates the CALL count of a line whose
// whole subject is money, and names a bucket the money side cannot explain;
// - `sanitizer_stripped`: the verdict is not ok, and the cleaned text SHIPS (the stage's export is
// served from the derived checkpoint). The reader got what this call paid for.
//
// Everything else that answered, was billed, and left nothing usable behind is what the line is for.
func rowPaidForNothing(row store.RequestLogView) bool {
switch {
case row.TMHit != 0, row.OK != 0:
return false
case row.CostUSD <= 0:
return false
case row.Degraded == string(pipeline.FlagSanitizerStripped):
return false
}
return true
}
// thinkCell renders the thinking share of an already-paid completion (llm.Usage.ReasoningInCompletion).
//
// «?» rather than 0 when the provider reported nothing, and the distinction is the point of the column:
// `reason` beside it counts thinking billed ON TOP, which is 0 by definition for a subset-billing
// provider, so a reader who takes that 0 for an answer concludes «it did not think» about a call that
// spent its whole budget thinking. A column that cannot say «I was not told» repeats that mistake in a
// second place.
func thinkCell(n *int) string {
if n == nil {
return "?"
}
return strconv.Itoa(*n)
}
// errTail collapses the degraded/err columns into one bounded table tail (empty
// when the call was clean). Truncation is on a rune boundary: err carries raw chunks
// of provider bodies (CJK/Cyrillic), a byte slice would print broken UTF-8
@ -584,7 +734,11 @@ func errTail(degraded, errText string) string {
// function's frozen callback contract (render_test.go) is untouched.
func renderQuality(w io.Writer, q *pipeline.QualityReport) error {
fmt.Fprintf(w, "\n=== QUALITY (per-run, deterministic signals — observability, not a gate) ===\n")
fmt.Fprintf(w, "total units=%d · reached final=%d · with export text=%d\n", q.TotalUnits, q.ProcessedUnits, q.TextUnits)
// «Reached final» stood in the middle slot and is not what the counter holds: a position a run STOPPED
// on (a cut call, a re-attack a ceiling refused) has a final-stage row and has not reached anything —
// it was paid for and left unfinished. The operator gets the fact instead of the conclusion; the
// counter's own doc argues why such a row is counted (pipeline.QualityReport.ProcessedUnits).
fmt.Fprintf(w, "total units=%d · with a final-stage row=%d · with export text=%d\n", q.TotalUnits, q.ProcessedUnits, q.TextUnits)
// Claim-1: choppy paragraphs. ≈1.0 sentences/paragraph = choppy (the owner's complaint); higher = merged prose.
fmt.Fprintf(w, "STRUCTURE (claim-1 «choppy paragraphs»): sentences/narrative-paragraph=%.2f (sentences=%d / narrative-paragraphs=%d)\n",
q.MeanSentPerNarrPara, q.NarrativeSentences, q.NarrativeParagraphs)
@ -598,8 +752,16 @@ func renderQuality(w io.Writer, q *pipeline.QualityReport) error {
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)
// ⚠ THE EDITOR'S SHARE IS SPELLED OUT FOR THE SAME REASON, and it was missing while its numerator was
// being widened: the edit counter now counts an echo the editor made whatever happened to it next, so
// without this clause an operator read «echo edit=1 (100.0%)» over a book that had shipped clean prose.
editRecovered := ""
if q.EchoEditRecovered > 0 {
editRecovered = fmt.Sprintf(", %d recovered", q.EchoEditRecovered)
}
fmt.Fprintf(w, "STRIPS/ECHO: cosmetic-strip units=%d (%.1f%%, markdown+CJK) · echo draft=%d (%.1f%%%s) · echo edit=%d (%.1f%%%s)\n",
q.CosmeticStripUnits, 100*q.CosmeticStripRate, q.EchoDraftChunks, 100*q.EchoDraftRate, recovered,
q.EchoEditUnits, 100*q.EchoEditRate, editRecovered)
// The UNSIGNED-BANK line (pack-20 / D39.42 п.4): in the auto mode part of the bank the model is shown
// carries renderings nobody approved, and that has to be visible rather than implied. The follow rate
// is NOT a quality verdict — the unverified section explicitly grants the model the right to translate
@ -855,6 +1017,20 @@ func renderExport(w io.Writer, exp *pipeline.BookExport, asPlaintext bool) error
// inherited claim (see renderTranslate for the same reasoning).
fmt.Fprintf(w, "=== CHAPTER %d CHUNK %d — %s%s (verify) ===\n",
ce.Chapter, ce.ChunkIdx, ce.Disposition, flagParen(ce.FlagReason))
case !pipeline.FlagReason(ce.FlagReason).AnswersForResume() && ce.DroppedMembers == 0:
// ⛔ A STOP MARK IS NOT «FLAGGED FOR A HUMAN». A call a person cut, or a re-attack a ceiling
// refused, is a position that was paid for and not finished: the next run does it, and no
// human has anything to check. The banner below told the opposite, and it is the same
// sentence the book writer was corrected for one surface over (bookbuild.go) — the predicate
// is exported so both can ask it of the same reason instead of each inventing a rule.
//
// ⚠ AND ONLY WHEN NOTHING IS LOST FOR GOOD: a c-lite unit can carry a PERMANENTLY flagged
// member (its text is gone whatever the next run buys) and a stop mark on the stage that
// would have assembled it. «The next run re-does it» is then a half-truth that hides the
// other half, so such a unit falls through to the default below, which asks for a human —
// which is what the lost member actually needs.
fmt.Fprintf(w, "=== CHAPTER %d CHUNK %d — %s%s (paid for and NOT done — the next run re-does it) ===\n",
ce.Chapter, ce.ChunkIdx, ce.Disposition, flagParen(ce.FlagReason))
default:
// Substantive flag / upstream skip: no export text (D2 — contaminated output never ships).
fmt.Fprintf(w, "=== CHAPTER %d CHUNK %d — %s%s (not translated, flagged for a human) ===\n",

View file

@ -67,6 +67,7 @@ func TestRenderReportPrintsBanknoteCoverageNotJustTheTotal(t *testing.T) {
func() ([]store.RequestLogView, error) { return nil, nil },
func() ([]store.ChunkStatus, error) { return nil, nil },
func() ([]store.RetrievalState, error) { return states, nil },
pricedByAnswerer,
okLedger); err != nil {
t.Fatal(err)
}
@ -86,6 +87,7 @@ func TestRenderReportStaysSilentWithoutTheBanknoteChannel(t *testing.T) {
func() ([]store.RequestLogView, error) { return nil, nil },
func() ([]store.ChunkStatus, error) { return nil, nil },
func() ([]store.RetrievalState, error) { return states, nil },
pricedByAnswerer,
okLedger); err != nil {
t.Fatal(err)
}
@ -171,6 +173,7 @@ func TestTheReportNamesADeviationOnAChunkWhoseConfirmedCountIsZero(t *testing.T)
func() ([]store.RequestLogView, error) { return nil, nil },
func() ([]store.ChunkStatus, error) { return nil, nil },
func() ([]store.RetrievalState, error) { return states, nil },
pricedByAnswerer,
func() (float64, float64, error) { return 0, 0, nil })
if err != nil {
t.Fatalf("renderReport: %v", err)

View file

@ -9,6 +9,7 @@ import (
"testing"
"unicode/utf8"
"textmachine/backend/internal/ledger"
"textmachine/backend/internal/llm"
"textmachine/backend/internal/pipeline"
"textmachine/backend/internal/store"
@ -20,6 +21,10 @@ import (
func okLedger() (float64, float64, error) { return 0.123456, 0, nil }
// pricedByAnswerer is the report's price-basis resolver for a catalogue that knows every slug the rows
// name — the ordinary case, where the substituted-price legend must stay silent.
func pricedByAnswerer(string, string) ledger.PriceBasis { return ledger.PriceByAnswerer }
func TestRenderTranslateOKAndLedger(t *testing.T) {
var b strings.Builder
res := &pipeline.BookResult{BookID: "b1", TotalUSD: 0.01, Chunks: []pipeline.ChunkOutcome{{
@ -84,6 +89,7 @@ func TestRenderReportColumnsAndErrTail(t *testing.T) {
func() ([]store.RequestLogView, error) { return rows, nil },
func() ([]store.ChunkStatus, error) { return nil, nil },
func() ([]store.RetrievalState, error) { return nil, nil },
pricedByAnswerer,
okLedger); err != nil {
t.Fatal(err)
}
@ -140,6 +146,7 @@ func TestRenderReportSections(t *testing.T) {
func() ([]store.RequestLogView, error) { return nil, nil },
func() ([]store.ChunkStatus, error) { return flags, nil },
func() ([]store.RetrievalState, error) { return states, nil },
pricedByAnswerer,
okLedger); err != nil {
t.Fatal(err)
}
@ -165,6 +172,7 @@ func TestRenderReportPartialOutputOnMidAuditError(t *testing.T) {
func() ([]store.RequestLogView, error) { return rows, nil },
func() ([]store.ChunkStatus, error) { return nil, errors.New("chunk_status corrupted") },
func() ([]store.RetrievalState, error) { t.Fatal("must not be reached"); return nil, nil },
pricedByAnswerer,
okLedger)
if err == nil || err.Error() != "chunk_status corrupted" {
t.Fatalf("mid-audit error must propagate, got %v", err)

View file

@ -14,6 +14,9 @@ import (
"syscall"
"testing"
"time"
"textmachine/backend/internal/pipeline"
"textmachine/backend/internal/store"
)
// stoppedaccount_test.go: what a person is told after they stop a run (backlog row 389).
@ -44,7 +47,7 @@ func arrivingProvider(t *testing.T, free int, arrived chan<- struct{}, hold time
return srv
}
var accountNumbers = regexp.MustCompile(`positions with a verdict: (\d+); positions the stop cut mid-call: (\d+)`)
var accountNumbers = regexp.MustCompile(`positions with a verdict: (\d+); positions the next run re-does: (\d+)`)
func TestAStoppedRunTellsTheOperatorWhatItBought(t *testing.T) {
if testing.Short() {
@ -83,10 +86,10 @@ func TestAStoppedRunTellsTheOperatorWhatItBought(t *testing.T) {
printed := out.String()
t.Logf("what the operator saw on stdout:\n%s", printed)
for _, want := range []string{
"RUN STOPPED", // which exit this was
"book ledger: committed=$", // what it spent, read from the store rather than from a result
"positions with a verdict:", // what it finished
"positions the stop cut mid-call:", // what the resume will buy again
"RUN STOPPED", // which exit this was
"book ledger: committed=$", // what it spent, read from the store rather than from a result
"positions with a verdict:", // what it finished
"positions the next run re-does:", // what the resume will buy again
} {
if !strings.Contains(printed, want) {
t.Fatalf("the stopped run said nothing about %q.\nWhat it printed:\n%s", want, printed)
@ -111,3 +114,38 @@ func TestAStoppedRunTellsTheOperatorWhatItBought(t *testing.T) {
}
t.Logf("non-vacuous: %s position(s) with a verdict, %s cut mid-call", resolved, cut)
}
// TestTheStoppedAccountCountsEveryPositionTheNextRunRedoes is the account's SPLIT, asked of the rows
// directly — the run above cannot produce the second cause at all, and the split was keyed on the first.
//
// ⛔ THE DEFECT IT PINS WAS ONE EDIT OLD. The split read `FlagReason == FlagCancelled`, which is a second
// copy of «is this row an answer the next run serves» (pipeline.ResolvedForResume). The day a second
// non-resolved reason existed — a retry whose money ran out — the copy counted a position the next run
// re-does among the ones WITH a verdict, and the operator was told the rest is served for $0.
func TestTheStoppedAccountCountsEveryPositionTheNextRunRedoes(t *testing.T) {
rows := []store.ChunkStatus{
{Chapter: 1, ChunkIdx: 0, Stage: "draft", Disposition: "ok"},
{Chapter: 1, ChunkIdx: 1, Stage: "draft", Disposition: "flagged", FlagReason: string(pipeline.FlagCancelled)},
{Chapter: 1, ChunkIdx: 2, Stage: "draft", Disposition: "flagged", FlagReason: string(pipeline.FlagRetryUnaffordable)},
// A real verdict: a flag the next run does NOT re-do, so the split has something on both sides and
// «everything is re-done» cannot pass either.
{Chapter: 1, ChunkIdx: 3, Stage: "draft", Disposition: "flagged", FlagReason: string(pipeline.FlagHardRefusal)},
}
var out bytes.Buffer
renderStoppedRun(&out, "test-book",
func() (float64, float64, error) { return 0.004, 0, nil },
func() ([]store.ChunkStatus, error) { return rows, nil })
printed := out.String()
t.Logf("what the operator saw:\n%s", printed)
m := accountNumbers.FindStringSubmatch(printed)
if m == nil {
t.Fatalf("the account does not carry its two counts in the shape this file reads:\n%s", printed)
}
// Two of the four rows are answers (ok + the refusal); two are positions the next run re-does (the
// stop's mark and the money mark).
if m[1] != "2" || m[2] != "2" {
t.Fatalf("the account says %s position(s) with a verdict and %s the next run re-does, want 2 and 2: "+
"a row whose money ran out is NOT an answer — the next run re-attacks it, and counting it as "+
"resolved tells an operator the rest is served for $0", m[1], m[2])
}
}

View file

@ -3608,8 +3608,8 @@
"edits": [
{
"file": "internal/pipeline/cutcall.go",
"find": "\tif !errors.Is(err, context.Canceled) || !errors.As(err, &cut) {",
"replace": "\tif errors.Is(err, context.Canceled) || !errors.As(err, &cut) {"
"find": "\tif errors.Is(err, context.Canceled) && errors.As(err, &cut) {",
"replace": "\tif !errors.Is(err, context.Canceled) && errors.As(err, &cut) {"
}
]
},
@ -3621,9 +3621,9 @@
"battery": true,
"edits": [
{
"file": "internal/pipeline/cutcall.go",
"find": "\treturn FlagReason(cs.FlagReason) != FlagCancelled",
"replace": "\treturn true"
"file": "internal/pipeline/disposition.go",
"find": "\tcase FlagCancelled, FlagRetryUnaffordable:\n\t\treturn false",
"replace": "\tcase FlagRetryUnaffordable:\n\t\treturn false"
}
]
},
@ -3846,8 +3846,8 @@
"edits": [
{
"file": "internal/pipeline/status.go",
"find": "\tFlagDecodeError: 4,\n\tFlagAttemptTimeout: 4,\n",
"replace": "\tFlagDecodeError: 4,\n"
"find": "\tFlagDecodeError: 4,\n\tFlagAttemptTimeout: 4,\n",
"replace": "\tFlagDecodeError: 4,\n"
}
]
},
@ -3897,7 +3897,7 @@
"id": "CUTCALL-a-reply-that-outruns-our-own-write-books-zero",
"why": "net/http hands back a response before WroteRequest fires (Request.write defers it onto the write loop; roundTrip returns on the response channel), so a provider answering EARLY — which DeepSeek documents doing while a request waits to be scheduled — can have its 200 overtake our own callback while the request body still drains. Read as «not delivered» that call books $0 and is re-asked under the full attempt budget: the very leak this pack closes, entering through its own door.",
"package": "./internal/llm/",
"run": "TestAReplyThatOutrunsOurOwnWriteIsStillDelivered",
"run": "TestAReplyThatOutrunsOurOwnWriteIsStillDelivered|TestDeliveryEvidenceIsWhatTheMoneyBitAsks",
"battery": true,
"edits": [
{
@ -3967,7 +3967,7 @@
"id": "CUTCALL-a-refusal-pays-when-the-reply-outruns-the-write",
"why": "the regression the early-200 fix introduced, and the more expensive direction of the same bit. A provider that REFUSES (401/403/413/quota-429) and resets while our body is still writing gives GotFirstResponseByte=true and a WRITE error, so http.Client.Do returns the write failure and the status line is never in our hands. Taking the response byte alone as proof of delivery paid an estimate for every one: measured 22 refusals in 25, one of them $0.80 for a request the provider declined — and the delivered-cut retry sent the whole body again.",
"package": "./internal/llm/",
"run": "TestARefusalIsNotAPurchaseEvenWhenTheReplyOutrunsTheWrite",
"run": "TestARefusalIsNotAPurchaseEvenWhenTheReplyOutrunsTheWrite|TestDeliveryEvidenceIsWhatTheMoneyBitAsks",
"battery": true,
"edits": [
{
@ -4084,7 +4084,7 @@
"edits": [
{
"file": "internal/pipeline/stagerun.go",
"find": "\tdefer func() {\n\t\tr.recordCancelledStage(ctx, cancelledPosition{\n\t\t\tstage: st, chunk: ch, snapshotID: snapID, contentHash: contentHash,\n\t\t\tcumCostUSD: cumCost, attempts: attemptsMade,\n\t\t}, err)\n\t}()\n",
"find": "\tdefer func() {\n\t\tr.recordStoppedPosition(ctx, stoppedPosition{\n\t\t\tstage: st, chunk: ch, snapshotID: snapID, contentHash: contentHash,\n\t\t\tcumCostUSD: cumCost, attempts: attemptsMade, paidAttempts: judged, inHand: last,\n\t\t\tfirstFlagReason: firstFlagReason,\n\t\t}, err)\n\t}()\n",
"replace": ""
}
]
@ -4462,8 +4462,8 @@
"edits": [
{
"file": "internal/pipeline/status.go",
"find": "\tFlagAttemptTimeout: 4,\n",
"replace": "\tFlagAttemptTimeout: 5,\n"
"find": "\tFlagAttemptTimeout: 4,\n",
"replace": "\tFlagAttemptTimeout: 5,\n"
}
]
},
@ -4644,8 +4644,8 @@
"edits": [
{
"file": "internal/pipeline/cutcall.go",
"find": "\tif !errors.Is(err, context.Canceled) || !errors.As(err, &cut) {\n",
"replace": "\tif !errors.As(err, &cut) || cut.Cause != llm.CutByParent {\n"
"find": "\tif errors.Is(err, context.Canceled) && errors.As(err, &cut) {\n",
"replace": "\tif errors.As(err, &cut) && cut.Cause == llm.CutByParent {\n"
}
]
},
@ -4967,5 +4967,818 @@
"replace": "\tn := 0\n\tfor n < len(x) && n < len(y) && x[n] == y[n] {\n\t\tn++\n\t}\n\treturn n >= minStemRunes && len(x)-n <= 1 && len(y)-n <= 1"
}
]
},
{
"id": "Z-empty-remedy-fires-for-length-too",
"why": "the split is the whole pack: a truncated ANSWER must keep buying room (D2.3) and only an EMPTY one buys less thinking; dropping the flag test makes one remedy answer both failures again",
"package": "./internal/pipeline/",
"edits": [
{
"file": "internal/pipeline/stagerun.go",
"find": "\t\t\tif att.cls.Reason == FlagEmpty && r.Pipeline.Retries.LowerEffortOnEmpty {",
"replace": "\t\t\tif r.Pipeline.Retries.LowerEffortOnEmpty {"
}
],
"battery": true
},
{
"id": "Z-lower-effort-ignores-the-knob",
"why": "the knob is what keeps every shipping config byte-identical; ignoring it lands the remedy switched ON for books nobody measured it for",
"package": "./internal/pipeline/",
"edits": [
{
"file": "internal/pipeline/stagerun.go",
"find": "\t\t\tif att.cls.Reason == FlagEmpty && r.Pipeline.Retries.LowerEffortOnEmpty {",
"replace": "\t\t\tif att.cls.Reason == FlagEmpty {"
}
],
"battery": true
},
{
"id": "Z-lowered-attempt-doubles-the-budget-too",
"why": "the point of the remedy is LESS thinking at the SAME budget; doubling as well re-introduces the spend D39.86 falsified and makes the cheaper arm cost the same",
"package": "./internal/pipeline/",
"edits": [
{
"file": "internal/pipeline/stagerun.go",
"find": "\t\t\t\t\teffort = lower\n\t\t\t\t\tregens++",
"replace": "\t\t\t\t\teffort = lower\n\t\t\t\t\tescalations++\n\t\t\t\t\tregens++"
}
],
"battery": true
},
{
"id": "Z-reduction-does-not-spend-the-retry-budget",
"why": "regens is the budget `regenerate_before_escalate` names; if a reduction does not spend it, a unit walks the whole ladder AND then doubles, buying calls the config never granted",
"package": "./internal/pipeline/",
"edits": [
{
"file": "internal/pipeline/stagerun.go",
"find": "\t\t\t\t\teffort = lower\n\t\t\t\t\tregens++",
"replace": "\t\t\t\t\teffort = lower"
}
],
"battery": true
},
{
"id": "Z-ladder-steps-below-the-lowest-emitting-level",
"why": "below `low` lies switching a provider's thinking OFF, which arms the echo mine on dense CJK (D19.1 п.2) — a guardrailed decision a retry policy must never take on its own",
"package": "./internal/config/",
"edits": [
{
"file": "internal/config/models.go",
"find": "\tcase \"medium\":\n\t\tnext = \"low\"",
"replace": "\tcase \"medium\":\n\t\tnext = \"low\"\n\tcase \"low\":\n\t\tnext = \"off\""
}
],
"battery": true
},
{
"id": "Z-ladder-offered-where-the-level-never-reaches-the-wire",
"why": "under extra_body_disable the body carries thinking on/off and no level, so a step there is a byte-identical request at the same price — a second copy of the call that just failed",
"package": "./internal/config/",
"edits": [
{
"file": "internal/config/models.go",
"find": "\tif control != llm.ReasoningNone && control != llm.ReasoningEffortField {\n\t\treturn \"\", false\n\t}",
"replace": "\tif control == llm.ReasoningMandatory {\n\t\treturn \"\", false\n\t}"
}
],
"battery": true
},
{
"id": "Z-unset-effort-steps-down-where-a-floor-was-already-chosen",
"why": "with an off-switch available, `off` is a floor the capability itself chose; stepping from it claims a reduction that is not one and buys a call for nothing",
"package": "./internal/config/",
"edits": [
{
"file": "internal/config/models.go",
"find": "\t\tif control != llm.ReasoningNone {\n\t\t\treturn \"\", false\n\t\t}",
"replace": "\t\tif false {\n\t\t\treturn \"\", false\n\t\t}"
}
],
"battery": true
},
{
"id": "Z-subset-thinking-count-dropped-again",
"why": "the provider reports it on every reply and the adapter used to throw it away; that is why the one column able to say «thinking ate the budget» read 0 for the life of the project",
"package": "./internal/llm/",
"edits": [
{
"file": "internal/llm/provider_openai.go",
"find": "\t\tif rt, reported := resp.Usage.reasoningDetail(); reported {\n\t\t\tusage.ReasoningInCompletion = &rt\n\t\t}",
"replace": "\t\t_ = resp.Usage"
}
],
"battery": true
},
{
"id": "Z-subset-thinking-billed-a-second-time",
"why": "ledger.CostUSD prices CompletionTokens + ReasoningTokens; on a subset provider the thinking is already inside the completion, and surfacing it as money inflates every DeepSeek bill by the measured ~60%",
"package": "./internal/llm/",
"edits": [
{
"file": "internal/llm/provider_openai.go",
"find": "\t\t\tusage.ReasoningInCompletion = &rt",
"replace": "\t\t\tusage.ReasoningTokens = rt"
}
],
"battery": true
},
{
"id": "Z-absent-thinking-field-reads-as-a-measured-zero",
"why": "a provider that reported nothing and a call that did not think are different answers; spelling both 0 is the exact blindness the column was added to remove",
"package": "./internal/llm/",
"edits": [
{
"file": "internal/llm/httpllm.go",
"find": "\tif u.CompletionTokensDetails.ReasoningTokens == nil {\n\t\treturn 0, false\n\t}",
"replace": "\tif u.CompletionTokensDetails.ReasoningTokens == nil {\n\t\treturn 0, true\n\t}"
}
],
"battery": true
},
{
"id": "Z-telemetry-writes-a-zero-instead-of-no-answer",
"why": "the column is nullable ON PURPOSE; writing 0 for a row nobody asked makes every book already on disk claim its calls did not think",
"package": "./internal/store/",
"edits": [
{
"file": "internal/store/requestlog.go",
"find": "\t\trl.CompletionTokens, rl.ReasoningTokens, rl.ReasoningInCompletion,",
"replace": "\t\trl.CompletionTokens, rl.ReasoningTokens, 0,"
}
],
"battery": true
},
{
"id": "Z-price-substitution-reports-itself-as-the-answerer",
"why": "the basis is the only thing that separates «billed by the model that answered» from «billed by one that did not»; collapsing it silences the warning and the report legend at once",
"package": "./internal/ledger/",
"edits": [
{
"file": "internal/ledger/pricing.go",
"find": "\t\treturn mp, PriceByRequested",
"replace": "\t\treturn mp, PriceByAnswerer"
}
],
"battery": true
},
{
"id": "Z-thrown-away-total-counts-replayed-rows",
"why": "a replayed row bought nothing this run; counting it inflates both the waste and its share, and the line's whole value is that the share is arguable",
"package": "./cmd/tmctl/",
"edits": [
{
"file": "cmd/tmctl/render.go",
"find": "\tcase row.TMHit != 0, row.OK != 0:\n\t\treturn false",
"replace": "\tcase row.OK != 0:\n\t\treturn false"
}
],
"battery": true
},
{
"id": "Z-thrown-away-share-counts-replayed-rows",
"why": "the share is the argument, and a replayed row's money was already booked by the row it replays; counting it in the denominator makes a quarter of a book's spend read as a seventh",
"package": "./cmd/tmctl/",
"edits": [
{
"file": "cmd/tmctl/render.go",
"find": "\t\t\tif row.TMHit == 0 {\n\t\t\t\tpaid += row.CostUSD\n\t\t\t}",
"replace": "\t\t\tif true {\n\t\t\t\tpaid += row.CostUSD\n\t\t\t}"
}
],
"battery": true
},
{
"id": "Z-thinking-column-prints-zero-for-no-answer",
"why": "a provider that reported nothing and a call that did not think are different answers; a cell that spells both 0 re-creates in the report the exact blindness the column was added to remove",
"package": "./cmd/tmctl/",
"edits": [
{
"file": "cmd/tmctl/render.go",
"find": "\tif n == nil {\n\t\treturn \"?\"\n\t}",
"replace": "\tif n == nil {\n\t\treturn \"0\"\n\t}"
}
],
"battery": true
},
{
"id": "Z-thinking-column-collapses-a-measured-zero",
"why": "a provider that reported 0 has ANSWERED the question; spelling that answer the same way as silence is the exact blindness the column was added to remove, in the one place a reader meets it",
"package": "./cmd/tmctl/",
"edits": [
{
"file": "cmd/tmctl/render.go",
"find": "\tif n == nil {\n\t\treturn \"?\"\n\t}",
"replace": "\tif n == nil || *n == 0 {\n\t\treturn \"?\"\n\t}"
}
],
"battery": true
},
{
"id": "Z-thinking-column-defaults-old-rows-to-zero",
"why": "a DEFAULT on the new column makes every row of every book already on disk claim its call did not think — a claim nobody measured, about the very runs this column exists to explain",
"package": "./internal/store/",
"edits": [
{
"file": "internal/store/migrate.go",
"find": "\tALTER TABLE request_log ADD COLUMN reasoning_in_completion INTEGER;",
"replace": "\tALTER TABLE request_log ADD COLUMN reasoning_in_completion INTEGER NOT NULL DEFAULT 0;"
}
],
"battery": true
},
{
"id": "Z-waste-line-counts-calls-that-cost-nothing",
"why": "a transport failure that never reached a billed response writes ok=0 with cost_usd=0; counting it inflates the CALL count of a line whose whole subject is money, and prints a bucket the money side cannot explain",
"package": "./cmd/tmctl/",
"edits": [
{
"file": "cmd/tmctl/render.go",
"find": "\tcase row.CostUSD <= 0:\n\t\treturn false",
"replace": "\tcase false:\n\t\treturn false"
}
],
"battery": true
},
{
"id": "Z-waste-line-counts-answers-that-shipped",
"why": "a cosmetic sanitizer strip is not an ok verdict and its cleaned text IS exported; counting it says the reader paid for nothing when the reader got the text",
"package": "./cmd/tmctl/",
"edits": [
{
"file": "cmd/tmctl/render.go",
"find": "\tcase row.Degraded == string(pipeline.FlagSanitizerStripped):\n\t\treturn false",
"replace": "\tcase false:\n\t\treturn false"
}
],
"battery": true
},
{
"id": "Z-ladder-steps-to-a-word-the-loader-never-validated",
"why": "a step outside the neutral vocabulary reaches the wire from the retry path without ever passing the gate that word list exists to be; the vendor's own levels are unreachable from our config for exactly that reason",
"package": "./internal/config/",
"edits": [
{
"file": "internal/config/models.go",
"find": "\tcase \"high\":\n\t\tnext = \"medium\"",
"replace": "\tcase \"high\":\n\t\tnext = \"minimal\""
}
],
"battery": true
},
{
"id": "Z-running-report-hides-the-thinking-share",
"why": "the post-mortem reader learns after the money is spent; the operator watching a paid run is the one who can still act, and the stage line is where they look",
"package": "./cmd/tmctl/",
"edits": [
{
"file": "cmd/tmctl/render.go",
"find": "\t\t\tfmt.Fprintf(w, \" %-8s %-22s %-8s %-8s%s $%.6f (cum $%.6f) in=%d (cached=%d) out=%d+%d think=%s att=%d %dms finish=%s\\n\",\n\t\t\t\tst.Stage, st.Model, how, st.Disposition, flagSuffix(st.FlagReason),\n\t\t\t\tst.CostUSD, st.CumCostUSD,\n\t\t\t\tst.Usage.PromptTokens, st.Usage.CachedTokens,\n\t\t\t\tst.Usage.CompletionTokens, st.Usage.ReasoningTokens, thinkCell(st.Usage.ReasoningInCompletion),\n\t\t\t\tst.Attempts, st.LatencyMS, st.FinishReason)",
"replace": "\t\t\tfmt.Fprintf(w, \" %-8s %-22s %-8s %-8s%s $%.6f (cum $%.6f) in=%d (cached=%d) out=%d+%d att=%d %dms finish=%s\\n\",\n\t\t\t\tst.Stage, st.Model, how, st.Disposition, flagSuffix(st.FlagReason),\n\t\t\t\tst.CostUSD, st.CumCostUSD,\n\t\t\t\tst.Usage.PromptTokens, st.Usage.CachedTokens,\n\t\t\t\tst.Usage.CompletionTokens, st.Usage.ReasoningTokens,\n\t\t\t\tst.Attempts, st.LatencyMS, st.FinishReason)"
}
],
"battery": true
},
{
"id": "Z-shipping-config-can-turn-the-remedy-on-silently",
"why": "the gate stands over DATA, so the planting has to be data: one word in a shipping YAML turns an unmeasured spend on, and before this gate nothing in the tree went red for it — the pack's own report calls the landing INERT, which without a gate is a promise, not a property",
"package": "./internal/config/",
"battery": true,
"edits": [
{
"file": "configs/pipeline-c1.yaml",
"find": " regenerate_echo_before_escalate: 1",
"replace": " regenerate_echo_before_escalate: 1\n lower_effort_on_empty: true"
}
]
},
{
"id": "Z-lowering-warn-loses-its-levels",
"battery": true,
"why": "the asked-for effort lives in the request hash and nowhere else, so this WARN is the only place a run ever records WHICH level an attempt was bought at; the operator-message catalogue compares source literals and stays green when the fields go",
"package": "./internal/pipeline/",
"edits": [
{
"file": "internal/pipeline/stagerun.go",
"find": "\t\t\t\t\t\"effort\", effort, \"next_effort\", lower, \"max_tokens\", maxTokens)",
"replace": "\t\t\t\t\t\"max_tokens\", maxTokens)"
}
]
},
{
"id": "Z-price-substitution-warn-silenced",
"battery": true,
"why": "the price basis is computed at settle and stored nowhere; silence it and the pack's claim «the live path warns» rests on a line that never sounds, with the catalogue gate still green",
"package": "./internal/pipeline/",
"edits": [
{
"file": "internal/pipeline/stagerun.go",
"find": "\tif basis.Substituted() {",
"replace": "\tif false && basis.Substituted() {"
}
]
},
{
"id": "Z-unnamed-bucket-prints-empty",
"battery": true,
"why": "a money line that counts a bucket and cannot name it gives the reader a number they have no way to ask about — the defect the first review found in this very line",
"package": "./cmd/tmctl/",
"edits": [
{
"file": "cmd/tmctl/render.go",
"find": "\t\t\treason = \"unnamed\"",
"replace": "\t\t\treason = \"\""
}
]
},
{
"id": "Z-thinking-share-marshals-on-every-call",
"battery": true,
"why": "Usage is serialised into every checkpoint; without omitempty a provider that reports nothing writes different bytes than before the field existed, breaking the byte-identity discipline its own comment claims",
"package": "./internal/llm/",
"edits": [
{
"file": "internal/llm/llm.go",
"find": "\tReasoningInCompletion *int `json:\",omitempty\"`",
"replace": "\tReasoningInCompletion *int"
}
]
},
{
"id": "Z-ladder-answers-for-a-model-it-never-saw",
"battery": true,
"why": "an unlisted slug resolves to the OpenAI-compat baseline, so the ladder would answer «step to low» for a model with no declared wire — and for the empty string, which is what a caller reaching for the wrong field hands over",
"package": "./internal/config/",
"edits": [
{
"file": "internal/config/models.go",
"find": "\tif _, known := m.Models[modelName]; !known {\n\t\treturn \"\", false\n\t}",
"replace": "\tif false {\n\t\treturn \"\", false\n\t}"
}
]
},
{
"id": "RETRYSTOP-a-refused-re-attack-leaves-no-mark",
"battery": true,
"why": "a ceiling that refuses the RE-ATTACK of an already-paid attempt used to leave the position with no chunk_status row at all, so the unit read `pending` — indistinguishable from one nobody had started — while its first attempt was paid for and on disk. Measured on this fixture before the branch existed: committed=$0.003640 with ONE row for the two units the run had touched (backlog row 291)",
"package": "./internal/pipeline/",
"run": "TestTheRefusedRetryLeavesTheUnitAMarkAndNotAnEmptyPosition",
"edits": [
{
"file": "internal/pipeline/cutcall.go",
"find": "\tif p.paidAttempts > 0 && errors.Is(err, errReserveCeiling) {\n\t\treturn stopMark{\n\t\t\treason: FlagRetryUnaffordable,\n\t\t\tdetail: ceilingStopDetail(p, err),\n\t\t\t// \u26a0 ONE FEWER THAN `attempts`, AND THE BRANCH ABOVE DOES NOT SUBTRACT \u2014 the two stops differ\n\t\t\t// in exactly this. `attempts` is the index the loop is ON (attemptsMade = attempt + 1, set\n\t\t\t// before the error check), so for a cancelled call it counts the call that DID go out, while\n\t\t\t// here the index it counts bought nothing at all. What is left is every index this position\n\t\t\t// really consumed, burned keys included \u2014 the same thing the ok path's count includes, so a\n\t\t\t// row cut from `paidAttempts` instead would silently drop a burn this position paid for.\n\t\t\t//\n\t\t\t// \u26a0 IT CANNOT GO NEGATIVE, and the reason is not local: `paidAttempts > 0` above means the loop\n\t\t\t// classified something, and the loop sets attemptsMade = attempt + 1 \u2265 1 before any error is\n\t\t\t// read (stagerun.go). A future shape that marked a position without that guarantee would have\n\t\t\t// to bring the floor with it.\n\t\t\tattempts: p.attempts - 1,\n\t\t\t// \u26a0 BOTH MARKS CARRY IT, and the cancelled one did not until this pack: a position stopped over\n\t\t\t// its SECOND attempt has a first failure too, and the same blinding applied to it. One rule for\n\t\t\t// the two stop marks rather than a rule and an exception.\n\t\t\tfirstFlag: recoveredFirstFlag(p.firstFlagReason, FlagRetryUnaffordable),\n\t\t}, true\n\t}\n",
"replace": ""
}
]
},
{
"id": "RETRYSTOP-the-mark-is-keyed-on-the-attempt-count",
"battery": true,
"why": "the attempt COUNT includes the index the reservation was refused at (attemptsMade = attempt + 1, set before the error check) and the indices walked over burned keys — so keyed on it, the mark is written for a position that was never translated once, and a flagged row is an answer a resume serves: the unit goes terminal holding burned money and no text",
"package": "./internal/pipeline/",
"run": "TestACeilingRefusingAFreshAttemptLeavesNoRow|TestStopMarkForAsksWhetherAnythingWasEverBought",
"edits": [
{
"file": "internal/pipeline/cutcall.go",
"find": "\tif p.paidAttempts > 0 && errors.Is(err, errReserveCeiling) {",
"replace": "\tif p.attempts > 0 && errors.Is(err, errReserveCeiling) {"
}
]
},
{
"id": "RETRYSTOP-the-mark-is-keyed-on-the-money",
"battery": true,
"why": "«money was spent on this position» is the OTHER plausible reading of the predicate and it is wrong in one case that really happens: a stopped run leaves a BURNED key — money with no result — and nothing classified, so a mark keyed on cost invents a half-done unit out of a position whose only purchase produced nothing",
"package": "./internal/pipeline/",
"run": "TestStopMarkForAsksWhetherAnythingWasEverBought",
"edits": [
{
"file": "internal/pipeline/cutcall.go",
"find": "\tif p.paidAttempts > 0 && errors.Is(err, errReserveCeiling) {",
"replace": "\tif p.cumCostUSD > 0 && errors.Is(err, errReserveCeiling) {"
}
]
},
{
"id": "RETRYSTOP-the-mark-counts-the-refused-reservation",
"battery": true,
"why": "a refused reservation is not an attempt that happened: counting it puts a call that was never dialled into the row a person reads, and the row then disagrees with the checkpoints the money is summed from",
"package": "./internal/pipeline/",
"run": "TestTheRefusedRetryLeavesTheUnitAMarkAndNotAnEmptyPosition|TestStopMarkForAsksWhetherAnythingWasEverBought",
"edits": [
{
"file": "internal/pipeline/cutcall.go",
"find": "\t\t\tattempts: p.attempts - 1,",
"replace": "\t\t\tattempts: p.attempts,"
}
]
},
{
"id": "RETRYSTOP-the-money-mark-is-served-as-an-answer",
"battery": true,
"why": "`retry_unaffordable` is the one flag money CURES (D4: raise the ceiling, resume, the reader gets a good translation). Read as a resolved verdict, the resume serves it for $0 and never re-attacks — the unit is permanently degraded and the only way back is a redrive, which DELETES the checkpoints and re-buys attempt 0 as well",
"package": "./internal/pipeline/",
"run": "TestToppingUpByTheStatedShortfallFinishesTheUnit",
"edits": [
{
"file": "internal/pipeline/disposition.go",
"find": "\tcase FlagCancelled, FlagRetryUnaffordable:\n\t\treturn false",
"replace": "\tcase FlagCancelled:\n\t\treturn false"
}
]
},
{
"id": "RETRYSTOP-the-mark-borrows-the-cancelled-reason",
"battery": true,
"why": "nobody stopped that run by hand. A flag lying about its cause is forbidden in its own right (D39.93 п.2): `cancelled` sends an operator looking for a stop button that was never pressed, and hides the one remedy that works — topping up",
"package": "./internal/pipeline/",
"run": "TestTheRefusedRetryLeavesTheUnitAMarkAndNotAnEmptyPosition",
"edits": [
{
"file": "internal/pipeline/cutcall.go",
"find": "\t\t\treason: FlagRetryUnaffordable,",
"replace": "\t\t\treason: FlagCancelled,"
}
]
},
{
"id": "RETRYSTOP-the-detail-forgets-what-the-paid-attempt-answered",
"battery": true,
"why": "the reason column now says why the re-attack never happened, so what the PAID attempt came back as is on the row or nowhere: the WARN line carrying it dies with the process, and the next morning nobody can tell a truncation that needed a bigger budget from an empty reply that needed less thinking",
"package": "./internal/pipeline/",
"run": "TestTheRefusedRetryLeavesTheUnitAMarkAndNotAnEmptyPosition|TestStopMarkForAsksWhetherAnythingWasEverBought",
"edits": [
{
"file": "internal/pipeline/cutcall.go",
"find": "\treturn fmt.Sprintf(\"attempt %d was paid for and came back %s; %s refused to reserve the re-attack%s \u2014 raise the ceiling and the resume finishes this unit\",\n\t\tp.inHand.attempt, p.inHand.cls.Reason, ceiling, missing)",
"replace": "\treturn fmt.Sprintf(\"attempt %d was paid for; %s refused to reserve the re-attack%s \u2014 raise the ceiling and the resume finishes this unit\",\n\t\tp.inHand.attempt, ceiling, missing)"
}
]
},
{
"id": "RETRYSTOP-a-stop-that-states-no-shortfall-prints-a-zero",
"battery": true,
"why": "a day-scope refusal states no shortfall (the day ceiling sums every book in the store, so this book's committed figure is not the day's). Printed as «short by 0 micro-USD» it reads as «you are not short of anything», which is the opposite of what happened",
"package": "./internal/pipeline/",
"run": "TestStopMarkForAsksWhetherAnythingWasEverBought",
"edits": [
{
"file": "internal/pipeline/cutcall.go",
"find": "\tif shortfall > 0 {\n\t\tmissing = fmt.Sprintf(\", short by %d micro-USD\", shortfall)",
"replace": "\tif shortfall >= 0 {\n\t\tmissing = fmt.Sprintf(\", short by %d micro-USD\", shortfall)"
}
]
},
{
"id": "RETRYSTOP-the-money-mark-ranks-with-the-budget-symptom",
"battery": true,
"why": "`retry_unaffordable` IS a length/empty failure plus the fact that the remedy could not be bought. Ranked with them, a chapter whose unit died for lack of money reports «truncated» as its worst problem and sends a person to fix a budget formula instead of topping up",
"package": "./internal/pipeline/",
"run": "TestTheSeverityTableMeansWhatItsCommentsSay",
"edits": [
{
"file": "internal/pipeline/status.go",
"find": "\tFlagRetryUnaffordable: 4,",
"replace": "\tFlagRetryUnaffordable: 6,"
}
]
},
{
"id": "ECHOREC-a-superseded-echo-counts-as-recovered",
"battery": true,
"why": "«no longer the verdict» is not «recovered». A re-roll that came back with a DIFFERENT failure leaves first_flag_reason=cjk_artifact on a FLAGGED row that ships no text, and counted as a recovery the report claims we fixed an echo nobody fixed. Live rather than latent: regenerate_echo_before_escalate: 1 stands in all four shipping pipelines",
"package": "./internal/pipeline/",
"run": "TestASupersededEchoIsNotCountedAsRecovered",
"edits": [
{
"file": "internal/pipeline/quality.go",
"find": "\t\t\t\tdraftEcho++\n\t\t\t\tif shippedText(cs) {\n\t\t\t\t\tdraftEchoRecovered++\n\t\t\t\t}",
"replace": "\t\t\t\tdraftEcho++\n\t\t\t\tdraftEchoRecovered++"
}
]
},
{
"id": "STOPACC-the-account-names-one-cause-instead-of-asking-the-rule",
"battery": true,
"why": "the stopped-run account splits a book's rows into «answers the next run serves for $0» and «positions it re-does». Spelled as a reason NAME it is a second copy of pipeline.ResolvedForResume, and the day a second non-resolved reason existed (a retry whose money ran out) the copy counted a position the next run re-attacks among the ones WITH a verdict — an operator reading that is told the rest is served for $0 and that the book is further along than it is",
"package": "./cmd/tmctl/",
"run": "TestTheStoppedAccountCountsEveryPositionTheNextRunRedoes",
"edits": [
{
"file": "cmd/tmctl/render.go",
"find": "\t\tif !pipeline.ResolvedForResume(&cs[i]) {",
"replace": "\t\tif pipeline.FlagReason(cs[i].FlagReason) == pipeline.FlagCancelled {"
}
]
},
{
"id": "READERHOLE-the-stop-mark-tells-the-reader-to-wait-for-a-human",
"battery": true,
"why": "the withheld sentence tells the reader the fragment «требует проверки человеком» (langpacks/<target>/reader.txt). For a stop MARK — a call a person cut, a re-attack a ceiling refused — that is false: nothing is wrong with the fragment, it was paid for and not finished, and the next run does it. A reader acting on that sentence waits for a human who cannot help, and the operator's refusal text says the same thing about a unit that needs only money",
"package": "./internal/pipeline/",
"run": "TestTheReadersFileDoesNotSendThemToWaitForAHumanWhoCannotHelp",
"edits": [
{
"file": "internal/pipeline/bookbuild.go",
"find": "\t\t\tif FlagReason(ce.FlagReason).AnswersForResume() || ce.DroppedMembers > 0 {\n\t\t\t\tmark(words.HoleWithheld)\n\t\t\t} else {\n\t\t\t\tmark(words.HolePending)\n\t\t\t}\n",
"replace": "\t\t\tmark(words.HoleWithheld)\n"
}
]
},
{
"id": "READERHOLE-every-withheld-unit-becomes-merely-untranslated",
"battery": true,
"why": "the other direction of the same branch, and the one a careless fix produces: saying «ещё не переведён» about a REFUSAL or a contaminated output hides the only holes a human can actually do something about. The reader is then told to wait for a run that will never fix it",
"package": "./internal/pipeline/",
"run": "TestTheReadersFileDoesNotSendThemToWaitForAHumanWhoCannotHelp",
"edits": [
{
"file": "internal/pipeline/bookbuild.go",
"find": "\t\t\tif FlagReason(ce.FlagReason).AnswersForResume() || ce.DroppedMembers > 0 {\n\t\t\t\tmark(words.HoleWithheld)\n\t\t\t} else {\n\t\t\t\tmark(words.HolePending)\n\t\t\t}\n",
"replace": "\t\t\tmark(words.HolePending)\n"
}
]
},
{
"id": "RETRYSTOP-the-mark-forgets-the-echo-the-book-paid-for",
"battery": true,
"why": "the mark's own reason says why the PURCHASE did not happen, so what the paid attempt came back as lives in first_flag_reason or nowhere. Dropped, a draft that ECHOED and whose re-roll a ceiling refused stays in the echo metric's denominator and leaves its numerator: the echo rate FALLS on the very run that bought the echo — the 25.07 shape («echo_draft=0.0% of 20»), one column along, invisible to every other surface",
"package": "./internal/pipeline/",
"run": "TestAnEchoSomebodyPaidForStaysInTheMetricWhenTheMoneyRanOut|TestStopMarkForAsksWhetherAnythingWasEverBought",
"edits": [
{
"file": "internal/pipeline/cutcall.go",
"find": "\t\t\tfirstFlag: recoveredFirstFlag(p.firstFlagReason, FlagRetryUnaffordable),",
"replace": "\t\t\tfirstFlag: \"\","
}
]
},
{
"id": "RETRYSTOP-the-cancelled-mark-forgets-what-failed-first",
"battery": true,
"why": "the same column on the other stop mark: a run cut over a position's SECOND attempt has a first failure too, and without it that echo leaves the metric the same way. This half had no carrier at all before the pack — the cancelled mark never wrote the column",
"package": "./internal/pipeline/",
"run": "TestStopMarkForAsksWhetherAnythingWasEverBought",
"edits": [
{
"file": "internal/pipeline/cutcall.go",
"find": "\t\t\tfirstFlag: recoveredFirstFlag(p.firstFlagReason, FlagCancelled),",
"replace": "\t\t\tfirstFlag: \"\","
}
]
},
{
"id": "RETRYSTOP-a-redrive-buys-the-paid-attempt-again",
"battery": true,
"why": "a redrive RESETS its targets, and ResetChunkStages DELETES their checkpoints — including the one holding the already-paid text of attempt 0 behind a `retry_unaffordable` mark. Read as an ordinary flagged row, the operator's default gesture after a run full of flags buys that attempt a second time, while the resume needed only the re-attack. Before the mark existed the position had no row and a redrive could not reach it at all",
"package": "./internal/pipeline/",
"run": "TestARedriveDoesNotBuyThePaidAttemptAgain",
"edits": [
{
"file": "internal/pipeline/status.go",
"find": "\t\t\tif cs.Disposition == string(DispFlagged) && resolvedForResume(&cs) && sel.matches(cs) {",
"replace": "\t\t\tif cs.Disposition == string(DispFlagged) && sel.matches(cs) {"
}
]
},
{
"id": "RETRYSTOP-a-marked-unit-is-charged-a-grant-slot",
"battery": true,
"why": "the completeness test predicts «no provider call», and a stop mark is a position the next run RE-DOES for money. Counted as recorded, the unit skips the `carried` class and lands in `rework`, which TAKES a grant slot (granted()) and is queued behind fresh book — so a purchase of N chapters pays for a unit an earlier run already paid to START, which is exactly what the --max-units help text promises never happens",
"package": "./internal/pipeline/",
"run": "TestAMarkedUnitIsCarriedAndNotChargedAGrantSlot",
"edits": [
{
"file": "internal/pipeline/volume.go",
"find": "\tanswered, _, want := unitPositionsOnFile(u, rows, draftStages, editStages)\n\treturn answered == want",
"replace": "\t_, started, want := unitPositionsOnFile(u, rows, draftStages, editStages)\n\treturn started == want"
}
]
},
{
"id": "RETRYSTOP-the-wave-counter-reports-the-book-as-done",
"battery": true,
"why": "the wave counters are what the MONEY WIRE is made of (events.go beginWaves → moneyLedger → units_resolved/units_deferred). Counting a stop mark as a resolution makes the resume of a paused book publish «resolved 2, deferred 0» on a run that bought nothing and left the same hole — the buyer deciding whether to top up is told the book owes nothing. Measured before the guard: done 1→2 and deferred 1→0 between two identical stopped runs",
"package": "./internal/pipeline/",
"run": "TestTheWaveCountersDoNotTellTheBuyerTheBookIsDone",
"edits": [
{
"file": "internal/pipeline/status.go",
"find": "\t\tif cs.Disposition == string(DispFlagged) && !resolvedForResume(&cs) {\n\t\t\tcontinue\n\t\t}",
"replace": ""
}
]
},
{
"id": "RETRYSTOP-the-plaintext-export-calls-a-stop-mark-a-humans-problem",
"battery": true,
"why": "the third surface of one sentence: «not translated, flagged for a human» is false of a position that was paid for and not finished — no human has anything to check, the next run does it. The book writer and the stopped-run account were corrected first and this banner kept saying the opposite in the same file",
"package": "./cmd/tmctl/",
"run": "TestThePlaintextExportDoesNotCallAStopMarkAHumansProblem",
"edits": [
{
"file": "cmd/tmctl/render.go",
"find": "\t\t\tcase !pipeline.FlagReason(ce.FlagReason).AnswersForResume() && ce.DroppedMembers == 0:",
"replace": "\t\t\tcase false:"
}
]
},
{
"id": "ECHOREC-the-editors-own-echo-leaves-the-metric",
"battery": true,
"why": "the edit counter sees an editor echo only while it is still the row's VERDICT. Without the second arm, an echo a re-roll RECOVERED is invisible (older), and an echo whose re-roll a ceiling refused leaves the numerator while its mark row stays in `editRows` — so the edit echo RATE FALLS on the run that just paid for an editor echo. Measured on that shape: cost_usd=0.001820 with echo_edit_units=0",
"package": "./internal/pipeline/",
"run": "TestTheEDITORsOwnEchoStaysCountableWhenTheMoneyRanOut",
"edits": [
{
"file": "internal/pipeline/quality.go",
"find": "\t\t\tcase cs.FirstFlagReason == string(FlagCJKArtifact):\n\t\t\t\teditEcho++\n\t\t\t\tif shippedText(cs) {\n\t\t\t\t\teditEchoRecovered++\n\t\t\t\t}\n\t\t\t}",
"replace": "\t\t\t}"
}
]
},
{
"id": "READERHOLE-a-lost-member-is-promised-back-by-the-next-run",
"battery": true,
"why": "a c-lite unit can carry a PERMANENTLY flagged member (its text is gone whatever the next run buys) together with a stop mark on the stage that would have assembled it. «Paid for and NOT done — the next run re-does it» is then a half-truth that hides the half a human has to act on, and the gap marker is not printed either",
"package": "./cmd/tmctl/",
"run": "TestThePlaintextExportDoesNotCallAStopMarkAHumansProblem",
"edits": [
{
"file": "cmd/tmctl/render.go",
"find": "\t\t\tcase !pipeline.FlagReason(ce.FlagReason).AnswersForResume() && ce.DroppedMembers == 0:",
"replace": "\t\t\tcase !pipeline.FlagReason(ce.FlagReason).AnswersForResume():"
}
]
},
{
"id": "READERHOLE-the-reader-is-promised-a-member-no-purchase-returns",
"battery": true,
"why": "the same half-truth in the reader's own file: «ещё не переведён» about a unit whose member is permanently flagged promises a next run that cannot bring that text back. The withheld phrase asks for a human, which is what the lost member needs",
"package": "./internal/pipeline/",
"run": "TestTheReadersFileDoesNotSendThemToWaitForAHumanWhoCannotHelp",
"edits": [
{
"file": "internal/pipeline/bookbuild.go",
"find": "\t\t\tif FlagReason(ce.FlagReason).AnswersForResume() || ce.DroppedMembers > 0 {",
"replace": "\t\t\tif FlagReason(ce.FlagReason).AnswersForResume() {"
}
]
},
{
"id": "READERHOLE-the-operator-is-told-only-the-cheerful-half",
"battery": true,
"why": "the build's refusal text is what the person who can ACT reads. A unit can be «paid for and not done» AND short a member for good; told only the first half he tops up, resumes, and the build refuses again with a different hole. The drop count was not even on the withheld hole record, so this surface could not ask the question the other two had learned",
"package": "./internal/pipeline/",
"run": "TestTheOperatorsRefusalTellsHimBothHalves",
"edits": [
{
"file": "internal/pipeline/bookbuild.go",
"find": "\t\t\tcase h.Dropped > 0:",
"replace": "\t\t\tcase false:"
}
]
},
{
"id": "ECHOREC-a-recovered-echo-loses-its-recovery-when-the-strip-ships-it",
"battery": true,
"why": "the recovery question is «did the unit SHIP», not «is the row ok». Keyed on the ok verdict alone it loses a real cure: the hop's answer can come back with a cosmetic leak, which the sanitizer strips and SHIPS (final_hash points at the derived export), so an echo that was genuinely fixed reports as none — on the three shipping pipelines where the sanitizer gate is on",
"package": "./internal/pipeline/",
"run": "TestARecoveredEchoIsReportedAsRecoveredOnBothStages",
"edits": [
{
"file": "internal/pipeline/quality.go",
"find": "func shippedText(cs store.ChunkStatus) bool { return cs.FinalHash != \"\" }",
"replace": "func shippedText(cs store.ChunkStatus) bool { return cs.Disposition == string(DispOK) }"
}
]
},
{
"id": "ECHOREC-the-editors-recovered-subset-leaves-the-report",
"battery": true,
"why": "the FIELD echo_edit_recovered is what keeps the widened numerator readable: the counter now counts an echo the editor made whatever happened next, so a book that recovered one reports echo_edit=1 with nothing saying it cost the book nothing. Attacks the subset, not the surface that prints it — the operator's own line is ECHOHEAD-* in ./cmd/tmctl/",
"package": "./internal/pipeline/",
"run": "TestARecoveredEchoIsReportedAsRecoveredOnBothStages",
"edits": [
{
"file": "internal/pipeline/quality.go",
"find": "\t\t\t\tif shippedText(cs) {\n\t\t\t\t\teditEchoRecovered++\n\t\t\t\t}",
"replace": ""
}
]
},
{
"id": "ECHOHEAD-the-editors-recovered-share-leaves-the-operators-line",
"battery": true,
"why": "the line the operator reads is the surface the recovered split exists for, and it had no catcher at all: mutating editRecovered away left ./cmd/tmctl/ green end to end. Without the clause he reads «echo edit=2 (25.0%)» over a book whose editor echo was cured and whose prose shipped clean",
"package": "./cmd/tmctl/",
"run": "TestTheOperatorsEchoLineCarriesBothRecoveredShares",
"edits": [
{
"file": "cmd/tmctl/render.go",
"find": "\tif q.EchoEditRecovered > 0 {\n\t\teditRecovered = fmt.Sprintf(\", %d recovered\", q.EchoEditRecovered)\n\t}",
"replace": ""
}
]
},
{
"id": "ECHOHEAD-the-drafts-recovered-share-leaves-the-operators-line",
"battery": true,
"why": "the translator half of the same line, unpinned since it was built (the pack that copied its numerator to the editor copied the gap too). Without it a book whose every draft echo the escalation fixed reads «echo draft=4 (50.0%)» — a rate that looks like live breakage and cost nothing beyond the wasted primary call",
"package": "./cmd/tmctl/",
"run": "TestTheOperatorsEchoLineCarriesBothRecoveredShares",
"edits": [
{
"file": "cmd/tmctl/render.go",
"find": "\tif q.EchoDraftRecovered > 0 {\n\t\trecovered = fmt.Sprintf(\", %d recovered by escalation\", q.EchoDraftRecovered)\n\t}",
"replace": ""
}
]
},
{
"id": "ECHOHEAD-the-editors-slot-prints-the-drafts-share",
"battery": true,
"why": "two stages, two shares, one line: a writer that fills the editor's slot from the draft counter tells the operator the EDITOR recovered what the translator did. Caught only by a fixture where one stage recovered and the other did not, which is why the pin carries that table rather than a single all-non-zero case",
"package": "./cmd/tmctl/",
"run": "TestOneStagesRecoveryDoesNotFillTheOtherStagesSlot",
"edits": [
{
"file": "cmd/tmctl/render.go",
"find": "\tif q.EchoEditRecovered > 0 {\n\t\teditRecovered = fmt.Sprintf(\", %d recovered\", q.EchoEditRecovered)\n\t}",
"replace": "\teditRecovered = recovered"
}
]
},
{
"id": "ECHOHEAD-a-recovered-clause-is-printed-at-zero",
"battery": true,
"why": "the clause must be ABSENT when nothing was recovered, or the report tells the operator a book recovered echoes it never had — the mirror of the defect and the reason the pin carries a zero control at all",
"package": "./cmd/tmctl/",
"run": "TestTheRecoveredSharesAreAbsentWhenNothingWasRecovered",
"edits": [
{
"file": "cmd/tmctl/render.go",
"find": "\tif q.EchoEditRecovered > 0 {",
"replace": "\tif q.EchoEditRecovered >= 0 {"
}
]
},
{
"id": "ECHOHEAD-the-totals-line-swaps-its-two-counters",
"battery": true,
"why": "«with a final-stage row» and «with export text» are the two halves of the money question — how many positions the book has spent on versus how many a reader can open — and a stop mark is exactly the unit where they differ. Swapped, the headline reports the book as more finished than it is",
"package": "./cmd/tmctl/",
"run": "TestTheOperatorsEchoLineCarriesBothRecoveredShares",
"edits": [
{
"file": "cmd/tmctl/render.go",
"find": "q.TotalUnits, q.ProcessedUnits, q.TextUnits)",
"replace": "q.TotalUnits, q.TextUnits, q.ProcessedUnits)"
}
]
},
{
"id": "ECHOHEAD-a-draft-recovered-clause-is-printed-at-zero",
"battery": true,
"why": "the mirror of the editor's zero case, and the catalogue under-recorded what the pin already guards: printed at zero the translator half tells the operator a book recovered echoes it never had. Both halves of one line, both plantings, or the next reader of the catalogue believes one covers two",
"package": "./cmd/tmctl/",
"run": "TestTheRecoveredSharesAreAbsentWhenNothingWasRecovered",
"edits": [
{
"file": "cmd/tmctl/render.go",
"find": "\tif q.EchoDraftRecovered > 0 {",
"replace": "\tif q.EchoDraftRecovered >= 0 {"
}
]
},
{
"id": "ECHOREC-the-edit-arm-shrinks-to-the-final-stage",
"battery": true,
"why": "the edit arm counts the edit WAVE, not the final stage, and `waveStages` puts every non-translator stage in that wave — a pipeline declaring `select`+`edit` has an echo row that is not the unit's last word. Narrowed to the final stage the metric loses the editor echo of every multi-stage edit wave, and the loss is invisible on the one-stage fixtures the rest of the package uses. ⚠ The narrowing is ANDed onto the set test rather than replacing it: dropping the set's only use makes `editStageNames` declared-and-not-used, the mutant does not build, and the tool reports NOTHING RAN — an unmeasured entry, which is worse than a surviving one",
"package": "./internal/pipeline/",
"run": "TestACuredEchoOnAMidEditStageIsStillCuredWhenTheUnitDiesLater",
"edits": [
{
"file": "internal/pipeline/quality.go",
"find": "case editStageNames[cs.Stage] && inManifest[k]:",
"replace": "case editStageNames[cs.Stage] && cs.Stage == r.finalStageName() && inManifest[k]:"
}
]
},
{
"id": "ECHOREC-the-editors-recovery-stops-asking-whether-anything-shipped",
"battery": true,
"why": "the mirror of ECHOREC-a-superseded-echo-counts-as-recovered on the arm that had only the positive half of its guard: with the gate gone every superseded editor echo counts as cured, so a run whose re-roll a CEILING refused reports «echo edit=1 (100.0%), 1 recovered» over a book nobody cured anything in — the inversion of the defect the clause was built for. Measured before the pin existed: dropping the gate left the whole package green",
"package": "./internal/pipeline/",
"run": "TestTheEDITORsOwnEchoStaysCountableWhenTheMoneyRanOut",
"edits": [
{
"file": "internal/pipeline/quality.go",
"find": "\t\t\t\teditEcho++\n\t\t\t\tif shippedText(cs) {\n\t\t\t\t\teditEchoRecovered++\n\t\t\t\t}",
"replace": "\t\t\t\teditEcho++\n\t\t\t\teditEchoRecovered++"
}
]
}
]

View file

@ -43,6 +43,16 @@ providers:
base_url: https://api.deepseek.com/v1
api_key_env: DEEPSEEK_API_KEY
reasoning: subset # thinking внутри completion_tokens
# ⚠ ПАРАМЕТРЫ СЭМПЛИНГА У ДУМАЮЩЕЙ МОДЕЛИ ИГНОРИРУЮТСЯ. Вендор-дока, снята живьём 2026-08-30
# (`curl` HTTP 200, страница 108 336 байт, sha256 `f28c4324…`; запись — `docs/experiments/00-provider-quirks.md`,
# раздел «Thinking / reasoning», нумерованный пункт 3 — читать ТАМ, а не здесь. ⚠ НЕ раздел
# «Параметры сэмплинга»: это другая таблица, и этой цитаты в ней нет): «Thinking mode does not support the `temperature`, `top_p`,
# `presence_penalty`, or `frequency_penalty` parameters… setting these parameters will not trigger an
# error but will also have no effect». Размышление у обеих моделей включено по умолчанию ⇒
# `temperature: 0.3`/`0.4` боевого ростера доезжают до провода и не делают НИЧЕГО.
# Параметр НЕ снят сознательно: `temperature` входит в `RequestHash`, и его удаление пере-купило бы
# каждую книгу в переводе ради нулевого изменения в ответах. «Настроить температуру редактора» —
# мёртвая ручка; звать надо ручку эффорта (`stages[].reasoning`).
# ⚠️ ЭХО-МИНА (трасса 2026-07-04, PROGRESS «Ответ Полигону»): на плотном CJK
# DeepSeek с thinking ВЫКЛ воспроизводимо возвращает ИСХОДНИК вместо перевода
# (тихий отказ, HTTP 200). Держится только тем, что reasoning:"off" через

View file

@ -0,0 +1,58 @@
package config
import (
"path/filepath"
"testing"
)
// lowereffort_shipping_test.go pins the OTHER half of the same data decision its neighbour pins: every
// shipping pipeline leaves `retries.lower_effort_on_empty` OFF.
//
// WHY A TEST AND NOT THE GO DEFAULT. The Go zero value is already false, and that is exactly why the
// files need a gate: turning the remedy on is a one-word edit to a YAML that nothing would notice. The
// report of the pack that built it says «the landing is inert»; without this test that sentence is a
// promise, not a property — and the sibling's doc comment states the rule in as many words: an un-pinned
// data decision is one a later config edit reverts silently, on the money path, without anything going
// red.
//
// WHAT THE OFF STATE IS PROTECTING, in numbers rather than caution:
//
// - The one stage the knob can actually move is the EDITOR. The draft already rides `low`, the lowest
// emitting step, so ReducedEffort returns no step for it; the editor rides the vendor default and
// steps to `low`.
// - The editor role's echo safety at `low` on dense CJK is NOT MEASURED. The published echo numbers
// are the TRANSLATOR's (00-provider-quirks: one raw-Chinese output in sixteen on flash; zero in five
// on deepseek-v4-pro through escalation hops), and the quirks doc names the gap itself and requires
// an echo control in the editor role before the step. Backlog row 433 carries the condition.
// - Flipping it mid-book is not free either: effort and max_tokens are both in the request hash, so
// every unit that already holds a stored regeneration buys one more call. Measured across this
// machine: 58 such units on 24 books.
//
// Mutation this catches: set the key true in any shipping pipeline → RED, naming the file.
func TestShippingPipelinesDoNotLowerEffortOnEmpty(t *testing.T) {
m, err := LoadModels(filepath.Join("..", "..", "configs", "models.yaml"))
if err != nil {
t.Fatalf("load the shipping models.yaml: %v", err)
}
// The same four files the sibling gate walks, and for the same reason: an arm exists to isolate the
// EDITOR, so a retry policy that differed between an arm and its baseline would put a second
// variable into the comparison the arm is for.
for _, pf := range []string{
"pipeline-c1.yaml", "pipeline-c2.yaml",
"pipeline-arm-glm.yaml", "pipeline-arm-mistral.yaml",
} {
p, err := LoadPipeline(filepath.Join("..", "..", "configs", pf), m, "zh-ru", nil)
if err != nil {
t.Fatalf("load %s: %v", pf, err)
}
if p.Retries.LowerEffortOnEmpty {
t.Errorf("%s: retries.lower_effort_on_empty is ON. The remedy is built and deliberately "+
"INERT: the only stage it moves is the editor, whose echo safety at a lowered effort on "+
"dense CJK has never been measured (the published numbers are the translator's), and "+
"flipping it mid-book re-buys one call per unit that already holds a regeneration — 58 "+
"units on 24 books when that was last counted. Turning it on is a decision backlog row "+
"433 carries, and it needs the editor-role echo control the quirks doc asks for, not a "+
"config edit", pf)
}
}
}

View file

@ -450,6 +450,78 @@ func (m *Models) AdditiveReasoningTokens(modelName, _ string, declaredBuffer int
return 0
}
// ReducedEffort returns the neutral effort one step BELOW `effort` on modelName's wire, and whether
// such a step exists at all.
//
// It sits beside ThinksOnWire, which is the sibling it actually resembles: both take a model and a
// neutral effort and answer from the RESOLVED capability, so neither can drift from the wire it
// describes. (AdditiveReasoningTokens is the cousin, not the twin — it discards its effort argument
// and answers from the provider's BILLING semantic, which is a different question about a different
// field.) No per-model table of vendor levels is needed: ValidReasoningEffort is deliberately
// provider-blind, and a table of its own would be a new mechanism rather than a use of one.
//
// WHY A LADDER AT ALL. On a subset-billing provider thinking and answer share one max_tokens, so a
// completion that fills the budget and returns no text spent it all thinking. Buying a bigger budget
// for that is the move D39.86 falsified by measurement: at the vendor default effort, 8496 → 16992
// grew reasoning_content from 15 424 to 21 528 characters and the reply stayed empty. Less thinking at
// the same budget is the remedy that matches the cause; more budget is the remedy for a cut that
// happened WHILE the answer was being written.
//
// WHERE A LADDER EXISTS AT ALL: only where the LEVEL reaches the wire, which is control none and
// control effort. Under extra_body_disable the body carries thinking on or off and nothing else, so
// high→medium is a byte-identical request at the same price; under mandatory nothing reasoning-related
// is emitted. Offering a step there would buy a second copy of the call that just failed.
//
// WHY IT STOPS AT `low`. The bottom is the lowest EMITTING step, never a value that switches thinking
// off. Turning a provider's thinking off is a different decision with a ratified guardrail on it —
// dense CJK plus thinking-off returns the untranslated source at HTTP 200 (echoes_when_thinking_off,
// D19.1 п.2) — and a retry policy must not take it by accident.
//
// WHY ONLY control=none STEPS DOWN FROM AN UNSET EFFORT. There, "" and "off" both emit nothing and the
// vendor's own default stands — and that control is chosen precisely for a model we may not switch
// off, so low|medium|high is the ONLY way its thinking can be sized (D39.87: «never turned off» is not
// «never configured»). An `off` on such a model is therefore the operator who believed they had turned
// thinking down and had not, and an explicit `low` replaces a level we do not own with the lowest one
// we do. Where an off-switch exists the same words mean a floor the capability itself chose, and
// nothing here has evidence that the vendor's default sits above `low`.
func (m *Models) ReducedEffort(modelName, effort string) (string, bool) {
// An UNKNOWN model gets no ladder. Resolving one yields the OpenAI-compat baseline, which would
// answer «a step exists» for a slug this catalogue has never seen — and for the empty string, which
// is what a caller reaching for the wrong field hands over. The remedy's whole premise is that the
// step is derived from THIS model's declared wire; with no declaration there is nothing to derive
// from, and a wrong step buys a call at a level nobody chose.
if _, known := m.Models[modelName]; !known {
return "", false
}
control := m.ResolveCapability(modelName).Reasoning.Control
// "" is what applyToBody's own defaulting treats as ReasoningNone, and this has to read the wire the
// same way ThinksOnWire does — a predicate that disagreed with the body it describes would be a
// second opinion about one call.
if control == "" {
control = llm.ReasoningNone
}
if control != llm.ReasoningNone && control != llm.ReasoningEffortField {
return "", false
}
next := ""
switch effort {
case "high":
next = "medium"
case "medium":
next = "low"
case "", "off":
if control != llm.ReasoningNone {
return "", false
}
next = "low"
default: // low, and any word the loader would have refused anyway
return "", false
}
// The ladder speaks the ONE vocabulary the loader accepts, so a step can never reach the wire
// through the retry path in a form a config could not have been written with.
return next, ValidReasoningEffort(next)
}
// MinMaxTokens is the resolved per-model max_tokens FLOOR (D24.3), computed through
// the same provider→model capability layering as the wire shape (ResolveCapability),
// so what the runner floors against always matches what the snapshot folds. 0 = no

View file

@ -168,6 +168,42 @@ type Retries struct {
// echo/not), where a re-gen recovers ~7.6× cheaper than the hop. The echo GATE (threshold/detection) is
// untouched: only the RESPONSE to an echo changes.
RegenerateEchoBeforeEscalate int `yaml:"regenerate_echo_before_escalate"`
// LowerEffortOnEmpty makes an EMPTY completion regenerate with one step LESS thinking at the SAME
// budget, instead of with the same thinking at twice the budget. It spends a regeneration from the
// budget above either way — this changes what the retry BUYS, never how many it may buy.
//
// The two flags the runner re-attacks are two different failures, and the doubling is the right
// answer to only one of them. A `length` cut with text in it ran out of room WHILE WRITING, so more
// room is the cure (D2.3). An `empty` reply filled max_tokens before the answer began: on a
// subset-billing provider that budget went to thinking, and buying more of it is the move D39.86
// falsified — at the vendor default effort a doubled ceiling grew reasoning_content from 15 424 to
// 21 528 characters and the reply stayed empty. The same principle D2.3 already applies to a
// repetition loop (a bigger budget only buys more loop, so flag instead of paying): do not buy more
// budget for a process that expands to fill it.
//
// OPT-IN, default false, and the default is not timidity. Less thinking is not free on dense CJK:
// the echo mine re-arms as effort drops, and how much thinking a step actually removes has never
// been measured on repeats. So the mechanism is here and the VALUE is a measurement someone still
// has to buy; until then every shipping config renders byte-identically to before this key
// existed, including its snapshot.
//
// ⚠ AND THE RISK HAS TO BE QUOTED FOR THE RIGHT ROLE. On the shipping pipeline the draft already
// sits at `low`, which is the floor, so this key CANNOT move it — the one stage it can move is the
// EDITOR, which rides the vendor default. The published echo numbers are for other roles: one
// raw-Chinese output in sixteen on flash as a TRANSLATOR, and zero in five on
// deepseek-v4-pro at `low` over dense CJK — also as a translator, through escalation hops. The
// quirks doc states the gap in as many words: the editor role's echo safety at `low` on CJK is
// not measured, and an echo control in THAT role is the precondition for the step. Quoting the
// translator's number here would be answering with a risk nobody asked about.
//
// The condition for turning this on, and who carries it, is backlog row 433.
//
// ⚠ WHAT FLIPPING IT MID-BOOK COSTS, named here rather than discovered on a bill: it changes the
// SHAPE of attempt ≥ 1 — effort and max_tokens are both in the request hash — so a unit that already
// holds a stored regeneration will not find it and buys one more call. Attempt 0 is untouched and
// the snapshot does not move, so nothing already DELIVERED is re-bought; the cost is one call per
// unit that had already been retried.
LowerEffortOnEmpty bool `yaml:"lower_effort_on_empty"`
}
// Stage is one pipeline pass.

View file

@ -0,0 +1,171 @@
package config
import (
"fmt"
"os"
"path/filepath"
"testing"
"time"
)
// reducedeffort_test.go pins the thinking ladder Models.ReducedEffort derives from a model's RESOLVED
// capability — the data that already says what "off" means on each wire — so that no per-model table of
// vendor levels has to exist for a retry to ask for less thinking.
func modelsWithEveryReasoningControl(t *testing.T) *Models {
t.Helper()
dir := t.TempDir()
path := filepath.Join(dir, "models.yaml")
if err := os.WriteFile(path, []byte(fmt.Sprintf(`
prices_checked: %q
default_model: none-model
providers:
ds: { kind: openai, base_url: http://y, reasoning: subset }
models:
# control=none (DeepSeek's shape): low|medium|high go out, "" and "off" emit nothing and leave the
# provider thinking at its OWN default.
none-model: { provider: ds, price: { input_per_m: 1, output_per_m: 2 } }
# control=effort (grok/ollama): "off" sends an explicit floor value.
effort-model:
provider: ds
price: { input_per_m: 1, output_per_m: 2 }
capabilities: { reasoning: { control: effort, off_effort: none } }
# control=extra_body_disable (GLM): "off" AND "" merge a disable body.
disable-model:
provider: ds
price: { input_per_m: 1, output_per_m: 2 }
capabilities: { reasoning: { control: extra_body_disable, off_extra_body: { thinking: { type: disabled } } } }
# control=mandatory (Gemini 3.1 Pro): nothing reasoning-related reaches the wire.
mandatory-model:
provider: ds
price: { input_per_m: 1, output_per_m: 2 }
capabilities: { reasoning: { control: mandatory } }
`, time.Now().UTC().Format("2006-01-02"))), 0o644); err != nil {
t.Fatal(err)
}
m, err := LoadModels(path)
if err != nil {
t.Fatalf("models load: %v", err)
}
return m
}
func TestReducedEffortWalksDownTheEmittingSteps(t *testing.T) {
m := modelsWithEveryReasoningControl(t)
// Only these two controls put the LEVEL on the wire (llm.Capability.applyToBody emits
// reasoning_effort for them and for nobody else), so only here can a step change the call.
for _, model := range []string{"none-model", "effort-model"} {
if got, ok := m.ReducedEffort(model, "high"); !ok || got != "medium" {
t.Fatalf("%s: high should step to medium, got %q ok=%t", model, got, ok)
}
if got, ok := m.ReducedEffort(model, "medium"); !ok || got != "low" {
t.Fatalf("%s: medium should step to low, got %q ok=%t", model, got, ok)
}
// The ladder stops at the lowest EMITTING step. Below it lies switching a provider's thinking
// off, which arms the echo mine on dense CJK (D19.1 п.2) — a decision with its own guardrail,
// never something a retry policy takes on its own.
if got, ok := m.ReducedEffort(model, "low"); ok {
t.Fatalf("%s: low is the floor, a retry must not reach for an off-switch, got %q", model, got)
}
}
}
// TestAWireThatCannotHearTheLevelGetsNoLadder is the half a code read misses. Under
// extra_body_disable the body says thinking ON or OFF and never which level, so `high` and `medium`
// marshal to the SAME bytes at the same price: a step there is not a cheaper retry, it is a second
// copy of the call that just failed, bought under a different attempt key.
func TestAWireThatCannotHearTheLevelGetsNoLadder(t *testing.T) {
m := modelsWithEveryReasoningControl(t)
for _, effort := range []string{"", "off", "low", "medium", "high"} {
if got, ok := m.ReducedEffort("disable-model", effort); ok {
t.Fatalf("extra_body_disable carries no level; stepping %q to %q would re-buy the same request", effort, got)
}
}
}
// TestOnlyAnUnownedDefaultStepsDownFromOff is the per-control reading, and it is the half the whole
// remedy rests on: `off` is not the bottom of every wire. With control=none nothing is emitted and the
// VENDOR's default stands — thinking ON by that control's definition, `high` on DeepSeek — so an
// explicit `low` replaces a level we do not own with one we do. With control=effort or
// extra_body_disable, `off` already resolves to a floor the capability itself chose, and there is
// nothing beneath it to ask for.
func TestOnlyAnUnownedDefaultStepsDownFromOff(t *testing.T) {
m := modelsWithEveryReasoningControl(t)
for _, effort := range []string{"", "off"} {
if got, ok := m.ReducedEffort("none-model", effort); !ok || got != "low" {
t.Fatalf("control=none at %q rides the vendor default and must step to an explicit low, got %q ok=%t", effort, got, ok)
}
for _, model := range []string{"effort-model", "disable-model"} {
if got, ok := m.ReducedEffort(model, effort); ok {
t.Fatalf("%s at %q already sits on its own floor; stepping to %q would claim a reduction that is not one", model, effort, got)
}
}
}
}
// TestMandatoryThinkingHasNoLadder: where a disable attempt is a 400 and the neutral knob is swallowed
// whole, there is no step to take at any level, and pretending otherwise would buy a second identical
// call at the same price.
func TestMandatoryThinkingHasNoLadder(t *testing.T) {
m := modelsWithEveryReasoningControl(t)
for _, effort := range []string{"", "off", "low", "medium", "high"} {
if got, ok := m.ReducedEffort("mandatory-model", effort); ok {
t.Fatalf("mandatory thinking cannot be sized from here; at %q it offered %q", effort, got)
}
}
}
// TestReducedEffortSpeaksOnlyTheEngineVocabulary keeps the ladder inside the ONE word list the loader
// accepts: a step the config could not have been written with would reach the wire from the retry path
// without ever passing that gate.
//
// ⚠ THE EXPECTED WORDS ARE SPELLED OUT HERE, not asked of ValidReasoningEffort. ReducedEffort returns
// `next, ValidReasoningEffort(next)`, so `ok && !ValidReasoningEffort(got)` is identically false and an
// assertion built on it is green for EVERY possible implementation — including one that steps `high` to
// `minimal`. Measured: that exact mutation leaves such an assertion passing. A test of a guard must not
// be written in terms of the guard.
func TestReducedEffortSpeaksOnlyTheEngineVocabulary(t *testing.T) {
m := modelsWithEveryReasoningControl(t)
// The ladder may only ever hand back one of these two. Anything else — a vendor word like `xhigh`,
// an off-switch, or a typo — is a value the loader has never validated.
allowed := map[string]bool{"medium": true, "low": true}
for _, model := range []string{"none-model", "effort-model", "disable-model", "mandatory-model"} {
for _, effort := range []string{"", "off", "low", "medium", "high"} {
got, ok := m.ReducedEffort(model, effort)
if !ok {
if got != "" {
t.Fatalf("%s at %q: no step exists, so the word must be empty, got %q", model, effort, got)
}
continue
}
if !allowed[got] {
t.Fatalf("%s at %q stepped to %q; the only steps this ladder may produce are medium and low", model, effort, got)
}
if !ValidReasoningEffort(got) {
t.Fatalf("%s at %q stepped to %q, which the loader would reject", model, effort, got)
}
}
}
}
// TestAnUnknownModelGetsNoLadder closes the hole that made the RUNNER's pins satisfiable by the wrong
// object. Resolving an unlisted slug yields the OpenAI-compat baseline, so the ladder used to answer
// «step to low» for a model this catalogue has never seen — and for the EMPTY STRING, which is what a
// caller reaching for the wrong field (ResolvedHop instead of ResolvedModel, on a stage with no hop)
// hands over. Measured before this: swapping the runner's argument to the hop left every pin green,
// because both names resolved to the same baseline.
func TestAnUnknownModelGetsNoLadder(t *testing.T) {
m := modelsWithEveryReasoningControl(t)
for _, name := range []string{"", "not-in-the-catalogue", "none-model-0813"} {
for _, effort := range []string{"", "off", "low", "medium", "high"} {
if got, ok := m.ReducedEffort(name, effort); ok {
t.Fatalf("model %q is not in the catalogue; there is no declared wire to derive a step "+
"from, yet %q stepped to %q", name, effort, got)
}
}
}
// The control: the SAME question about a listed model still answers, or this test would pass on a
// function that refuses everything.
if got, ok := m.ReducedEffort("none-model", "off"); !ok || got != "low" {
t.Fatalf("a catalogued model must still get its step, got %q ok=%t", got, ok)
}
}

View file

@ -52,20 +52,47 @@ func (p *Pricer) PriceFor(model string) ModelPrice {
return p.defaultPrice
}
// PriceBasis names WHICH model's row a completion was priced from. It is returned rather than inferred
// because the fallback is invisible from the outside: the same call site, the same slugs, and a bill
// that is right or wrong by a factor nobody is told about.
type PriceBasis string
const (
// PriceByAnswerer — the model that actually answered is in the catalogue. The ordinary case.
PriceByAnswerer PriceBasis = "answerer"
// PriceByRequested — the answering slug is unknown and the bill was taken from the model we ASKED
// for. Safe in one direction and not in the other: it stops a premium answer being billed at a cheap
// default, and it lets a provider that routed DOWN be billed at the expensive pin. Measured on the
// run of 11.09: 28 of 33 rows answered `deepseek-flash`, a slug the catalogue does not carry.
PriceByRequested PriceBasis = "requested"
// PriceByAnchor — neither slug is known and the global default anchor was used. Never $0, and never
// a number anybody chose for this call.
PriceByAnchor PriceBasis = "anchor"
)
// Substituted reports whether the bill came from a model that did not answer this call.
func (b PriceBasis) Substituted() bool { return b != PriceByAnswerer }
// PriceForResponse prices a completion by the model that ACTUALLY answered,
// but if that id is unknown (the provider returned a canonicalized/dated
// slug like claude-sonnet-5-2026xxxx) falls back to the REQUESTED model's
// price BEFORE the global anchor — otherwise a premium answer would be billed at
// the cheap default (systematic under-accounting, review finding). Only if both
// are unknown — the default anchor (never $0).
func (p *Pricer) PriceForResponse(requested, actual string) ModelPrice {
//
// The basis travels WITH the price, out of the same lookup, so a caller cannot describe one branch
// while billing through another. The guard the comment above names covers exactly one direction — a
// premium answer must not be billed at a cheap default — and the opposite direction has no guard at
// all: ask for a premium model, get a cheaper one, pay the premium pin. That is the bill the reader
// sees, so the least a caller owes is to know it happened.
func (p *Pricer) PriceForResponse(requested, actual string) (ModelPrice, PriceBasis) {
if mp, ok := p.prices[actual]; ok {
return mp
return mp, PriceByAnswerer
}
if mp, ok := p.prices[requested]; ok {
return mp
return mp, PriceByRequested
}
return p.defaultPrice
return p.defaultPrice, PriceByAnchor
}
// CostUSD prices one completion by its usage. Formula (invariant from

View file

@ -56,18 +56,46 @@ func TestPriceForResponse(t *testing.T) {
if err != nil {
t.Fatal(err)
}
// The model that actually responded is known — price by it.
if p.PriceForResponse("claude-sonnet-5", "claude-sonnet-5").InputPerM != 2.0 {
t.Fatal("known actual model must price by actual")
// The model that actually responded is known — price by it, and say so.
if price, basis := p.PriceForResponse("claude-sonnet-5", "claude-sonnet-5"); price.InputPerM != 2.0 || basis != PriceByAnswerer {
t.Fatalf("known actual model must price by actual, got %v basis=%q", price.InputPerM, basis)
}
// Provider returned a dated slug of a premium model — NOT the cheap anchor, but
// the price of the REQUESTED model (otherwise a systematic under-accounting of the premium).
if got := p.PriceForResponse("claude-sonnet-5", "claude-sonnet-5-20260101").InputPerM; got != 2.0 {
t.Fatalf("unknown actual must fall back to requested model price (2.0), got %v", got)
if price, basis := p.PriceForResponse("claude-sonnet-5", "claude-sonnet-5-20260101"); price.InputPerM != 2.0 || basis != PriceByRequested {
t.Fatalf("unknown actual must fall back to requested model price (2.0), got %v basis=%q", price.InputPerM, basis)
}
// Both unknown — the default anchor (never $0).
if got := p.PriceForResponse("mystery", "also-mystery").InputPerM; got != 0.14 {
t.Fatalf("both unknown must fall back to default anchor, got %v", got)
if price, basis := p.PriceForResponse("mystery", "also-mystery"); price.InputPerM != 0.14 || basis != PriceByAnchor {
t.Fatalf("both unknown must fall back to default anchor, got %v basis=%q", price.InputPerM, basis)
}
}
// TestTheBillNamesTheModelItCameFrom is the half the price alone cannot carry. A substitution is
// invisible from the figure: the same call site returns the same type whether the rate belongs to the
// model that answered or to one that did not, and only the second case can bill a reader for a model
// they did not get. Measured on the run of 11.09: 28 of 33 rows answered under a slug the catalogue
// does not list, and every one of them was priced from the request with nothing recording it.
func TestTheBillNamesTheModelItCameFrom(t *testing.T) {
cheap := ModelPrice{InputPerM: 0.30, OutputPerM: 1.20}
dear := ModelPrice{InputPerM: 1.32, OutputPerM: 3.96}
p, err := NewPricer(map[string]ModelPrice{"pro": dear, "flash": cheap}, cheap)
if err != nil {
t.Fatal(err)
}
// The direction with no guard on it: a request for the premium model, answered under a slug the
// catalogue does not carry, billed at the premium pin.
price, basis := p.PriceForResponse("pro", "pro-0813")
if !basis.Substituted() {
t.Fatal("a bill taken from a model that did not answer must say so")
}
if price.OutputPerM != dear.OutputPerM {
t.Fatalf("the fixture must actually price by the request, got %v", price.OutputPerM)
}
// The control: the ordinary case must NOT claim a substitution, else the signal is noise and the
// warning it drives fires on every call.
if _, basis := p.PriceForResponse("flash", "flash"); basis.Substituted() {
t.Fatalf("a call priced by the model that answered is not a substitution, got %q", basis)
}
}

View file

@ -0,0 +1,43 @@
package ledger
import (
"testing"
"textmachine/backend/internal/llm"
)
// TestTheThinkingShareOfACompletionPricesNothing keeps the diagnostic off the money path.
//
// llm.Usage carries two thinking counts and only one is money: ReasoningTokens is added to the
// completion here, ReasoningInCompletion is a SUBSET of a completion already priced. On the measured
// DeepSeek shape thinking is ~60% of a call, so pricing the subset field would inflate every bill on
// the only provider the shipping configs call.
//
// The assertion is built so the money is UNAVOIDABLE rather than likely: the completion is large, the
// output price is non-zero, and the baseline cost is required to be positive — a formula change that
// zeroed the output term would otherwise make this pass by comparing nothing with nothing.
func TestTheThinkingShareOfACompletionPricesNothing(t *testing.T) {
price := ModelPrice{InputPerM: 1.32, CachedPerM: 0.044, OutputPerM: 3.96}
base := llm.Usage{PromptTokens: 6908, CachedTokens: 1152, CompletionTokens: 16000}
want := CostUSD(price, base)
if want <= 0 {
t.Fatalf("the fixture must actually cost money, else this test compares two zeros: %v", want)
}
for _, share := range []int{0, 1, 8000, 16000} {
n := share
withThinking := base
withThinking.ReasoningInCompletion = &n
if got := CostUSD(price, withThinking); got != want {
t.Fatalf("a thinking share of %d moved the bill %v -> %v; the subset field is a diagnostic and must price nothing", share, want, got)
}
}
// The other field still IS money, so a pin that passed for both would be measuring nothing.
onTop := base
onTop.ReasoningTokens = 16000
if got := CostUSD(price, onTop); got <= want {
t.Fatalf("reasoning billed ON TOP must raise the bill above %v, got %v — the control that proves this test can see a price move at all", want, got)
}
}

View file

@ -662,6 +662,50 @@ func TestAStopDuringABackoffKeepsTheEvidence(t *testing.T) {
}
}
// TestDeliveryEvidenceIsWhatTheMoneyBitAsks is the DIRECT table over the money bit, and it is here
// because the end-to-end fixture below cannot be the only carrier: that one depends on what net/http's
// write loop and read loop do relative to each other, and an acceptance probe of 40 iterations showed the
// subject arriving 39 times and a full 401 once — so the pin red by LUCK and the mutant survived on the
// fortieth. This one answers the same question with no race in it at all.
//
// The wants are written as LITERALS rather than computed from the expression, because a table that
// re-derives the formula it is checking agrees with any formula. All EIGHT combinations of the three bits
// are present: the first version of this table had a duplicate instead of the eighth, which looks like
// coverage and is not.
func TestDeliveryEvidenceIsWhatTheMoneyBitAsks(t *testing.T) {
for _, tc := range []struct {
name string
wrote, firstByte, answered bool
want bool
why string
}{
{name: "our bytes left for the peer", wrote: true, want: true,
why: "the request was written: the provider has it, whatever it does next"},
{name: "our bytes left and a reply came back", wrote: true, firstByte: true, answered: true, want: true},
{name: "an early 2xx overtook our own write callback", firstByte: true, answered: true, want: true,
why: "net/http returns a response as soon as the response channel fires, while WroteRequest runs on the write loop — a provider answering while the body still drains is delivery, and read as «not delivered» it was re-asked three times"},
{name: "a refusal reset the connection while we were still writing", firstByte: true, want: false,
why: "THE MONEY BIT: 401/403/413 plus an RST gives a response BYTE and a write error, so Do returns the write failure and the status line is never in our hands. Taking the byte alone as proof paid for 22 refusals in 25, one of them $0.80"},
{name: "a reply object with no byte recorded", answered: true, want: false,
why: "a state the transport does not produce — a reply implies its first byte — and the bit must not invent delivery out of the half it cannot have alone"},
{name: "nothing left and nothing came back", want: false},
{name: "a byte came back, no reply, and the write did finish", wrote: true, firstByte: true, want: true,
why: "the write is sufficient by itself; this row is what keeps the `answered` half from being read as a REQUIREMENT"},
{name: "the write finished and a reply came back with no byte recorded", wrote: true, answered: true, want: true,
why: "the eighth combination, for completeness: the write alone already decides it, so no reading of the `answered` half can take delivery away"},
} {
t.Run(tc.name, func(t *testing.T) {
var tr deliveryTrace
tr.wrote.Store(tc.wrote)
tr.firstByte.Store(tc.firstByte)
if got := tr.delivered(tc.answered); got != tc.want {
t.Fatalf("delivered(answered=%v) with wrote=%v first_byte=%v = %v, want %v — %s",
tc.answered, tc.wrote, tc.firstByte, got, tc.want, tc.why)
}
})
}
}
// TestARefusalIsNotAPurchaseEvenWhenTheReplyOutrunsTheWrite is the regression the FIX for the early-200
// leak introduced, and it is the more expensive of the two directions.
//
@ -672,8 +716,26 @@ func TestAStopDuringABackoffKeepsTheEvidence(t *testing.T) {
// at 22 refusals in 25, one of them $0.80 booked for a request the provider declined — and the
// delivered-cut retry sent the whole body to it a second time.
//
// So the reply counts as evidence only where a reply actually reached us. Here it did not.
// ⛔ THE FIXTURE NOW MAKES THE SUBJECT NEARLY CERTAIN INSTEAD OF A COIN TOSS, and the old one is why. It
// sent a COMPLETE 401 and raced the client's read against its own reset: the acceptance probe got the
// subject 39 times in 40 and a fully-read 401 once, and on that fortieth run the test passed while asking
// about something else entirely — `delivered` was never consulted, because a parsed status goes down the
// status branch. Two things changed. A header block that is never COMPLETED cannot become a status line at
// all, so no run of this fixture can take that branch; and the server never drains the body, so a huge one
// cannot finish writing however fast the machine is.
//
// ⚠ WHAT IS STILL A WINDOW, NAMED RATHER THAN CLAIMED AWAY: the 200 ms between the partial reply and the
// reset is what gives the client's read loop time to see the byte. It is wide — that loop has nothing else
// to do — but a host that starves it for a fifth of a second would leave the trace without its first byte,
// and THEN this case is about a socket that said nothing instead. The deterministic death of the mutant
// lives in the table above, which has no timing in it at all; this case is what proves the wire can
// produce the state the table describes.
//
// The two cases are one fixture on purpose. The second is the CONTROL for the first: it proves this
// server's partial reply really does reach the client's trace as a first byte, which is the premise the
// first case cannot observe from outside (the trace is the transport's own).
func TestARefusalIsNotAPurchaseEvenWhenTheReplyOutrunsTheWrite(t *testing.T) {
var conns atomic.Int64
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
@ -685,36 +747,83 @@ func TestARefusalIsNotAPurchaseEvenWhenTheReplyOutrunsTheWrite(t *testing.T) {
if aerr != nil {
return
}
conns.Add(1)
go func(c net.Conn) {
// Refuse the moment the request line is in, then RESET — the body is still writing, so
// the client sees a write error and never gets a status line.
// Read the request LINE and nothing else: with the body never drained, a large one fills
// the socket buffers and the client's write blocks until the reset below.
br := bufio.NewReader(c)
if _, rerr := br.ReadString('\n'); rerr != nil {
c.Close()
return
}
fmt.Fprint(c, "HTTP/1.1 401 Unauthorized\r\nContent-Length: 9\r\n\r\nno access")
// A refusal whose header block never ends. The client gets a response BYTE it can never
// turn into a status line — «the status was never in our hands», as a certainty.
fmt.Fprint(c, "HTTP/1.1 401 Unau")
// Long enough that the client's reader — which has nothing else to do — has consumed those
// bytes before the reset discards them.
time.Sleep(200 * time.Millisecond)
if tc, ok := c.(*net.TCPConn); ok {
_ = tc.SetLinger(0) // RST rather than a graceful close
_ = tc.SetLinger(0) // RST rather than a graceful close: the pending write FAILS
}
c.Close()
}(c)
}
}()
addr := "http://" + ln.Addr().String()
huge := strings.Repeat("длинный исходный текст главы. ", 200000)
c := NewOpenAICompatClient(OpenAICompatConfig{Name: "p", BaseURL: "http://" + ln.Addr().String(), Profile: cutProfile(2 * time.Second)}, nil)
_, err = c.Complete(context.Background(), LLMRequest{Model: "m", Messages: []Message{{Role: "user", Content: huge}}, MaxTokens: 16})
t.Run("the body was still writing when the refusal arrived", func(t *testing.T) {
conns.Store(0)
huge := strings.Repeat("длинный исходный текст главы. ", 200000) // ~11 MB, and the server never reads it
c := NewOpenAICompatClient(OpenAICompatConfig{Name: "p", BaseURL: addr, Profile: cutProfile(2 * time.Second)}, nil)
_, err := c.Complete(context.Background(), LLMRequest{Model: "m", Messages: []Message{{Role: "user", Content: huge}}, MaxTokens: 16})
if err == nil {
t.Fatal("a refused request must not report success")
}
// ⛔ THE PREMISE, ASSERTED AND PRINTED — the half this pin used to leave to chance. A status line
// in the error means the client READ a whole reply, which is the other subject: the money bit is
// never consulted there, and a pass would be a pass about nothing.
attempts := conns.Load()
t.Logf("premise: the provider was connected to %d time(s) and the call failed with: %v", attempts, err)
if strings.Contains(err.Error(), "401") || strings.Contains(err.Error(), "status") {
t.Fatalf("the client came back with a STATUS, so this run never reached the delivery question: %v", err)
}
// Every attempt must have hit the same wall, or the verdict below is about whichever one the error
// came from: nothing was bought, so the whole retry budget is spent (cutProfile's MaxAttempts).
if attempts != 3 {
t.Fatalf("the provider was connected to %d time(s), want 3: a call that bought nothing is retried to the end of the budget, and a different number means the attempts did not all reach this wall", attempts)
}
var cut *AttemptCutError
if errors.As(err, &cut) {
t.Fatalf("the provider REFUSED this request and generated nothing; booking an estimate for it "+
"charges a reader for a call that never ran, and the delivered-cut retry sends the whole body "+
"again. got %+v", cut)
}
})
if err == nil {
t.Fatal("a refused request must not report success")
}
var cut *AttemptCutError
if errors.As(err, &cut) {
t.Fatalf("the provider REFUSED this request and generated nothing; booking an estimate for it "+
"charges a reader for a call that never ran, and the delivered-cut retry sends the whole body "+
"again. got %+v", cut)
}
t.Run("the body got out before the refusal arrived", func(t *testing.T) {
conns.Store(0)
// A small body fits in the socket buffers, so the write COMPLETES even though nobody reads it.
c := NewOpenAICompatClient(OpenAICompatConfig{Name: "p", BaseURL: addr, Profile: cutProfile(2 * time.Second)}, nil)
_, err := c.Complete(context.Background(), LLMRequest{Model: "m", Messages: []Message{{Role: "user", Content: "короткий текст"}}, MaxTokens: 16})
if err == nil {
t.Fatal("a reset connection must not report success")
}
var cut *AttemptCutError
if !errors.As(err, &cut) {
t.Fatalf("a request that WAS written is delivered, and the cut is what carries that fact: %v", err)
}
t.Logf("the control: delivered=%v after_headers=%v billable=%v cause=%s connections=%d",
cut.Delivered, cut.AfterHeaders, cut.Billable, cut.Cause, conns.Load())
// THE CONTROL ITSELF: the partial reply reached the client's trace. That is the premise the case
// above stands on and cannot see, and it is asserted here where a cut exists to carry it.
if !cut.AfterHeaders {
t.Fatal("this fixture's partial reply never reached the client's trace, so the case above was not about a response byte at all — it was about a socket that said nothing")
}
// And the money: a first byte with no 2xx in hand is NOT a purchase, written or not.
if cut.Billable {
t.Fatalf("a refusal the client could not even parse was booked as billable: %+v", cut)
}
})
}
// TestARedirectIsNotADelivery: `Do` spans the WHOLE redirect chain and the delivery trace does not reset

View file

@ -199,6 +199,22 @@ func (c Capability) applyToBody(m map[string]any, maxTokens int, temperature flo
c = c.withDefaults()
m[string(c.Budget)] = maxTokens
// ⚠ TempSend DOES NOT MEAN THE VALUE DOES ANYTHING. On a model that is thinking, DeepSeek
// documents the parameter as accepted and inert: «Thinking mode does not support the temperature,
// top_p, presence_penalty, or frequency_penalty parameters… setting these parameters will not
// trigger an error but will also have no effect» (vendor page, read live 2026-08-30, recorded in
// docs/experiments/00-provider-quirks.md, section «Thinking / reasoning», numbered item 3 — go
// there, not to this line, for the current reading. NOT the «Параметры сэмплинга» section, which
// is a different table and does not carry this quote).
// Both shipping stages ride it: the roster sends temperature 0.3 and 0.4 to models whose thinking
// is on, so tuning either is a change to a knob that reaches the wire and moves nothing in the
// answer.
//
// It is still SENT, and deliberately. Temperature is part of RequestHash, so dropping it would
// re-key every checkpoint of every book in flight — a loud --resnapshot whose entire effect on the
// text is nil — and the same is true of declaring the inertness on Capability, which the job
// snapshot marshals. The knob is left where it is and named here instead; what it costs to make
// the silence structural is one full re-translation that changes no output.
switch c.Temp {
case TempOmit:
// never emitted

View file

@ -223,7 +223,7 @@ func retryLoop[T any](ctx context.Context, profile RetryProfile, name string, lo
// ⚠ THIS CLOSES THE MONEY HALF ONLY. What leaves here carries the cut with the strongest
// money claim, which is deliberately NOT the stop when an earlier paid break outranks it — so
// the runner's mark cannot be decided by this error's first cause. The mark asks its own
// question (pipeline's recordCancelledStage: was the run stopped, and did ANY cut deliver),
// question (pipeline's recordStoppedPosition: was the run stopped, and did ANY cut deliver),
// and a guard that read the first cause instead left the position with no row at all.
return zero, chainError(cancelledDuring(ctx.Err(), lastErr), owedCut)
case <-time.After(backoff):
@ -418,8 +418,14 @@ type openAIUsage struct {
// xAI reports reasoning tokens here SEPARATELY from completion_tokens and
// bills them at the output rate; OpenAI-spec providers count them inside
// completion_tokens. The adapter decides which semantics apply.
//
// The count is a POINTER because an absent field and a reported zero are different answers, and
// a subset provider returns both: DeepSeek fills this on every reply, and the value is genuinely
// 0 on a call that did not think. Decoding both into 0 would make «we never asked» indis-
// tinguishable from «it did not think», which is precisely the blindness reasoningDetail exists
// to remove.
CompletionTokensDetails struct {
ReasoningTokens int `json:"reasoning_tokens"`
ReasoningTokens *int `json:"reasoning_tokens"`
} `json:"completion_tokens_details"`
// DeepSeek historically reports cache usage in its own top-level fields
// instead of prompt_tokens_details. Parse both so the Phase-0 cache
@ -437,6 +443,16 @@ func (u openAIUsage) cacheRead() int {
return u.PromptCacheHitTokens
}
// reasoningDetail returns the provider's reported thinking-token count and whether it reported one
// at all. The second result is what keeps a blind row from reading as a measured zero — the whole
// difference between «this call did not think» and «nobody asked».
func (u openAIUsage) reasoningDetail() (int, bool) {
if u.CompletionTokensDetails.ReasoningTokens == nil {
return 0, false
}
return *u.CompletionTokensDetails.ReasoningTokens, true
}
type openAIResponse struct {
ID string `json:"id"`
Model string `json:"model"`

View file

@ -49,6 +49,27 @@ type Usage struct {
// must leave 0 to avoid double-billing; Anthropic bills thinking inside
// output_tokens, so its adapter also leaves 0.
ReasoningTokens int
// ReasoningInCompletion is the SUBSET of CompletionTokens the provider says was thinking — the
// same relation CachedTokens has to PromptTokens, and the field the question «what ate the
// budget» is answered from. It is NEVER money: ledger.CostUSD prices CompletionTokens +
// ReasoningTokens, and adding this on top would bill a subset provider's thinking twice (on the
// measured DeepSeek share, ~60% of every call).
//
// The two fields answer different questions and are two fields for that reason. ReasoningTokens asks
// «how much is billed ON TOP»; this asks «how much of what we already paid for was not the
// answer». A model that fills max_tokens with thinking and returns an empty body is INVISIBLE in
// the first and plain in the second — which is the whole failure of the cold run of 11.09, where
// three calls bought 8496/8496/16000 completion tokens and returned zero characters.
//
// nil means the provider reported no such field AT ALL, and that is deliberately not 0: a
// measured zero (a call that did not think — DeepSeek returns those) and an unasked question are
// different answers, and a column that spells them the same way reads as measured when it is
// blind. An old checkpoint, written before the field existed, decodes to nil for the same reason.
// omitempty, like MinMaxTokens and SystemMessages on Capability and for the same reason: a nil
// pointer must marshal to nothing at all, so a checkpoint written by a provider that reports no
// such field is byte-identical to one written before this field existed. A pointer to a MEASURED
// zero is not nil and still marshals — the distinction survives the wire to disk.
ReasoningInCompletion *int `json:",omitempty"`
}
// Neutral finish reasons. The coverage gate branches on these: a length cut

View file

@ -20,7 +20,10 @@ type ReasoningSemantics string
const (
// ReasoningSubset: thinking is counted INSIDE completion_tokens (OpenAI
// spec, ollama, DeepSeek). ReasoningTokens stays 0 to avoid double-billing.
// spec, ollama, DeepSeek). ReasoningTokens stays 0 to avoid double-billing;
// the count the provider reports is kept as Usage.ReasoningInCompletion, which
// prices nothing and answers what share of an already-paid completion was not
// the answer.
ReasoningSubset ReasoningSemantics = "subset"
// ReasoningAdditive: thinking is reported separately and billed ON TOP of
// completion_tokens (xAI — verified in vojo against cost_in_usd_ticks;
@ -90,7 +93,7 @@ func NewOpenAICompatClient(cfg OpenAICompatConfig, logger *slog.Logger) LLMClien
// reported (0) the identity is unverifiable, so we keep the known-correct xAI additive
// default rather than blind the ledger.
func additiveReasoning(ctx context.Context, log *slog.Logger, provider string, u openAIUsage) int {
rt := u.CompletionTokensDetails.ReasoningTokens
rt, _ := u.reasoningDetail()
if rt <= 0 {
return 0
}
@ -133,6 +136,15 @@ func (c *openAICompatClient) Complete(ctx context.Context, req LLMRequest) (*LLM
CompletionTokens: resp.Usage.CompletionTokens,
}
switch c.reasoning {
case ReasoningSubset:
// Thinking is already inside CompletionTokens, so it is not money here — ReasoningTokens
// stays 0 and the count is recorded as the SUBSET it is. Without this arm the number is
// parsed and dropped, and `reasoning_tokens` reads 0 for every subset provider forever,
// which is how the one column that can say «thinking ate the budget» went silent for the
// whole life of the project while DeepSeek was reporting it on every reply.
if rt, reported := resp.Usage.reasoningDetail(); reported {
usage.ReasoningInCompletion = &rt
}
case ReasoningAdditive:
usage.ReasoningTokens = additiveReasoning(ctx, c.http.log, c.http.name, resp.Usage)
case ReasoningAdditiveTotal:

View file

@ -0,0 +1,155 @@
package llm
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// serveUsage answers one OpenAI-compat completion with the given usage block verbatim, so a test can
// say exactly which fields the PROVIDER sent — including the difference between a field carrying 0 and
// a field that is not there at all.
func serveUsage(t *testing.T, usage string) *httptest.Server {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
openAIOK(t, w, `{"id":"r1","choices":[{"message":{"content":"x"},"finish_reason":"stop"}],"usage":`+usage+`}`)
}))
t.Cleanup(srv.Close)
return srv
}
func completeWith(t *testing.T, srv *httptest.Server, sem ReasoningSemantics) *LLMResponse {
t.Helper()
c := NewOpenAICompatClient(OpenAICompatConfig{Name: "p", BaseURL: srv.URL, Profile: fastProfile(), Reasoning: sem}, nil)
resp, err := c.Complete(context.Background(), LLMRequest{Model: "m", Messages: []Message{{Role: "user", Content: "u"}}, MaxTokens: 16})
if err != nil {
t.Fatal(err)
}
return resp
}
// TestSubsetReasoningIsRecordedWithoutBeingBilled is the thinking share of an already-paid completion:
// on a subset-billing provider the number the provider reports must arrive as ReasoningInCompletion and
// must NOT arrive as ReasoningTokens, which ledger.CostUSD adds to the completion.
//
// The measured shape this stands on: DeepSeek fills completion_tokens_details on every reply, and the
// identity total == prompt + completion holds on all of it — the thinking is already paid for inside
// completion_tokens. Surfacing it as ReasoningTokens would bill it a second time.
func TestSubsetReasoningIsRecordedWithoutBeingBilled(t *testing.T) {
srv := serveUsage(t, `{"prompt_tokens":10,"completion_tokens":8496,"total_tokens":8506,
"completion_tokens_details":{"reasoning_tokens":8496}}`)
resp := completeWith(t, srv, ReasoningSubset)
if resp.Usage.ReasoningInCompletion == nil {
t.Fatal("a reported thinking count must be recorded, not dropped: ReasoningInCompletion is nil")
}
if got := *resp.Usage.ReasoningInCompletion; got != 8496 {
t.Fatalf("ReasoningInCompletion = %d, want the provider's 8496", got)
}
if resp.Usage.ReasoningTokens != 0 {
t.Fatalf("a subset provider's thinking is already inside completion_tokens; billing it again would double-charge it: ReasoningTokens = %d", resp.Usage.ReasoningTokens)
}
if resp.Usage.CompletionTokens != 8496 {
t.Fatalf("CompletionTokens = %d, want the provider's 8496 untouched", resp.Usage.CompletionTokens)
}
}
// TestAReportedZeroIsNotTheSameAsNoAnswer is the whole point of the pointer. A subset provider returns
// a genuine 0 for a call that did not think, and a provider that reports no such field returns nothing
// at all; spelling both as 0 is what made `reasoning_tokens` read as a measured number for the life of
// the project while nobody had asked the question.
func TestAReportedZeroIsNotTheSameAsNoAnswer(t *testing.T) {
reportedZero := completeWith(t, serveUsage(t,
`{"prompt_tokens":10,"completion_tokens":1617,"total_tokens":1627,"completion_tokens_details":{"reasoning_tokens":0}}`), ReasoningSubset)
if reportedZero.Usage.ReasoningInCompletion == nil {
t.Fatal("a provider that REPORTED zero thinking has answered the question; that answer must not read as silence")
}
if got := *reportedZero.Usage.ReasoningInCompletion; got != 0 {
t.Fatalf("a reported zero must stay zero, got %d", got)
}
noField := completeWith(t, serveUsage(t,
`{"prompt_tokens":10,"completion_tokens":1617,"total_tokens":1627}`), ReasoningSubset)
if noField.Usage.ReasoningInCompletion != nil {
t.Fatalf("a provider that reported nothing must leave the question unanswered, got %d", *noField.Usage.ReasoningInCompletion)
}
emptyDetails := completeWith(t, serveUsage(t,
`{"prompt_tokens":10,"completion_tokens":1617,"total_tokens":1627,"completion_tokens_details":{}}`), ReasoningSubset)
if emptyDetails.Usage.ReasoningInCompletion != nil {
t.Fatalf("a details block without the key is still no answer, got %d", *emptyDetails.Usage.ReasoningInCompletion)
}
}
// TestAdditiveBillingLeavesTheSubsetFieldEmpty keeps the two questions apart from the other side: where
// thinking bills ON TOP, the count is money and belongs to ReasoningTokens alone. A number in both
// fields would be one call's thinking described twice, and the next reader summing them would overcount
// exactly the share this pack exists to make visible.
func TestAdditiveBillingLeavesTheSubsetFieldEmpty(t *testing.T) {
body := `{"prompt_tokens":10,"completion_tokens":5,"total_tokens":65,"completion_tokens_details":{"reasoning_tokens":50}}`
additive := completeWith(t, serveUsage(t, body), ReasoningAdditive)
if additive.Usage.ReasoningTokens != 50 {
t.Fatalf("additive billing must surface the count as money, got %d", additive.Usage.ReasoningTokens)
}
if additive.Usage.ReasoningInCompletion != nil {
t.Fatalf("additive thinking is NOT inside the completion; the subset field must stay empty, got %d", *additive.Usage.ReasoningInCompletion)
}
total := completeWith(t, serveUsage(t,
`{"prompt_tokens":22,"completion_tokens":2,"total_tokens":847}`), ReasoningAdditiveTotal)
if total.Usage.ReasoningTokens != 823 {
t.Fatalf("additive_total must derive 823 from the total, got %d", total.Usage.ReasoningTokens)
}
if total.Usage.ReasoningInCompletion != nil {
t.Fatalf("additive_total thinking is outside completion_tokens; the subset field must stay empty, got %d", *total.Usage.ReasoningInCompletion)
}
}
// TestAnUnansweredThinkingShareAddsNoBytes pins the marshalling half of the field's contract. Usage is
// serialised verbatim into every checkpoint's usage_json, so a field that emitted `"…":null` on every
// call would change the bytes of every checkpoint a provider that reports nothing ever writes — the
// byte-identity discipline Capability.MinMaxTokens and SystemMessages carry two files away, and which
// the comment on this field claims for itself.
//
// Measured before this test existed: deleting the tag survived all four packages, the golden included.
// Nothing HASHES usage_json, which is exactly why nothing went red — and exactly why the claim needed a
// pin of its own rather than a neighbour's.
func TestAnUnansweredThinkingShareAddsNoBytes(t *testing.T) {
silent, err := json.Marshal(Usage{PromptTokens: 10, CompletionTokens: 8496})
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(silent), "ReasoningInCompletion") {
t.Fatalf("a provider that reported nothing must add no bytes at all, got %s", silent)
}
// A MEASURED zero is an answer and must survive to disk; `omitempty` omits a nil pointer, never a
// pointer to zero, and this is the assertion that says so out loud.
zero := 0
measured, err := json.Marshal(Usage{PromptTokens: 10, CompletionTokens: 1617, ReasoningInCompletion: &zero})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(measured), `"ReasoningInCompletion":0`) {
t.Fatalf("a measured zero must reach the checkpoint as a zero, got %s", measured)
}
// And it round-trips: an old checkpoint decodes to nil, a new one to the number it carried.
var back Usage
if err := json.Unmarshal(silent, &back); err != nil {
t.Fatal(err)
}
if back.ReasoningInCompletion != nil {
t.Fatalf("a checkpoint written without the field must decode as unanswered, got %d", *back.ReasoningInCompletion)
}
if err := json.Unmarshal(measured, &back); err != nil {
t.Fatal(err)
}
if back.ReasoningInCompletion == nil || *back.ReasoningInCompletion != 0 {
t.Fatalf("a measured zero must decode as a measured zero, got %v", back.ReasoningInCompletion)
}
}

View file

@ -137,7 +137,11 @@ type hole struct {
Chapter int
Unit int
Reason string // the unit's flag reason (withheld) or the dropped member's (incomplete)
Dropped int // incomplete: how many members are missing
// Dropped is how many members of the unit are missing for good. Filled for `incomplete` (text shipped
// without a member) AND for `withheld` (a stop mark over a unit that also lost one), because the
// operator's refusal text has to say both halves: topping up the ceiling finishes the unit and still
// cannot bring the member back.
Dropped int
}
// bookIdentifierPrefix is the URN namespace of dc:identifier: the book id under it is deterministic —
@ -453,7 +457,19 @@ func describeHoles(bookID string, exp *BookExport, holes []hole, staleUnknown bo
case HolePending:
fmt.Fprintf(&sb, "\n chapter %d unit %d: pending (not yet translated)", h.Chapter, h.Unit)
case HoleWithheld:
fmt.Fprintf(&sb, "\n chapter %d unit %d: withheld%s", h.Chapter, h.Unit, parenReason(h.Reason))
// The operator gets the same THREE-way distinction the reader's file and the export banner get,
// and the third part is the one that costs him a second run: a stop mark is work the next run
// buys — unless the unit is ALSO short a member for good, which no purchase brings back. Told
// only the first half, he tops up, resumes, and the build refuses again with a different hole.
switch {
case FlagReason(h.Reason).AnswersForResume():
fmt.Fprintf(&sb, "\n chapter %d unit %d: withheld%s", h.Chapter, h.Unit, parenReason(h.Reason))
case h.Dropped > 0:
fmt.Fprintf(&sb, "\n chapter %d unit %d: withheld — paid for and NOT done, AND %d member(s) of it are missing for good; the next run finishes the unit but cannot bring those back%s",
h.Chapter, h.Unit, h.Dropped, parenReason(h.Reason))
default:
fmt.Fprintf(&sb, "\n chapter %d unit %d: withheld — paid for and NOT done; the next run re-does it%s", h.Chapter, h.Unit, parenReason(h.Reason))
}
case HoleIncomplete:
fmt.Fprintf(&sb, "\n chapter %d unit %d: incomplete — %d member(s) of the unit missing from its text%s", h.Chapter, h.Unit, h.Dropped, parenReason(h.Reason))
case HoleStale:
@ -560,8 +576,41 @@ func assembleBook(exp *BookExport, stale map[UnitRef]bool, title, language strin
case HolePending:
mark(words.HolePending)
case HoleWithheld:
mark(words.HoleWithheld)
holes = append(holes, hole{Kind: HoleWithheld, Chapter: ce.Chapter, Unit: ce.ChunkIdx, Reason: ce.FlagReason})
// ⛔ WHICH SENTENCE DEPENDS ON WHETHER THE ROW IS A VERDICT, and it used to depend on nothing.
// The withheld phrase tells the reader the fragment «requires a human check» (langpacks/<target>/
// reader.txt, `hole.withheld`), which is true of a refusal or a contaminated output — and FALSE
// of a stop mark: a call a person cut, or a re-attack a ceiling refused, is a position that was
// paid for and not finished, and no human has anything to check. The next run does it. Of the
// phrases that exist, `hole.pending` («not translated yet») is the true one for those two.
//
// ⚠ THE REASON STAYS ON THE HOLE EITHER WAY, and that is deliberate rather than incidental: a
// third phrase of its own («paid for, not finished: top up and resume») is then a ROW IN
// reader.txt plus a branch here, not a re-design of this predicate. The question is asked of the
// REASON (AnswersForResume) so this site and every other asker cannot drift; the askers are
// counted in one place only — beside AnswersForResume in disposition.go — so the number has a
// single carrier to keep honest.
//
// ⚠ AND WHERE THE REASON DOES *NOT* REACH, so nobody builds on a promise this does not make:
// BuildReport carries COUNTS and no reasons, so `withheld_units` holds both kinds and the
// platform — which reads exactly five counters of it — cannot tell «needs money» from «needs a
// human». The refusal text below (describeHoles) does carry it, and so does the export record.
// Splitting the counter is a contract change across two zones and is named in the pack's report
// rather than smuggled in here.
// ⚠ AND «DROPPED MEMBERS» KEEPS THE WITHHELD PHRASE WHATEVER THE REASON: a c-lite unit whose
// member is permanently flagged has lost that text for good, so «not translated yet» would
// promise a next run that cannot bring it back. The reader is told a human is needed, which is
// true of the member even when the stage that would have assembled it merely ran out of money.
if FlagReason(ce.FlagReason).AnswersForResume() || ce.DroppedMembers > 0 {
mark(words.HoleWithheld)
} else {
mark(words.HolePending)
}
// ⛔ AND THE DROP COUNT TRAVELS WITH IT, because the operator's refusal text is the THIRD surface
// of this distinction and it could not ask without the field: a unit can be both «paid for and
// not done» and short a member FOR GOOD, and that is the one person who can act on the second
// half. Before this the reader and the export had the caveat and the operator did not — the
// «took the form, not the guarantee» class, one surface further along.
holes = append(holes, hole{Kind: HoleWithheld, Chapter: ce.Chapter, Unit: ce.ChunkIdx, Reason: ce.FlagReason, Dropped: ce.DroppedMembers})
continue
case HoleStale:
mark(words.HoleStale)

View file

@ -44,16 +44,37 @@ type cutCall struct {
escalation bool
}
// cancelledPosition is the chunk×stage a stopped run was working on. It is a struct for the reason
// stoppedPosition is the chunk×stage a stopped run was working on. It is a struct for the reason
// cutCall is: `snapID` and `contentHash` are adjacent strings, and a swap between them compiles,
// writes a row addressed to nothing, and is found by nobody.
type cancelledPosition struct {
type stoppedPosition struct {
stage config.Stage
chunk chunk.Chunk
snapshotID string
contentHash string
cumCostUSD float64
attempts int
// paidAttempts is how many attempts of this position produced a CLASSIFICATION — a reply that was
// paid for (freshly, or on an earlier run and replayed from its checkpoint) and judged.
//
// ⛔ IT IS NOT `attempts`, AND THE DIFFERENCE IS A UNIT. `attempts` follows the attempt INDEX, which
// walks over burned keys and includes the index a reservation was refused at — so after a stopped run
// the first fresh purchase of a position happens at index ≥ 1 with nothing classified at all. A mark
// keyed on the index would write a verdict about a unit nobody has translated once.
paidAttempts int
// inHand is the last attempt that WAS classified: its index and what it answered. The ceiling mark
// below needs both, because its own reason says why the re-attack never happened and nothing else on
// the row would then say what the paid attempt came back as.
inHand stageAttempt
// firstFlagReason is what the FIRST attempt of this position failed with — the telemetry column
// (chunk_status.first_flag_reason) every row whose own verdict is no longer that failure carries.
//
// ⛔ A MARK THAT DROPPED IT WOULD BLIND THE ECHO METRIC ON A RUN THAT PAID FOR AN ECHO. The metric
// counts a draft whose first attempt echoed through `flag_reason OR first_flag_reason` (quality.go):
// a marked row's flag_reason says why the PURCHASE did not happen, so without this column the unit
// stays in the denominator and leaves the numerator, and the echo rate FALLS on the run that bought
// the echo — the 25.07 defect («echo_draft=0.0% of 20») one column further along.
firstFlagReason FlagReason
}
// settleUSDForCutCall is the ONE place that answers «what does a call we cut short cost».
@ -178,18 +199,48 @@ func cutErrLine(err error, deliveries int, cost float64) string {
return fmt.Sprintf("[the provider was asked %d times; %s] %s", deliveries, booked, err.Error())
}
// recordCancelledStage marks a position whose call a HUMAN stopped, so the stop leaves no unexplained
// gap. It never changes the outcome it is called on: the run is ending, and this says what it was
// doing when it did.
// stopMark is the row a stopped run leaves on the position it was working on: its reason, the sentence a
// person reads afterwards, and how many attempts that position actually made.
type stopMark struct {
reason FlagReason
detail string
attempts int
// firstFlag is the superseded first failure, written exactly as the ok path writes it
// (recoveredFirstFlag): empty when the row's own verdict IS that failure, so nothing is counted twice.
firstFlag string
}
// stopMarkFor decides WHETHER a stop leaves a mark on this position and WHICH one. It is a pure function
// of the position and the error so the decision can be asked directly in a test, rather than only through
// a run that has to be arranged to stop in the right way.
//
// The mark carries its own reason. The call was healthy — the stop button ended it — so
// `flagged(cancelled)` reads as «stopped; the resume re-does it», while a shared flag would send an
// operator hunting for a defect that is not there and no mark at all would let the chapter export a
// hole nobody knows about. A flag lying about its cause is forbidden in its own right (D39.93 п.2).
// TWO stops leave a mark, and they are different facts about the same shape — money was spent on this
// position and the work behind it was not finished:
//
// A write failure is logged, not returned: the caller is already returning the error that stopped the
// run, and replacing it with a bookkeeping failure would hide why the run stopped.
func (r *Runner) recordCancelledStage(ctx context.Context, p cancelledPosition, err error) {
// - a HUMAN stopped the run over a call that was already on the wire (`cancelled`). The call was
// healthy, so the mark reads «stopped; the resume re-does it on the same budget»;
// - a USD CEILING refused to reserve the RE-ATTACK of an attempt this position had already paid for
// (`retry_unaffordable`, backlog row 291). The run stops like any other ceiling halt — that part is
// ratified and unchanged — but the position used to leave NO row at all, so the unit read `pending`,
// indistinguishable from one nobody had started, while its first attempt was paid for and on disk.
// Measured on the fixture of retrystopmark_test.go before this existed: `committed=$0.003640` with
// ONE chunk_status row for the two units the run had touched.
//
// ⛔ EVERY OTHER STOP LEAVES NOTHING, and that is a statement rather than an omission. A ceiling that
// refuses attempt 0 has bought nothing this position can DELIVER: `pending` is then the truth about the
// TEXT, and a row would invent a half-done unit. That is why the ceiling branch asks `paidAttempts`, not
// the attempt index.
//
// ⚠ «NOTHING IT CAN DELIVER» IS NARROWER THAN «NOTHING», and the difference is a door this pack leaves
// open: a position whose only purchase was a BURNED key (money, no result — burnedByCut) and whose next
// index a ceiling refused has `committed > 0` with no row, which is the same invisible shape this mark
// exists to end, minus any text to account for. Closing it needs a reason of its own («paid for, nothing
// came back, nothing translated») or the owner's word that a burn is visible through the ledger alone —
// and the table of stopMarkFor pins today's answer so the next reader finds a decision, not a gap.
//
// A flag lying about its cause is forbidden in its own right (D39.93 п.2), which is why the second case
// gets a reason of its own instead of borrowing `cancelled`: nobody stopped that run by hand.
func stopMarkFor(p stoppedPosition, err error) (stopMark, bool) {
// ⛔ THE STOP IS ASKED OF THE ERROR AS A WHOLE, and the delivery of ANY cut in it — not of whichever
// cut `errors.As` happens to reach first. A retry chain hands up the cut with the strongest money
// claim (llm's chainError), which is deliberately the EARLIER one when a later free cut would mask
@ -202,7 +253,83 @@ func (r *Runner) recordCancelledStage(ctx context.Context, p cancelledPosition,
// errors.As answers both halves, and a walk over the error tree looking for a delivered one would be
// asking a question that cannot come back different.
var cut *llm.AttemptCutError
if !errors.Is(err, context.Canceled) || !errors.As(err, &cut) {
if errors.Is(err, context.Canceled) && errors.As(err, &cut) {
return stopMark{
reason: FlagCancelled,
detail: "the run was stopped while this call was in flight; it was paid for at the reservation estimate and the resume re-does it on the same budget",
attempts: p.attempts,
firstFlag: recoveredFirstFlag(p.firstFlagReason, FlagCancelled),
}, true
}
if p.paidAttempts > 0 && errors.Is(err, errReserveCeiling) {
return stopMark{
reason: FlagRetryUnaffordable,
detail: ceilingStopDetail(p, err),
// ⚠ ONE FEWER THAN `attempts`, AND THE BRANCH ABOVE DOES NOT SUBTRACT — the two stops differ
// in exactly this. `attempts` is the index the loop is ON (attemptsMade = attempt + 1, set
// before the error check), so for a cancelled call it counts the call that DID go out, while
// here the index it counts bought nothing at all. What is left is every index this position
// really consumed, burned keys included — the same thing the ok path's count includes, so a
// row cut from `paidAttempts` instead would silently drop a burn this position paid for.
//
// ⚠ IT CANNOT GO NEGATIVE, and the reason is not local: `paidAttempts > 0` above means the loop
// classified something, and the loop sets attemptsMade = attempt + 1 ≥ 1 before any error is
// read (stagerun.go). A future shape that marked a position without that guarantee would have
// to bring the floor with it.
attempts: p.attempts - 1,
// ⚠ BOTH MARKS CARRY IT, and the cancelled one did not until this pack: a position stopped over
// its SECOND attempt has a first failure too, and the same blinding applied to it. One rule for
// the two stop marks rather than a rule and an exception.
firstFlag: recoveredFirstFlag(p.firstFlagReason, FlagRetryUnaffordable),
}, true
}
return stopMark{}, false
}
// ceilingStopDetail is the sentence the money mark leaves behind. It carries three things the row cannot
// get anywhere else: WHAT the paid attempt answered (the reason column now says why the re-attack never
// happened, not what came back), WHICH ceiling refused, and HOW MUCH was missing. The log line says the
// same, and dies with the process; the row is what a person reads the next morning.
//
// ⚠ IT NAMES THE RE-ATTACK, and that is only true while every OTHER paid step of a position degrades on a
// ceiling instead of propagating it: the escalation hop, the repair sub-step and the terminology pass all
// catch errReserveCeiling (escalation.go, repair.go, terminologist.go), so the one that reaches here is
// the attempt loop's. A new paid sub-step that let a ceiling through would make this sentence misname
// which purchase was refused.
func ceilingStopDetail(p stoppedPosition, err error) string {
// WHICH ceiling is named by the typed stop, and the whole noun phrase is built here rather than left to
// a placeholder inside the sentence: a bare «%s» with a defensive default produced «the a USD ceiling»
// on the day the type was not there, and this line is what a person reads the next morning.
//
// ⚠ THE DEFAULT IS UNREACHABLE TODAY, and the condition is worth naming rather than trusting: both
// wrappers of errReserveCeiling are inside a *CeilingHalt (stagerun.go), so an `errors.Is` that holds
// cannot sit on an `errors.As` that fails. It becomes reachable the day something wraps that sentinel
// without the type — and then the sentence still reads.
ceiling, shortfall := "a USD ceiling", int64(0)
var halt *CeilingHalt
if errors.As(err, &halt) {
ceiling, shortfall = "the "+halt.Scope+" USD ceiling", halt.ShortfallMicroUSD
}
// «Short by N» is omitted rather than printed as zero when the stop did not state it (a day-scope
// refusal, or a ledger read that failed — shortfallMicroUSD): a money figure of 0 reads as «you are
// not short of anything», which is the opposite of what happened.
missing := ""
if shortfall > 0 {
missing = fmt.Sprintf(", short by %d micro-USD", shortfall)
}
return fmt.Sprintf("attempt %d was paid for and came back %s; %s refused to reserve the re-attack%s — raise the ceiling and the resume finishes this unit",
p.inHand.attempt, p.inHand.cls.Reason, ceiling, missing)
}
// recordStoppedPosition marks a position a stopped run was working on, so the stop leaves no unexplained
// gap. It never changes the outcome it is called on: the run is ending, and this says what it was
// doing when it did. WHICH stops leave a mark, and why, is stopMarkFor above.
//
// A write failure is logged, not returned: the caller is already returning the error that stopped the
// run, and replacing it with a bookkeeping failure would hide why the run stopped.
func (r *Runner) recordStoppedPosition(ctx context.Context, p stoppedPosition, err error) {
mark, marked := stopMarkFor(p, err)
if !marked {
return
}
// ⛔ A CUT MUST NOT BLIND A VERDICT IT DID NOT PRODUCE, and this row is an UPSERT: chunk_status is
@ -242,14 +369,21 @@ func (r *Runner) recordCancelledStage(ctx context.Context, p cancelledPosition,
if uerr := r.Store.UpsertChunkStatus(store.ChunkStatus{
BookID: r.Book.BookID, Chapter: p.chunk.Chapter, ChunkIdx: p.chunk.ChunkIdx, Stage: p.stage.Name,
SnapshotID: p.snapshotID, ContentHash: p.contentHash,
Disposition: string(DispFlagged), FlagReason: string(FlagCancelled),
Attempts: p.attempts, FinalHash: "", CostUSD: p.cumCostUSD,
Detail: "the run was stopped while this call was in flight; it was paid for at the reservation estimate and the resume re-does it on the same budget",
Disposition: string(DispFlagged), FlagReason: string(mark.reason),
Attempts: mark.attempts, FinalHash: "", CostUSD: p.cumCostUSD,
Detail: mark.detail, FirstFlagReason: mark.firstFlag,
}); uerr != nil {
r.Log.ErrorContext(ctx, "could not mark the stopped position; its money is recorded but the chunk will read as never started",
"stage", p.stage.Name, "chapter", p.chunk.Chapter, "chunk", p.chunk.ChunkIdx, "err", uerr)
return
}
if mark.reason == FlagRetryUnaffordable {
r.Log.WarnContext(ctx, "re-attack denied by a USD ceiling; the position is marked paid-but-unfinished so it does not read as never started, and a raised ceiling finishes it on resume",
"stage", p.stage.Name, "chapter", p.chunk.Chapter, "chunk", p.chunk.ChunkIdx,
"paid_attempt", p.inHand.attempt, "paid_reason", string(p.inHand.cls.Reason),
"cost_usd", fmt.Sprintf("%.6f", p.cumCostUSD), "detail", mark.detail)
return
}
r.Log.WarnContext(ctx, "the run was stopped over a call that had already gone out; the position is marked cancelled and the resume re-does it on the same budget",
"stage", p.stage.Name, "chapter", p.chunk.Chapter, "chunk", p.chunk.ChunkIdx, "cost_usd", fmt.Sprintf("%.6f", p.cumCostUSD))
}
@ -285,10 +419,29 @@ func (r *Runner) paidAfterBurns(st config.Stage, model, snapID string, ch chunk.
}
// resolvedForResume says a stored disposition is an ANSWER the resume may serve without calling
// anybody. Everything terminal is; `cancelled` is the one row that is not, because it records a stop
// rather than a verdict and the work behind it was never done. Reading it as terminal degenerates the
// whole construction into its opposite — never re-doing anything that was interrupted — which is the
// failure mode this design is most at risk of, since it would look perfectly green.
// anybody. Everything terminal is; two rows are not, and they are not for the same reason — each
// records something that was never DONE rather than a verdict about what came back:
//
// - `cancelled` — a person stopped the run over a call that was already on the wire;
// - `retry_unaffordable` — the first attempt was paid for and flagged, and a ceiling refused to
// reserve the retry. It is the one flag money CURES (D4: raise the ceiling, resume, the reader gets
// a good translation), so a resume must re-attack it rather than serve the shortfall as an answer.
// Re-attacking costs nothing while the ceiling stands: attempt 0 replays from its checkpoint for $0
// and the refusal recurs, which is why the re-attack is safe to repeat on every resume.
//
// Reading either as terminal degenerates the whole construction into its opposite — never re-doing
// anything that was interrupted — which is the failure mode this design is most at risk of, since it
// would look perfectly green.
func resolvedForResume(cs *store.ChunkStatus) bool {
return FlagReason(cs.FlagReason) != FlagCancelled
return FlagReason(cs.FlagReason).AnswersForResume()
}
// ResolvedForResume is the same question for the CLI, which has to split a book's rows into «answers the
// next run serves for $0» and «positions it will do again».
//
// ⛔ IT IS EXPORTED RATHER THAN RE-WRITTEN THERE, and the reason is a defect that existed for the length
// of one edit: the stopped-run account had the split spelled as `FlagReason == FlagCancelled`, a second
// copy of this rule — and the day a SECOND non-resolved reason existed (retry_unaffordable) that copy
// started counting a position the resume re-does as one with a verdict, telling an operator the rest is
// served for $0. One predicate, asked by both surfaces.
func ResolvedForResume(cs *store.ChunkStatus) bool { return resolvedForResume(cs) }

View file

@ -1,6 +1,7 @@
package pipeline
import (
"bufio"
"context"
"database/sql"
"encoding/json"
@ -57,12 +58,41 @@ type cutServer struct {
behave func(i int, w http.ResponseWriter, r *http.Request) // i is 1-based
arrived chan struct{} // closed when the FIRST request lands
once sync.Once
srv *httptest.Server
// flushed is closed when a handler FLUSHES its first response byte — the server side of the «a reply
// came back» boundary two of the nine rows stand on. See flushWatch.
flushed chan struct{}
flushOnce sync.Once
srv *httptest.Server
}
// flushWatch wraps the handler's ResponseWriter so the FIRST flush says so on a channel.
//
// ⛔ IT EXISTS BECAUSE ONE ROW'S BOUNDARY WAS ARRANGED BY HOPE. «The stop landed AFTER a response byte
// came back» was built by sleeping 50 ms from the request's ARRIVAL — a window that had to cover the
// handler being scheduled, the header write, the flush and the client's read — and it missed 2 runs in 8
// under load. It then failed on the MONEY, three steps from the cause, which is how a fixture that did
// not build its own situation reads as a defect in the code.
//
// Flush and Hijack are forwarded EXPLICITLY: wrapping a ResponseWriter hides the optional interfaces, and
// three of the nine rows hijack the connection — a hijack that stopped working would turn them into
// silent passes, which is worse than the flake this removes.
type flushWatch struct {
http.ResponseWriter
cs *cutServer
}
func (f flushWatch) Flush() {
f.ResponseWriter.(http.Flusher).Flush()
f.cs.flushOnce.Do(func() { close(f.cs.flushed) })
}
func (f flushWatch) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return f.ResponseWriter.(http.Hijacker).Hijack()
}
func newCutServer(t *testing.T, behave func(i int, w http.ResponseWriter, r *http.Request)) *cutServer {
t.Helper()
cs := &cutServer{behave: behave, arrived: make(chan struct{})}
cs := &cutServer{behave: behave, arrived: make(chan struct{}), flushed: make(chan struct{})}
cs.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
cs.mu.Lock()
@ -71,7 +101,7 @@ func newCutServer(t *testing.T, behave func(i int, w http.ResponseWriter, r *htt
cs.bodies = append(cs.bodies, string(body))
cs.mu.Unlock()
cs.once.Do(func() { close(cs.arrived) })
cs.behave(i, w, r)
cs.behave(i, flushWatch{w, cs}, r)
}))
t.Cleanup(cs.srv.Close)
return cs
@ -298,6 +328,13 @@ type cutRow struct {
// wantSameBudget: the re-done call must carry the max_tokens of the one that was cut — a doubling
// here would buy twice the call for a health nobody lost.
wantSameBudget bool
// assertBoundary says this row's money stands on WHICH SIDE of the «a response byte came back»
// boundary the stop landed, so the row asserts the cut's own evidence and PRINTS it instead of
// letting a money mismatch three steps away speak for a fixture that missed its moment.
assertBoundary bool
// wantAfterHeaders is that side. It also decides what the canceller waits for: a row that must stop
// AFTER the boundary waits for the handler's own flush rather than for a sleep to have covered it.
wantAfterHeaders bool
}
func TestTheNineOutcomesOfACall(t *testing.T) {
@ -331,6 +368,7 @@ func TestTheNineOutcomesOfACall(t *testing.T) {
cancelOnArrival: true,
wantFirstCalls: 1, wantPaid: true, wantFlag: FlagCancelled, wantDisp: DispFlagged,
wantEstimated: true, wantResumeCall: 1, wantSameBudget: true,
assertBoundary: true, wantAfterHeaders: true,
}, {
name: "after 2xx · connection lost",
behave: func(t *testing.T, _ int, w http.ResponseWriter, _ *http.Request) {
@ -353,6 +391,7 @@ func TestTheNineOutcomesOfACall(t *testing.T) {
cancelOnArrival: true,
wantFirstCalls: 1, wantPaid: false, wantFlag: FlagCancelled, wantDisp: DispFlagged,
wantEstimated: false, wantResumeCall: 1, wantSameBudget: true,
assertBoundary: true, wantAfterHeaders: false,
}, {
name: "before headers · connection lost",
behave: func(t *testing.T, _ int, w http.ResponseWriter, _ *http.Request) {
@ -399,6 +438,16 @@ func TestTheNineOutcomesOfACall(t *testing.T) {
go func() {
<-srv.arrived
// The request is with the provider; this is the delivered-and-stopped case.
if row.wantAfterHeaders {
// …and the boundary must be CROSSED first. Waiting for the handler's own flush
// leaves only the client's read of an already-sent byte inside the sleep below,
// instead of the whole handler in it. The bound is the fixture's own: a flush
// that never comes must not hang the test (a hang has no colour).
select {
case <-srv.flushed:
case <-time.After(4 * time.Second):
}
}
time.Sleep(50 * time.Millisecond)
cancel()
}()
@ -409,6 +458,30 @@ func TestTheNineOutcomesOfACall(t *testing.T) {
t.Fatalf("the SERVER was asked %d time(s), want %d (first run err: %v)", firstCalls, row.wantFirstCalls, firstErr)
}
if row.assertBoundary {
var cut *llm.AttemptCutError
if !errors.As(firstErr, &cut) {
t.Fatalf("this row's money stands on a cut call's own evidence and the run left none: %v", firstErr)
}
// The величина the acceptance criterion asks for: the boundary this run actually crossed,
// printed beside the side the row is about.
t.Logf("the boundary crossed: after_headers=%v billable=%v bytes_read=%d whitespace_only=%v elapsed=%s cause=%s",
cut.AfterHeaders, cut.Billable, cut.BytesRead, cut.WhitespaceOnly,
cut.Elapsed.Round(time.Millisecond), cut.Cause)
if cut.AfterHeaders != row.wantAfterHeaders {
t.Fatalf("the stop landed on the WRONG side of the «a reply came back» boundary "+
"(after_headers=%v, want %v): this fixture did not build the situation the row "+
"asserts money about, so the money check below would answer about another one",
cut.AfterHeaders, row.wantAfterHeaders)
}
// And the money bit itself, at the source rather than in the ledger: `Billable` is what
// decides whether the settle books the estimate, so the row's wantPaid must be ITS value.
if cut.Billable != row.wantPaid {
t.Fatalf("the cut says billable=%v and the row expects paid=%v — the ledger assertion "+
"below would then be measuring a different call", cut.Billable, row.wantPaid)
}
}
m := readMoney(t, bookPath)
assertCutMoney(t, row, m)

View file

@ -185,7 +185,7 @@ func TestACutOverAPositionThatAlreadyShippedTextKeepsIt(t *testing.T) {
//
// The mark exists for the position that has NO verdict: a run cut over its first attempt would otherwise
// leave the money booked and the position reading «never started», which is the invisible hole
// recordCancelledStage was written for. The guard added for the row above must not have eaten it.
// recordStoppedPosition was written for. The guard added for the row above must not have eaten it.
func TestACutOverAVirginPositionStillMarksIt(t *testing.T) {
rec := &reqRec{}
var armed atomic.Bool

View file

@ -100,6 +100,31 @@ const (
// records money with no result — which burnedByCut catches BEFORE anything classifies it, so no
// replay of that row ever reaches a disposition. The guard is that predicate, not a name.
// FlagRetryUnaffordable is the verdict of a unit whose FIRST attempt was paid for and came back
// retryable (length/empty, or an echo the re-roll was meant to answer) and whose RETRY a USD ceiling
// refused to reserve. D2's own ending — «retry up to the cap, then flag» — with the retry never bought.
//
// ⛔ IT IS ITS OWN REASON BECAUSE `length` WOULD LIE ABOUT THE CAUSE (D39.204 п.4). A reader and an
// operator have to tell «we tried everything the budget allowed» from «the money ran out before the
// second attempt»: the first is answered by a redrive or a better model, the second by topping up, and
// a flag that says the former sends a person to fix something that is not broken. Before it existed
// the refusal left the attempt loop as an ERROR instead — earlier than chunk_status — so the unit had
// no row at all: it read `pending` forever, every resume replayed attempt 0 for $0 and died on the
// retry again, and the run departed `exit 4` with every BOUGHT unit delivered.
//
// ⚠ NEITHER `retryable` NOR `escalatable` IS ASKED OF IT, and that is a fact about where it comes
// from rather than a policy: both predicates are asked of a LIVE classification (stagerun.go,
// escalation.go), and classify() never produces this reason — it is written by the stop mark
// (cutcall.go) on a run that is ending. Nothing in this run re-attacks or escalates the position
// because the run stops; what finishes it is money, which is why the row is deliberately NOT resolved
// for resume (resolvedForResume) — raise the ceiling and the resume re-attacks the unit and walks the
// whole path, escalation hop included.
//
// ⚠ That last sentence holds only while resolvedForResume keeps answering false for it. A change that
// made this row terminal would turn the cheapest flag in the book into a permanent one, and would do
// it silently: the row, its reason and its money all look exactly the same either way.
FlagRetryUnaffordable FlagReason = "retry_unaffordable"
// Reserved for later steps — DEFINED for contract stability, NOT emitted by
// Milestone 2. coverage_fail / excision_suspect are verdicts of the configurable
// coverage gate (step 6); hard_block / upstream_not_ok are for HTTP-level
@ -229,6 +254,40 @@ func (r FlagReason) escalatable() bool {
return false
}
// AnswersForResume says a flag is a VERDICT about what came back, rather than a MARK that a run stopped
// over this position with the work unfinished. The two stop marks — a call a person cut (`cancelled`) and
// a re-attack a ceiling refused (`retry_unaffordable`) — are the whole of the second class: both record
// money spent and work NOT done, and the next run re-does them.
//
// ⛔ IT IS ASKED OF THE REASON SO EVERY SURFACE CAN ASK THE SAME QUESTION, and it is EXPORTED for the
// same reason: three of the surfaces that must ask it hold an export RECORD and not a stored row, so a
// row-shaped predicate leaves them nothing to ask and they each invent a rule.
//
// TWELVE sites ask it, counted rather than remembered: grep -i for resolvedForResume and AnswersForResume
// over the non-test sources gives 25 lines today, which are 9 prose mentions (this paragraph included) + 3
// declarations + the delegate's own body + the TWELVE askers. ⚠ Only the last number is worth anything: the
// other three move the moment any comment mentions the predicate, so a reader who finds 26 lines should
// re-derive the breakdown rather than conclude an asker appeared. NINE ask it of the stored ROW: the resume fast-path (stagerun.go), the wave counters (the
// `waveShape.resolved` pair, and through them the money ledger), the unit's state (`resolveChunkState`),
// the redrive's target AND its stage list, the volume classifier's two questions (`unitPositionsOnFile`,
// `rowsResumeFree`), this mark's own prev-row guard, and the stopped-run account in the CLI. THREE ask it
// of the REASON, because what they hold is an export record and not a row: the reader's phrase in the book
// writer, the operator's refusal text beside it, and the plaintext export's banner. Four of the twelve
// already asked (the prev-row guard, the resume fast-path, `resolveChunkState`, `rowsResumeFree` — that is
// what `git show HEAD` answers); the rest either asked by NAME (`== FlagCancelled`, a copy that went wrong
// the day a second non-resolved reason existed) or did not ask at all — and three of those left a person
// wrong about the same position, though not in the same way. TWO said something false, in these words: the reader's own file says «требует проверки человеком» (langpacks reader.txt,
// `hole.withheld`) and the plaintext export's banner says «flagged for a human», about a position that needs
// nothing but the next run. The THIRD, the operator's refusal text, printed the bare `withheld (cancelled)`
// — it never said a human was needed, and it never said the thing that would have let him act either.
func (r FlagReason) AnswersForResume() bool {
switch r {
case FlagCancelled, FlagRetryUnaffordable:
return false
}
return true
}
// disposition maps a reason to the resolved chunk×stage state.
func (r FlagReason) disposition() Disposition {
if r == reasonOK {
@ -474,10 +533,19 @@ func degenerateLoop(text string) bool {
// the re-do has to be a NEW attempt index — so the two dimensions had to come apart, and this
// parameter is the one that is about money.
//
// ⚠ THE FORMULA AND ITS OUTPUT ARE UNCHANGED FOR EVERY PATH THAT EXISTS TODAY: both loops that
// regenerate increment the doubling count with the attempt index, so on a run with no cut call the
// two are identical and the snapshot does not move. That equality is the reason this could land on
// books that are mid-translation at all — moving maxTokensPolicyVersion would re-pay every one of them.
// ⚠ THE FORMULA AND ITS OUTPUT ARE UNCHANGED FOR EVERY PATH A SHIPPING CONFIG CAN REACH, and the
// qualifier is load-bearing. Three regeneration paths call this: the content retry, the echo re-roll,
// and — since the empty-reply remedy — a retry that lowers the thinking level INSTEAD of doubling. The
// first two increment the doubling count with the attempt index, so on a run with no cut call the two
// are identical and the snapshot does not move. The third does not increment it, deliberately: that is
// what makes the remedy cheaper than the disease.
//
// ⛔ SO THE REASON maxTokensPolicyVersion NEED NOT MOVE IS CONDITIONAL, not absolute: it holds while
// Retries.LowerEffortOnEmpty is off in every shipping config, which is pinned
// (config.TestShippingPipelinesDoNotLowerEffortOnEmpty) and carried as a decision by backlog row 433.
// Turning that key on does NOT move this version either — attempt 0 is untouched and the snapshot does
// not fold Retries — but it does re-key attempt ≥ 1 for the books that already hold one, which is a
// cost named at the key itself. A reader who removes that pin has removed this paragraph's premise.
func maxTokensForAttempt(base, escalations int) int {
if escalations <= 0 {
return base

View file

@ -0,0 +1,370 @@
package pipeline
import (
"context"
"errors"
"strings"
"testing"
"textmachine/backend/internal/obs"
"textmachine/backend/internal/store"
)
// echorecovered_test.go: «no longer the verdict» is not «recovered».
//
// The echo metric counts a draft whose FIRST attempt echoed even when the row's own verdict is something
// else — that split is what keeps a recovered echo visible (minirun_fixes_test.go). The RECOVERY half of
// the split used to be read off the same column: `first_flag_reason == cjk_artifact` was counted as an
// echo somebody fixed, without asking what the row says NOW. A re-roll that came back with a DIFFERENT
// failure lands exactly there — the unit ships nothing and the report says the echo was recovered.
//
// It is live rather than theoretical: `regenerate_echo_before_escalate: 1` stands in all four shipping
// pipelines (backend/configs/pipeline-c1.yaml, -c2, -arm-glm, -arm-mistral), so the re-roll this fixture
// drives is the one production runs.
// TestASupersededEchoIsNotCountedAsRecovered drives the whole path rather than writing the row by hand,
// because the claim has two halves: the row shape is REACHABLE on a shipping configuration, and the
// counter reads it correctly. A hand-written row would prove only the second.
func TestASupersededEchoIsNotCountedAsRecovered(t *testing.T) {
rec := &reqRec{}
calls := 0
srv := newJSONProvider(rec, func(body string) (string, string) {
if isEditBody(body) {
return "ОТРЕДАКТИРОВАННЫЙ ПЕРЕВОД", "stop"
}
calls++
if calls == 1 {
// The echo: the source handed back, dense enough for the classifier to call it an artifact.
return strings.Repeat(suzukiSource, 3), "stop"
}
// The re-roll answers in the target language and is CUT at the budget: a different failure, and
// with the regeneration budget now spent it is the row's verdict.
return "ЧЕРНОВИК ОБОРВАН", "length"
})
defer srv.Close()
bookPath := setupProjectOpts(t, srv.URL, projectOpts{
source: suzukiSource, glossarySeed: suzukiSeed,
// Both budgets as the shipping pipelines carry them: one content regeneration, one echo re-roll.
// They share the `regens` counter, which is why the length cut below cannot be re-attacked.
regenerate: 1, regenerateEcho: 1,
})
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)
}
// The premise, printed: the row shape this test is about — an echo that is no longer the verdict, on a
// unit that ships NOTHING. Without it the assertions below could pass on a row nobody is arguing about.
t.Logf("the row the re-roll left: disposition=%q flag_reason=%q first_flag_reason=%q attempts=%d final_hash=%q",
cs.Disposition, cs.FlagReason, cs.FirstFlagReason, cs.Attempts, cs.FinalHash)
if cs.FirstFlagReason != string(FlagCJKArtifact) || cs.FlagReason != string(FlagLength) {
t.Fatalf("premise broken: this fixture must leave first_flag_reason=cjk_artifact with the verdict moved to length, got %q/%q — the counter below would then be asked about a different row",
cs.FirstFlagReason, cs.FlagReason)
}
if cs.Disposition != string(DispFlagged) {
t.Fatalf("premise broken: the unit must still be flagged, got %q", cs.Disposition)
}
q, qerr := r.QualityReport()
if qerr != nil {
t.Fatal(qerr)
}
t.Logf("echo metric: chunks=%d recovered=%d rate=%.3f", q.EchoDraftChunks, q.EchoDraftRecovered, q.EchoDraftRate)
if q.EchoDraftChunks != 1 {
t.Errorf("echo_draft_chunks = %d, want 1 — the translator echoed, and that is what this metric watches", q.EchoDraftChunks)
}
if q.EchoDraftRecovered != 0 {
t.Errorf("echo_draft_recovered = %d, want 0: nothing recovered this unit — the re-roll failed differently and the unit ships no text. A recovery count that reads only `first_flag_reason` reports our success at papering over an echo where there was none",
q.EchoDraftRecovered)
}
}
// TestAnEchoSomebodyPaidForStaysInTheMetricWhenTheMoneyRanOut is the OTHER half of the same column, and
// the one the money mark put at risk: a draft that ECHOED at attempt 0 and whose echo re-roll a ceiling
// refused.
//
// ⛔ THE ROW'S OWN REASON THEN SAYS WHY THE PURCHASE DID NOT HAPPEN, not what came back — so unless the
// mark carries the superseded first failure, the unit stays in the metric's DENOMINATOR (it has a draft
// row) and leaves its NUMERATOR, and the echo rate FALLS on the very run that paid for an echo. That is
// the shape of the 25.07 mini-run («echo_draft=0.0% of 20»), one column further along, and it is invisible
// to every other surface: the money is right, the mark is right, the rate is wrong.
func TestAnEchoSomebodyPaidForStaysInTheMetricWhenTheMoneyRanOut(t *testing.T) {
rec := &reqRec{}
calls := 0
srv := newJSONProvider(rec, func(body string) (string, string) {
calls++
// Every call echoes: the re-roll this fixture is about is never bought, so only the first one runs.
return strings.Repeat(suzukiSource, 3), "stop"
})
defer srv.Close()
bookPath := setupProjectOpts(t, srv.URL, projectOpts{
source: suzukiSource, glossarySeed: suzukiSeed,
// The echo re-roll budget the four shipping pipelines carry. Draft-only so the unit's money is
// one call plus the refused re-roll and nothing else.
regenerate: 1, regenerateEcho: 1, draftOnly: true,
})
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
r := newRunner(t, bookPath)
defer r.Close()
manifest, merr := r.bookChunks()
if merr != nil {
t.Fatal(merr)
}
if len(manifest) != 1 {
t.Fatalf("fixture must be ONE unit, got %d", len(manifest))
}
ceiling, _, _ := ceilingThatRefusesTheReattack(t, r, manifest[0], 0)
r.CeilingUSD = ceiling
_, err := r.TranslateBook(ctx)
var halt *CeilingHalt
if !errors.As(err, &halt) {
t.Fatalf("the re-roll must be refused by the ceiling, got %v", err)
}
if calls != 1 {
t.Fatalf("the provider was called %d time(s), want 1: the echo was bought and the re-roll was not", calls)
}
cs, cerr := r.Store.GetChunkStatus("test-book", manifest[0].Chapter, manifest[0].ChunkIdx, "draft")
if cerr != nil || cs == nil {
t.Fatalf("the stopped position has no row: %v %v", cs, cerr)
}
t.Logf("the mark the stop left: disposition=%q flag_reason=%q first_flag_reason=%q cost_usd=%.6f",
cs.Disposition, cs.FlagReason, cs.FirstFlagReason, cs.CostUSD)
if FlagReason(cs.FlagReason) != FlagRetryUnaffordable {
t.Fatalf("premise broken: the row must be the money mark, got %q", cs.FlagReason)
}
if cs.FirstFlagReason != string(FlagCJKArtifact) {
t.Fatalf("the mark dropped what the PAID attempt came back as (first_flag_reason=%q): the echo this run bought is now invisible to the metric that exists to find it", cs.FirstFlagReason)
}
q, qerr := r.QualityReport()
if qerr != nil {
t.Fatal(qerr)
}
t.Logf("echo metric: rows=%d chunks=%d recovered=%d rate=%.3f", 1, q.EchoDraftChunks, q.EchoDraftRecovered, q.EchoDraftRate)
if q.EchoDraftChunks != 1 || q.EchoDraftRate == 0 {
t.Errorf("echo_draft_chunks=%d rate=%.3f, want 1 and non-zero — the translator echoed and the book paid for it", q.EchoDraftChunks, q.EchoDraftRate)
}
// And the other half of the same column, which this pack made honest: nothing RECOVERED this unit —
// the re-roll was never bought.
if q.EchoDraftRecovered != 0 {
t.Errorf("echo_draft_recovered=%d, want 0: the re-roll this echo needed was never bought", q.EchoDraftRecovered)
}
}
// TestTheEDITORsOwnEchoStaysCountableWhenTheMoneyRanOut is the same column on the stage that was missing
// it, and the pack is what made the gap cost something: the mark gives the edit position a row, `editRows`
// counts it unconditionally, and the numerator used to see an editor echo ONLY while it was still the
// row's verdict — so the edit echo RATE FELL on a run that had just paid for an editor echo.
//
// The blindness itself is older (an echo a re-roll RECOVERED was invisible too). Both halves are closed by
// the same second arm, and this fixture drives the half the pack created: the editor echoes, the echo
// re-roll is refused by a ceiling, and the run stops.
func TestTheEDITORsOwnEchoStaysCountableWhenTheMoneyRanOut(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, func(body string) (string, string) {
if isEditBody(body) {
// The EDITOR hands the source back — an echo of its own making, dense enough to be called one.
return strings.Repeat(suzukiSource, 3), "stop"
}
return "ЧЕРНОВИК ПЕРЕВОДА", "stop"
})
defer srv.Close()
bookPath := setupProjectOpts(t, srv.URL, projectOpts{
source: suzukiSource, glossarySeed: suzukiSeed,
regenerate: 1, regenerateEcho: 1,
})
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
r := newRunner(t, bookPath)
defer r.Close()
manifest, merr := r.bookChunks()
if merr != nil {
t.Fatal(merr)
}
// The window is computed for the EDIT stage, which sizes and renders from the DRAFT the provider above
// returns — the same rule runStage follows, asked through the same helper.
ceiling, _, _ := ceilingThatRefusesTheReattackAt(t, r, 1, manifest[0], "ЧЕРНОВИК ПЕРЕВОДА", 1)
r.CeilingUSD = ceiling
_, err := r.TranslateBook(ctx)
var halt *CeilingHalt
if !errors.As(err, &halt) {
t.Fatalf("the editor's echo re-roll must be refused by the ceiling, got %v", err)
}
editStage := r.Pipeline.Stages[len(r.Pipeline.Stages)-1].Name
cs, cerr := r.Store.GetChunkStatus("test-book", manifest[0].Chapter, manifest[0].ChunkIdx, editStage)
if cerr != nil || cs == nil {
t.Fatalf("the stopped EDIT position has no row: %v %v", cs, cerr)
}
t.Logf("the edit row the stop left: disposition=%q flag_reason=%q first_flag_reason=%q cost_usd=%.6f",
cs.Disposition, cs.FlagReason, cs.FirstFlagReason, cs.CostUSD)
if FlagReason(cs.FlagReason) != FlagRetryUnaffordable || cs.FirstFlagReason != string(FlagCJKArtifact) {
t.Fatalf("premise broken: this fixture must leave the EDIT row as the money mark over an echo, got %q/%q",
cs.FlagReason, cs.FirstFlagReason)
}
if cs.CostUSD <= 0 {
t.Fatalf("premise broken: the editor's echo must have been PAID for, got cost_usd=%.6f", cs.CostUSD)
}
q, qerr := r.QualityReport()
if qerr != nil {
t.Fatal(qerr)
}
t.Logf("echo metric: edit units=%d rate=%.3f · draft chunks=%d", q.EchoEditUnits, q.EchoEditRate, q.EchoDraftChunks)
if q.EchoEditUnits != 1 || q.EchoEditRate == 0 {
t.Errorf("echo_edit_units=%d rate=%.3f, want 1 and non-zero: the EDITOR echoed and the book paid $%.6f for it — a row in the denominator with nothing in the numerator makes the rate fall on the run that bought the echo",
q.EchoEditUnits, q.EchoEditRate, cs.CostUSD)
}
// ⛔ AND THE OTHER SIDE OF THE SAME GUARD, which nothing asserted anywhere until a reader mutated it:
// this echo was NOT cured — the ceiling refused the re-roll and the row carries no hash — so the
// recovered subset must stay EMPTY. The draft arm has both sides of its guard (three tests assert its
// zero); the edit arm had only the positive one, and dropping `shippedText` from it left the package
// green while an operator read «echo edit=1 (100.0%), 1 recovered» over a book nobody cured anything in.
if q.EchoEditRecovered != 0 {
t.Errorf("echo_edit_recovered=%d, want 0: the money ran out BEFORE the re-roll, the row carries no final hash, and calling that a recovery inverts the very clause it was built for",
q.EchoEditRecovered)
}
// The control: the translator did NOT echo in this fixture, so the draft counter must stay at zero —
// otherwise the assertion above could be passing on a counter that says «echo» about everything.
if q.EchoDraftChunks != 0 {
t.Errorf("echo_draft_chunks=%d, want 0: the translator answered in the target language here", q.EchoDraftChunks)
}
}
// TestARecoveredEchoIsReportedAsRecoveredOnBothStages is the GUARD the widened numerators needed, and the
// reason it exists is the same on both waves: a counter that says «echo» about a book which shipped clean
// prose reads as fresh breakage unless the recovered share is spelled out beside it.
//
// ⛔ AND THE RECOVERY QUESTION IS «DID THE UNIT SHIP», NOT «IS THE ROW OK». Keyed on the `ok` verdict alone
// it lost a real cure: the escalation hop's answer can come back with a COSMETIC leak, which the sanitizer
// STRIPS and ships (final_hash points at the derived export, and the executor calls that outcome recovered
// in its own words) — so an echo that was genuinely fixed reported as none, on the three shipping pipelines
// where the sanitizer gate is on. The rows below are written straight into the store because this is a
// READ-MODEL claim; that both shapes are reachable was measured by an adversarial pass (a stripped
// recovery shipping 275 bytes with `first_flag_reason=cjk_artifact`).
func TestARecoveredEchoIsReportedAsRecoveredOnBothStages(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, draftEdit)
defer srv.Close()
bookPath := setupProjectOpts(t, srv.URL, projectOpts{source: suzukiSource, glossarySeed: suzukiSeed})
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
r := newRunner(t, bookPath)
defer r.Close()
if _, err := r.TranslateBook(ctx); err != nil {
t.Fatal(err)
}
manifest, merr := r.bookChunks()
if merr != nil {
t.Fatal(merr)
}
ch := manifest[0]
draftStage, editStage := r.Pipeline.Stages[0].Name, r.Pipeline.Stages[len(r.Pipeline.Stages)-1].Name
// Both rows: an echo at the first attempt, a verdict that is no longer it, and TEXT on disk — one
// through an `ok` verdict, one through a cosmetic strip whose cleaned remainder IS the export.
for _, row := range []store.ChunkStatus{
{BookID: "test-book", Chapter: ch.Chapter, ChunkIdx: ch.ChunkIdx, Stage: draftStage,
Disposition: string(DispOK), FirstFlagReason: string(FlagCJKArtifact), FinalHash: "draft-hash", Attempts: 2},
{BookID: "test-book", Chapter: ch.Chapter, ChunkIdx: ch.ChunkIdx, Stage: editStage,
Disposition: string(DispFlagged), FlagReason: string(FlagSanitizerStripped),
FirstFlagReason: string(FlagCJKArtifact), FinalHash: "edit-hash", Attempts: 2},
} {
if err := r.Store.UpsertChunkStatus(row); err != nil {
t.Fatal(err)
}
}
q, qerr := r.QualityReport()
if qerr != nil {
t.Fatal(qerr)
}
t.Logf("draft: echo=%d recovered=%d · edit: echo=%d recovered=%d",
q.EchoDraftChunks, q.EchoDraftRecovered, q.EchoEditUnits, q.EchoEditRecovered)
if q.EchoDraftChunks != 1 || q.EchoDraftRecovered != 1 {
t.Errorf("draft echo=%d recovered=%d, want 1 and 1: the translator echoed and the unit shipped", q.EchoDraftChunks, q.EchoDraftRecovered)
}
if q.EchoEditUnits != 1 || q.EchoEditRecovered != 1 {
t.Errorf("edit echo=%d recovered=%d, want 1 and 1: the editor echoed, the strip shipped the cleaned text, and a headline without the recovered share reads as fresh breakage on a book that delivered",
q.EchoEditUnits, q.EchoEditRecovered)
}
}
// TestACuredEchoOnAMidEditStageIsStillCuredWhenTheUnitDiesLater pins the reading BOTH recovery counters
// deliberately take — the ROW, not the unit — on the only shape where the two come apart on the edit side.
// `waveStages` puts every non-translator stage in the edit wave, so a wave with a second stage has a first
// stage whose row is not the unit's last word. ⚠ The reachable shape is a second `editor` stage, which is
// what this fixture builds and what the engine actually runs: the one config in the repo declaring two
// (`select`+`edit` in c2) is REFUSED by CheckRunnable, so citing it as evidence would repeat a counterexample
// the repo has already withdrawn. The sibling pin above holds `secondEditStage`
// false, so it reports for the class and measures the one-stage slice; this is the slice it is silent about,
// and the pack's own doc claimed the opposite until a reader ran the shape.
//
// ⛔ WHY THIS IS THE READING AND NOT A DEFECT: the metric asks what the MODEL did and what curing it cost
// (D39.18 — it watches the model, not our success at papering over it). A later stage flagging the unit for
// its own reasons does not un-cure the echo, and the unit's absence from the book is what the hole counters
// report. The day somebody wants «recoveries that reached the reader», that is a DIFFERENT counter, and this
// test is where the difference is written down.
func TestACuredEchoOnAMidEditStageIsStillCuredWhenTheUnitDiesLater(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, draftEdit)
defer srv.Close()
bookPath := setupProjectOpts(t, srv.URL, projectOpts{source: suzukiSource, glossarySeed: suzukiSeed, secondEditStage: true})
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
r := newRunner(t, bookPath)
defer r.Close()
if _, err := r.TranslateBook(ctx); err != nil {
t.Fatal(err)
}
// The premise, asserted rather than assumed: three stages, and the echoed one is NOT the last.
if len(r.Pipeline.Stages) != 3 {
t.Fatalf("the fixture must declare a second edit-wave stage, got %d stages", len(r.Pipeline.Stages))
}
midEdit, finalStage := r.Pipeline.Stages[1].Name, r.Pipeline.Stages[2].Name
if midEdit == finalStage {
t.Fatalf("mid stage %q is the final stage — this test is about a row that is not the unit's last word", midEdit)
}
manifest, merr := r.bookChunks()
if merr != nil {
t.Fatal(merr)
}
ch := manifest[0]
for _, row := range []store.ChunkStatus{
// The editor echoed on the mid stage and a re-roll cured it: the row's verdict is no longer the
// echo and its output became a checkpoint the next stage read.
{BookID: "test-book", Chapter: ch.Chapter, ChunkIdx: ch.ChunkIdx, Stage: midEdit,
Disposition: string(DispOK), FirstFlagReason: string(FlagCJKArtifact), FinalHash: "mid-hash", Attempts: 2},
// …and then the LAST stage refused, so the unit ships nothing for a reason that has nothing to do
// with the echo.
{BookID: "test-book", Chapter: ch.Chapter, ChunkIdx: ch.ChunkIdx, Stage: finalStage,
Disposition: string(DispFlagged), FlagReason: string(FlagHardRefusal), FinalHash: "", Attempts: 1},
} {
if err := r.Store.UpsertChunkStatus(row); err != nil {
t.Fatal(err)
}
}
q, qerr := r.QualityReport()
if qerr != nil {
t.Fatal(qerr)
}
t.Logf("edit echo=%d recovered=%d rate=%.2f · text_units=%d processed_units=%d",
q.EchoEditUnits, q.EchoEditRecovered, q.EchoEditRate, q.TextUnits, q.ProcessedUnits)
// The fixture's own premise: the unit shipped NOTHING. Without this the assertion below would also pass
// on a report where the unit was fine, and the test would be about the ordinary one-stage shape again.
if q.TextUnits != 0 {
t.Fatalf("text_units=%d, want 0 — the unit must ship nothing, or this test is not about the shape it names", q.TextUnits)
}
// The denominator proves BOTH edit-wave rows are in the wave: 1 of 2, not 1 of 1.
if q.EchoEditRate != 0.5 {
t.Fatalf("edit echo rate=%.2f, want 0.50 over the two edit-wave rows — the wave must hold both stages", q.EchoEditRate)
}
if q.EchoEditUnits != 1 || q.EchoEditRecovered != 1 {
t.Fatalf("edit echo=%d recovered=%d, want 1 and 1: the echo WAS cured on the stage that made it, and a later stage's refusal is the hole counters' business, not the echo metric's",
q.EchoEditUnits, q.EchoEditRecovered)
}
}

View file

@ -245,10 +245,25 @@ func TestTheSeverityTableMeansWhatItsCommentsSay(t *testing.T) {
}
}
// «They rank together because they are the same thing to a reader — the chunk is lost and the money
// is spent.» A lost connection is not a third member: it reaches no disposition, so it wears no rank.
if rank(FlagDecodeError) != rank(FlagAttemptTimeout) {
t.Fatalf("the paid-and-nothing-came-back reasons must share one rank: decode=%d timeout=%d",
rank(FlagDecodeError), rank(FlagAttemptTimeout))
// is spent.» A lost connection is not a member: it reaches no disposition, so it wears no rank.
//
// ⚠ THE THIRD MEMBER JOINED WITH A CLAIM OF ITS OWN (row 291): `retry_unaffordable` is a position that
// paid for an attempt and whose re-attack a ceiling refused, so to a reader it is the same sentence —
// the chunk is lost and the money is spent — and the table says so. Asserted here because a rank
// written in a comment and nowhere else is a claim with no carrier, and a third member added to the
// comment alone would have left the trio's equality measured on two of three.
for _, r := range []FlagReason{FlagAttemptTimeout, FlagRetryUnaffordable} {
if rank(FlagDecodeError) != rank(r) {
t.Fatalf("the paid-and-nothing-came-back reasons must share one rank: decode=%d %s=%d",
rank(FlagDecodeError), r, rank(r))
}
}
// «It IS one of those two, plus the fact that the remedy could not be bought» — a chapter whose unit
// died for lack of money must not report `length` as its worst problem and send a person to fix a
// budget formula instead of topping up.
if rank(FlagRetryUnaffordable) >= rank(FlagLength) || rank(FlagRetryUnaffordable) >= rank(FlagEmpty) {
t.Fatalf("a re-attack nobody could afford must out-rank the budget symptom it supersedes: "+
"unaffordable=%d length=%d empty=%d", rank(FlagRetryUnaffordable), rank(FlagLength), rank(FlagEmpty))
}
// «An unrecognised string must not out-rank a diagnosis the engine actually made.»
for r, s := range flagSeverity {

View file

@ -18,7 +18,11 @@ const (
HoleNone HoleKind = ""
// HolePending: a manifest unit with no final row — not translated yet (BookExport.PendingUnits).
HolePending HoleKind = "pending"
// HoleWithheld: a final row with no text — a substantive flag / upstream skip withheld it.
// HoleWithheld: a final row with no text. Usually a substantive flag or an upstream skip withheld it —
// but NOT always: a STOP MARK (a call a person cut, a re-attack a ceiling refused) leaves the same
// shape and withheld nothing, it simply has not been bought yet. Readers that tell a person WHY must
// ask the reason (FlagReason.AnswersForResume), which is what the book writer and the plaintext export
// do; the counters behind this kind deliberately do not split the two (named in the pack's report).
HoleWithheld HoleKind = "withheld"
// HoleIncomplete: text present, a member chunk of it missing (ChunkExport.DroppedMembers > 0).
HoleIncomplete HoleKind = "incomplete"

View file

@ -0,0 +1,484 @@
package pipeline
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"sync"
"testing"
"time"
)
// lowereffort_test.go: the retry that reads WHICH failure it is answering — `retries.lower_effort_on_empty`
// (config.Retries.LowerEffortOnEmpty), spent in stagerun.go's attempt loop.
//
// The engine had one remedy for two failures. A `length` cut with text in it ran out of room while the
// answer was being written, and a bigger budget is the cure (D2.3). An `empty` reply filled max_tokens
// before the answer began — on a subset-billing provider that budget went to thinking — and a bigger
// budget there is the move D39.86 falsified: at the vendor default effort, doubling 8496 to 16992 grew
// reasoning_content from 15 424 to 21 528 characters and the reply stayed empty.
//
// The paid run of 11.09 is the shape these tests model: four double-payments worth $0.106472 = 25.4% of
// the book, three of them `empty` at the ceiling (8496 · 8496 · 16000 completion tokens, zero characters
// of text) and one a truncated draft.
// burnsTheWholeCeiling answers every draft call the way the cold run's failures did: finish_reason
// `length`, no content at all, and completion_tokens EQUAL TO THE CEILING IT WAS GIVEN. That last part
// is the measured fact the money rests on — on all four failures of the run, completion_tokens was the
// max_tokens granted — and it is what makes a doubled retry cost exactly twice as much as the attempt
// it is repeating.
//
// When answerWhenEffortIsExplicit is set, a call that carries a reasoning_effort key answers normally;
// that arm is a MODEL of «less thinking leaves room for the answer», not evidence for it, and it is
// used only where a test is about what the engine does with a recovery, never about whether one happens.
// loweredDraftText is what ONLY the lowered draft attempt answers. It shares no substring with the
// fixture's editor reply, so an assertion about the draft cannot be satisfied by the editor's output.
const loweredDraftText = "ДРАФТ НА СНИЖЕННОЙ СТУПЕНИ"
type burnsTheWholeCeiling struct {
mu sync.Mutex
drafts int
answerWhenEffortIsExplicit bool
}
func (b *burnsTheWholeCeiling) handler(rec *reqRec) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
raw, _ := io.ReadAll(r.Body)
body := string(raw)
rec.record(body)
var req struct {
Model string `json:"model"`
MaxTokens int `json:"max_tokens"`
ReasoningEffort string `json:"reasoning_effort"`
}
_ = json.Unmarshal(raw, &req)
if req.Model == "" {
req.Model = "fake-model"
}
if isEditBody(body) {
fmt.Fprintf(w, `{"id":"f","model":%q,"choices":[{"message":{"content":"ОТРЕДАКТИРОВАННЫЙ ПЕРЕВОД"},"finish_reason":"stop"}],
"usage":{"prompt_tokens":100,"completion_tokens":200,"total_tokens":300}}`, req.Model)
return
}
b.mu.Lock()
b.drafts++
b.mu.Unlock()
if b.answerWhenEffortIsExplicit && req.ReasoningEffort != "" {
fmt.Fprintf(w, `{"id":"f","model":%q,"choices":[{"message":{"content":%q},"finish_reason":"stop"}],
"usage":{"prompt_tokens":100,"completion_tokens":200,"total_tokens":300,
"completion_tokens_details":{"reasoning_tokens":40}}}`, req.Model, loweredDraftText)
return
}
// The whole ceiling, bought and thrown away: no text, and every token of the budget spent
// thinking.
fmt.Fprintf(w, `{"id":"f","model":%q,"choices":[{"message":{"content":""},"finish_reason":"length"}],
"usage":{"prompt_tokens":100,"completion_tokens":%d,"total_tokens":%d,
"completion_tokens_details":{"reasoning_tokens":%d}}}`,
req.Model, req.MaxTokens, req.MaxTokens+100, req.MaxTokens)
}
}
func (b *burnsTheWholeCeiling) count() int { b.mu.Lock(); defer b.mu.Unlock(); return b.drafts }
// setupLowerEffort writes a one-chunk book whose draft rides the PROVIDER's own default thinking level
// — `reasoning: "off"` on a model with no reasoning capability is an absent key, which is the shape the
// shipping editor stage has (pipeline-c1.yaml) and the one the run's most expensive single failure sat
// on ($0.071009 for zero characters).
func setupLowerEffort(t *testing.T, providerURL string, lowerOnEmpty bool) string {
t.Helper()
dir := t.TempDir()
writeFile(t, filepath.Join(dir, "prompts", "translator.md"),
"Переводи с {{source_lang}} на {{target_lang}}.\n---USER---\n{{text}}")
writeFile(t, filepath.Join(dir, "prompts", "editor.md"),
"Редактируй перевод.\n---USER---\nИсходник: {{text}}\nЧерновик перевода для редактуры: {{draft}}")
writeFile(t, filepath.Join(dir, "models.yaml"), fmt.Sprintf(`
prices_checked: %q
default_model: fake-model
providers:
fake:
kind: openai
base_url: %q
reasoning: subset
timeouts: { attempt_s: 5, max_attempts: 2, backoff_cap_s: 1 }
models:
fake-model:
provider: fake
price: { input_per_m: 1.0, cached_per_m: 0.1, cache_write_per_m: 0, output_per_m: 2.0 }
`, time.Now().UTC().Format("2006-01-02"), providerURL))
writeFile(t, filepath.Join(dir, "pipeline.yaml"), fmt.Sprintf(`
core: C1
version: 1
defaults: { max_output_ratio: 2.0, min_max_tokens: 512 }
retries: { regenerate_before_escalate: 1, regenerate_echo_before_escalate: 0, lower_effort_on_empty: %t }
stages:
- { name: draft, role: translator, model: fake-model, prompt_override: prompts/translator.md, prompt_version: v-test, temperature: 0.3, reasoning: "off" }
- { name: edit, role: editor, model: fake-model, prompt_override: prompts/editor.md, prompt_version: v-test, temperature: 0.4, reasoning: "off" }
escalation: { budget_usd: 0 }
`, lowerOnEmpty))
writeFile(t, filepath.Join(dir, "source.txt"), "静かな図書館の朝。")
writeFile(t, filepath.Join(dir, "book.yaml"), `
book_id: test-book
title: Тест
source_lang: ja
target_lang: ru
genre: ранобэ
audience: тест
venuti: 0.5
honorifics: keep
transcription: polivanov
footnotes: minimal
pipeline: pipeline.yaml
models: models.yaml
source_file: source.txt
ceilings: { book_usd: 5.0, day_usd: 10.0 }
`)
return filepath.Join(dir, "book.yaml")
}
// draftBodies returns the recorded request bodies of the DRAFT calls, in order.
func draftBodies(rec *reqRec) []string {
var out []string
for _, b := range rec.all() {
if !isEditBody(b) {
out = append(out, b)
}
}
return out
}
func bodyField(t *testing.T, body string) (maxTokens int, effort string) {
t.Helper()
var req struct {
MaxTokens int `json:"max_tokens"`
ReasoningEffort string `json:"reasoning_effort"`
}
if err := json.Unmarshal([]byte(body), &req); err != nil {
t.Fatalf("request body is not JSON: %v", err)
}
return req.MaxTokens, req.ReasoningEffort
}
// TestTheRetryForAnEmptyReplyBuysLessThinkingNotMoreBudget is the pack's claim measured on the wire and
// on the ledger at once, with NO assumption that lowering effort recovers anything: the provider burns
// whatever ceiling it is handed and returns nothing in both arms, so both flag, and the only difference
// is what the SECOND attempt was allowed to spend.
//
// The evidence for «the effort was lowered» is read off the SECOND request's own bytes, not off a field
// carried over from the first: an assertion satisfied by the previous attempt's state would be green on
// an engine that changed nothing.
func TestTheRetryForAnEmptyReplyBuysLessThinkingNotMoreBudget(t *testing.T) {
type arm struct {
lowerOnEmpty bool
wantMaxTok int // what the SECOND draft call was granted
wantEffort string // what the SECOND draft call asked for
}
var spend [2]float64
for i, a := range []arm{
{lowerOnEmpty: false, wantMaxTok: 1024, wantEffort: ""}, // the behaviour that shipped: twice the budget, same thinking
{lowerOnEmpty: true, wantMaxTok: 512, wantEffort: "low"}, // the remedy that matches the cause
} {
t.Run(fmt.Sprintf("lower_effort_on_empty=%t", a.lowerOnEmpty), func(t *testing.T) {
prov := &burnsTheWholeCeiling{}
rec := &reqRec{}
srv := httptest.NewServer(prov.handler(rec))
defer srv.Close()
r := newRunner(t, setupLowerEffort(t, srv.URL, a.lowerOnEmpty))
defer r.Close()
res, err := r.TranslateBook(context.Background())
if err != nil {
t.Fatal(err)
}
if len(res.Chunks) != 1 {
t.Fatalf("want 1 chunk, got %d", len(res.Chunks))
}
// Both arms end the same way, which is what keeps this test about the PRICE of the retry
// rather than about whether a retry helps.
if got := res.Chunks[0].FlagReason; got != FlagEmpty {
t.Fatalf("both arms must end flagged empty, got %q", got)
}
if got := prov.count(); got != 2 {
t.Fatalf("draft calls = %d, want 2 — one attempt and one regeneration, the budget the "+
"config grants either way", got)
}
bodies := draftBodies(rec)
if len(bodies) != 2 {
t.Fatalf("recorded draft bodies = %d, want 2", len(bodies))
}
if mt, eff := bodyField(t, bodies[0]); mt != 512 || eff != "" {
t.Fatalf("the FIRST attempt must be untouched by the knob: max_tokens=%d effort=%q, want 512 and the configured no-key", mt, eff)
}
mt, eff := bodyField(t, bodies[1])
if mt != a.wantMaxTok {
t.Fatalf("second attempt max_tokens = %d, want %d", mt, a.wantMaxTok)
}
if eff != a.wantEffort {
t.Fatalf("second attempt reasoning_effort = %q, want %q", eff, a.wantEffort)
}
committed, _, err := r.Store.SpentUSD(r.Book.BookID)
if err != nil {
t.Fatal(err)
}
if committed <= 0 {
t.Fatalf("the fixture must really spend money, else the comparison below is two zeros: %v", committed)
}
spend[i] = committed
})
}
// The money is structural, not incidental: the failing attempt bills its whole ceiling, so doubling
// the ceiling doubles what the failure costs. The direction is the knob's property; the ratio is
// this fixture's price table.
if !(spend[1] < spend[0]) {
t.Fatalf("answering an empty reply with less thinking must cost LESS than answering it with twice "+
"the budget, got lower_effort=%.8f doubling=%.8f", spend[1], spend[0])
}
}
// TestTheLoweredAttemptIsARealAttempt closes the half the wire assertion cannot see: the differently
// shaped request must be a normal paid attempt whose answer is USED — checkpointed, classified and
// shipped — and not a probe whose result is discarded.
func TestTheLoweredAttemptIsARealAttempt(t *testing.T) {
prov := &burnsTheWholeCeiling{answerWhenEffortIsExplicit: true}
rec := &reqRec{}
srv := httptest.NewServer(prov.handler(rec))
defer srv.Close()
r := newRunner(t, setupLowerEffort(t, srv.URL, true))
defer r.Close()
res, err := r.TranslateBook(context.Background())
if err != nil {
t.Fatal(err)
}
oc := res.Chunks[0]
if oc.Disposition != DispOK {
t.Fatalf("the recovered chunk must ship, got %s/%s", oc.Disposition, oc.FlagReason)
}
// ⚠ THE DRAFT STAGE'S OWN TEXT, NOT FinalText. The last stage is the editor, and this fixture's
// editor answers the same words unconditionally — so an assertion on the shipped text is satisfied
// by the EDITOR even when the lowered draft returned something else entirely, which is the check
// believing a different call than the one it names.
if len(oc.Stages) == 0 || oc.Stages[0].Stage != "draft" {
t.Fatalf("the fixture must put the draft first, got %+v", oc.Stages)
}
if got := oc.Stages[0].Text; got != loweredDraftText {
t.Fatalf("the draft stage must carry the LOWERED attempt's own words, got %q want %q", got, loweredDraftText)
}
// The FIRST failure keeps its durable trace even though the unit recovered — without it a book that
// paid twice reports as clean, which is how the mini-run of 25.07 reported echo_draft=0.0%.
cs, err := r.Store.GetChunkStatus(r.Book.BookID, 1, 0, "draft")
if err != nil || cs == nil {
t.Fatalf("the recovered unit must have a durable row: %v %v", cs, err)
}
if cs.FirstFlagReason != string(FlagEmpty) {
t.Fatalf("the recovered unit must still remember what it recovered FROM, got %q", cs.FirstFlagReason)
}
if got := prov.count(); got != 2 {
t.Fatalf("draft calls = %d, want 2", got)
}
}
// TestLowerEffortKnobMovesNoSnapshot is the gate that lets this land on books already in flight. The
// knob lives in Retries, which is deliberately outside buildSnapshotID, so turning it on must not
// invalidate a single paid checkpoint. Grepping snapshot.go for the field name would prove only that a
// NAME is absent; this renders the ids from two real configs that differ in that key and nothing else.
//
// ⚠ THE LOAD PATH IS MEASURED FIRST, and the ordering is load-bearing (the lesson of the sibling gate,
// echoregen_test.go): most snapshot inputs are resolved at LOAD, before a Runner exists, so a fold
// derived from the same YAML key would sail straight through an in-memory poke while two BOOKS
// differing only in that key rendered different ids.
//
// ⚠ WHAT IT DOES NOT SAY. Turning the knob on changes the SHAPE of attempt ≥ 1 — a different effort and
// a different budget are both in RequestHash — so a unit that already holds a stored regeneration at
// the doubled budget will not find it and will buy one more call. Attempt 0 is untouched, so nothing
// already delivered is re-bought; the cost of flipping this mid-book is one call per unit that had
// already been retried, and it is named here rather than discovered on a bill.
func TestLowerEffortKnobMovesNoSnapshot(t *testing.T) {
prov := &burnsTheWholeCeiling{answerWhenEffortIsExplicit: true}
rec := &reqRec{}
srv := httptest.NewServer(prov.handler(rec))
defer srv.Close()
renderFor := func(t *testing.T, lower bool) (string, string) {
t.Helper()
rr := newRunner(t, setupLowerEffort(t, srv.URL, lower))
defer rr.Close()
if got := rr.Pipeline.Retries.LowerEffortOnEmpty; got != lower {
t.Fatalf("the fixture's YAML did not reach the loaded config: want %t, got %t", lower, got)
}
d, _, err := rr.snapshotIDForWave(waveDraft)
if err != nil {
t.Fatalf("render the draft snapshot at lower=%t: %v", lower, err)
}
e, _, err := rr.snapshotIDForWave(waveEdit)
if err != nil {
t.Fatalf("render the edit snapshot at lower=%t: %v", lower, err)
}
return d, e
}
dOff, eOff := renderFor(t, false)
dOn, eOn := renderFor(t, true)
if dOn != dOff || eOn != eOff {
t.Fatalf("the knob moved a wave snapshot — every paid checkpoint of every book would be re-bought "+
"(--resnapshot).\n draft %s -> %s\n edit %s -> %s", dOff, dOn, eOff, eOn)
}
// The in-memory arm, as the cheap second axis: the field itself must not reach the id either.
r := newRunner(t, setupLowerEffort(t, srv.URL, false))
defer r.Close()
read := func(what string) (string, string) {
t.Helper()
d, _, err := r.snapshotIDForWave(waveDraft)
if err != nil {
t.Fatalf("render the draft snapshot (%s): %v", what, err)
}
e, _, err := r.snapshotIDForWave(waveEdit)
if err != nil {
t.Fatalf("render the edit snapshot (%s): %v", what, err)
}
return d, e
}
draftOff, editOff := read("knob off")
r.Pipeline.Retries.LowerEffortOnEmpty = true
draftOn, editOn := read("knob on")
if draftOn != draftOff || editOn != editOff {
t.Fatalf("the knob's field reached a wave snapshot.\n draft %s -> %s\n edit %s -> %s",
draftOff, draftOn, editOff, editOn)
}
}
// TestATruncatedAnswerStillBuysMoreBudget is the other half of the split, and without it the pack would
// have replaced one blind remedy with another. A `length` cut that carried TEXT ran out of room while
// the answer was being written — D2.3's own case — so it must keep getting a bigger budget even with
// the knob on, and its effort must not move.
func TestATruncatedAnswerStillBuysMoreBudget(t *testing.T) {
rec := &reqRec{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
raw, _ := io.ReadAll(r.Body)
body := string(raw)
rec.record(body)
if isEditBody(body) {
fmt.Fprint(w, `{"id":"f","model":"fake-model","choices":[{"message":{"content":"ОТРЕДАКТИРОВАННЫЙ ПЕРЕВОД"},"finish_reason":"stop"}],
"usage":{"prompt_tokens":100,"completion_tokens":200,"total_tokens":300}}`)
return
}
// A draft cut mid-sentence: text IS there, the budget ran out around it.
fmt.Fprint(w, `{"id":"f","model":"fake-model","choices":[{"message":{"content":"ЧЕРНОВИК ПЕРЕВОДА, оборванный на середине фразы и"},"finish_reason":"length"}],
"usage":{"prompt_tokens":100,"completion_tokens":512,"total_tokens":612}}`)
}))
defer srv.Close()
r := newRunner(t, setupLowerEffort(t, srv.URL, true)) // the knob is ON, and must still not fire here
defer r.Close()
if _, err := r.TranslateBook(context.Background()); err != nil {
t.Fatal(err)
}
bodies := draftBodies(rec)
if len(bodies) != 2 {
t.Fatalf("recorded draft bodies = %d, want 2", len(bodies))
}
mt, eff := bodyField(t, bodies[1])
if mt != 1024 {
t.Fatalf("a truncated ANSWER must be re-asked with more room: second attempt max_tokens = %d, want 1024", mt)
}
if eff != "" {
t.Fatalf("a truncated answer says nothing about thinking; the effort must stay as configured, got %q", eff)
}
}
// TestTheLoweringIsAnnouncedWithBothLevels pins the WARN that is the ONLY durable trace of WHICH effort
// an attempt was bought at: the level lives in the request hash and nowhere else, so a run on disk can
// show that an attempt thought less and never what it was asked for.
//
// ⚠ THE OPERATOR-MESSAGE CATALOGUE DOES NOT COVER THIS. That gate compares the literals present in the
// SOURCE against a file; a line can keep its literal, lose its fields, or never be reached at all, and
// the catalogue stays green. Measured: deleting `"effort", effort, "next_effort", lower` from this call
// left the whole package green before this test existed.
func TestTheLoweringIsAnnouncedWithBothLevels(t *testing.T) {
prov := &burnsTheWholeCeiling{answerWhenEffortIsExplicit: true}
rec := &reqRec{}
srv := httptest.NewServer(prov.handler(rec))
defer srv.Close()
var log bytes.Buffer
r := newRunner(t, setupLowerEffort(t, srv.URL, true))
defer r.Close()
r.Log = slog.New(slog.NewTextHandler(&log, &slog.HandlerOptions{Level: slog.LevelWarn}))
if _, err := r.TranslateBook(context.Background()); err != nil {
t.Fatal(err)
}
line := warnLine(t, log.String(), "regenerating with less thinking")
// BOTH levels, because either alone is unreadable: «next_effort=low» does not say what was given up,
// and «effort=off» does not say what was asked for instead.
for _, want := range []string{`effort=off`, `next_effort=low`, `max_tokens=512`, `reason=empty`} {
if !strings.Contains(line, want) {
t.Fatalf("the lowering must announce %q — it is the only place the ASKED-FOR level is ever\nrecorded: %s", want, line)
}
}
}
// TestASubstitutedPriceIsAnnouncedOnTheCallThatWasBilled is the other half of the same gap: the price
// basis is computed at settle and stored nowhere, so this line is the live path's only voice. §4.5 of
// the pack claims «the live path warns»; measured, `if false && basis.Substituted()` left the package
// green before this test.
func TestASubstitutedPriceIsAnnouncedOnTheCallThatWasBilled(t *testing.T) {
// The provider answers under a slug the catalogue does not carry, which is the measured shape:
// deepseek-v4-flash was asked for and `deepseek-flash` answered.
rec := &reqRec{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
raw, _ := io.ReadAll(r.Body)
rec.record(string(raw))
fmt.Fprint(w, `{"id":"f","model":"fake-model-0813","choices":[{"message":{"content":"ЧЕРНОВИК ПЕРЕВОДА"},"finish_reason":"stop"}],
"usage":{"prompt_tokens":100,"completion_tokens":200,"total_tokens":300}}`)
}))
defer srv.Close()
var log bytes.Buffer
r := newRunner(t, setupLowerEffort(t, srv.URL, false))
defer r.Close()
r.Log = slog.New(slog.NewTextHandler(&log, &slog.HandlerOptions{Level: slog.LevelWarn}))
if _, err := r.TranslateBook(context.Background()); err != nil {
t.Fatal(err)
}
line := warnLine(t, log.String(), "priced by a model that did not answer")
for _, want := range []string{`requested=fake-model`, `answered=fake-model-0813`, `priced_by=requested`} {
if !strings.Contains(line, want) {
t.Fatalf("the substitution must name %q — both slugs and the rate that was used, or the\nreader cannot tell which direction the bill went: %s", want, line)
}
}
// The control, without which the test above proves nothing: a call priced by the model that ANSWERED
// must stay silent, else the line fires on every call and stops being a signal.
var quiet bytes.Buffer
prov := &burnsTheWholeCeiling{answerWhenEffortIsExplicit: true}
srv2 := httptest.NewServer(prov.handler(&reqRec{})) // answers under the model it was asked for
defer srv2.Close()
r2 := newRunner(t, setupLowerEffort(t, srv2.URL, false))
defer r2.Close()
r2.Log = slog.New(slog.NewTextHandler(&quiet, &slog.HandlerOptions{Level: slog.LevelWarn}))
if _, err := r2.TranslateBook(context.Background()); err != nil {
t.Fatal(err)
}
if strings.Contains(quiet.String(), "priced by a model that did not answer") {
t.Fatalf("a call priced by its own answerer must say nothing:\n%s", quiet.String())
}
}
// warnLine returns the first logged line containing needle, so an assertion about that line's FIELDS
// cannot be satisfied by attributes of some other message in the stream.
func warnLine(t *testing.T, out, needle string) string {
t.Helper()
for _, l := range strings.Split(out, "\n") {
if strings.Contains(l, needle) {
return l
}
}
t.Fatalf("no logged line contains %q — the message never sounded:\n%s", needle, out)
return ""
}

View file

@ -5,6 +5,7 @@ import (
"strings"
"textmachine/backend/internal/checks"
"textmachine/backend/internal/store"
)
// quality.go: the DETERMINISTIC per-run quality-report (D39 layer 5, H5-no-in-loop-quality-signal) —
@ -26,9 +27,11 @@ type QualityReport struct {
// TextUnits is the number of units whose exported final text was available for the structural KPI
// (done or cosmetically-stripped); a flagged-empty unit contributes no prose.
TextUnits int `json:"text_units"`
// ProcessedUnits is the number of units that REACHED the final stage (a final-stage row exists: ok,
// cosmetic-strip, or skipped-because-a-member-flagged) — the strip-rate denominator, so the rate is a
// bounded [0,1] fraction of processed units.
// ProcessedUnits is the number of units carrying a final-stage row of ANY kind: ok, cosmetic-strip,
// skipped-because-a-member-flagged, or a STOP MARK a run left on the position (`cancelled`,
// `retry_unaffordable` — paid for, not finished). It is the strip-rate denominator, so that rate stays a
// bounded [0,1] fraction. «Units that REACHED the final stage» stood here and is too strong for the
// fourth kind; why the mark is counted anyway is argued at the counter itself (search ProcessedUnits++).
ProcessedUnits int `json:"processed_units"`
// Claim-1 structural KPI (choppy paragraphs). MeanSentPerNarrPara ≈ 1 is choppy (one sentence per
@ -49,18 +52,46 @@ type QualityReport struct {
// echo (edit) measure different things and were conflated by the old single echo_rate + a c-lite
// re-derive hack. echo_draft = the DRAFT quality (fraction of draft-stage rows flagged cjk_artifact,
// INCLUDING a c-lite dropped member — the translator echoed even if the editor recovered the unit),
// 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,
// computed DIRECTLY from the draft rows (no per-unit re-derivation). echo_edit = the EDITOR's own
// quality (fraction of edit-stage rows whose EDITOR output echoed — a skipped edit row is a draft echo,
// not an editor one, so it is excluded from the numerator).
//
// ⚠ «THE DELIVERED QUALITY» STOOD HERE AND IS NO LONGER WHAT THIS NUMBER MEANS. It counted an editor
// echo only while the echo was still the row's VERDICT, so a book that recovered one read zero — and
// once a stop mark gave a refused re-attack a row, the rate FELL on the run that had paid for the echo.
// The numerator now counts the editor's echo whatever happened to it next, exactly as the draft side
// does and for the same ratified reason (the metric watches the MODEL, not our success at papering
// over it). Consequence to read deliberately: `echo_edit_rate` on a book measured BEFORE this change is
// not comparable with one measured after, and `EchoEditRecovered` below is what keeps the headline from
// reading as fresh breakage on a book that shipped clean.
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.
//
// ⚠ THIS NUMERATOR WAS NARROWED by the same pack that widened the edit one, so that the two sides ask
// one question. It used to count every echo the row's verdict no longer was — including a re-roll that
// came back with a DIFFERENT failure and a re-attack a ceiling refused, neither of which produced
// anything — and it now asks shippedText. Consequence: `echo_draft_recovered` measured before this
// change reads HIGHER than the same rows read now, so the two are not comparable across it.
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
// EchoEditUnits counts edit-stage ROWS whose editor output echoed. ⚠ THE NAME SAYS UNITS AND THE COUNTER
// SAYS ROWS, and the two are the same number only while the edit wave has one stage — which every
// EXECUTABLE shipping config has today, so no published number moves. The pin on a two-stage edit wave
// makes the difference visible (rate 0.50 over two rows of ONE unit); renaming a key the report already
// publishes is a question for the owner, asked in this pack's report rather than answered here.
EchoEditUnits int `json:"echo_edit_units"`
// EchoEditRecovered is the same split the draft side carries, for the same reason: the subset of
// EchoEditUnits whose ROW produced text that went on, so the echo itself cost the book nothing beyond
// the call it wasted. ROW and not unit, exactly as on the draft side — where the edit wave declares a
// second stage, a later stage of it can flag and the unit ship nothing, and the echo was cured all the
// same (shippedText says why this is the deliberate reading). Taking the numerator from the draft side
// without taking this guard is what left an operator reading «echo edit=1 (100.0%)» over a book that
// had delivered clean prose.
EchoEditRecovered int `json:"echo_edit_recovered,omitempty"`
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).
@ -181,6 +212,42 @@ type ChunkQuality struct {
RepairCandidates int `json:"repair_candidates,omitempty"`
}
// shippedText says this ROW produced text that went on — its `final_hash` points at a checkpoint. It does
// NOT say the unit shipped, and that holds on BOTH arms of the echo split below.
//
// On the DRAFT arm the two never coincide wherever an editor follows: the draft row can carry a hash and
// the editor withhold the unit afterwards (measured: `echo_draft_recovered=1` beside `text_units=0` and a
// `withheld` hole), and all four shipping configs put the final stage after the draft.
//
// On the EDIT arm they coincide only while the edit WAVE has ONE stage — `waveStages` puts every
// non-translator stage in that wave (snapshot.go), so a second stage there makes the first one a row that
// is not the unit's last word, and its cured echo is then counted while the unit ships nothing (measured
// on that shape: `echo_edit_recovered=1` beside `text_units=0`). ⚠ WHAT IS AND IS NOT REACHABLE TODAY,
// because the near miss is easy to write down wrong: of the four shipping configs three declare a
// one-stage edit wave, and the fourth — c2, with `select`+`edit` — the engine REFUSES to run at all
// (`CheckRunnable`: core C2, and `role: judge`, are Phase-0 unexecutable; runner.go calls it before the
// store is even opened). So the shape is unreachable on every EXECUTABLE shipping config, and c2 is no
// evidence of anything — the repo has already withdrawn one counterexample resting on it
// (docs/architecture/13-tech-debt-anchors.md, «c2 неисполняем CheckRunnable»). What makes the shape live
// is data, not code: a SECOND `editor` stage in an executable config passes CheckRunnable and runs, which
// is exactly what the pin for this builds. ⚠ And one stage in the wave is necessary, not sufficient: a
// unit whose source moved under it (`HoleStale`) or that lost a member still parts company with its row.
// volume.go states the same trap about the same column in its own words (⛔ THE SHIPPING ROW AND NOT ANY ROW).
//
// ⛔ AND THE ECHO COUNTERS READ THE ROW DELIBERATELY, both of them: the metric asks what the MODEL did and
// what curing it cost (D39.18 — it watches the model, not our success at papering over it). An echo the hop
// fixed cost the book nothing whether or not a LATER stage then flagged the unit for its own reasons, and
// the unit's absence is what the hole counters report. ⚠ THIS IS THE READING THE CODE TAKES, NOT A
// RATIFIED ONE: the axis «echo counted over shipped text only» is an OPEN question left to the owner in
// D39.18 itself (tech-debt anchor «echo только в шипнутом тексте»), and if it is decided the other way the
// change is a different counter beside this one, not a quiet re-pointing of a name the report publishes.
//
// It is asked of `final_hash` rather than of the disposition because that is the fact it needs: a row
// without a pointer produced nothing whatever it is called, and a row with one did — an `ok` verdict, or
// the one flagged verdict whose cleaned remainder IS the export (a cosmetic sanitizer strip, which
// stagerun.go calls recovered in its own words).
func shippedText(cs store.ChunkStatus) bool { return cs.FinalHash != "" }
// QualityReport builds the read-only per-run quality projection. It opens no jobs, reserves nothing,
// makes no LLM call — it reads the persisted chunk_status / retrieval_state and, for each chunk with
// an exported final text, the $0 final checkpoint to recompute the structural KPI. Safe to run
@ -330,9 +397,11 @@ func (r *Runner) QualityReport() (*QualityReport, error) {
// Split echo by stage (D39.18 owner decision): echo_draft over the DRAFT-stage rows (translator quality,
// 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, draftEchoRecovered, editRows, editEcho int
// is needed), echo_edit over the EDIT-stage rows (the EDITOR's own quality WHATEVER HAPPENED TO THE ECHO
// NEXT — only the editor's own echo counts, and a skipped edit row is a draft echo not an editor one).
// «The delivered quality» stood here, and the field's own caveat says why it stopped being true.
// Ghost-guarded like the rest (live chunks / units).
var draftRows, draftEcho, draftEchoRecovered, editRows, editEcho, editEchoRecovered int
for _, cs := range statuses {
k := chunkKey{cs.Chapter, cs.ChunkIdx}
switch {
@ -340,26 +409,65 @@ func (r *Runner) QualityReport() (*QualityReport, error) {
draftRows++
// 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.
// c-lite dropped member), `first_flag_reason` is an echo the row's own verdict is no longer.
// 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.
//
// ⛔ «NO LONGER THE VERDICT» IS NOT «RECOVERED», and counting it as one was a lie the column
// could tell by itself. A superseded echo reaches this branch FIVE ways and only two are a
// recovery: a later attempt or the escalation hop translated the unit properly and the row was
// written `ok`; the hop's answer was a COSMETIC strip, whose cleaned text ships and which the
// executor itself calls recovered (stagerun.go); the re-roll came back with a DIFFERENT failure;
// a USD ceiling refused to buy the re-roll at all and the row carries `retry_unaffordable`; or a
// person cut the run mid-call and it carries `cancelled` — THIS PACK gave that mark the first-flag
// column too (cutcall.go), which is what makes the fifth way reachable at all. The last three
// ship nothing, and `regenerate_echo_before_escalate: 1` stands in all four shipping pipelines,
// so the re-roll that produces them is live rather than theoretical.
//
// ⚠ SO THE RECOVERY COUNT ASKS WHETHER THE ROW PRODUCED TEXT, not which disposition it wears:
// keyed on `ok` alone it loses the stripped recovery — a real cure, text on disk, reported as
// none. ⚠ On THIS arm that shape needs a draft-only pipeline: the sanitizer runs on the final
// stage only (chunkrun.go), so wherever an editor follows, a draft row is never
// `sanitizer_stripped` and the stripped recovery arrives on the edit arm below — where the gate
// is on in three of the four shipping pipelines. The echo count asks neither question, because
// the model echoed either way.
switch {
case cs.FlagReason == string(FlagCJKArtifact):
draftEcho++
case cs.FirstFlagReason == string(FlagCJKArtifact):
draftEcho++
draftEchoRecovered++
if shippedText(cs) {
draftEchoRecovered++
}
}
case editStageNames[cs.Stage] && inManifest[k]:
editRows++
if cs.Disposition == string(DispFlagged) && cs.FlagReason == string(FlagCJKArtifact) {
editEcho++ // the EDITOR's OWN output echoed (a skipped edit row means the drafts echoed, not the editor)
// The EDITOR's OWN output echoed — and, exactly as on the draft side above, BOTH columns say so.
// A skipped edit row means the drafts echoed and not the editor, and it carries no first flag,
// so it is counted by neither arm.
//
// ⛔ THE SECOND ARM USED TO BE MISSING, AND THIS PACK MADE THE GAP COST SOMETHING. Without it the
// counter sees an editor echo only while it is still the row's VERDICT: an echo a re-roll
// recovered read as zero (pre-existing), and — once the stop mark gave the position a row — an
// echo whose re-roll a ceiling REFUSED left the numerator while staying in `editRows`, so the
// edit echo RATE FELL on the run that had just paid for an echo. Measured on that shape by an
// adversarial pass: `cost_usd=0.001820` on the editor's own echo, `echo_edit_units=0`. It is the
// same rule the draft counter states in its own words — the metric watches the MODEL, not our
// success at papering over it (D39.18) — applied to the stage that was missing it.
switch {
case cs.Disposition == string(DispFlagged) && cs.FlagReason == string(FlagCJKArtifact):
editEcho++
case cs.FirstFlagReason == string(FlagCJKArtifact):
editEcho++
if shippedText(cs) {
editEchoRecovered++
}
}
}
}
rep.EchoDraftChunks, rep.EchoDraftRecovered, rep.EchoEditUnits = draftEcho, draftEchoRecovered, editEcho
rep.EchoDraftChunks, rep.EchoDraftRecovered = draftEcho, draftEchoRecovered
rep.EchoEditUnits, rep.EchoEditRecovered = editEcho, editEchoRecovered
if draftRows > 0 {
rep.EchoDraftRate = float64(draftEcho) / float64(draftRows)
}
@ -375,8 +483,15 @@ func (r *Runner) QualityReport() (*QualityReport, error) {
if cs.Stage != lastStage || !inManifest[k] {
continue // the final verdict lives on the final stage's row (per unit); drop ghost leader rows
}
// Every unit that REACHED the final stage has exactly one lastStage row (ok, cosmetic-strip, or
// skipped-because-a-member-flagged) — the strip-rate denominator.
// Every unit with a lastStage row counts once in the strip-rate denominator (ok, cosmetic-strip, or
// skipped-because-a-member-flagged).
//
// ⚠ «REACHED THE FINAL STAGE» USED TO STAND HERE AND IS TOO STRONG NOW, though less so than it
// looks: a stop mark IS written on a stage the run was executing and did pay for (an attempt on
// `retry_unaffordable`, a call on the wire for `cancelled`) — what it did not do is finish. So the
// row belongs in a «positions this book has spent on» denominator and not in a «units that have a
// final answer» one. Left alone deliberately: the alternative is a denominator that moves between
// two runs of the same book, which is worse for a rate somebody compares across runs.
rep.ProcessedUnits++
if cs.FlagReason == string(FlagSanitizerStripped) {
rep.CosmeticStripUnits++ // a stripped unit carried a markdown OR CJK cosmetic leak (F6)

View file

@ -0,0 +1,126 @@
package pipeline
import (
"os"
"path/filepath"
"strings"
"testing"
"textmachine/backend/internal/lang"
)
// readerholewords_test.go: what the READER'S FILE says at a hole — the one surface the product exists for.
//
// ⛔ THE DEFECT THIS PINS WAS FOUND IN THE WORDS, NOT IN A TEST. The writer chose the sentence by the hole's
// KIND alone, and `withheld` says «этот фрагмент … требует проверки человеком». That is true of a refusal
// and of a contaminated output. It is FALSE of a stop MARK — a call a person cut, or a re-attack a ceiling
// refused — because nothing is wrong with that fragment: it was paid for, not finished, and the next run
// does it. A reader was being sent to wait for a human who cannot help, and the money mark of backlog row
// 291 was about to join the same sentence (before it, such a unit had no row at all and the file said the
// true thing: «ещё не переведён»).
// readerWordsFor writes a minimal target-language word set and loads it, returning distinguishable
// sentences so a test can say WHICH one the writer chose.
func readerWordsFor(t *testing.T) lang.ReaderWords {
t.Helper()
root := t.TempDir()
if err := os.MkdirAll(filepath.Join(root, "ru"), 0o755); err != nil {
t.Fatal(err)
}
writeFile(t, filepath.Join(root, "ru", "reader.txt"),
"hole.pending\tНЕ ПЕРЕВЕДЕНО.\nhole.withheld\tТРЕБУЕТ ПРОВЕРКИ ЧЕЛОВЕКОМ.\nhole.incomplete\tНедостаёт {dropped}.\n"+
"hole.stale\tУстарело.\nhole.ghost\tВне нарезки {ghost}.\nnotice.holes\tПропусков {holes} из {total}.\nnotice.ghost\tВне нарезки всего {ghost}.\n")
words, present, err := lang.LoadReaderWords(root, "ru")
if err != nil || !present {
t.Fatalf("reader words: present=%v err=%v", present, err)
}
return words
}
// TestTheReadersFileDoesNotSendThemToWaitForAHumanWhoCannotHelp is the pin, and it is a table over the two
// CLASSES rather than over one reason: a mark the next run erases, and a verdict a person must look at.
func TestTheReadersFileDoesNotSendThemToWaitForAHumanWhoCannotHelp(t *testing.T) {
words := readerWordsFor(t)
for _, tc := range []struct {
name string
reason FlagReason
dropped int
want string
}{
{name: "a re-attack no money was left for", reason: FlagRetryUnaffordable, want: "НЕ ПЕРЕВЕДЕНО."},
{name: "a call a person cut", reason: FlagCancelled, want: "НЕ ПЕРЕВЕДЕНО."},
// A stop mark over a unit that ALSO lost a member for good: no purchase brings that text back, so
// the cheerier sentence would promise what the next run cannot deliver.
{name: "a stop mark over a unit that lost a member for good", reason: FlagRetryUnaffordable, dropped: 1, want: "ТРЕБУЕТ ПРОВЕРКИ ЧЕЛОВЕКОМ."},
// The control, and without it the assertion above would pass on a writer that said «not translated»
// about EVERYTHING: a verdict a human really does have to look at keeps the withheld sentence.
{name: "a provider refusal", reason: FlagHardRefusal, want: "ТРЕБУЕТ ПРОВЕРКИ ЧЕЛОВЕКОМ."},
{name: "a contaminated output that was dropped", reason: FlagSanitizerDefect, want: "ТРЕБУЕТ ПРОВЕРКИ ЧЕЛОВЕКОМ."},
} {
t.Run(tc.name, func(t *testing.T) {
exp := &BookExport{
BookID: "syn", TotalUnits: 1,
Chunks: []ChunkExport{{Chapter: 1, ChunkIdx: 0, Disposition: string(DispFlagged), FlagReason: string(tc.reason),
FinalText: "", DroppedMembers: tc.dropped, DroppedReason: string(FlagHardRefusal)}},
}
// The premise: this record IS the hole kind whose sentence is under test. A projection that
// classified differently would make the assertion below about another branch entirely.
if got := UnitHole(exp.Chunks[0]); got != HoleWithheld {
t.Fatalf("the fixture's unit is hole %q, not %q — this test is about the withheld branch", got, HoleWithheld)
}
book, holes, err := assembleBook(exp, nil, "книга", "ru", words)
if err != nil {
t.Fatal(err)
}
if len(book.Chapters) != 1 || len(book.Chapters[0].Paragraphs) != 1 {
t.Fatalf("the file is not one chapter with one marked hole: %+v", book.Chapters)
}
got := book.Chapters[0].Paragraphs[0]
t.Logf("reason %q ⇒ the reader reads %q", tc.reason, got)
if !strings.Contains(got, tc.want) {
t.Fatalf("the reader reads %q for reason %q, want %q: a stop mark is work the next run buys, and telling a reader it needs a human is a sentence they can act on wrongly",
got, tc.reason, tc.want)
}
// ⛔ AND THE REASON SURVIVES ON THE HOLE. It is what makes a third sentence of its own («paid
// for, not finished: top up and resume») a row in reader.txt plus a branch, rather than a
// re-design: an operator's refusal text and any later phrase both read it from here.
if len(holes) != 1 || holes[0].Reason != string(tc.reason) {
t.Fatalf("the hole lost its reason: %+v", holes)
}
})
}
}
// TestTheOperatorsRefusalTellsHimBothHalves is the THIRD surface of the same distinction, and the one the
// first two repairs skipped: the build's refusal text is what the person who can ACT reads, and it was
// telling him only the cheerful half. A unit can be «paid for and not done» AND short a member for good;
// told only the first, he tops up, resumes, and the build refuses again with a different hole.
//
// ⛔ The reason the other two surfaces got the caveat and this one could not: the hole record it reads did
// not carry the drop count for a withheld unit at all. The field existed for `incomplete` and was simply
// not filled — «took the form, not the guarantee», one surface further along.
func TestTheOperatorsRefusalTellsHimBothHalves(t *testing.T) {
exp := &BookExport{BookID: "syn", TotalUnits: 3}
holes := []hole{
{Kind: HoleWithheld, Chapter: 1, Unit: 0, Reason: string(FlagRetryUnaffordable)},
{Kind: HoleWithheld, Chapter: 2, Unit: 0, Reason: string(FlagRetryUnaffordable), Dropped: 1},
// The control: a real verdict keeps the plain sentence, so «both halves» above is a statement about
// this shape and not what the text says about everything.
{Kind: HoleWithheld, Chapter: 3, Unit: 0, Reason: string(FlagHardRefusal)},
}
err := describeHoles("syn", exp, holes, false)
if err == nil {
t.Fatal("a book with holes and no --partial must refuse")
}
printed := err.Error()
t.Logf("what the operator reads:\n%s", printed)
for _, want := range []string{
"chapter 1 unit 0: withheld — paid for and NOT done; the next run re-does it (retry_unaffordable)",
"chapter 2 unit 0: withheld — paid for and NOT done, AND 1 member(s) of it are missing for good; the next run finishes the unit but cannot bring those back (retry_unaffordable)",
"chapter 3 unit 0: withheld (hard_refusal)",
} {
if !strings.Contains(printed, want) {
t.Fatalf("the refusal does not say %q", want)
}
}
}

View file

@ -210,7 +210,11 @@ func (r *Runner) projectRebill(statuses []store.ChunkStatus, manifest []chunk.Ch
// answers «could any rendered byte have moved» from the stored manifest's validity key, which
// folds the source SHA — so the expensive answer is only bought once there is a question.
if !r.sourceMovedUnderTheRows() {
continue // resumes at $0, and the probe says no input under it moved
// Nothing here will be RE-PAID for: the probe says no input under these rows moved, which is
// the question this projection answers. ⚠ It is not the same as «resumes at $0» — a stop mark
// is re-attacked and bought on the next run — but that purchase is not a RE-payment and does
// not belong in this figure.
continue
}
hashes, herr := reproduce()
if herr != nil {

View file

@ -131,7 +131,8 @@ func (r *Runner) repriceCheckpoint(cu store.CheckpointUsage) (usd float64, price
}
// The floor: these tokens at the answering model's price on today's table — the same
// PriceForResponse ordering settle used, so a canonicalised slug still finds its price.
usd = ledger.CostUSD(r.Pricer.PriceForResponse(cu.ModelRequested, cu.ModelActual), u)
floorPrice, _ := r.Pricer.PriceForResponse(cu.ModelRequested, cu.ModelActual)
usd = ledger.CostUSD(floorPrice, u)
today, ok := r.currentModelFor(cu.Stage, cu.Escalation)
if !ok {
return usd, true, false

View file

@ -0,0 +1,802 @@
package pipeline
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"textmachine/backend/internal/chunk"
"textmachine/backend/internal/llm"
"textmachine/backend/internal/obs"
"textmachine/backend/internal/runevents"
"textmachine/backend/internal/store"
)
// retrystopmark_test.go: the acceptance battery for backlog row 291 — a USD ceiling that refuses the
// RETRY of an attempt this book ALREADY PAID FOR stops the run like any other ceiling, and the position
// it stopped over gets a MARK instead of reading as never started.
//
// Every test here is $0: the provider is an httptest server, no vendor key is touched, and the ceiling
// that drives the whole fixture is derived from the engine's OWN estimate of its own calls.
// The two chapter markers. Each fixture chapter is fixtureChapterRunes of ONE Han character, which is
// what lets the fake provider tell the chapters apart: the body carries the source text verbatim.
const (
cleanChapterRune = "文"
retriedChapterRune = "字"
)
// retryStopSource is two single-chunk chapters: the first answers cleanly, the second answers
// finish=length on its FIRST call so the attempt axis asks for a doubled budget, which is the purchase
// this fixture's ceiling refuses.
func retryStopSource() string {
return strings.Repeat(cleanChapterRune, fixtureChapterRunes) + "。\n\n" +
strings.Repeat(retriedChapterRune, fixtureChapterRunes) + "。"
}
// The two drafts the provider answers with. They differ so a test can say WHICH call's text a finished
// unit is holding — «the retry finished the unit» is otherwise indistinguishable from «attempt 0 was
// served from its checkpoint».
const (
draftTruncated = "ЧЕРНОВИК ОБОРВАН НА ПОТОЛКЕ"
draftAfterRetry = "ЧЕРНОВИК ПЕРЕВОДА ПОСЛЕ РЕТРАЯ"
)
// lengthOncePerChapter answers finish=length for the FIRST call on the retried chapter and `stop` for
// every other call, counting calls per chapter. The count is the test's only instrument for «no fresh
// paid call happened», so it is taken per chapter rather than in total.
type lengthOncePerChapter struct {
srv *httptest.Server
mu sync.Mutex
n map[string]int
}
func newLengthOncePerChapter(t *testing.T) *lengthOncePerChapter {
t.Helper()
p := &lengthOncePerChapter{n: map[string]int{}}
p.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
marker := cleanChapterRune
if strings.Contains(string(body), strings.Repeat(retriedChapterRune, 8)) {
marker = retriedChapterRune
}
p.mu.Lock()
p.n[marker]++
nth := p.n[marker]
p.mu.Unlock()
text, finish := draftAfterRetry, "stop"
if marker == retriedChapterRune && nth == 1 {
// A short, varied completion: the degeneration detector must NOT claim a repetition loop
// (degenerateLoop needs 30+ words), or the verdict would be loop_degenerate — deterministic,
// NOT retryable — and this fixture would never reach the retry it is about.
text, finish = draftTruncated, "length"
}
tb, _ := json.Marshal(text)
fmt.Fprintf(w, `{"id":"fake","model":"fake-model","choices":[{"message":{"content":%s},"finish_reason":%q}],
"usage":{"prompt_tokens":1000,"completion_tokens":500,"prompt_tokens_details":{"cached_tokens":200}}}`,
tb, finish)
}))
t.Cleanup(p.srv.Close)
return p
}
func (p *lengthOncePerChapter) calls(marker string) int {
p.mu.Lock()
defer p.mu.Unlock()
return p.n[marker]
}
func (p *lengthOncePerChapter) total() int {
p.mu.Lock()
defer p.mu.Unlock()
return p.n[cleanChapterRune] + p.n[retriedChapterRune]
}
// retryStopFixture builds the project and returns it together with the manifest, asserting the SHAPE the
// whole battery depends on: TWO output units, the retried one second. A fixture that silently cut
// differently would still run — and would measure a ceiling refusing something else.
//
// ⚠ The two units are two CHUNKS of one chapter rather than two chapters, and that is measured rather
// than chosen: this fixture's source language ships no langpack, so no heading rule fires and the ingest
// reads the whole file as chapter 1 (the first version of this file asserted two chapters and said so —
// `chapters [1 1]`). It makes no difference to the subject: a draft-only pipeline makes every CHUNK its
// own output unit (outputUnits), and chunk_status is keyed per chunk.
func retryStopFixture(t *testing.T, providerURL string) (string, []chunk.Chunk) {
t.Helper()
bookPath := setupProjectOpts(t, providerURL, projectOpts{
source: retryStopSource(),
// The retry axis must be OPEN (a budget of one regeneration) — with 0 the flagged attempt is
// terminal and no reservation for a second attempt is ever asked for.
regenerate: 1,
// Draft-only: the draft IS the shipping stage, so the fixture's money is two calls plus the
// refused retry and nothing else.
draftOnly: true,
})
r := newRunner(t, bookPath)
defer r.Close()
manifest, err := r.bookChunks()
if err != nil {
t.Fatal(err)
}
if len(manifest) != 2 || manifest[0].ChunkIdx == manifest[1].ChunkIdx {
t.Fatalf("fixture must cut into 2 units, got %d chunk(s) at indices %v", len(manifest), indicesOf(manifest))
}
if !strings.Contains(manifest[1].Text, retriedChapterRune) {
t.Fatalf("the RETRIED chapter must be the second unit, or the ceiling arithmetic below is about the wrong call: chunk 1 text starts %.12q", manifest[1].Text)
}
return bookPath, manifest
}
func indicesOf(chunks []chunk.Chunk) [][2]int {
out := make([][2]int, 0, len(chunks))
for _, c := range chunks {
out = append(out, [2]int{c.Chapter, c.ChunkIdx})
}
return out
}
// retryStopCeiling is a ceiling that admits every ATTEMPT 0 of this fixture and refuses the DOUBLED
// retry. It is derived from the engine's own arithmetic — callEstimateUSD over the messages runStage
// renders and the budget maxTokensForAttempt hands it — rather than written as a literal or re-assembled
// from the same ingredients a second time, because two derivations of one number drift and then the
// fixture is quietly about a different situation.
//
// ⚠ The money the engine COMMITS per call is smaller than what it RESERVES (EstimateUSD is deliberately
// pessimistic), so the window is wide: [committed of one call + est0, committed of two + est1). Both ends
// are printed and the premise «the window exists» is asserted, because a pricing or sizing change that
// closed it would otherwise make this whole file pass while measuring nothing.
func retryStopCeiling(t *testing.T, r *Runner, manifest []chunk.Chunk) (ceiling, est0, est1 float64) {
// The retried unit is the SECOND of two, so its attempt 0 is admitted with one call's spend already
// committed and its re-attack refused with two.
return ceilingThatRefusesTheReattack(t, r, manifest[1], 1)
}
// ceilingThatRefusesTheReattack is the window for ANY fixture of this shape, in the engine's own
// arithmetic: `admitted` is how many of this fixture's calls are already committed when the position's
// attempt 0 asks for its reservation.
func ceilingThatRefusesTheReattack(t *testing.T, r *Runner, ch chunk.Chunk, admitted int) (ceiling, est0, est1 float64) {
t.Helper()
// The draft stage sizes and renders from the SOURCE; the edit stage from the draft, which is why the
// twin below takes both rather than two copies of this arithmetic existing.
return ceilingThatRefusesTheReattackAt(t, r, 0, ch, ch.Text, admitted)
}
// ceilingThatRefusesTheReattackAt is the same window for ANY stage of the pipeline: `sizingText` is what
// that stage's budget is sized from and what its template receives as the prior draft (runStage's own
// rule — the source for the translator, the previous stage's text after it).
func ceilingThatRefusesTheReattackAt(t *testing.T, r *Runner, stageIdx int, ch chunk.Chunk, sizingText string, admitted int) (ceiling, est0, est1 float64) {
t.Helper()
st := r.Pipeline.Stages[stageIdx]
vars := RenderVars{Book: r.Book, Text: ch.Text}
if stageIdx > 0 {
vars.Draft = sizingText
}
msgs, err := MessagesWithInjection(r.templates[st.Name], vars, "")
if err != nil {
t.Fatal(err)
}
base := r.baseMaxTokensFor(st, EstimateTokens(sizingText))
est0 = r.callEstimateUSD(st, st.ResolvedModel, msgs, base)
est1 = r.callEstimateUSD(st, st.ResolvedModel, msgs, maxTokensForAttempt(base, 1))
lo := float64(admitted)*fakeCallUSD + est0 // this position's attempt 0 must be admitted
hi := float64(admitted+1)*fakeCallUSD + est1 // its doubled re-attack must NOT be
if lo >= hi {
t.Fatalf("no ceiling can admit attempt 0 and refuse the re-attack: lo=%.6f hi=%.6f (est0=%.6f est1=%.6f call=%.6f)", lo, hi, est0, est1, fakeCallUSD)
}
ceiling = (lo + hi) / 2
t.Logf("fixture window: est0=$%.6f est1=$%.6f committed/call=$%.6f ⇒ ceiling=$%.6f admits attempt 0 (needs ≤$%.6f) and refuses the re-attack (needs $%.6f)",
est0, est1, fakeCallUSD, ceiling, lo, hi)
return ceiling, est0, est1
}
func draftRowOf(t *testing.T, r *Runner, rows []store.ChunkStatus, ch chunk.Chunk) *store.ChunkStatus {
t.Helper()
for i := range rows {
if rows[i].Chapter == ch.Chapter && rows[i].ChunkIdx == ch.ChunkIdx && rows[i].Stage == r.Pipeline.Stages[0].Name {
return &rows[i]
}
}
return nil
}
// TestTheRefusedRetryLeavesTheUnitAMarkAndNotAnEmptyPosition is pin 1 of row 291, and the measurement is
// the one the tree itself uses to describe the earlier incident of this class (stagerun.go, «THE MARK FOR
// A STOPPED POSITION IS ATTACHED TO EVERY EXIT»): money booked against a position with NO chunk_status
// row at all. Before this pack the refused retry left exactly that — committed > 0 and zero rows for the
// unit — so the unit read `pending`, indistinguishable from one nobody had started, while its first
// attempt was paid for and on disk.
func TestTheRefusedRetryLeavesTheUnitAMarkAndNotAnEmptyPosition(t *testing.T) {
prov := newLengthOncePerChapter(t)
bookPath, manifest := retryStopFixture(t, prov.srv.URL)
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
r := newRunner(t, bookPath)
defer r.Close()
ceiling, _, _ := retryStopCeiling(t, r, manifest)
r.CeilingUSD = ceiling
_, err := r.TranslateBook(ctx)
var halt *CeilingHalt
if !errors.As(err, &halt) {
t.Fatalf("a ceiling that cannot afford the retry must stop the run as a ceiling halt, got %v", err)
}
// The premise, asserted and printed: the refusal fell on the RETRY, not on a fresh attempt 0. Two
// calls reached the provider — one per chapter — and the third, the retry, was never dialled.
if got, retried := prov.total(), prov.calls(retriedChapterRune); got != 2 || retried != 1 {
t.Fatalf("fixture did not reach the retry refusal: provider saw %d call(s) (%d on the retried chapter); want 2 and 1", got, retried)
}
committed, reserved, serr := r.Store.SpentUSD("test-book")
if serr != nil {
t.Fatal(serr)
}
rows, rerr := r.Store.ChunkStatusesForBook("test-book")
if rerr != nil {
t.Fatal(rerr)
}
row := draftRowOf(t, r, rows, manifest[1])
// The control величина beside the claim: the OTHER chapter's row proves the query and the store are
// answering about an existing subject, so a missing row below is a missing row and not a blind read.
clean := draftRowOf(t, r, rows, manifest[0])
t.Logf("after the stop: committed=$%.6f reserved=$%.6f · chunk_status rows=%d · row for the clean unit=%v · row for the retried unit=%v",
committed, reserved, len(rows), clean != nil, row != nil)
if committed <= 0 {
t.Fatalf("the fixture bought nothing, so there is no paid position to mark: committed=%.6f", committed)
}
if clean == nil {
t.Fatal("the clean unit has no row either — the read is blind and the assertion below would be vacuous")
}
if row == nil {
t.Fatalf("THE HOLE: the book paid $%.6f and the unit whose retry was refused has NO chunk_status row, so every reading model sees it as `pending` — not translated yet", committed)
}
if got, want := FlagReason(row.FlagReason), FlagRetryUnaffordable; got != want {
t.Fatalf("the mark must name the money as the cause, got disposition=%q reason=%q (want %q): a reason that says `length` sends a person to fix a budget formula instead of topping up",
row.Disposition, got, want)
}
if row.Disposition != string(DispFlagged) {
t.Fatalf("the mark's disposition is %q, want %q", row.Disposition, DispFlagged)
}
if row.FinalHash != "" {
t.Fatalf("the mark must not point at an export: final_hash=%q on a position that never produced shippable text", row.FinalHash)
}
if row.CostUSD <= 0 {
t.Fatalf("the mark reports cost_usd=%.6f for a position whose attempt 0 was paid for — the row would say the money went nowhere", row.CostUSD)
}
// The row has to carry what the WARN line cannot: the log dies with the process, and the row is what
// a person reads afterwards. The primary failure is the one fact FlagReason no longer states.
if !strings.Contains(row.Detail, string(FlagLength)) {
t.Fatalf("the mark's detail does not say what the paid attempt answered: %q", row.Detail)
}
if row.Attempts != 1 {
t.Fatalf("the mark counts %d attempt(s) for one paid call: a refused reservation is not an attempt that happened", row.Attempts)
}
}
// TestTheRefusedRetryStopCarriesEnoughToTopUpWith is pin 2, and it asserts the NUMBER rather than the
// presence of the frame: «an event arrived» is true of a shortfall of zero, which tells a buyer nothing.
// The sufficiency half is proved by execution in the resume test below — topping up by exactly this
// figure is what admits the same call.
func TestTheRefusedRetryStopCarriesEnoughToTopUpWith(t *testing.T) {
prov := newLengthOncePerChapter(t)
bookPath, manifest := retryStopFixture(t, prov.srv.URL)
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
r := newRunner(t, bookPath)
defer r.Close()
ceiling, est0, est1 := retryStopCeiling(t, r, manifest)
r.CeilingUSD = ceiling
_, err := r.TranslateBook(ctx)
var halt *CeilingHalt
if !errors.As(err, &halt) {
t.Fatalf("want a ceiling halt, got %v", err)
}
committed, reserved, serr := r.Store.SpentUSD("test-book")
if serr != nil {
t.Fatal(serr)
}
t.Logf("stop frame: scope=%q shortfall=%d micro-USD · committed=$%.6f reserved=$%.6f ceiling=$%.6f (est0=$%.6f est1=$%.6f)",
halt.Scope, halt.ShortfallMicroUSD, committed, reserved, ceiling, est0, est1)
if halt.Scope != "book" {
t.Fatalf("the shortfall is stated for the BOOK scope only (the day ceiling sums every book in the store), and this stop says scope=%q", halt.Scope)
}
if halt.ShortfallMicroUSD <= 0 {
t.Fatal("the stop states no shortfall: the buyer is told the run paused and not by how much it is short, which is the whole of the metadata this stop is supposed to carry")
}
// The figure must be the distance to the ceiling rather than the price of the call — the one money
// number allowed to leave (D39.203). Reserved is zero here (nothing is in flight at a sequential
// refusal), so the arithmetic is exact and can be asserted rather than bounded.
want := int64((committed + est1 - ceiling) * 1e6)
if halt.ShortfallMicroUSD < want || halt.ShortfallMicroUSD > want+1 {
t.Fatalf("shortfall %d micro-USD, want %d (committed %.6f + denied estimate %.6f ceiling %.6f, rounded up)",
halt.ShortfallMicroUSD, want, committed, est1, ceiling)
}
}
// TestToppingUpByTheStatedShortfallFinishesTheUnit is pin 3 — the three-run sequence, and the ONE pin of
// this file that is green both before and after this pack. It is a GUARD, not a proof: the resume path it
// protects is ratified (D4 — the book pauses durably and a raised ceiling resumes it), and this pack's own
// change (a mark the resume must NOT read as an answer) is exactly the kind of edit that can break it
// silently. Stated here so nobody reads its green as evidence the hole was closed; pin 1 above is what
// reds on the defect.
func TestToppingUpByTheStatedShortfallFinishesTheUnit(t *testing.T) {
prov := newLengthOncePerChapter(t)
bookPath, manifest := retryStopFixture(t, prov.srv.URL)
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
// RUN 1 — stops on the refused retry.
r1 := newRunner(t, bookPath)
ceiling, _, _ := retryStopCeiling(t, r1, manifest)
r1.CeilingUSD = ceiling
_, err := r1.TranslateBook(ctx)
var halt *CeilingHalt
if !errors.As(err, &halt) {
r1.Close()
t.Fatalf("run 1 must stop on the ceiling, got %v", err)
}
shortfall := halt.ShortfallMicroUSD
callsAfterRun1 := prov.total()
r1.Close()
// RUN 2 — the same ceiling. The position is re-attacked (its mark is not an answer), attempt 0 is
// replayed from its checkpoint for $0, and the retry is refused again: no fresh paid call, no endless
// loop, the same stop.
r2 := newRunner(t, bookPath)
r2.CeilingUSD = ceiling
_, err = r2.TranslateBook(ctx)
var halt2 *CeilingHalt
if !errors.As(err, &halt2) {
r2.Close()
t.Fatalf("run 2 under the same ceiling must stop the same way, got %v", err)
}
if got := prov.total(); got != callsAfterRun1 {
r2.Close()
t.Fatalf("the resume bought %d fresh call(s) under a ceiling that has not moved (was %d, now %d): a position whose money ran out must replay, not re-buy",
got-callsAfterRun1, callsAfterRun1, got)
}
// ⛔ AND THE RE-WRITTEN MARK MUST STILL CARRY THE MONEY. The resume's fresh spend is zero — attempt 0
// came from its checkpoint — so a mark built from «what this run paid» would overwrite the row with
// cost_usd=0 and put chunk_status below SUM(checkpoints) for the position, which is the ledger
// invariant the projection a person decides on is read through.
rows2, r2err := r2.Store.ChunkStatusesForBook("test-book")
if r2err != nil {
r2.Close()
t.Fatal(r2err)
}
row2 := draftRowOf(t, r2, rows2, manifest[1])
if row2 == nil || FlagReason(row2.FlagReason) != FlagRetryUnaffordable || row2.CostUSD <= 0 {
r2.Close()
t.Fatalf("after a resume that bought nothing the mark reads %+v: the row must still name the money reason AND the money already spent on this position", row2)
}
t.Logf("after the resume under the same ceiling: fresh provider calls=%d · the mark still reads reason=%q cost_usd=%.6f attempts=%d",
prov.total()-callsAfterRun1, row2.FlagReason, row2.CostUSD, row2.Attempts)
r2.Close()
// RUN 3 — the ceiling raised by EXACTLY the shortfall the stop stated. This is the sufficiency half
// of pin 2: not «some bigger number works», but «the number the buyer was given works».
raised := ceiling + float64(shortfall)/1e6
r3 := newRunner(t, bookPath)
defer r3.Close()
r3.CeilingUSD = raised
res, err := r3.TranslateBook(ctx)
if err != nil {
t.Fatalf("topping up by the stated shortfall ($%.6f → $%.6f, +%d micro-USD) did not let the run finish: %v", ceiling, raised, shortfall, err)
}
rows, rerr := r3.Store.ChunkStatusesForBook("test-book")
if rerr != nil {
t.Fatal(rerr)
}
row := draftRowOf(t, r3, rows, manifest[1])
committed, _, _ := r3.Store.SpentUSD("test-book")
t.Logf("after the top-up: provider calls=%d (was %d) · committed=$%.6f · row=%+v", prov.total(), callsAfterRun1, committed, row)
if row == nil || row.Disposition != string(DispOK) {
t.Fatalf("the unit was not finished by the resume: row=%+v", row)
}
if got := prov.calls(retriedChapterRune); got != 2 {
t.Fatalf("the retried chapter saw %d call(s), want 2 — attempt 0 replayed for $0 and the retry bought once", got)
}
if res.Flagged != 0 {
t.Fatalf("the finished run still reports %d flagged unit(s)", res.Flagged)
}
// The delivered text must be the RETRY's, not attempt 0's: that is what «the resume finished the
// unit» means, and a checkpoint-served answer would look identical in the counters alone.
found := false
for _, c := range res.Chunks {
if c.Chapter == manifest[1].Chapter && c.ChunkIdx == manifest[1].ChunkIdx {
found = true
if !strings.Contains(c.FinalText, draftAfterRetry) {
t.Fatalf("the finished unit ships %.40q — not the retry's text", c.FinalText)
}
}
}
if !found {
t.Fatalf("the finished run carries no result for ch%d/chunk%d", manifest[1].Chapter, manifest[1].ChunkIdx)
}
}
// TestStopMarkForAsksWhetherAnythingWasEverBought is the DIRECT table over the decision, and it exists
// because the end-to-end tests above cannot reach two of its rows at all: a position that paid for a
// BURNED key and never classified a reply is left behind by a stopped run, not by a ceiling, and arranging
// one through the provider is a race where the rule is a predicate.
//
// ⛔ The row that matters most is «money was spent, nothing was classified». It is the shape a position
// has after a stopped run burned attempt 0: runAttempt walks over burned keys, so the FIRST fresh purchase
// of that position happens at an index ≥ 1 with nothing in hand. A mark written there would be a verdict
// about a unit nobody has translated once — and since a flagged row is an answer a resume serves
// (resolvedForResume), the unit would be terminal, holding the burned money and no text. The predicate
// therefore asks `paidAttempts`, and the two wrong readings of it — the attempt INDEX and «money was
// spent» — are the two rows below that must answer «no mark».
func TestStopMarkForAsksWhetherAnythingWasEverBought(t *testing.T) {
ceiling := func(shortfall int64, scope string) error {
return &CeilingHalt{Scope: scope, ShortfallMicroUSD: shortfall,
err: fmt.Errorf("pipeline: book USD ceiling reached: %w", errReserveCeiling)}
}
stopped := fmt.Errorf("the run ended: %w", context.Canceled)
delivered := &llm.AttemptCutError{Provider: "fake", Cause: llm.CutByParent, Delivered: true}
paid := stoppedPosition{
attempts: 2, paidAttempts: 1, cumCostUSD: 0.00182,
inHand: stageAttempt{attempt: 0, cls: classification{FlagLength, "truncated at max_tokens"}},
}
burnedOnly := stoppedPosition{attempts: 2, paidAttempts: 0, cumCostUSD: 0.00182}
// A position whose FIRST attempt echoed: the echo re-roll is the purchase that was refused, and the
// echo is what the row must remember — its own reason will say only why the re-roll never happened.
echoed := stoppedPosition{
attempts: 2, paidAttempts: 1, cumCostUSD: 0.00182,
inHand: stageAttempt{attempt: 0, cls: classification{FlagCJKArtifact, "CJK share 97% in output (untranslated echo)"}},
firstFlagReason: FlagCJKArtifact,
}
// A burn at index 0, a classified attempt at 1, the refusal at 2: the shape a position has when a
// stopped run left money with no result behind it and the next run bought the answer itself.
afterABurn := stoppedPosition{
attempts: 3, paidAttempts: 1, cumCostUSD: 0.00364,
inHand: stageAttempt{attempt: 1, cls: classification{FlagEmpty, "empty completion (finish=stop)"}},
}
fresh := stoppedPosition{attempts: 1, paidAttempts: 0}
for _, tc := range []struct {
name string
p stoppedPosition
err error
want FlagReason
attempts int
// wantFirst is the superseded first failure the mark must carry. It is asserted because without it
// an echo the book PAID for leaves the echo metric's numerator while staying in its denominator —
// the rate falls on the run that bought the echo (echorecovered_test.go drives that end to end).
wantFirst string
says []string
omits []string
}{
{name: "a ceiling refused the re-attack of a paid attempt", p: paid, err: ceiling(3710, "book"),
want: FlagRetryUnaffordable, attempts: 1,
says: []string{"attempt 0", string(FlagLength), "book", "3710 micro-USD", "raise the ceiling"}},
{name: "the day ceiling states no shortfall", p: paid, err: ceiling(0, "day"),
want: FlagRetryUnaffordable, attempts: 1,
says: []string{"attempt 0", "day"},
omits: []string{"short by"}},
{name: "a re-attack refused at a position that had burned a key first", p: afterABurn, err: ceiling(3710, "book"),
want: FlagRetryUnaffordable, attempts: 2,
says: []string{"attempt 1", string(FlagEmpty)}},
{name: "a re-attack refused over an echo the book had paid for", p: echoed, err: ceiling(3710, "book"),
want: FlagRetryUnaffordable, attempts: 1, wantFirst: string(FlagCJKArtifact),
says: []string{string(FlagCJKArtifact)}},
{name: "a human stopped a delivered call over a position that had already failed once", p: echoed,
err: fmt.Errorf("%w: %w", stopped, delivered),
want: FlagCancelled, attempts: 2, wantFirst: string(FlagCJKArtifact)},
{name: "a human stopped a delivered call", p: paid, err: fmt.Errorf("%w: %w", stopped, delivered),
want: FlagCancelled, attempts: 2, says: []string{"in flight", "the same budget"}},
{name: "a ceiling refused a position that only ever paid for a burned key", p: burnedOnly, err: ceiling(3710, "book")},
{name: "a ceiling refused a fresh attempt 0", p: fresh, err: ceiling(3710, "book")},
{name: "a stop with no delivered call", p: paid, err: stopped},
{name: "an ordinary infra failure", p: paid, err: errors.New("the store is gone")},
} {
t.Run(tc.name, func(t *testing.T) {
mark, marked := stopMarkFor(tc.p, tc.err)
if tc.want == "" {
if marked {
t.Fatalf("a mark was written where the position has nothing to report: %+v", mark)
}
return
}
if !marked {
t.Fatalf("no mark: the position paid $%.6f and the stop would leave it reading as never started", tc.p.cumCostUSD)
}
if mark.reason != tc.want {
t.Fatalf("reason %q, want %q", mark.reason, tc.want)
}
if mark.attempts != tc.attempts {
t.Fatalf("the mark counts %d attempt(s), want %d: a refused reservation is not an attempt that happened", mark.attempts, tc.attempts)
}
if mark.firstFlag != tc.wantFirst {
t.Fatalf("the mark carries first_flag_reason=%q, want %q: the column is how a failure the book PAID for stays countable once the row's own reason says something else",
mark.firstFlag, tc.wantFirst)
}
for _, want := range tc.says {
if !strings.Contains(mark.detail, want) {
t.Fatalf("the detail a person reads afterwards does not say %q: %s", want, mark.detail)
}
}
for _, never := range tc.omits {
if strings.Contains(mark.detail, never) {
t.Fatalf("the detail says %q about a stop that stated no figure — a money number of zero reads as «you are not short of anything»: %s", never, mark.detail)
}
}
})
}
}
// TestACeilingRefusingAFreshAttemptLeavesNoRow is the other side of the predicate, asserted through a run:
// a position that bought NOTHING must stay `pending`, because pending is then the TRUTH. A mark there
// would invent a half-done unit out of a unit nobody started, and it is exactly what the wrong predicate
// (the attempt index, or «money moved in this run») produces.
func TestACeilingRefusingAFreshAttemptLeavesNoRow(t *testing.T) {
prov := newLengthOncePerChapter(t)
bookPath, manifest := retryStopFixture(t, prov.srv.URL)
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
r := newRunner(t, bookPath)
defer r.Close()
// A ceiling that admits nothing at all: the very first attempt 0 is refused.
r.CeilingUSD = 0.0000001
_, err := r.TranslateBook(ctx)
var halt *CeilingHalt
if !errors.As(err, &halt) {
t.Fatalf("want a ceiling halt, got %v", err)
}
if got := prov.total(); got != 0 {
t.Fatalf("premise broken: the provider saw %d call(s), so something WAS bought and this is no longer the unbought case", got)
}
committed, reserved, serr := r.Store.SpentUSD("test-book")
if serr != nil {
t.Fatal(serr)
}
rows, rerr := r.Store.ChunkStatusesForBook("test-book")
if rerr != nil {
t.Fatal(rerr)
}
t.Logf("after a stop that bought nothing: committed=$%.6f reserved=$%.6f · chunk_status rows=%d (units in the manifest: %d)",
committed, reserved, len(rows), len(manifest))
if committed != 0 {
t.Fatalf("the fixture paid $%.6f — this test is about a position that bought nothing", committed)
}
if len(rows) != 0 {
t.Fatalf("a stop that bought nothing wrote %d chunk_status row(s): %+v — a unit nobody translated must read `pending`, which is the truth, and a flag would invent a half-done unit the resume is free to re-buy", len(rows), rows)
}
// ⛔ THE CONTROL FOR A ZERO, THROUGH THE SAME QUERY. «No row for this position» and «this read cannot
// see rows at all» are the same output, and the manifest count printed above comes from a DIFFERENT
// instrument (bookChunks). So the ceiling is raised and the book run: the same call must now answer
// with rows, which is what makes the zero above a fact about the position rather than about the read.
r.CeilingUSD = 0
if _, err := r.TranslateBook(ctx); err != nil {
t.Fatalf("the control run under the book's own ceiling must finish: %v", err)
}
after, aerr := r.Store.ChunkStatusesForBook("test-book")
if aerr != nil {
t.Fatal(aerr)
}
t.Logf("control: the same query, after a run that was allowed to buy — chunk_status rows=%d (provider calls=%d)", len(after), prov.total())
if len(after) != len(manifest) {
t.Fatalf("the control read %d row(s) for %d unit(s): the zero above may have been this query's answer to everything", len(after), len(manifest))
}
}
// TestARedriveDoesNotBuyThePaidAttemptAgain is a MONEY pin, and the defect it closes was created by this
// very pack: before the mark existed, a position whose re-attack a ceiling refused had NO row, so a
// redrive could not see it. A flagged row is a redrive target by default — and ResetChunkStages DELETES
// the position's checkpoints, including the one holding the ALREADY PAID text of attempt 0. The operator's
// natural gesture after a run full of flags («tmctl redrive», no selector) would then buy that attempt
// again, while the resume needed only the re-attack.
//
// The control is in the same store and through the same query: flip the row's reason to a real verdict and
// the target appears. Without it «no targets» would pass on a selector that matches nothing at all.
func TestARedriveDoesNotBuyThePaidAttemptAgain(t *testing.T) {
prov := newLengthOncePerChapter(t)
bookPath, manifest := retryStopFixture(t, prov.srv.URL)
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
r1 := newRunner(t, bookPath)
ceiling, _, _ := retryStopCeiling(t, r1, manifest)
r1.CeilingUSD = ceiling
_, err := r1.TranslateBook(ctx)
var halt *CeilingHalt
if !errors.As(err, &halt) {
r1.Close()
t.Fatalf("the run must stop on the ceiling, got %v", err)
}
r1.Close()
r2 := newRunner(t, bookPath)
defer r2.Close()
// The operator's default gesture: every flagged position, whatever the reason.
sum, _, rerr := r2.Redrive(ctx, RedriveSelector{Chapter: -1, ChunkIdx: -1, DryRun: true})
if rerr != nil {
t.Fatal(rerr)
}
st, serr := r2.Status(ctx)
if serr != nil {
t.Fatal(serr)
}
t.Logf("after the stop: redrive targets=%d · status says done=%d flagged=%d in_progress=%d pending=%d",
len(sum.Targets), st.Chapters[0].UnitsDone, st.Chapters[0].UnitsFlagged, st.Chapters[0].UnitsInProgress, st.Chapters[0].UnitsPending)
if len(sum.Targets) != 0 {
t.Fatalf("the redrive targets %+v: its reset DELETES the checkpoints of those positions, and the one behind this mark holds the paid text of attempt 0 — the resume replays it for $0 and buys only the re-attack", sum.Targets)
}
// Asking for the reason BY NAME does not change the answer: the position is not a verdict, and the
// cheap remedy (a raised ceiling and a resume) is the only one that finishes it.
byName, _, nerr := r2.Redrive(ctx, RedriveSelector{Chapter: -1, ChunkIdx: -1, Reason: string(FlagRetryUnaffordable), DryRun: true})
if nerr != nil {
t.Fatal(nerr)
}
if len(byName.Targets) != 0 {
t.Fatalf("named explicitly, the mark became a target again: %+v", byName.Targets)
}
// THE CONTROL, on the same store and through the same query: the position carries a real verdict, and
// the redrive must find it. A «no targets» that held whatever the data said would be vacuous.
row, gerr := r2.Store.GetChunkStatus("test-book", manifest[1].Chapter, manifest[1].ChunkIdx, "draft")
if gerr != nil || row == nil {
t.Fatalf("the marked row is gone: %v %v", row, gerr)
}
verdict := *row
verdict.FlagReason = string(FlagHardRefusal)
if uerr := r2.Store.UpsertChunkStatus(verdict); uerr != nil {
t.Fatal(uerr)
}
ctrl, _, cerr := r2.Redrive(ctx, RedriveSelector{Chapter: -1, ChunkIdx: -1, DryRun: true})
if cerr != nil {
t.Fatal(cerr)
}
t.Logf("control: with the SAME row carrying a real verdict, redrive targets=%d", len(ctrl.Targets))
if len(ctrl.Targets) != 1 {
t.Fatalf("the control found %d target(s) for a genuinely flagged position: the assertion above was about a selector that matches nothing", len(ctrl.Targets))
}
}
// TestAMarkedUnitIsCarriedAndNotChargedAGrantSlot is a MONEY pin over the volume classifier, and the
// defect it closes was created by this pack: the mark gave the position a row, the completeness test
// counted rows rather than ANSWERS, and a unit an earlier run had already paid to start was classified
// `rework` — which TAKES a grant slot (granted()) and is queued behind fresh book. The `--max-units` help
// text promises the opposite in so many words: «a unit an EARLIER run started and never shipped is
// finished outside the grant, so the same unit is never charged a slot twice».
func TestAMarkedUnitIsCarriedAndNotChargedAGrantSlot(t *testing.T) {
prov := newLengthOncePerChapter(t)
bookPath, manifest := retryStopFixture(t, prov.srv.URL)
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
r1 := newRunner(t, bookPath)
ceiling, _, _ := retryStopCeiling(t, r1, manifest)
r1.CeilingUSD = ceiling
_, err := r1.TranslateBook(ctx)
var halt *CeilingHalt
if !errors.As(err, &halt) {
r1.Close()
t.Fatalf("the run must stop on the ceiling, got %v", err)
}
r1.Close()
r := newRunner(t, bookPath)
defer r.Close()
chunks, cerr := r.bookChunks()
if cerr != nil {
t.Fatal(cerr)
}
statuses, serr := r.Store.ChunkStatusesForBook("test-book")
if serr != nil {
t.Fatal(serr)
}
units := r.outputUnits(chunks)
class, _, kerr := r.classifyUnits(units, chunks, statuses, nil)
if kerr != nil {
t.Fatal(kerr)
}
marked := class[chunkKey{manifest[1].Chapter, manifest[1].ChunkIdx}]
clean := class[chunkKey{manifest[0].Chapter, manifest[0].ChunkIdx}]
t.Logf("volume classes: the marked unit=%d, the delivered one=%d (fresh=%d free=%d rework=%d carried=%d)",
marked, clean, unitFresh, unitFree, unitRework, unitCarried)
if marked != unitCarried {
t.Fatalf("the marked unit classifies %d, want carried (%d): `rework` charges this run's grant for a unit an earlier run already paid to start, and `fresh` would call it new book",
marked, unitCarried)
}
// The control, through the same call: the unit that really was delivered is FREE — so «carried» above
// is a statement about this unit and not what the classifier answers for everything.
if clean != unitFree {
t.Fatalf("the delivered unit classifies %d, want free (%d) — the classifier is answering something else entirely", clean, unitFree)
}
}
// TestTheWaveCountersDoNotTellTheBuyerTheBookIsDone is the MONEY WIRE pin, and it is the widest defect
// this pack created: the wave counter counted a unit as resolved once its rows EXISTED, so the mark made
// it full. Measured before the fix on this fixture — run 1 stopped with one unit marked and published
// `money{units_resolved:1,units_deferred:1}`; the resume under the SAME ceiling, which bought nothing and
// left the same hole, published `draft{done:2,total:2}` and `money{units_resolved:2,units_deferred:0}`.
// A buyer deciding whether to top up was being told the book owed nothing.
func TestTheWaveCountersDoNotTellTheBuyerTheBookIsDone(t *testing.T) {
prov := newLengthOncePerChapter(t)
bookPath, manifest := retryStopFixture(t, prov.srv.URL)
ceiling := 0.0
for run := 1; run <= 2; run++ {
r := newRunner(t, bookPath)
if run == 1 {
ceiling, _, _ = retryStopCeiling(t, r, manifest)
}
r.CeilingUSD = ceiling
_, err := r.TranslateBook(obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}))
var halt *CeilingHalt
if !errors.As(err, &halt) {
r.Close()
t.Fatalf("run %d must stop on the ceiling, got %v", run, err)
}
r.Close()
}
// Read the journal as BYTES, the way the platform tails it — not by asking the engine's structs.
raw, rerr := os.ReadFile(filepath.Join(filepath.Dir(bookPath), runevents.JournalFile))
if rerr != nil {
t.Fatal(rerr)
}
type money struct {
UnitsResolved int `json:"units_resolved"`
UnitsDeferred int `json:"units_deferred"`
}
var ledgers []money
var progress []string
for _, l := range strings.Split(strings.TrimSpace(string(raw)), "\n") {
if l == "" {
continue
}
var env struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
if err := json.Unmarshal([]byte(l), &env); err != nil {
t.Fatalf("the stream is not readable NDJSON: %v (%s)", err, l)
}
switch env.Type {
case string(runevents.TypeFinished):
var f struct {
Outcome string `json:"outcome"`
Money *money `json:"money"`
}
if err := json.Unmarshal(env.Data, &f); err != nil {
t.Fatal(err)
}
if f.Money != nil {
ledgers = append(ledgers, *f.Money)
}
case string(runevents.TypeProgress):
progress = append(progress, string(env.Data))
}
}
t.Logf("money ledgers published, in order: %+v", ledgers)
t.Logf("progress frames: %s", strings.Join(progress, " · "))
if len(ledgers) != 2 {
t.Fatalf("want one money ledger per stopped run, got %d — the fixture did not publish what this test reads", len(ledgers))
}
for i, l := range ledgers {
if l.UnitsResolved != 1 || l.UnitsDeferred != 1 {
t.Fatalf("run %d published money{resolved:%d deferred:%d}, want 1 and 1: the unit whose re-attack was refused is NOT resolved — nothing bought it, and telling a buyer the book owes %d units is the number they decide a top-up on",
i+1, l.UnitsResolved, l.UnitsDeferred, l.UnitsDeferred)
}
}
// And the resync channel must agree with the stream: one wave-resolved unit of two.
r := newRunner(t, bookPath)
defer r.Close()
st, serr := r.Status(context.Background())
if serr != nil {
t.Fatal(serr)
}
t.Logf("status says draft=%+v · chapter done=%d flagged=%d in_progress=%d",
st.Progress.Draft, st.Chapters[0].UnitsDone, st.Chapters[0].UnitsFlagged, st.Chapters[0].UnitsInProgress)
if st.Progress.Draft.Done != 1 || st.Progress.Draft.Total != 2 {
t.Fatalf("the resync channel says the draft wave finished %d of %d units: the platform folds this and the stream into ONE column, so they must not disagree",
st.Progress.Draft.Done, st.Progress.Draft.Total)
}
}

View file

@ -120,6 +120,10 @@ type projectOpts struct {
// glossaryTokenBudget overrides context.glossary_token_budget. 0 keeps the fixture's historical 800, so
// every pre-existing project is byte-identical; a tiny value is how the EVICTION path is reachable at all.
glossaryTokenBudget int
// regenerateEcho sets retries.regenerate_echo_before_escalate — the echo re-roll budget, which every
// shipping pipeline carries at 1. 0 OMITS the key, so every pre-existing project's pipeline.yaml is
// byte-identical and its snapshot id does not move.
regenerateEcho int
// draftOnly drops the editor stage, so the DRAFT is the shipping output (the pipeline shape the wave
// executor calls draft-only). false keeps the historical two-stage fixture byte-identical.
draftOnly bool
@ -191,12 +195,12 @@ models:
core: C1
version: 1
defaults: { max_output_ratio: 2.0, min_max_tokens: %d }
retries: { regenerate_before_escalate: %d }
retries: { regenerate_before_escalate: %d%s }
context: { glossary_injection: selective, glossary_token_budget: %d }
waves: { workers: %d }
stages:
- { name: draft, role: translator, model: fake-model, prompt_override: prompts/translator.md, prompt_version: v-test, temperature: 0.3, reasoning: "off" }
%s%s`, o.minMaxTokens, o.regenerate, o.glossaryTokenBudget, o.waveWorkers, editStage, gatesBlock))
%s%s`, o.minMaxTokens, o.regenerate, echoRegenKey(o.regenerateEcho), o.glossaryTokenBudget, o.waveWorkers, editStage, gatesBlock))
sourceName := "source.txt"
if len(o.epub) > 0 {
@ -233,6 +237,15 @@ ceilings: { book_usd: %g, day_usd: 2.0 }
return filepath.Join(dir, "book.yaml")
}
// echoRegenKey renders the echo re-roll budget as a key to APPEND, or nothing at all. Omitting it rather
// than writing a zero is what keeps every fixture that predates the option byte-identical.
func echoRegenKey(n int) string {
if n <= 0 {
return ""
}
return fmt.Sprintf(", regenerate_echo_before_escalate: %d", n)
}
func setupProject(t *testing.T, providerURL string) string {
return setupProjectOpts(t, providerURL, projectOpts{regenerate: 1})
}

View file

@ -139,35 +139,70 @@ func (r *Runner) runStage(ctx context.Context, st config.Stage, stageIdx int, sn
var last stageAttempt
anyFresh := false
attemptsMade := 0
// ⛔ THE MARK FOR A STOPPED POSITION IS ATTACHED TO EVERY EXIT, not to the attempt loop's. It was
// written at the loop's error return, and the hop and the repair sub-step both leave this function
// through OTHER returns — so a run stopped over an escalation hop settled the
// money and left NO chunk_status row at all: the invisible hole §4.2 forbids, measured as
// `committed=0.001176` with `chunk_status_rows=0`. A deferred rule sees whichever return fires, and
// closes over the counters so it reports what had accumulated by then. recordCancelledStage is a
// no-op for every error that is not a delivered call a person stopped, so this costs nothing on the
// ordinary paths.
defer func() {
r.recordCancelledStage(ctx, cancelledPosition{
stage: st, chunk: ch, snapshotID: snapID, contentHash: contentHash,
cumCostUSD: cumCost, attempts: attemptsMade,
}, err)
}()
// judged counts the attempts that came back and were CLASSIFIED — paid for freshly or replayed from
// an already-paid checkpoint. It is declared up here with the other counters rather than beside the
// loop because the stop mark below closes over it: «did this position ever buy a reply» is the
// question that separates a stop worth marking from one where `pending` is the truth.
judged := 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.
//
// ⛔ AND THE STOP MARK NEEDS IT FOR THE SAME REASON THE RECOVERED ROW DOES, which is why it is
// declared above the mark's defer. A run stopped on a refused re-attack leaves a row whose
// flag_reason says why the PURCHASE did not happen, and that column then says nothing about what the
// paid attempt came back as — so an echo somebody paid for would leave the echo metric's numerator
// while staying in its denominator, and the rate would FALL on the very run that bought the echo.
// That is the 25.07 shape again, one column further along.
firstFlagReason := FlagReason("")
// escalations counts BUDGET DOUBLINGS; attempt counts KEYS. They move together on every
// regeneration and come apart on exactly one path: re-doing a call the engine itself cut short,
// which needs a fresh request_hash (the old one already holds that call's money) at the budget it
// was already granted. See maxTokensForAttempt.
// ⛔ THE MARK FOR A STOPPED POSITION IS ATTACHED TO EVERY EXIT, not to the attempt loop's. It was
// written at the loop's error return, and the hop and the repair sub-step both leave this function
// through OTHER returns — so a run stopped over an escalation hop settled the
// money and left NO chunk_status row at all: the invisible hole §4.2 forbids, measured as
// `committed=0.001176` with `chunk_status_rows=0`. A deferred rule sees whichever return fires, and
// closes over the counters so it reports what had accumulated by then. recordStoppedPosition is a
// no-op for every stop that leaves nothing to say (stopMarkFor), so this costs nothing on the
// ordinary paths.
defer func() {
r.recordStoppedPosition(ctx, stoppedPosition{
stage: st, chunk: ch, snapshotID: snapID, contentHash: contentHash,
cumCostUSD: cumCost, attempts: attemptsMade, paidAttempts: judged, inHand: last,
firstFlagReason: firstFlagReason,
}, err)
}()
// escalations counts BUDGET DOUBLINGS; attempt counts KEYS. They come apart on TWO paths, and the
// two are different in kind:
//
// - re-doing a call the engine itself cut short, which needs a fresh request_hash (the old one
// already holds that call's money) at the budget it was already granted;
// - a regeneration that answers an EMPTY reply with less thinking instead of more room. It is an
// attempt and a regeneration, and deliberately NOT a doubling — that is the whole remedy.
//
// The second path is reachable only with Retries.LowerEffortOnEmpty, which no shipping config turns
// on (pinned: config.TestShippingPipelinesDoNotLowerEffortOnEmpty). See maxTokensForAttempt.
escalations := 0
judged := 0
// regens counts REGENERATIONS — the extra attempts the flag policy is allowed to buy, which is what
// `regenerate_before_escalate` names and what both budgets below are spent from. It is a separate
// number from the doublings because a regeneration does not have to be one: an empty reply may be
// re-asked with less thinking at the same budget. While every regeneration doubles the budget the
// two numbers are equal, which is why a config that does not ask for the other remedy renders the
// same wire it always did.
regens := 0
// effort is the thinking level THIS attempt is asked at. It starts as the stage's configured value
// and only ever goes down, one ladder step per regeneration, so the loop cannot circle: the ladder
// is finite and each step is strictly lower than the last (config.Models.ReducedEffort).
effort := st.Reasoning
for attempt := 0; ; attempt++ {
maxTokens := maxTokensForAttempt(baseMaxTokens, escalations)
att, err := r.runAttempt(ctx, st, st.ResolvedModel, snapID, ch, job, attempt, maxTokens, msgs, false, isFinal, true)
// The attempt's stage is the stage AS CALLED — a copy carrying this attempt's effort. Copying
// rather than threading an extra argument keeps ONE definition of the call's identity
// (attemptRequest reads the stage), so the wire, the request hash, the reservation estimate and
// the snapshot description can never disagree about which effort was asked for.
attemptStage := st
attemptStage.Reasoning = effort
att, err := r.runAttempt(ctx, attemptStage, st.ResolvedModel, snapID, ch, job, attempt, maxTokens, msgs, false, isFinal, true)
cumCost += att.cumCost
runCost += att.runCost
// ⛔ THE COUNT IS A FACT ABOUT WHAT HAPPENED, not about whether it succeeded — and it is recorded
@ -192,18 +227,35 @@ func (r *Runner) runStage(ctx context.Context, st config.Stage, stageIdx int, sn
// Flagged: re-attack only the retryable subset, only while regenerations
// remain (a bigger budget on the attempt axis, D2.3). Everything else is
// deterministic — a same-model retry would re-refuse and re-bill (D2.2).
if att.cls.Reason.retryable() && escalations < maxRegen {
if att.cls.Reason.retryable() && regens < maxRegen {
// An EMPTY reply used the whole budget before writing anything, so on a model whose
// thinking shares that budget the cure is less thinking, not more room — the same rule
// D2.3 states for a repetition loop, where a bigger budget only buys more loop. Opt-in
// (Retries.LowerEffortOnEmpty) and only while the ladder has a step left; otherwise this
// falls through to the doubling below, which is what recovered these chunks before.
if att.cls.Reason == FlagEmpty && r.Pipeline.Retries.LowerEffortOnEmpty {
if lower, ok := r.Models.ReducedEffort(st.ResolvedModel, effort); ok {
r.Log.WarnContext(ctx, "stage returned nothing at the full budget, regenerating with less thinking at the SAME budget",
"stage", st.Name, "chapter", ch.Chapter, "chunk", ch.ChunkIdx,
"attempt", attempt, "reason", string(att.cls.Reason),
"effort", effort, "next_effort", lower, "max_tokens", maxTokens)
effort = lower
regens++
continue
}
}
r.Log.WarnContext(ctx, "stage flagged, regenerating with a larger budget",
"stage", st.Name, "chapter", ch.Chapter, "chunk", ch.ChunkIdx,
"attempt", attempt, "reason", string(att.cls.Reason), "next_max_tokens", maxTokensForAttempt(baseMaxTokens, escalations+1))
escalations++
regens++
continue
}
// Echo (cjk_artifact) OPT-IN re-generation before escalation (row 77 / D39.61): on a provider whose
// echo is STOCHASTIC per call, a same-model re-gen recovers ~7.6× cheaper than the escalation hop.
// Default 0 ⇒ this never fires and echo escalates straight away (the prior behaviour); the echo GATE
// is untouched — only the RESPONSE changes.
if att.cls.Reason == FlagCJKArtifact && escalations < echoRegen {
if att.cls.Reason == FlagCJKArtifact && regens < echoRegen {
r.Log.WarnContext(ctx, "echo flagged, regenerating before escalation (echo is stochastic per call, D39.61)",
"stage", st.Name, "chapter", ch.Chapter, "chunk", ch.ChunkIdx, "attempt", attempt)
// ⚠ THE ECHO RE-GEN COUNTS AS A DOUBLING TOO, and it must. It is a fresh roll of a
@ -212,6 +264,7 @@ func (r *Runner) runStage(ctx context.Context, st config.Stage, stageIdx int, sn
// move max_tokens, and with it request_hash, and with it every echo-regenerated
// checkpoint on disk. The doubling axis was split to add a case, not to re-price one.
escalations++
regens++
continue
}
break
@ -366,10 +419,14 @@ 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.
// recoveredFirstFlag returns the first attempt's failure ONLY when the row's own verdict is no longer 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.
//
// ⚠ «NOT THE VERDICT ANY MORE» DOES NOT MEAN «RECOVERED», whatever this function is named. The verdict
// also moves when a regeneration fails differently, and when a USD ceiling refuses to buy the
// regeneration at all (FlagRetryUnaffordable) — both leave a FLAGGED row with a superseded first failure.
// A reader that wants recoveries asks the row's disposition as well (quality.go, draftEchoRecovered).
func recoveredFirstFlag(first, verdict FlagReason) string {
if first == "" || first == verdict {
return ""
@ -778,8 +835,18 @@ func (r *Runner) runAttempt(ctx context.Context, st config.Stage, model, snapID
// Price by the model that actually answered, with a fallback to the REQUESTED
// one (not to a cheap global anchor) if the provider returned a canonicalized slug.
price := r.Pricer.PriceForResponse(model, modelActual)
price, basis := r.Pricer.PriceForResponse(model, modelActual)
cost := ledger.CostUSD(price, resp.Usage)
if basis.Substituted() {
// The bill was taken from a model that did not answer this call, and nothing downstream can
// tell: the ledger, the checkpoint and the telemetry row all record a number with no note of
// where its rate came from. It is said once per call, at the moment the substitution is a
// fact, because the direction it protects against is not symmetric — a provider that routes
// DOWN is billed at the pin of the model we asked for, and the reader pays the difference.
r.Log.WarnContext(ctx, "priced by a model that did not answer: the answering slug is not in the catalogue",
"stage", st.Name, "requested", model, "answered", modelActual, "priced_by", string(basis),
"output_per_m", price.OutputPerM, "cost_usd", fmt.Sprintf("%.6f", cost))
}
// A paid model (InputPerM>0) returned 2xx with zero usage — $0 would blind the
// ceiling: take a conservative estimate. For local ($0 price) zero usage is
// normal — it stays $0.
@ -845,6 +912,7 @@ func (r *Runner) runAttempt(ctx context.Context, st config.Stage, model, snapID
rl.PromptTokens, rl.CachedTokens = resp.Usage.PromptTokens, resp.Usage.CachedTokens
rl.CacheCreationTokens = resp.Usage.CacheCreationTokens
rl.CompletionTokens, rl.ReasoningTokens = resp.Usage.CompletionTokens, resp.Usage.ReasoningTokens
rl.ReasoningInCompletion = resp.Usage.ReasoningInCompletion
rl.CostUSD, rl.LatencyMS, rl.FinishReason = cost, att.latency, resp.FinishReason
rl.OK, rl.Degraded = att.cls.ok(), degradedTag(att.cls)
if estimatedCost {

View file

@ -46,13 +46,24 @@ type WaveCounter struct {
// was raised about. These two counters split that single number by wave.
//
// DONE HERE MEANS RESOLVED, NOT OK. A unit counts for a wave once every chunk_status row that wave owes
// it exists — ok, flagged or skipped alike. Two reasons, both load-bearing:
// it is an ANSWER — ok, flagged or skipped alike. Two reasons, both load-bearing:
//
// - a flagged unit is FINISHED as far as work goes (nothing re-attempts it without an explicit
// - a flagged unit is FINISHED as far as work goes (nothing re-attempts a VERDICT without an explicit
// `tmctl redrive`), so excluding it would leave a progress bar permanently short of its own
// denominator on any book with a single flagged chunk;
// - the ok/flagged split is already carried, unconflated, by Done/Flagged/GlossaryMissFlagged below.
//
// ⛔ AND A STOP MARK IS NOT AN ANSWER, WHICH IS WHY THE WORD IS «ANSWER» AND NOT «ROW». The sentence above
// used to read «every row that wave owes it EXISTS», and both of its reasons quietly assumed the row was a
// decision: a `cancelled` or `retry_unaffordable` row is a position the PLAIN RESUME re-does — and a
// redrive refuses it, deliberately (Redrive below), so «nothing re-attempts it» is false in both
// directions. The cost was measured on this pack's own fixture: run 1 stops with one unit marked and
// publishes `money{units_resolved:1, units_deferred:1}`, and the resume under the same ceiling — which
// buys NOTHING and leaves the same hole — published `draft{done:2,total:2}` and
// `money{units_resolved:2, units_deferred:0}`. The buyer deciding whether to top up was being told the
// book owes nothing. The counters that reach the money wire are these (events.go beginWaves →
// moneyLedger), so this predicate is where the honesty lives.
//
// Consequence to read deliberately, ON AN EDIT PIPELINE: Edit.Done ≥ Done, and the gap is every unit the
// edit wave RESOLVED but the unit-level verdict did not call done — a unit the post-check GATE flagged
// (its edit row is ok; the gate flips the UNIT after the stage loop), a c-lite unit whose edit shipped
@ -71,9 +82,9 @@ type PhaseProgress struct {
}
// waveShape is the row arithmetic behind "this wave is done with this unit": which stage names belong to
// each wave and how many rows each owes a unit. A row exists only once the wave DECIDED (ok, flagged or
// skipped alike — see PhaseProgress), and chunk_status is keyed (book, chapter, chunk, stage), so
// counting the rows a wave wrote IS the resolution test.
// each wave and how many rows each owes a unit. A row that is an ANSWER exists only once the wave DECIDED
// (ok, flagged or skipped alike — see PhaseProgress), and chunk_status is keyed (book, chapter, chunk,
// stage), so counting the answering rows a wave wrote IS the resolution test.
//
// It is one definition because two readers need it: this projection, over stored rows, and the live event
// emitter (events.go), which counts the same units as it resolves them. A second copy of the arithmetic
@ -91,11 +102,18 @@ func (r *Runner) waveShape() waveShape {
return waveShape{draftNames: stageNameSet(d), editNames: stageNameSet(e), nDraft: len(d), nEdit: len(e)}
}
// resolved reports, for one unit's stored rows, whether each wave has written every row it owes it. A
// resolved reports, for one unit's stored rows, whether each wave has written every ANSWER it owes it. A
// wave the pipeline does not have answers false — its denominator is 0, not a total it can never reach.
func (w waveShape) resolved(u editUnit, rows []store.ChunkStatus) (draft, edit bool) {
draftRows, editRows := 0, 0
for _, cs := range rows {
for i := range rows {
cs := rows[i]
// A stop mark counts for nothing: the position it marks is one the next run re-does, so a wave
// that wrote it has not finished with this unit (see PhaseProgress — this is the predicate those
// two numbers on the money wire are made of).
if cs.Disposition == string(DispFlagged) && !resolvedForResume(&cs) {
continue
}
switch {
case w.draftNames[cs.Stage]:
draftRows++
@ -423,13 +441,26 @@ var flagSeverity = map[FlagReason]int{
FlagLoopDegenerate: 3,
// Paid, and nothing usable came back. `decode_error` is a 2xx whose body would not parse;
// `attempt_timeout` is a call OUR deadline cut while the provider was still generating it. They rank
// `attempt_timeout` is a call OUR deadline cut while the provider was still generating it;
// `retry_unaffordable` is a flagged first attempt whose retry a USD ceiling refused. They rank
// together because they are the same thing to a reader — the chunk is lost and the money is spent —
// and because neither is a verdict about the TEXT: both say the transport or its deadline needs
// fixing, and both are re-driveable once it is. A lost connection is NOT a third member: it never
// reaches a disposition at all (see disposition.go), so a rank for it would be a rank nothing wears.
FlagDecodeError: 4,
FlagAttemptTimeout: 4,
// and because none of them is a verdict about the TEXT: the first two say the transport or its
// deadline needs fixing, the third says the ceiling does, and all three are re-driveable once it is.
// A lost connection is NOT a fourth member: it never reaches a disposition at all (see
// disposition.go), so a rank for it would be a rank nothing wears.
//
// ⚠ `retry_unaffordable` RANKS HERE WITHOUT A READER TODAY, and saying so is the honest half. The
// passport is the only consumer of these numbers (flagReasonSeverity, below), and it never sees this
// reason: resolveChunkState skips a row that is not an answer BEFORE it records one, so a
// money-marked unit reads `in_progress` and carries no worst-flag at all. The rank exists because the
// table is exhaustive by test — an unranked reason falls to severityUnknown and would out-rank
// nothing — and `cancelled` sits here on exactly the same footing.
// It is ORDERED rather than parked, though, and the order is the claim the test pins: this reason IS
// a length/empty failure plus the fact that the remedy could not be bought, so the day something does
// read it, «truncated» must not be what a chapter reports as its worst problem.
FlagDecodeError: 4,
FlagAttemptTimeout: 4,
FlagRetryUnaffordable: 4,
FlagGlossaryMiss: 5,
FlagLength: 6,
@ -1125,7 +1156,14 @@ func (r *Runner) Redrive(ctx context.Context, sel RedriveSelector) (*RedriveSumm
targeted := false
reason := ""
for _, cs := range rows {
if cs.Disposition == string(DispFlagged) && sel.matches(cs) {
// ⛔ A STOP MARK IS NOT A VERDICT, SO IT IS NOT A REDRIVE TARGET — and the cost of reading it as
// one is a paid call bought twice. ResetChunkStages DELETES the position's checkpoints, and the
// checkpoint behind a `retry_unaffordable` mark holds the ALREADY PAID text of attempt 0: the
// resume replays it for $0 and buys only the re-attack, while a redrive throws it away and buys
// the whole unit again. Before the mark existed such a position had no row at all, so a redrive
// could not reach it; the mark must not put paid work into its radius. A `cancelled` mark is the
// same rule and loses nothing by it — the resume re-does that position anyway.
if cs.Disposition == string(DispFlagged) && resolvedForResume(&cs) && sel.matches(cs) {
targeted, reason = true, cs.FlagReason
break
}
@ -1135,7 +1173,10 @@ func (r *Runner) Redrive(ctx context.Context, sel RedriveSelector) (*RedriveSumm
}
var stages []string
for _, cs := range rows {
if cs.Disposition == string(DispFlagged) || cs.Disposition == string(DispSkipped) {
// Same question for the stages this target resets: a stop mark of ANOTHER stage of the same unit
// is work the next run does, not work a redrive must re-buy.
if cs.Disposition == string(DispSkipped) ||
(cs.Disposition == string(DispFlagged) && resolvedForResume(&cs)) {
stages = append(stages, cs.Stage)
}
}

View file

@ -21,10 +21,11 @@ bookbuild.go staleUnits "build: the stale check could not run; whether the sourc
bookrun.go translateBook "the run ended before its VOLUME grant was used up — the stop below is NOT the volume ceiling"
chunkrun.go persistRetrievalState "could not record what the draft wave was shown; this chunk's injection is unknown, not empty"
chunkrun.go reportEvicted "memory: the injection token budget DROPPED bank rows before the model saw them — these terms had no canon on the wire for those units"
cutcall.go recordCancelledStage "could not mark the stopped position; its money is recorded but the chunk will read as never started"
cutcall.go recordCancelledStage "could not read the position's stored disposition before marking it stopped; the mark is written and may replace an earlier verdict"
cutcall.go recordCancelledStage "the run was stopped over a call that had already gone out; the position is marked cancelled and the resume re-does it on the same budget"
cutcall.go recordCancelledStage "the run was stopped over a position that ALREADY held a verdict from an earlier run; that verdict and its text are kept rather than overwritten by the stop mark (the resume re-does this position, and the stopped call's money is on the ledger)"
cutcall.go recordStoppedPosition "could not mark the stopped position; its money is recorded but the chunk will read as never started"
cutcall.go recordStoppedPosition "could not read the position's stored disposition before marking it stopped; the mark is written and may replace an earlier verdict"
cutcall.go recordStoppedPosition "re-attack denied by a USD ceiling; the position is marked paid-but-unfinished so it does not read as never started, and a raised ceiling finishes it on resume"
cutcall.go recordStoppedPosition "the run was stopped over a call that had already gone out; the position is marked cancelled and the resume re-does it on the same budget"
cutcall.go recordStoppedPosition "the run was stopped over a position that ALREADY held a verdict from an earlier run; that verdict and its text are kept rather than overwritten by the stop mark (the resume re-does this position, and the stopped call's money is on the ledger)"
cutcall.go settleCutCall "we cut a delivered call; the reservation estimate is charged as an ESTIMATE only when the provider had acknowledged it with a reply"
escalation.go maybeEscalate "escalation hop denied by a USD ceiling; keeping the primary flag"
escalation.go maybeEscalate "stage escalated to a fallback model"
@ -88,10 +89,12 @@ snapshotdiff.go describeSnapshotMoveFor "snapshot guard: the stored payload coul
stagerun.go releaseReservation "reservation release failed (reserved_usd leaks and tightens ceilings until the next process restart)"
stagerun.go runAttempt "SpentUSD read failed while formatting the ceiling error; money detail omitted"
stagerun.go runAttempt "paid 2xx with zero usage; settling the reservation estimate to keep the ceiling honest"
stagerun.go runAttempt "priced by a model that did not answer: the answering slug is not in the catalogue"
stagerun.go runStage "echo flagged, regenerating before escalation (echo is stochastic per call, D39.61)"
stagerun.go runStage "job re-pinned to new snapshot (--resnapshot)"
stagerun.go runStage "stage flagged"
stagerun.go runStage "stage flagged, regenerating with a larger budget"
stagerun.go runStage "stage returned nothing at the full budget, regenerating with less thinking at the SAME budget"
stagerun.go setJobStatus "job status update failed (non-fatal; jobs table may lag chunk_status)"
status.go Status "CONFIG-DRIFT — stored rows carry a stage the current config does not run (renamed or removed since the run); the shipping rows are not the ones the run shipped"
status.go Status "config-drift check failed for a wave; drift state is UNKNOWN, not none"

View file

@ -745,13 +745,23 @@ func (r *Runner) classifyUnits(units []editUnit, chunks []chunk.Chunk, statuses
// call. The run would pay for more units than were bought and, with Deferred still 0, would not even
// report a volume stop. Found by this pack's own adversarial pass, reproduced before it was fixed.
//
// Positions are counted, not dispositions: a completed unit always leaves a row for every position it
// has — ok, flagged, or the `skipped` rows recordSkippedStages writes downstream of a flag, including the
// all-members-flagged case where the editor never runs. So full coverage IS terminality, and it is the
// property that actually predicts "no provider call".
// Positions are counted, not dispositions — with ONE exception, and it is the property this predicate
// exists for: a completed unit always leaves a row for every position it has (ok, flagged, or the
// `skipped` rows recordSkippedStages writes downstream of a flag, including the all-members-flagged case
// where the editor never runs), so full coverage IS terminality and is what actually predicts "no
// provider call".
//
// ⛔ THE EXCEPTION IS A STOP MARK, AND WITHOUT IT THIS PREDICATE PREDICTS THE OPPOSITE OF WHAT IT SAYS. A
// `cancelled` or `retry_unaffordable` row is a position the next run RE-DOES, for money — so a unit whose
// last position carries one is fully COVERED and not at all terminal. Counted as recorded, it skipped the
// `unitCarried` branch below and landed in `unitRework`, which TAKES A GRANT SLOT (granted() above) and is
// queued behind fresh book — exactly the charge the carried class exists to prevent, and the thing the
// `--max-units` help text promises never happens («a unit an EARLIER run started and never shipped is
// finished outside the grant»). Measured on the mark fixture before this line: class `rework` for a unit
// whose attempt 0 was paid and whose text the export does not ship.
func unitFullyRecorded(u editUnit, rows []store.ChunkStatus, draftStages, editStages []wavedStage) bool {
have, want := unitPositionsOnFile(u, rows, draftStages, editStages)
return have == want
answered, _, want := unitPositionsOnFile(u, rows, draftStages, editStages)
return answered == want
}
// unitStarted reports whether ANY position this run would execute for the unit already has a stored row —
@ -759,8 +769,12 @@ func unitFullyRecorded(u editUnit, rows []store.ChunkStatus, draftStages, editSt
// enumeration as unitFullyRecorded's, asked the other way round, so the two cannot disagree about what a
// position is.
func unitStarted(u editUnit, rows []store.ChunkStatus, draftStages, editStages []wavedStage) bool {
have, _ := unitPositionsOnFile(u, rows, draftStages, editStages)
return have > 0
// ⚠ ROWS, not answers — the opposite half of the pair above, and deliberately so: a STOP MARK is the
// strongest evidence there is that an earlier run began this unit and spent money on it, while being
// no answer at all. Asked about answers, a unit whose only row is a mark would read «never started»
// and be charged as fresh book.
_, started, _ := unitPositionsOnFile(u, rows, draftStages, editStages)
return started > 0
}
// unitPositionsOnFile counts the positions this run would execute for the unit — every member × every
@ -768,19 +782,34 @@ func unitStarted(u editUnit, rows []store.ChunkStatus, draftStages, editStages [
// rows — and how many of them already have a stored row. A row for a stage the pipeline no longer runs is
// not a position and is not counted: it is neither evidence of a start under this pipeline nor work this
// run would do.
func unitPositionsOnFile(u editUnit, rows []store.ChunkStatus, draftStages, editStages []wavedStage) (have, want int) {
//
// It returns TWO counts over the same enumeration, because its two callers ask different questions of the
// same rows: `answered` is positions whose row is a verdict (will this run call a provider — see
// unitFullyRecorded), `started` is positions with ANY row (did an earlier run begin this unit — see
// unitStarted). They differ exactly on the two stop marks, and one enumeration is what keeps the pair from
// disagreeing about what a position IS.
func unitPositionsOnFile(u editUnit, rows []store.ChunkStatus, draftStages, editStages []wavedStage) (answered, started, want int) {
type pos struct {
chapter, chunkIdx int
stage string
}
stored := make(map[pos]bool, len(rows))
for _, cs := range rows {
stored[pos{cs.Chapter, cs.ChunkIdx, cs.Stage}] = true
answers := make(map[pos]bool, len(rows))
for i := range rows {
cs := rows[i]
p := pos{cs.Chapter, cs.ChunkIdx, cs.Stage}
stored[p] = true
if cs.Disposition != string(DispFlagged) || resolvedForResume(&cs) {
answers[p] = true
}
}
count := func(p pos) {
want++
if stored[p] {
have++
started++
}
if answers[p] {
answered++
}
}
for _, m := range u.Members {
@ -791,7 +820,7 @@ func unitPositionsOnFile(u editUnit, rows []store.ChunkStatus, draftStages, edit
for _, ws := range editStages {
count(pos{u.Chapter, u.FirstChunkIdx, ws.st.Name})
}
return have, want
return answered, started, want
}
// rowsResumeFree is the per-row half of the predicate above.
@ -827,6 +856,14 @@ func (r *Runner) rowsResumeFree(rows []store.ChunkStatus, draftNames, editNames
// runs. Asked first — where it was written — it made a unit PAID for a cancelled row of a stage
// that will never be called again, which is the same error in the opposite direction: an operator
// who edits the pipeline over a stopped run would spend volume slots on nothing.
//
// ⚠ SINCE THE COMPLETENESS TEST LEARNED THE SAME QUESTION, THIS IS THE SECOND LINE AND NOT THE
// FIRST: unitFullyRecorded counts positions whose row is an ANSWER, so a unit carrying a stop mark
// no longer reaches this function at all (it is `carried` before it gets here). The guard stays
// because it is the one that makes the ANSWER to «does this row resume for free» correct on its own
// terms — it is pinned directly (TestACancelledRowIsNotAFreeResume) rather than through the
// classifier — and because a caller that ever asks this without asking completeness first must not
// be handed the wrong answer.
if !resolvedForResume(&cs) {
return false
}

View file

@ -41,12 +41,18 @@ 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).
// FirstFlagReason is the disposition reason of the FIRST attempt whenever the row's own verdict is no
// longer that failure. It is "" when the first attempt already resolved ok or when the verdict IS it,
// and it is NOT the row's verdict — FlagReason is. It exists because such a 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).
//
// ⚠ «A LATER ATTEMPT RECOVERED THE CHUNK» USED TO STAND HERE AND IS TOO NARROW: the verdict also moves
// when a regeneration fails DIFFERENTLY, and when a run stops over the position — a stop mark carries
// the superseded failure too (pipeline/cutcall.go), precisely so an echo the book PAID for stays
// countable once the reason column says why the purchase did not happen. Whoever reads this column for
// recoveries must ask the row's disposition as well (pipeline/quality.go).
// Observability only: re-derived from the stored checkpoints on every run, never a verdict, never wire.
FirstFlagReason string
// UpdatedAt is when this row was last WRITTEN, as the store spells it (`datetime('now')`: UTC,

View file

@ -306,8 +306,11 @@ var migrations = []string{
`,
// 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
// • chunk_status.first_flag_reason — the disposition reason of the FIRST attempt, kept whenever the
// row's own verdict is no longer that failure (a regenerate or the hop recovered the chunk, the
// re-attack failed differently, or a run STOPPED over the position and left a mark). «Recovered»
// alone stood here and was too narrow: a reader who takes a non-empty value for a recovery counts
// cures that did not happen — which is what pipeline/quality.go asks the disposition about. 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
@ -499,6 +502,25 @@ var migrations = []string{
);
CREATE INDEX IF NOT EXISTS wave_selection_book_idx ON wave_selection (book_id, wave);
`,
// v18 (row 422): WHAT ATE THE BUDGET. `reasoning_tokens` means «thinking billed ON TOP of the
// completion», which is 0 by definition on a subset-billing provider — so on DeepSeek, the only
// provider the shipping configs call, it has carried 0 for the entire life of the project, and a
// reader asking «did thinking eat max_tokens» got a zero that looks measured and is not.
//
// This column answers the other question: how much of the completion WE ALREADY PAID FOR was
// thinking rather than the answer. It is the provider's own number (DeepSeek reports it in
// completion_tokens_details on every reply), it prices nothing, and it is the evidence behind the
// cold run of 11.09, where three calls bought 8496/8496/16000 completion tokens and returned zero
// characters of text.
//
// ⚠ NULLABLE ON PURPOSE, against the NOT NULL DEFAULT 0 of every column around it. A subset
// provider that reports the field and a call that genuinely did not think both produce 0, and a
// provider that reports nothing must not be spelled the same way — that is the defect this
// migration exists to remove, and a DEFAULT 0 would re-introduce it on the first row. NULL = the
// question was not answered; every row written before this column is NULL for exactly that reason.
`
ALTER TABLE request_log ADD COLUMN reasoning_in_completion INTEGER;
`,
}
// DESIGN NOTE (D21.10 → BUILT by pack-19 / D39.55; kept as the record of what each type is FOR).

View file

@ -0,0 +1,154 @@
package store
import (
"database/sql"
"fmt"
"path/filepath"
"testing"
)
// TestTheThinkingColumnKeepsSilenceApartFromZero is the durable half of the distinction the adapter
// makes: a row whose provider reported a thinking count of 0 and a row whose provider reported nothing
// must not read the same way out of the database. They did for the whole life of the project — the
// column that was supposed to answer «did thinking eat max_tokens» is NOT NULL DEFAULT 0, so every row
// said zero and no reader could tell a measured zero from a question nobody asked.
func TestTheThinkingColumnKeepsSilenceApartFromZero(t *testing.T) {
s, _ := openTemp(t)
zero, eightK := 0, 8496
for _, row := range []RequestLog{
{BookID: "b", Chapter: 1, ChunkIdx: 0, Stage: "draft", Role: "translator", CompletionTokens: 8496, ReasoningInCompletion: &eightK, OK: false},
{BookID: "b", Chapter: 1, ChunkIdx: 1, Stage: "draft", Role: "translator", CompletionTokens: 1617, ReasoningInCompletion: &zero, OK: true},
{BookID: "b", Chapter: 1, ChunkIdx: 2, Stage: "draft", Role: "translator", CompletionTokens: 1617, OK: true},
} {
if err := s.InsertRequestLog(row); err != nil {
t.Fatal(err)
}
}
rows, err := s.RequestLogRows("b")
if err != nil {
t.Fatal(err)
}
if len(rows) != 3 {
t.Fatalf("rows = %d, want 3", len(rows))
}
// say renders the column the way a reader has to read it: a number, or the absence of one. Printing
// the pointer itself put an ADDRESS in the failure text, which tells whoever broke this nothing
// about what the column now says.
say := func(n *int) string {
if n == nil {
return "no answer"
}
return fmt.Sprintf("%d", *n)
}
spent, measuredZero, unasked := rows[0], rows[1], rows[2]
if spent.ReasoningInCompletion == nil || *spent.ReasoningInCompletion != 8496 {
t.Fatalf("a call that spent its whole budget thinking must say so, got %s", say(spent.ReasoningInCompletion))
}
if measuredZero.ReasoningInCompletion == nil {
t.Fatal("a MEASURED zero must survive the round trip as an answer, not as silence")
}
if *measuredZero.ReasoningInCompletion != 0 {
t.Fatalf("a measured zero must stay zero, got %d", *measuredZero.ReasoningInCompletion)
}
if unasked.ReasoningInCompletion != nil {
t.Fatalf("a row whose provider reported nothing must stay unanswered, got %s", say(unasked.ReasoningInCompletion))
}
}
// TestRowsWrittenBeforeTheThinkingColumnReadAsUnanswered covers the books already on disk, and it does
// so by actually MIGRATING one: the database is built at the schema an older binary wrote, a row is
// inserted through that older INSERT, and only then is the column added. A `DEFAULT 0` would have made
// that row claim its call did not think — a claim nobody measured, about the very runs whose money this
// column exists to explain.
//
// ⚠ THE OLD VINTAGE IS BUILT, NOT SIMULATED. Inserting a nil pointer into a database that already has
// the column proves only that nil writes NULL — it never runs the ALTER over a populated table, which
// is the whole subject. Measured: the earlier version of this test passed with the migration removed.
func TestRowsWrittenBeforeTheThinkingColumnReadAsUnanswered(t *testing.T) {
path := filepath.Join(t.TempDir(), "old.db")
// One vintage back: every migration except the head, recorded exactly the way applyStep records
// them, so the real migrator sees a project that is genuinely behind rather than one pretending.
db, err := sql.Open("sqlite", "file:"+path)
if err != nil {
t.Fatal(err)
}
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY)`); err != nil {
t.Fatal(err)
}
for v := 0; v < len(migrations)-1; v++ {
if _, err := db.Exec(migrations[v]); err != nil {
t.Fatalf("build vintage %d: %v", v+1, err)
}
if _, err := db.Exec(`INSERT INTO schema_version (version) VALUES (?)`, v+1); err != nil {
t.Fatal(err)
}
}
// The column must NOT exist yet, or the fixture is not one vintage back and the test is about
// nothing.
if columnExists(t, db, "request_log", "reasoning_in_completion") {
t.Fatal("the fixture is meant to be ONE VINTAGE BEHIND: request_log already carries the column the head migration adds")
}
// A row written by that older binary: the INSERT it had, without the column.
if _, err := db.Exec(`INSERT INTO request_log (book_id, chapter, chunk_idx, stage, role, completion_tokens, ok)
VALUES ('old', 1, 0, 'edit', 'editor', 16000, 0)`); err != nil {
t.Fatal(err)
}
if err := db.Close(); err != nil {
t.Fatal(err)
}
// The real migrator, over a populated table.
s, err := Open(path)
if err != nil {
t.Fatalf("migrate the old project: %v", err)
}
defer s.Close()
rows, err := s.RequestLogRows("old")
if err != nil {
t.Fatal(err)
}
if len(rows) != 1 {
t.Fatalf("rows = %d, want 1 — the row written before the column must survive the migration", len(rows))
}
if n := rows[0].ReasoningInCompletion; n != nil {
t.Fatalf("a row from before the column must read as unanswered, got %d", *n)
}
if rows[0].CompletionTokens != 16000 {
t.Fatalf("the rest of the row must be untouched, got completion=%d", rows[0].CompletionTokens)
}
}
// columnExists asks the database itself, so the fixture's premise is checked against sqlite rather than
// against what the test author believed the migration list contained.
func columnExists(t *testing.T, db *sql.DB, table, column string) bool {
t.Helper()
rows, err := db.Query(`SELECT name FROM pragma_table_info(?)`, table)
if err != nil {
t.Fatal(err)
}
defer rows.Close()
seen := 0
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
t.Fatal(err)
}
seen++
if name == column {
return true
}
}
if err := rows.Err(); err != nil {
t.Fatal(err)
}
if seen == 0 {
t.Fatalf("table %q has no columns at all — the fixture never built it", table)
}
return false
}

View file

@ -27,13 +27,17 @@ type RequestLog struct {
CacheCreationTokens int
CompletionTokens int
ReasoningTokens int
CostUSD float64
LatencyMS int
FinishReason string
TMHit bool // served from a checkpoint, no call was made
Degraded string
Err string
OK bool
// ReasoningInCompletion is the subset of CompletionTokens the provider reported as thinking
// (llm.Usage.ReasoningInCompletion). nil writes NULL — «the provider reported no such field»,
// which the column keeps distinct from a measured 0.
ReasoningInCompletion *int
CostUSD float64
LatencyMS int
FinishReason string
TMHit bool // served from a checkpoint, no call was made
Degraded string
Err string
OK bool
// Estimated marks a row whose CostUSD is a RESERVATION ESTIMATE, not a provider-reported cost (a billed
// decode failure, or a paid 2xx with zero usage — pack-13 point-9 / research/21 §1.10). DISPLAY-ONLY: it
// never re-derives money (CostUSD is untouched); it makes the estimated share of spend queryable so an
@ -59,13 +63,13 @@ func (s *Store) InsertRequestLog(rl RequestLog) error {
trace_id, book_id, chapter, chunk_idx, stage, role,
model_requested, model_actual, request_hash,
prompt_tokens, cached_tokens, cache_creation_tokens,
completion_tokens, reasoning_tokens,
completion_tokens, reasoning_tokens, reasoning_in_completion,
cost_usd, latency_ms, finish_reason, tm_hit, degraded, err, ok, estimated, est_tokens
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
rl.TraceID, rl.BookID, rl.Chapter, rl.ChunkIdx, rl.Stage, rl.Role,
rl.ModelRequested, rl.ModelActual, rl.RequestHash,
rl.PromptTokens, rl.CachedTokens, rl.CacheCreationTokens,
rl.CompletionTokens, rl.ReasoningTokens,
rl.CompletionTokens, rl.ReasoningTokens, rl.ReasoningInCompletion,
rl.CostUSD, rl.LatencyMS, rl.FinishReason, rl.TMHit, rl.Degraded, rl.Err, rl.OK, rl.Estimated, rl.EstTokens)
return err
}
@ -111,15 +115,18 @@ type RequestLogView struct {
CacheCreationTokens int
CompletionTokens int
ReasoningTokens int
CostUSD float64
LatencyMS int
FinishReason string
TMHit int
Degraded string
Err string
OK int
Estimated int // 1 = CostUSD is a reservation estimate, not provider-reported (pack-13 point-9)
EstTokens int // display-only fertility output-token estimate (pack-13 point-9); 0 unless Estimated
// ReasoningInCompletion is nil when the row has no answer: a provider that reports no such
// field, or a row written before the column existed. A measured 0 is a 0.
ReasoningInCompletion *int
CostUSD float64
LatencyMS int
FinishReason string
TMHit int
Degraded string
Err string
OK int
Estimated int // 1 = CostUSD is a reservation estimate, not provider-reported (pack-13 point-9)
EstTokens int // display-only fertility output-token estimate (pack-13 point-9); 0 unless Estimated
}
// RequestLogRows returns all request_log rows for a book (tmctl report / Phase 0
@ -129,16 +136,21 @@ func (s *Store) RequestLogRows(bookID string) ([]RequestLogView, error) {
return queryAll(s.r, `
SELECT ts, chapter, chunk_idx, stage, role, model_requested, model_actual,
request_hash, prompt_tokens, cached_tokens,
cache_creation_tokens, completion_tokens, reasoning_tokens,
cache_creation_tokens, completion_tokens, reasoning_tokens, reasoning_in_completion,
cost_usd, latency_ms, finish_reason, tm_hit, degraded, err, ok, estimated, est_tokens
FROM request_log WHERE book_id = ? ORDER BY id`,
func(rows *sql.Rows) (RequestLogView, error) {
var v RequestLogView
var ric sql.NullInt64
err := rows.Scan(&v.TS, &v.Chapter, &v.ChunkIdx, &v.Stage, &v.Role,
&v.ModelRequested, &v.ModelActual, &v.RequestHash,
&v.PromptTokens, &v.CachedTokens, &v.CacheCreationTokens,
&v.CompletionTokens, &v.ReasoningTokens, &v.CostUSD,
&v.CompletionTokens, &v.ReasoningTokens, &ric, &v.CostUSD,
&v.LatencyMS, &v.FinishReason, &v.TMHit, &v.Degraded, &v.Err, &v.OK, &v.Estimated, &v.EstTokens)
if ric.Valid {
n := int(ric.Int64)
v.ReasoningInCompletion = &n
}
return v, err
}, bookID)
}

View file

@ -0,0 +1,345 @@
# Бэкенд-пак: КУПЛЕНО — ЗНАЧИТ ДОСТАВЛЕНО. Юнит, который не доделается НИКОГДА
> Выдан оркестратором №23 (сессия `textmachine-11`) 11.09. Пак **$0** — платных вызовов в нём нет.
> Предмет решён владельцем 05.09 (`D39.204`); ты строишь решённое, а не выбираешь между вариантами.
## 1. Какая проблема и что решит твой результат
Читатель заплатил за главу. Движок купил первую попытку, получил флагуемый результат, пошёл за второй — и
**вторую ему не продали**: потолок книги или дня не вмещает резерв регенерации. ⚠ **И тут точность несущая,
первая редакция промта её теряла:** `maxTokensForAttempt` удваивает **только ВЫХОДНОЙ** бюджет, а резерв
считается оценкой по цене, промпту, потолку и буферу размышления (`backend/internal/pipeline/stagerun.go`,
греп `EstimateUSD`) — промпт и аддитивный буфер НЕ удваиваются. Значит «попытка 1 стоит ×2» — неправда, и
окно, в котором отказ неизбежен на ≥1 и невозможен на 0, считается по РЕАЛЬНОЙ оценке.
Дальше происходит вот что, и это проверяется одной строкой: **`backend/internal/pipeline/stagerun.go:204`=`infra failure; the cancelled-position mark`** — ЛЮБАЯ ошибка попытки уходит наверх из цикла, **РАНЬШЕ `UpsertChunkStatus`**.
**Следствия два, и оба стоят денег читателя:**
1. **Юнит не доделается НИКОГДА.** Чекпойнт попытки 0 есть (реплей за $0), строки `chunk_status` НЕТ
читающие модели видят юнит как `pending`, экспорт не отдаёт ничего. Каждый резюм реплеит попытку 0 за
$0 и умирает на попытке 1. **Прогон уходит `exit 4` при ПОЛНОСТЬЮ доставленных купленных юнитах.**
2. **Волну нельзя продолжить без ДЫРЫ в непрерывном префиксе** — то есть за это платят и соседние юниты,
которые в бюджет влезали (разброс резервов внутри волны замерен ×2.976).
**Решение владельца (`D39.204`, и оно НЕ обсуждается):** отказ резервации на РЕТРАЕ **деградирует во ФЛАГ**,
причина которого ОТЛИЧНА от исчерпания регенераций; текст попытки 0 оплачен и **отгружается**; волна идёт
дальше. ⛔ Вариант «минимальная покупка ≥ `step_max_usd`» **СНЯТ** владельцем — он стоил бы покупателю
входного порога в пять глав. Не реализуй его и не предлагай.
**И канон здесь сильнее, чем кажется:** `D2` п.2 уже ТРЕБУЕТ тегировать `flag_reason`. Значит новая
причина — не добавка к контракту, а его ИСПОЛНЕНИЕ. (Ссылка «смена `D2.3`», гулявшая по бэклогу, **неверна
дважды**: `D2` и так кончается флагом, а `D2.3` — про детектор вырождения перед удвоением.)
## 2. Зона записи и git
- Зона — **`backend/`**, и только она. `platform/` не трогать: её половина едет ОТДЕЛЬНЫМ промтом, но
**ОДНИМ АКТОМ с твоим** (см. §4.3).
- **Ты не коммитишь.** Готовое дерево передаёшь мне, я лендлю. `git add -A` запрещён каноном.
- Свою секцию итогов пиши в `docs/PROGRESS.md` — это единственное исключение из «`docs/` — зона
оркестратора». Чужие незакоммиченные файлы в дереве не трогай.
## 3. Карта чтения — ЗАКОН, дальше только по её ссылкам
1. `CLAUDE.md` — целиком.
2. **Тело `D39.204`** в `docs/architecture/05-decisions-log.md` (решение владельца) и **`D2` п.2** — он в
закрытой эре, тело в `docs/archive/architecture/05-decisions-D1-D38.md`, греп `^## D2\.`. ⚠ Подномер
собственного тела не имеет: `D2.3` — это ПУНКТ 3 тела `D2`.
3. **Ряд 291** в `docs/BACKLOG.md` — там весь разбор, включая **опровергнутую посылку прошлого
оркестратора**: «ретрай оставляет ФЛАГОВАННЫЙ юнит» — ЛОЖЬ, зона опровергла по строкам. Не наступи
второй раз.
4. `backend/internal/pipeline/stagerun.go` (цикл попыток и `runAttempt`), `escalation.go` (`errReserveCeiling`
и кто на нём УЖЕ деградирует), `disposition.go` (словарь `FlagReason`).
5. `docs/architecture/12-go-style-notes.md` — нормативы стиля, только больные места.
Код-факты добывай сам; `file:line` выше — отправные точки. **Код первичен.**
## 4. Состав
### 4.1 ⛔ КОНСТРУКЦИЯ ПЕРЕ-РЕШЕНА ВЛАДЕЛЬЦЕМ 11.09 — читай это ПЕРЕД кодом
**Слово владельца дословно:** «Надо о таком просто сообщать в виде метаданты, которая сейчас и пишется
предупреждением, и иметь возможность поднять просто потолок и возобновить прогон, так как **это ситуация
всё же СТАНДАРТНАЯ СО СТОПОМ, просто он произошёл по какой-то другой причине**».
**Это отменяет обе половины прежнего заказа — и моего, и `D39.204` п.4.** Ни «деградировать во флаг», ни
«волна идёт дальше» больше НЕ строится. Прежний план заводил новый исход; новый — **возвращает ситуацию в
УЖЕ ПОСТРОЕННЫЙ штатный путь остановки**, из которого она сегодня выпадает.
**Что это значит механически (моё чтение; спорь, если код скажет иначе):**
1. **Прогон ОСТАНАВЛИВАЕТСЯ, как при любом отказе по потолку.** Не продолжается с флагом. ⭐ И это заодно
снимает то, чем ряд 291 пугал: «продолжить волну нельзя без ДЫРЫ в непрерывном префиксе» — при остановке
дыры не возникает вовсе.
2. **Отказ ретраю публикует ту же метадату, что и любая остановка по потолку** — кадр
`Ceiling{Halted, Scope, ShortfallMicroUSD}` (`backend/internal/runevents/runevents.go`, греп
`ShortfallMicroUSD`). ⭐ **Он уже несёт ровно то, что владелец просит:** «пополни хотя бы на столько, и
тот же вызов будет допущен» — и это **ратифицировано им же, `D39.203`**. Строить новый носитель не надо.
**И твоя находка №2 этим РАЗРЕШАЕТСЯ, а не отменяется:** ты была права, что `ceilingReached`
идемпотентен по прогонному флагу и съел бы событие настоящей остановки — но теперь съедать НЕЧЕГО: эта
остановка И ЕСТЬ настоящая. Публикуй через штатный путь.
3. ⛔ **НАСТОЯЩИЙ ДЕФЕКТ — УЖЕ, чем я написал, и он назван в самом дереве.** Механизм «отметить
остановленную позицию» построен ИМЕННО против невидимых дыр:
`backend/internal/pipeline/stagerun.go:142`=`THE MARK FOR A STOPPED POSITION IS ATTACHED TO EVERY EXIT`,
и его комментарий приводит замер прежнего инцидента — «прогон, остановленный на эскалационном хопе,
рассчитал деньги и не оставил строки вовсе: `committed=0.001176`, `chunk_status_rows=0`». **Но метка —
no-op для всего, что не «доставленный вызов, который остановил человек»** (`recordCancelledStage`).
Отказ по потолку под это не подпадает ⇒ юнит остаётся без строки. **Вот дыра, и вот что ты закрываешь.**
4. **Поднял потолок → резюм ДОДЕЛЫВАЕТ юнит.** Значит строка, которую ты пишешь, обязана быть
НЕ-ТЕРМИНАЛЬНОЙ для резюма — по образцу `FlagCancelled`, единственной причины, у которой
`resolvedForResume` ложен (`backend/internal/pipeline/cutcall.go:293`=`FlagReason(cs.FlagReason) != FlagCancelled`),
и ложен именно потому, что это «оплачено, но не сделано». Твой случай той же природы.
**Делай РОВНО так:** остановка штатная · метадата через уже построенный кадр с недостачей · **юнит получает
строку, а не пустоту** · строка НЕ терминальна для резюма · после пополнения резюм доделывает.
**Решаешь сам и аргументируешь:** как назвать причину в строке юнита (или взять существующую) · где ставится
метка · как отличить «остановлено на ретрае» от «остановлено на попытке 0» в носителе, и надо ли отличать.
**Попытку 0 не трогать:** её отказ и сегодня останавливает книгу штатно — ратифицировано.
**КТО ПЛАТИТ — НЕ ПРЕДМЕТ ЭТОГО ПАКА.** Слово владельца 11.09: «пока платит за всё ЮЗЕР, потом будем
решать эту проблему». Механизм «платит продукт» не строить; существующее биллинговое поведение не трогать
ни в какую сторону, даже где оно выглядит непоследовательным. ⚠ И отдельной строкой отчёта НАЗОВИ (не чини):
объёмный леджер сегодня считает флагованный юнит НЕ доставленным, то есть грант за него не съеден — это
расходится со словом владельца и существует ДО твоего пака. Он объявил вопрос отложенным.
**Отгрузка обрезанного текста не строится**`D2` запрещает отгружать загрязнённое, и это не зависит от
того, кто платит.
### 4.2 Пин, ради которого пак существует
**«ОСТАНОВКА, КОТОРАЯ ЛЕЧИТСЯ ПОПОЛНЕНИЕМ».** Три утверждения, и они про разное:
1. **Юнит, на ретрае которого отказал потолок, ПОЛУЧАЕТ СТРОКУ.** Сегодня её нет — это и есть невидимая
дыра. Пин обязан печатать величину, доказывающую, что деньги были потрачены, а строка появилась: пустой
`chunk_status` рядом с ненулевым `committed` — ровно тот замер, которым дерево описывает прошлый
инцидент.
2. **Остановка несёт НЕДОСТАЧУ.** `ShortfallMicroUSD` ненулевой и таков, что пополнение на него допускает
тот же вызов. ⚠ Пин на «событие вообще есть» вырожден — утверждай ЧИСЛО.
3. **Пополнил потолок → резюм ДОДЕЛАЛ юнит.** ⛔ **И это ГАРД, а не доказательство лечения — поправка
исполняющей сессии, принята.** Моё «сегодня второй прогон умирает вечно» НЕВЕРНО: строки у юнита нет ⇒
гейт резюма не срабатывает ⇒ позиция ре-атакуется, попытка 0 реплеится из чекпойнта за $0, ретрай
покупается, и при поднятом потолке юнит доделывается **уже сегодня, без пака**. Вечно умирает только
резюм БЕЗ пополнения. ⇒ этот пин стережёт ратифицированное `D4`, чтобы пак его не сломал, и **на дефекте
он ЗЕЛЁН**. Так и напиши.
**И отсюда норма приёмки этого пака, которой у нас ещё не было ни в одном:** сними ЦВЕТ КАЖДОГО из трёх
пинов на дереве **ДО** фикса, числом, и положи замер в отчёт. На дефекте краснеет только первый. «Пин
зелен» и «пин стережёт» — разные утверждения, и без этого замера они сливаются.
**Про коды выхода.** Прежняя редакция промта требовала `exit 2` вместо `exit 4`. **Снято:** при штатной
остановке код остаётся тем, каким он и был для остановки по потолку. Предмет пака — не смена кода выхода, а
то, что за ним стоит: **строка вместо пустоты и лечимость пополнением.**
**Два гейта поймают тебя сами — назову, чтобы не терять круг:** `TestEveryFlagReasonIsRanked` потребует
завести новую причину в `flagSeverity` (`backend/internal/pipeline/status.go`), если ты её заводишь, а
`TestEveryOperatorMessageIsCatalogued` — внести новое сообщение в
`backend/internal/pipeline/testdata/operator-messages.txt` (сегодня в каталоге 134 строки, из них о потолке
уже говорят четыре — посмотри их прежде, чем писать пятую).
### 4.3 ⛔ ДЕФЕКТ, КОТОРЫЙ ТВОЙ ПАК НАКОРМИТ — и он НЕ латентный. Чинить ЗДЕСЬ
Нашла исполняющая сессия, посылку я пере-проверил и **она у неё неверна в СТОРОНУ МЯГКОСТИ**.
`backend/internal/pipeline/quality.go` (греп `draftEchoRecovered`) считает эхо ВОССТАНОВЛЕННЫМ по одному
признаку — `FirstFlagReason == cjk_artifact`, — **не спрашивая, флагована ли строка сейчас и чем**. Денежный
отказ эхо-рероллу даст ровно эту картину: первый флаг — эхо, итоговый — денежный, реролла не было, а отчёт
скажет «восстановлено».
**Сессия назвала это латентным «потому что ключ 0». Ключ НЕ ноль:** `regenerate_echo_before_escalate: 1`
стоит **во всех ЧЕТЫРЁХ боевых конфигах** (`backend/configs/pipeline-c1.yaml`, `-c2`, `-arm-glm`,
`-arm-mistral` — сверено мною по каждому). ⇒ дефект оживает **в день лендинга твоего пака**, а не когда-то
потом. **Чини его в этом паке и пинь** — «диспозиция в отчёте» здесь недостаточна.
### 4.4 Платформенная половина — **твоё дело назвать, не построить**
Карта заметок платформы **РУКОПИСНАЯ** (`platform/internal/ingest/notes.go`) и уезжает независимо от
словаря движка. Цена несделанного **замерена**: `PD-246` — минор 0.10.0 сутки жил ЛОЖНЫМ на проводе, причина
доезжала читателю как `unspecified`, и поймала это зона своей пере-проверкой ПОСЛЕ слов оркестратора
«приёмка закончена».
**отдельным пунктом отчёта** назови мне: точное значение новой причины на проводе · что платформа обязана
записать прогону, который кончился флагом вместо `exit 4` · какой её пин это стережёт. Я передам это
платформенной сессии, и **акт будет один на две половины**.
**И следствие, которое стоит назвать сразу, потому что оно про деньги читателя:** объёмный леджер считает
флагованный юнит НЕ доставленным (`backend/internal/pipeline/volume_test.go`, греп
`TestAFlaggedUnitIsNotReportedAsDelivered`). Значит грант за него не съеден, а книга честно короче — это
хорошая новость, но она обязана быть СКАЗАНА в отчёте, иначе следующая смена откроет её заново.
### 4.5 Приложение — ДВА пина, краснеющих по УДАЧЕ (ряд 379, ОБЕ половины)
Отдельная от §4.1 работа; делай её ПОСЛЕ основной. ⛔ **И мой диагноз здесь был НЕВЕРЕН — опровергнут
замером, а не мнением. Читай, чем именно, иначе повторишь мою ошибку.**
**Половина (б).** Запись каталога `CUTCALL-a-refusal-pays-when-the-reply-outruns-the-write` стережёт самую
дорогую половину денежного бита `delivered()`
(`backend/internal/llm/attemptcut.go:189`=`answered && t.firstByte.Load()`): провайдер ОТКАЗЫВАЕТ и рвёт
коннект, пока наше тело ещё пишется — байт ответа есть, статуса в руках нет, и чтение «байт = доставка»
оплачивает сметой каждый отказ (замер прежнего пака: 22 отказа из 25, один на $0.80).
**Два независимых замера на посадке (снятие `answered &&`), и они расходятся:** мой — **7 красных из 8**
(снят на нагруженной машине, рядом шла батарея); опровергателя на спокойной — **8 из 8**, а на сорока
процессах **39 FAIL / 1 PASS**. ⇒ пин краснеет по УДАЧЕ, это подтверждено обоими; частота зависит от
нагрузки, и поэтому «восемь красных подряд» **не является критерием приёмки**: сегодня, без единой правки,
восьмёрка выпадает примерно в 82 случаях из 100.
**А вот ПРИЧИНА не та, что я написал.** Я заказывал «сделать приход байта ответа НЕИЗБЕЖНЫМ до обрыва».
Зонд опровергателя (40 итераций, печать того, что реально получил клиент) дал **39 × обрыв с отказом
записи и 1 × ПОЛНЫЙ `401`**, и ни разу — «ни куска, ни статуса». То есть байт приходит практически всегда,
а мутант выживает в ДРУГОМ случае: клиент успевает вычитать `401` ЦЕЛИКОМ, `Do` возвращает не ошибку
записи, а ОТВЕТ, и `delivered()` не спрашивают вовсе — ошибка уходит статусной веткой. **Моё лекарство
било по половине, которая и так держится, и увеличивало вероятность ровно того исхода, в котором мутант
выживает.** (И «~6 МБ тела» тоже неверно: 11.0 МБ — Go не эскейпит кириллицу в JSON.)
**Задача — делай РОВНО так в части ЧТО, способ твой:**
1. **Тест утверждает СВОЮ ПОСЫЛКУ и печатает её:** что `Do` вернул ошибку ЗАПИСИ и что трасса видела
первый байт. **Сценарий не состоялся ⇒ тест КРАСНЫЙ**, а не зелёный «мимо предмета». Сегодня он зелен и
на полном `401`, то есть на другом предмете.
2. **Прямой табличный пин на сам `delivered()`** — он убивает посадку `answered &&` детерминированно, без
гонки вообще.
3. ⚠ У фикстуры `MaxAttempts: 3` — до трёх бросков кости на один `Complete`; **границу утверждай ПО
ПОПЫТКЕ**, а не по итогу вызова.
**Половина (а) — её первая редакция промта не заказывала вовсе.** Тот же ряд 379 несёт вторую половину:
две строки таблицы девяти в `backend/internal/pipeline/cutcall_test.go:402`=`time.Sleep(50 * time.Millisecond)`
разводятся ОКНОМ СТЕННЫХ ЧАСОВ, и по какую сторону границы заголовков сел `cancel()`, не утверждается
ничем — замер приёмки дал **2 красных из 8**. **Ряд закрывается ТОЛЬКО вместе с этой половиной.**
**Критерий приёмки берётся из самого ряда 379, он сильнее моего:** «каждый из двух пинов **ПЕЧАТАЕТ
величину, доказывающую пройденную границу, и ПАДАЕТ, когда сценарий не состоялся»**.
## 5. Где этот пак мягкий — четыре места, назвал я, веер и глубину выбираешь ты
Мандат самопроверки — **исполнением**; субагенты разрешены явно. Адверсариальный проход по СВОЕЙ ГОТОВОЙ
работе обязателен.
1. ⛔ **Пин, утверждающий только молчание, вакуумен ровно там, где сообщение ложно.** «Ошибка не вышла
наверх» — утверждение о молчании. Фикстура обязана быть такой, где новая причина ОБЯЗАНА прозвучать, и
тест утверждает её ИМЯ.
2. ⛔ **Фикстура обязана сделать отказ резервации НЕИЗБЕЖНЫМ на попытке ≥1 и НЕВОЗМОЖНЫМ на попытке 0**
и окно считается по РЕАЛЬНОЙ оценке резерва, а не по «×2» (см. §1).
Денежный пин, флейковый на мутанте, измеряет пустой сценарий: у нас уже был пин, дававший 2 красных из
8, потому что деньги были ВЕРОЯТНЫ, а не неизбежны. Гоняй свой не один раз.
3. ⛔ **Посадка обязана бить в тот же слой, что стережёт пин.** За прошлый пак мутация трижды оказалась не
про то: недостижимое условие в фикстуре · не собирающаяся посадка (НЕизмеренная — хуже выжившей) · код
против гейта над данными.
4. ⛔ **Новая причина — факт ПРОВОДА, а не словаря.** Тест, проверяющий только, что константа добавлена в
`FlagReason`, не доказывает, что читатель её увидит. Скажи, чем предъявлена дорога от флага до
отчёта/экспорта.
⚠ И общее, стоившее трёх смен подряд: **зелёная батарея плюс полный мутационный каталог сходимостью НЕ
являются.** В последних трёх паках направленный второй читатель находил 6, 7 и 9 дефектов при полной зелени.
### ⭐ Адверсариальная стойка — способ смотреть, не правило пака
- ⛔ **Автор и ревьюер — разные роли, даже когда это один ты.** Перечтение своей работы рубежом не
считается: поднимай читателя, которому НАЗВАНО, где мягко.
- ⛔ **Спрашивай у аномалии, о ЧЁМ она — о предмете или о твоём приборе.**
- ⛔ **Верный результат при неверном методе не краснеет нигде.** «Сошлось» — не доказательство.
- ⛔ **Утверждение о молчании вакуумно** без фикстуры, где оно ОБЯЗАНО прозвучать.
- ⛔ **Заимствованное число проверяется не на существование, а на ТУ ЛИ КЛЕТКУ:** та же роль, та же модель,
та же стадия, тот же режим?
- ⭐ **Главный вопрос отчёта — не «что не получилось», а «что ты знаешь и не сказала».**
### ⭐ Находки ВНЕ заказа — отдельный заказ владельца
Слово владельца 11.09: **находить проблемы даже там, где мы не ждём.** Задавай себе в каждой фазе вопрос, у
которого нет заранее известного ответа: **что здесь стоит дороже, работает хуже или ведёт себя страннее,
чем должно бы, — и о чём никто не спрашивал?** Ради чего он заведён: удорожание из-за падающих запросов на
НАШИХ настройках никто не искал — просто кто-то посмотрел в леджер без гипотезы и увидел 25.4 % цены книги
в выброшенных попытках, при зелёных тестах и молчащих гейтах.
**Докладывай отдельной секцией: криты · мажоры · регрессии · баги · жёсткие точки улучшения** — каждое
аргументированно, с носителем (`file:line`, число, команда) и с ценой в деньгах, тексте или доверии
читателя. ⚠ Секция обязана быть непустой ИЛИ нести строку «искал вот так, не нашёл» с перечислением мест:
«находок нет» и «не смотрел» в отчёте выглядят одинаково.
## 6. Предметные оси ревью
Выбери 13 и назови какие. Мой приор: **деньги под гонкой** · **терминальное состояние прогона** (что
видит платформа и что видит человек) · **обратная совместимость словаря причин**.
## 7. Записка-план ДО работы
Перед первой правкой — записка: что меняешь, чем предъявишь, где ждёшь сопротивления. Комплектность против
заказа сверяй механически.
## 8. Заявление = команда
Каждое число и каждая категорика отчёта — **с командой, которой получены**. Дифф `^func Test` — **исполнением,
не памятью**. Приёмка пере-снимает.
⛔ **И три нормы кодового пака, каждая ловит свой класс:**
- **перед отчётом сверь КАЖДЫЙ клейм с результатом инструмента ЭТОЙ сессии** — не с памятью о прогоне;
- **последний абзац отчёта оказался планом или обещанием? Сделай его СЕЙЧАС** — обещание в отчёте не
переживает смены;
- **в длинной сессии — интервальная самоверификация субагентом против ЯВНЫХ критериев** (`D39.121`), а не
в конце: проверка, отложенная до сдачи, проверяет уставшую работу уставшим взглядом.
## 9. Эхо-протокол старта
**Порядок двух первых действий:** сперва впиши свой блок в `/tmp/textmachine-channel` (§12), потом отправь
эхо — эхо без блока некуда адресовать.
**Эхо** — ≤10 строк СВОИМИ словами: что понял · что считаешь опасным · что считаешь неверным.
Не пересказ: дословный пересказ подтверждает канал, но не понимание, и ошибку промта повторяет вместе с
ним.
**Адрес бери из `/tmp/textmachine-channel`** — блок с `role=оркестратор`, — а не из этого промта: имя сессии
не переживает рестарт окружения. Перед отправкой сверься с `ListAgents`: файл переживает смерть сессии, а
`ListAgents` — нет.
## 10. Что НЕ удалось — обязательная секция отчёта
И вторая её половина: **«где прибор слеп и я это знаю»** — что невидимо · почему не чинил · чем
закрывается. ⭐ Норма прошлой смены: **«в коде названо, в отчёте нет — значит для следующей смены НЕ
названо».**
## 11. Канал вопросов и право отказаться
Конфликт промта с кодом или доками — **пинг мне, не интерпретация**. Право сказать «этого делать не надо» с
аргументом у тебя есть и им пользовались: в двух случаях из трёх правы были сессии, а не я.
**Тесты и гейты под зелень не подгонять.** Правка, вызванная ЗАКАЗАННОЙ сменой поведения, — обслуживание,
и протухший тест держать не нужно; но она **ОБЪЯВЛЯЕТСЯ** в отчёте: что изменилось, какой тест это
описывал, куда уехала гарантия.
### ⭐ Старший коллега — Fable 5. **Разрешён ОДИН на сессию (слово владельца 11.09)**
У тебя есть право поднять **одного** агента на модели **`fable`** и держать его как СОВЕТЧИКА при сомнениях —
тем же способом, каким это делает оркестратор. Владелец разрешил ровно одного на сессию.
**Как с ним работать, и форма здесь важнее числа:**
- **Поднимаешь ОДНОГО и держишь его весь пак.** Ценность этого агента — в НАКОПЛЕННОМ контексте смены:
он помнит, что ты уже решил и почему. ⛔ **Второго не поднимай** — вопросы ДОСЫЛАЮТСЯ первому (`SendMessage`
по его id), а не адресуются свежему. Свежий агент вместо накопленного — потеря именно того, ради чего он
заведён.
- **Модель задавай ЯВНО** (`model: "fable"`), иначе она унаследуется и ты не будешь знать, что у тебя
работает.
- **О чём его спрашивать:** сомнение в решении · развилка, где оба пути выглядят законными · «не заказываю
ли я уже построенное» · «не противоречит ли это ратифицированному» · спорная формулировка в отчёте.
**Спрашивай С КОНТЕКСТОМ и со СВОЕЙ рекомендацией** — вопрос без твоего варианта ответа даёт совет ни
о чём.
- ⛔ **Он советчик, а не источник истины: его ответ проверяется деревом.** У оркестратора он за смену
ошибся дважды на фактах (называл платный прогон бесплатным, объявлял ряд новым предметом) — и оба раза
его ВЫВОД оставался верным. Проверяй посылки, принимай выводы по существу.
- **Что он дал оркестратору за эту смену** — для калибровки, чего от него ждать: поймал, что я собирался
завести новый ряд бэклога под предмет, у которого ряд уже был; развернул порядок паков доводом, который
я не назвал; и назвал условие, без которого два прогона стали бы несравнимы.
## 12. Прямой канал
Механизм — `CLAUDE.md` §«Связь между сессиями». Впиши свой блок в `/tmp/textmachine-channel` ПЕРВЫМ
действием. Нужной роли нет ⇒ канала нет, и это нормальный случай: НЕ опрашивай сессии подряд.
## 13. Критерий завершённости
У каждого пункта заказа — исход (сделано · не делаю с доводом · пинг) · круги СОШЛИСЬ (последний не дал
НОВЫХ находок; прежние закрыты таблицей «находка → что сделано → ЧЕМ ПРЕДЪЯВЛЕНО») · **таблица мутаций
полная, выжившие названы, и КАЖДАЯ новая запись помечена `battery`** (прошлый пак этого не сделал, и его
«21/21 RED» оказался разовым прогоном смены, а не гейтом проекта) · числа сняты ПОСЛЕ последней правки ·
всё живое в ДЕРЕВЕ, а не в письме · явное **«работа завершена, править не планирую»**. Без последнего пак
считается идущим.

View file

@ -0,0 +1,305 @@
# Бэкенд-пак: В БАНК ПО РАЗНОБОЮ — впустить термин, который черновик уже написал двумя способами
> Выдан оркестратором №23 (сессия `textmachine-11`). Пак **$0** — платных вызовов в нём нет.
> Предмет куплен двумя платными прогонами; разбор состава банка сделан старшим коллегой по коду.
## 1. Какая проблема и что решит твой результат
Приоритет №1 владельца — **консистентный перевод длинной книги**. Его несёт банк памяти. И вот что мы
узнали о банке двумя прогонами одной и той же книги:
**Состав банка НЕ ПОВТОРЯЕТСЯ.** Побайтно один исходник, тождественный покупающий конфиг (sha
`pipeline-c1.yaml` совпал), одни и те же шесть чанков — предложений банка **A 98 · B 93 · общих 74,
Жаккар 0.63**. Расходится и состав, и ГРАНУЛЯРНОСТЬ ключа: один прогон предлагает `丙等资质` ·
`族长家老` · `舅父舅母`, другой — их составляющие. `舅父舅母` встречается в срезе **14 раз**, есть в
банке A и отсутствует в B.
⛔ **А ГДЕ ИМЕННО он не повторяется — теперь ИЗМЕРЕНО, и это главный результат двух платных прогонов:**
пересечение детерминированного и стохастического каналов (`both`) даёт **4 поверхности, Жаккар 1.00 —
идеально воспроизводимо**; банкнотный канал даёт 65 и 62 при Жаккаре **0.63**; а строк, предложенных
ТОЛЬКО детерминированным майнером, **НОЛЬ в обоих прогонах**. ⇒ **детерминизм не сломан — его 6 %, а
уникальный вклад майнера пуст.** ⚠ На СТРОКЕ банка этот провенанс теряется (все 69 и 66 несут
`source = "mined"`), и живёт он только в сайдкаре стопа.
**А собирается банк из ТРЁХ источников, и два из них стохастические** (прочитано старшим коллегой по
коду, никем прежде): детерминированный WHICH-майнинг по исходнику против контраста · **банкноты,
собранные ЧЕРНОВЫМИ волнами** (выход модели при `temperature > 0`) · **LLM-пасс терминолога**
(консолидация, классификация, род) — и именно он делает гранулярность.
**Твой результат решает вот что: сегодня нельзя ответить на вопрос «какая доля банка воспроизводима», и
поэтому нельзя ни починить недетерминизм, ни осознанно его принять.**
⛔ **И сразу — чего этот пак НЕ делает, чтобы ты не построила лишнего:**
- **НЕ запирает состав банка в снапшот.** Воспроизводимость там, где она нужна, УЖЕ построена:
`memory_version` входит в снапшот редакторской волны, резюм того же прогона банк не двигает. А
популяцию ПРЕДЛОЖЕНИЙ свежего прогона подписывает человек — ратифицировано владельцем (`D39.144`:
«черновик и черновой банк появляются вместе → один ОК всему банку»). **Запереть предложение = отменить
майнинг.**
- **НЕ строит второй путь кандидата в банк.** Он напрашивается — центральный терм книги `族`
(**29 вхождений**) не попал в банк НИ В ОДНОМ прогоне, и «род ↔ клан» есть крупнейшая находка вычитки.
Но ПОЧЕМУ он выпал — **не установлено**, и у гипотезы есть конкурент с носителем (см. §4.2).
- **НЕ трогает деньги и усилие моделей.** Это отдельный предмет, и он упирается в платный замер.
## 2. Зона записи и git
- Зона — **`backend/`**, и только она.
- ⛔ **ВНИМАНИЕ, ДЕРЕВО НЕ ПУСТОЕ:** незакоммиченные файлы в `backend/` — это СДАЧА ПРЕДШЕСТВЕННИКА
(пак «купленное доставлено»), и она **в зоне твоего пака**. Это НЕ «чужая незакоммиченная работа», от
которой канон велит держаться: её сессия закончила, пак закрыт актом, лендинг мой. Сверься со мной
пингом ПЕРЕД первой правкой в `cutcall.go`, `disposition.go`, `status.go` — я скажу, заландено ли.
- **Ты не коммитишь.** Готовое дерево передаёшь мне, я лендлю. `git add -A` запрещён каноном.
- Свою секцию итогов пиши в `docs/PROGRESS.md`. Чужие незакоммиченные файлы в дереве не трогай.
- ⚠ **`/tmp` на этой машине занят на 9697 %** (tmpfs, 5.9 ГБ). Забитый tmpfs маскируется под сломанную
сборку. Уводи `TMPDIR`/`GOTMPDIR` на диск и печатай `df -h /tmp` рядом с итогом любого прогона.
## 3. Карта чтения — ЗАКОН, дальше только по её ссылкам
1. `CLAUDE.md` — целиком.
2. **Тела `D39.144`** (подпись банка целиком — слово владельца) и **`D39.252`** (акт прогона B) в
`docs/architecture/05-decisions-log.md`, плюс **эррата 12.09-а** в шапке того же файла.
3. **Ряды 439 и 440** в `docs/BACKLOG.md` — там оба замера с числами и границами.
4. `backend/internal/pipeline/mining.go` — сборка банка: греп `MineBankStats`, `buildBankCandidates`,
`runTerminologist`, `Source:`.
5. **Ряд 223** в `docs/BACKLOG.md` (потолок эмиссии майнера) — он несёт конкурирующую гипотезу §4.2.
Код-факты добывай сама; `file:line` — отправные точки. **Код первичен.**
## 4. Состав
### 4.1 ⛔ ЗАМЕР ВПЕРЕДИ СТРОЙКИ: во что обойдётся приём кандидата ПО РАЗНОБОЮ. **$0, и до него код не трогаешь**
**Предмет пака — впустить в банк термин, который черновик УЖЕ написал двумя способами.** Сигнал для этого
считается и лежит в дереве: `Spread` — дословно «the disagreement signal: how many DISTINCT renderings the
drafts produced» (`backend/internal/terminology/terminology.go`, греп `func (c Candidate) Spread`). Рядом
живёт `Conventions` — тот же счёт ПОСЛЕ свёртки форм, различающихся только регистром/ё/пробелами.
⛔ **И в отборе кандидата не участвует НИ ОДИН из них: ноль упоминаний в `miner_emit.go` и
`miner_detect.go`.** Мы вычисляем нужный признак, показываем его человеку и не пускаем в решение.
⛔⛔ **НО СКОЛЬКО КАНДИДАТОВ ОН ВПУСТИТ — НЕИЗВЕСТНО, И ЭТО ГЛАВНЫЙ РИСК ПАКА.** Я посчитал по сайдкару
стопа прогона B: из 66 термов `spread ≥ 2` у **восьми (12 %)**, и `conventions` дал ровно то же
распределение (58/4/3/1) — то есть на этой книге свёртка форм не изменила ничего. ⚠ **Но это счёт по
ПРОШЕДШИМ кандидатам, а рост кардинальности зависит от ОТВЕРГНУТЫХ**, и их мы не сохраняем. **Число,
которое решает вопрос владельца, в уликах отсутствует.**
**ПЕРВАЯ РАБОТА ПАКА — получить это число, и только потом решать.** Делай РОВНО так:
1. **Сухой прогон сигнала по ЧЕРНОВИКАМ обоих платных прогонов** — материал заланден:
`/home/ubuntu-26/tm-coldrun-a/stand/books/bk_ROEHZBD46ALFI43E/project.db` (glossary 69) и
`/home/ubuntu-26/tm-coldrun-b/stand/books/bk_NH6275ZIRFFTLZHG/project.db` (glossary 66).
**НЕ бери `books/gu-zhenren/door-to-file-b/bankstop/project.db`** — там `glossary` пуст (0 строк), и
твой замер даст ноль, неотличимый от успеха. **Рядом с каждым нулём печатай контрольную величину.**
2. **Напечатай таблицу приёма при порогах** `spread ≥ 2`, `≥ 3`, и то же по `conventions` — **со
знаменателем: сколько поверхностей рассмотрено всего**. Отдельной строкой — сколько из впущенных НЕ
прошли бы нынешний тип-фильтр (то есть сколько из них обычные существительные, ради которых всё
затевается).
3. **Назови, что случится с `род`/`клан`** (`家族`, 9 вхождений; `族`, 29) — попадут ли они, и при каком
пороге.
**Если приём выходит за десятки на трёхглавом срезе — СТОП И ПИНГ МНЕ.** Владелец назвал взрыв
кардинальности главным риском, и решение о пороге — его, а не твоё и не моё.
### 4.2 ПРАВИЛО ПРИЁМА — **алгоритм проектируешь ТЫ, и вот всё, что я знаю о ловушках**
**Решаешь сама и аргументируешь:** на каком признаке принимать (`Spread`, `Conventions`, их сочетание с
частотой), какой порог, и КАК это встраивается — новым признаком в `emissionEligible` или отдельной
дверью рядом.
⛔ **ТИП-ФИЛЬТР — вот где сегодня стоит стена, и её надо назвать явно.**
`backend/internal/miner/miner_emit.go`, функция `emissionEligible`: **первым** условием
`if !hasAnyType(c.Types, "name", "place", "title") { return false }`, **вторым**
`c.Freq < emitMinFreq || subsumed[c.Src] || runeLen(c.Src) < 2`. ⇒ `族` отсечён **безусловно как
одноруний**, а `家族`**типом** (2 руны, freq 9 при пороге 5, то есть проходит всё остальное).
**Детерминированный канал по построению эмитирует только имена, места и титулы.** Обычные термины в
банк попадают — в прогоне B их **30 из 66**, — но ИСКЛЮЧИТЕЛЬНО через банкноту, то есть через то, что
черновик сам решил назвать.
⛔⛔ **ШЕСТЬ СПОСОБОВ, КОТОРЫМИ ЭТА СТРОЙКА МОЖЕТ ОБЕРНУТЬСЯ ПРОТИВ НАС. Владелец спросил о них прямо;
каждый обязан получить ответ в отчёте — замером, а не мнением.**
1. ⛔ **САМОЕ ЕДКОЕ: приём ПО ЧЕРНОВИКУ делает банк БОЛЬШЕ зависимым от стохастики, а не меньше.**
Мы чиним недетерминизм банка — а признак приёма берём из выхода модели при `temperature > 0`. Два
прогона одной книги дадут разный `Spread` ⇒ разный приём ⇒ разный банк. **Скажи, насколько:** посчитай
приём по обоим прогонам и дай пересечение. Если оно низкое — правило усиливает ровно ту болезнь,
которую лечит, и это надо знать ДО стройки.
2. **Морфология съест порог.** Русский склоняет: «род / рода / роду» — одна передача в трёх формах.
`Spread` считает СЫРЫЕ формы, `Conventions` — свёрнутые, и разница между ними и есть цена стеммера.
На прогоне B они совпали, но стеммер снимает ровно ОДНО окончание, и на другой книге совпадут вряд ли.
3. **Кардинальность стоит МЕСТА В ПРОМПТЕ.** Банк инъектируется редактору; больше строк — либо дороже
каждый вызов, либо вытеснение других строк, и вытеснение **молчит**. Найди, есть ли кап инъекции, и
что происходит при переполнении: усечение без предупреждения — это тихая потеря именно тех терминов,
ради консистентности которых банк существует.
4. **Каждый впущенный кандидат требует `dst`**а его даёт LLM-пасс терминолога. Больше кандидатов =
больше платных вызовов на стадии, где на прогоне B **97.6 % выхода ушло в думанье, а 2 вызова из 6
провалились**. Назови, во сколько вызовов обойдётся приём при твоём пороге.
5. **Человек подписывает банк ЦЕЛИКОМ** (`D39.144`, слово владельца: «один ОК всему банку»). Чем больше
строк, тем больше непросмотренного покрывает один ОК. Это не техническая цена, а продуктовая, и её
надо НАЗВАТЬ, а не решить.
6. **Обратная связь.** Банк инъектируется в черновик следующих глав ⇒ меняет их `Spread` ⇒ меняет приём.
Правило влияет на собственный вход. Скажи, видишь ли ты здесь расходящуюся петлю.
**Строить правило БЕЗ ответов на 1 и 3 нельзя** — это два места, где стройка «оборачивается против
нас» буквально. Остальные четыре назови и оцени.
### 4.2а ПРОВЕНАНС СТРОКИ — **доноси СУЩЕСТВУЮЩЕЕ, не проектируй новое**
Провенанс кандидата в дереве **есть**: `terminology.Origin` = `mined | banknote | both | alias`
(`backend/internal/terminology/terminology.go`, греп `OriginMined`), он печатается в таблицу стопа и
уходит в батч терминолога. **Теряется он на персистентной строке банка.** ⇒ доноси ЭТОТ словарь, второй
не заводи.
⭐ **И вот что он уже дал, посчитанный мною по сайдкарам обоих прогонов — это первое число пака, а не
следствие:** канал `both`**4 и 4 поверхности, Жаккар 1.00**; канал `banknote` — 65 и 62 при Жаккаре
**0.63**; строк «только майнер» — **0 и 0**. ⇒ детерминированная часть воспроизводится идеально, а
уникальный вклад майнера пуст.
⛔⛔ **ЗАПРЕТ, НАРУШЕНИЕ КОТОРОГО СТОИТ ПЕРЕ-ОПЛАТЫ КНИГ.** `Source == "mined"` — денежный дискриминатор
в **21 не-тестовом месте**; на нём стоит `excludeMined`, отделяющий БАЗОВЫЙ банк от обогащённого
(`backend/internal/membank/memory.go`), а базовый сворачивается в снапшот ЧЕРНОВОЙ волны. Новое ЗНАЧЕНИЕ
в `source` пройдёт мимо фильтра → майненые строки протекут в базовый банк → `baseMemoryVersion`
сдвинется → **черновая волна пере-оплатится по всей книге**. ⇒ провенанс едет **ОТДЕЛЬНЫМ полем**;
`source` не трогается; ⛔ **и новое поле НЕ добавляется в фолд `ComputeVersionScopedIn`** — он двигает
`memory_version` каждой книге.
**Пины обязательны:** значения `source` не изменились · `excludeMined` исключает ровно то же множество ·
`baseMemoryVersion` после правки НЕ сдвинулся.
⚠ И шов, о котором надо знать: строка попадает в БД не напрямую, а через YAML подписной карты →
подпись человека → `ParseEngineBankSeed`, где `source` штампуется заново. **Провенанс обязан пережить
круг через документ, который правит рука.** Плюс комментарий схемы (`backend/internal/store/migrate.go`,
греп `provenance:`) обещает значения `seed|ruby|auto`, а пишется `mined` — почини вместе с предметом.
### 4.3 `waves.workers` — **делай РОВНО так**
Прогон B шёл **строго последовательно**: сумма латентностей 18.5 мин при стенных 19.3, перекрытий ноль.
Причина — `Waves.Workers` **по умолчанию 1** (`backend/internal/config/pipeline.go`, греп
`Wave workers default to 1`), и в боевом конфиге ключ **не задан вовсе**. Комментарий у дефолта называет
это «wave-structured but sequential, deterministic run» — **но ратификации у этого нет**, а исполнитель
СТРОИЛСЯ под N: `waverun.go` (греп `result ORDER is deterministic`) прямо говорит, что порядок
результата детерминирован по индексу элемента **независимо от порядка завершения**.
**Что делаешь:** выставляешь `waves.workers` в боевых конфигах — **но число берёшь НЕ с потолка.**
`max_concurrency` задан у **ОДНОЙ модели из десяти** (`models.yaml`, греп `MaxConcurrency`; ноль =
без ограничений), и для deepseek его НЕТ. **Кап берётся из вендорской доки** — норма квирков «идём в
официальную доку, не гадаем», — а не назначается. Пин над боевыми конфигами: **`workers ≤ max_concurrency`
модели стадии**; нет капа у модели — нет и числа воркеров больше единицы, и это пинится.
**И ПИНИШЬ БАЙТ-ИДЕНТИЧНОСТЬ — но у неё ПЯТЬ способов выродиться, а не один.** Я назвал только первый;
остальные четыре дал старший коллега, и каждый делает пин пустым:
1. **Один элемент волне** — единица параллелится сама с собой.
2. **Порядок завершения не разошёлся** — четыре воркера над МГНОВЕННЫМ фейком заканчивают в порядке
старта. Фикстура обязана ЗАСТАВИТЬ порядок разойтись (фейк с латентностью, обратной индексу) и
**напечатать фактический порядок завершения**.
3. ⛔ **Не тот артефакт.** `request_log`, `events_outbox.seq`, `updated_at` расходятся ЗАКОННО при
конкуренции. Пин обязан НАЗВАТЬ множество сравниваемого: чекпойнты по `request_hash` · `chunk_status`
(диспозиция и финальный хеш) · экспорт побайтно · свёртка банка · `retrieval_state` · снапшот-ид — с
нормализацией сортировкой, по прецеденту `backend/internal/pipeline/golden_test.go` (греп там же).
⚠ И «снапшот идентичен» — тривиально: он считается из конфига ДО волны. Несущее — чекпойнты и финалы.
4. **Одна волна вместо двух** — единицы редактора многочанковые, и разойтись может именно там.
5. ⛔ **Пин не мутирован.** Посади зависимость от порядка (общий счётчик в колбэке либо свёртку банкнот по
порядку завершения) и покажи, что пин краснеет **ТЕКСТОМ про предмет**. Иначе он утверждает молчание.
**Гонки под `workers > 1` у нас проверяются** — 14 тестов задают больше одного воркера, 11 из них
четыре, пакет под `-race` чист (пере-снято мною 12.09). Но **ни один живой прогон с N > 1 не шёл**, и
это надо сказать в отчёте прямо.
### 4.4 ДЕНЕЖНАЯ ПОСЫЛКА, КОТОРАЯ ПЕРЕСТАЛА БЫТЬ ВЕРНОЙ — **делай РОВНО так**
`backend/configs/models.yaml`, блок `deepseek-v4-pro`: комментарий утверждает, что **«перебор бюджета
БЕСПЛАТЕН — резервация транзитна, списание идёт по фактическому usage»**. ⛔ **Это ложно с `D39.230`:**
оборванный вызов книжится **ПО ОЦЕНКЕ** (`backend/internal/pipeline/cutcall.go`, греп
`settleUSDForCutCall`), а оценка **пропорциональна `max_tokens`**. На этой посылке стоит решение «флор с
запасом, а не впритык» — и стоит теперь неверно.
**Что делаешь:** исправляешь текст посылки и **называешь в отчёте**, меняет ли это само решение о флоре
(пере-решать флор в этом паке НЕ надо — назови цену и отдай мне).
### 4.5 ПРОТУХШИЕ УТВЕРЖДЕНИЯ В КОДЕ — **реши сама, какие брать, и объяви**
Найдены старшим коллегой при разборе. **Твоя зона — только движковые:**
`config/pipeline.go` (греп `sequential, deterministic`) — против `waverun.go`; `reservegate.go` (греп
`THREE values`) — при четырёх константах, если ещё не чинено; `cutcall.go` (греп `the run was stopped`)
— на обрыве от падения соседа.
**Платформенные НЕ ТРОГАЙ** (`pgstore/credits.go`, `runner/runner.go`, `pgstore/runs.go`,
`ingest/events.go`) — их отнесу я. Назови в отчёте, какие движковые взяла и какие оставила с доводом.
## 5. Где этот пак мягкий — четыре места, назвал я, веер выбираешь ты
Мандат самопроверки — **исполнением**; субагенты разрешены явно. Адверсариальный проход по СВОЕЙ готовой
работе обязателен.
1. ⛔ **Провенанс, схлопывающий множественный источник в один ярлык, ХУЖЕ его отсутствия** — он выглядит
измерением. Кандидат, предложенный и майнером, и банкнотой, обязан быть отличим от однобокого.
2. ⛔ **Диагноз `族` легко получить НЕ ТОТ.** «Ноль после контраста» и «не смотрел контраст» выглядят
одинаково: у каждой ступени печатается ЗНАМЕНАТЕЛЬ. И спроси себя, на ЧЬИХ данных считаешь — банк A и
банк B разные, и ступень, где терм выпал, может отличаться между прогонами.
3. ⛔ **Пин байт-идентичности при 1 и 4 воркерах вырожден, если фикстура даёт ОДИН элемент волне.**
Одна единица параллелится сама с собой. Фикстура обязана дать волне столько элементов, чтобы порядок
завершения МОГ разойтись, и доказать это печатью.
4. ⛔ **Правка комментария — не правка механизма.** Если исправленная посылка §4.4 меняет чьё-то
решение, скажи ЧЬЁ; если не меняет — скажи, почему, и чем это предъявлено.
⚠ И общее, стоившее трёх смен подряд: **зелёная батарея плюс полный мутационный каталог сходимостью НЕ
являются.** Направленный второй читатель находил 6, 7, 9 и 13 дефектов при полной зелени.
## 6. Предметные оси ревью
Выбери 13 и назови какие. Мой приор: **провенанс под множественным источником** · **знаменатель на
каждой ступени диагноза** · **детерминизм под конкуренцией**.
## 7. Записка-план ДО работы
Перед первой правкой — записка в `docs/PROGRESS.md`: что меняешь, чем предъявишь, где ждёшь сопротивления.
## 8. Заявление = команда
Каждое число и каждая категорика — **с командой, которой получены**. Дифф `^func Test` — исполнением.
**Четыре нормы кодового пака:** сверь КАЖДЫЙ клейм с результатом инструмента ЭТОЙ сессии · последний
абзац отчёта оказался планом — сделай его СЕЙЧАС · в длинной сессии интервальная самоверификация
субагентом против ЯВНЫХ критериев (`D39.121`, тело в `docs/archive/architecture/05-decisions-D39-106-123.md`) ·
**пин на УСЛОВНОЕ сообщение обязан иметь фикстуру, где оно ОБЯЗАНО прозвучать** — тест, утверждающий
только молчание, вакуумен ровно там, где сообщение ложно. Этот пак заказывает условные печати в трёх
местах (счёт по источникам, таблица приёма, операторские строки) — каждая обязана иметь такую фикстуру.
## 9. Эхо-протокол старта
**Порядок двух первых действий:** сперва свой блок в `/tmp/textmachine-channel`, потом эхо.
**Эхо** — ≤10 строк СВОИМИ словами: что поняла · что считаешь опасным · что считаешь неверным. Не
пересказ: дословный пересказ подтверждает канал, но не понимание, и ошибку промта повторяет вместе с ним.
**Адрес бери из файла канала** (блок `role=оркестратор`), сверившись с `ListAgents`.
## 10. Что НЕ удалось — обязательная секция отчёта
И вторая половина: **«где прибор слеп и я это знаю»** — что невидимо · почему не чинила · чем
закрывается. ⭐ Норма: **«в коде названо, в отчёте нет — значит для следующей смены НЕ названо».**
На прошлом паке прямой вопрос дал ПЯТЬ находок сверх отчёта, включая улику, существовавшую в одной
копии. Отвечай на него до того, как я спрошу.
## 11. Канал вопросов и право отказаться
Конфликт промта с кодом — **пинг мне, не интерпретация**. Право сказать «этого делать не надо» с
аргументом у тебя есть, и им пользовались: в двух паках подряд сессия была права, а не я.
**Тесты и гейты под зелень не подгонять.** Правка, вызванная ЗАКАЗАННОЙ сменой поведения, —
обслуживание, и протухший тест держать не нужно; но она **ОБЪЯВЛЯЕТСЯ**: что изменилось, какой тест это
описывал, куда уехала гарантия.
**И слово владельца 12.09, которое сильнее всякой осторожности:** «нам не нужно держать неправильно
работающую функциональность из-за техдолга, тестов каких-то старых». Замерила, что починка дорога, —
это ПЛАН, а не довод держать дефект.
## 12. Прямой канал
Механизм — `CLAUDE.md` §«Связь между сессиями». Впиши свой блок ПЕРВЫМ действием. Нужной роли нет ⇒
канала нет, и это нормальный случай: НЕ опрашивай сессии подряд.
### ⭐ Старший коллега — Fable 5. **Разрешён ОДИН на сессию (слово владельца)**
Поднимаешь **одного** агента на модели **`fable`** и держишь весь пак. Вопросы ДОСЫЛАЕШЬ ему, второго не
поднимаешь: ценность в накопленном контексте. Модель задавай ЯВНО. Он **советчик, а не источник истины**
его посылки проверяются деревом. ⭐ Для калибровки: в этом паке ровно он прочитал, как собирается банк
(чего не сделал никто), и снял два моих пункта из пяти как лечение симптома.
## 13. Критерий завершённости
У каждого пункта — исход (сделано · не делаю с доводом · пинг) · **круги СОШЛИСЬ: последний не дал НОВЫХ
находок, а прежние закрыты ТАБЛИЦЕЙ «находка → что сделано → ЧЕМ ПРЕДЪЯВЛЕНО»** (без таблицы «круги
сошлись» остаётся самоотчётом) · **таблица мутаций
полная, выжившие названы, КАЖДАЯ новая запись помечена `battery`** · числа сняты ПОСЛЕ последней правки ·
`df -h /tmp` рядом с итогом гейта · всё живое в ДЕРЕВЕ, а не в письме · явное **«работа завершена,
править не планирую»**. Без последнего пак считается идущим.

File diff suppressed because one or more lines are too long

View file

@ -31,7 +31,7 @@
читателя нет (`json:"proposed` — 0 хитов при 356 json-тегах), живой экран 04.09 отдал `total 0, signed 0`.
3. **ВТОРАЯ ПАРА** — японская книга получает китайскую транскрипцию. «Требуется проекту», не показу zh→ru.
Условное (4): **CORS/same-origin**`Access-Control` 0 хитов при 201 Go-файле; предусловие прогона B.
Условное (4): **CORS/same-origin**`Access-Control` 0 хитов при 201 Go-файле. ⛔ **Испр. 11.09 (поправка старшего коллеги, пере-проверена по ячейке пункта 9): это НЕ предусловие прогона B.** B идёт «платформа → бэкенд → ФАЙЛ», то есть `curl`/API, а браузерного источника в нём нет вовсе — фронт заморожен тем же словом владельца. CORS — предусловие РАЗМОРОЗКИ ФРОНТА (пункт 6), и там его место.
**«Качественно готовы» имеет ВТОРОЕ чтение, и его надо назвать владельцу:** качество перевода на целевом
жанре не измерено и кодом не обещано. Эта готовность наступает только ПОСЛЕ настоящей книги — то есть после
@ -45,12 +45,12 @@
| 2 | **платформа** | правда у двери: `PD-455` · `PD-448` (разбор) · `PD-162` | $0 | ✅ **ЗАКРЫТ АКТОМ `D39.246`** с дофиксом по двум находкам приёмки |
| 3 | ~~полигон~~ | **прогон A**: настоящая книга до ФАЙЛА, платный провайдер | **факт $0.419424** | ✅ **ЗАКРЫТ АКТОМ `D39.247`**; строка 16 закрыта замером; ⚠ зона ВНЕ СКОУПА с 11.09 |
| 4 | **бэкенд** | ⛔ **КОНСИСТЕНТНОСТЬ, КОТОРУЮ МОЖНО ПРЕДЪЯВИТЬ**: ряды **406** · **407** · **408** · **409**, плюс купленные прогоном **417** (закон банка не виден в тексте) и **419** (стеммер: 11 из 18 промахов — ложные) | пак, почти весь $0 | ⏳ **ВЫДАН 11.09** сессии `textmachine-c9`; приёмка, круг 2 (шесть посадок выжили → дофикс) |
| 5 | **бэкенд** | «ВЫДАЧА, КОТОРУЮ ВИДИТ ЧИТАТЕЛЬ»: ряды **284** · **201** · **283** · **307** · **291** | пак | после 4; форму гранулярности решает дизайн глав |
| 5 | **бэкенд** | «ВЫДАЧА, КОТОРУЮ ВИДИТ ЧИТАТЕЛЬ» — ⛔ **РАЗОБРАН 11.09 ЧТЕНИЕМ РЯДОВ И РАЗВЕДЁН НА ДВА ПАКА, один ряд закрыт замером.** **(а) `291` ВЫДАН** — `BACKEND_BOUGHT_MEANS_DELIVERED_SESSION_PROMPT.md`, решено владельцем (`D39.204`), дефект жив и предъявлен строкой `stagerun.go:203`; акт ОДИН с платформенной половиной. **(б) `307` ЗАКРЫТ, а не заказан:** честная форма ПОСТРОЕНА и запинена коммитом `6ceb133``runOrderedChapters = (case when r.ordered_units is not null then null else r.ceiling_chapters end)`, плюс `TestACharacterOrdersVolumeIsPublishedInTheUnitItWasSoldIn`; ряд снят с таблицы. Заказать его значило бы заказать готовое — мой повторяющийся класс. **(в) `283` + `284` + `201` — СЛЕДУЮЩИЙ пак «структура и заголовки», и он идёт ПОСЛЕ прогона B:** 283 требует единственного окна пере-снапшота, а пере-снапшот двигает НАРЕЗКУ ⇒ единицы A и B станут несравнимыми, и B ответит на другой вопрос (поправка старшего коллеги, принята). У 284 сверх того нет слова владельца о ПОДПИСИ стадии | пак, $0 | ⏳ (а) выдан · (б) закрыт · (в) ждёт B |
| 6 | **фронт** | поверхность показа | пак | разморозка + пункты 15 |
| 7 | ~~полигон~~ | ⛔ **ЗОНА ВНЕ СКОУПА — слово владельца 11.09**; четыре промта уведены в `archive/prompts/` с баннерами | — | снято |
| 8 | **бэкенд/деньги** | ⛔ **«ПАДЕНИЕ, КОТОРОЕ ВИДНО»** — четверть COGS в одном механизме (**415**), слепые приборы отказа (**414**, **422**), молчащая подстановка цены. ⚠ Пин цены **413** из пака ВЫНУТ: это данные и отдельное решение | пак, $0 | ✍️ **НАПИСАН 11.09**`BACKEND_FAILURE_YOU_CAN_SEE_SESSION_PROMPT.md`; выдаётся СВЕЖЕЙ сессии сразу по закрытии 4 ⛔ **КРИТЕРИЙ ПРИЁМКИ ПАКА — МЕХАНИЗМ, А НЕ ПРОПОРЦИЯ** (поправка старшего коллеги 11.09): лекарство есть распространение ратифицированного образца `D2` п.3 на вторую причину, и оно верно при 25 % и при 5 % — пропорция задаёт очерёдность, не форму. ⇒ судить по строкам «ни одного удвоения без вердикта о вырождении; каждая выброшенная попытка — строкой леджера с причиной», а не по «с 25.4 % до X». Значит прогон B пак не гейтит и паком не гейтится.|
| 9 | **платформа+бэкенд** | ⭐ **ХОЛОДНЫЙ ПРОГОН B — платформа → бэкенд → файл, ВТОРОЙ раз.** Уточнение владельца 11.09: спрашивая «перепрогоны нужны», он имел в виду именно сквозной прогон, а не пере-запуск тестов. Покупает ЧЕТЫРЕ вещи, которых нет ни у кого: **(1)** миграция **v17** (`wave_selection`) пака консистентности ни разу не шла на живом прогоне — только в тестах; **(2)** прибор консистентности впервые даст числа с ЖИВОГО прогона, а не с пере-скана сохранённой базы прогона A — сегодня все `I1`/`I2` получены повторным чтением одних и тех же данных; **(3)** платформенный пак (идемпотентный резюм, терминальное состояние) проверяется сквозь, а не юнитами; **(4)** ⛔ **вторая точка по денежной дыре, и она нужна по норме, а не для красоты** — весь довод «четверть денег купила пустоту» стоит на ЧЕТЫРЁХ одиночных розыгрышах, а квирки §3д прямо говорят: смета DeepSeek с одиночного вызова может ошибиться на порядок, сметы строятся на ПОВТОРАХ. ⇒ у числа 25.4 % есть знак и нет величины, пока прогон не повторён. **Цена ≈$0.42 по образцу прогона A, машинный кап ≈$2.5.****ПРЕ-РЕГИСТРАЦИЯ ЗНАМЕНАТЕЛЯ, иначе B ответит числом на другой вопрос** (поправка старшего коллеги 11.09): B — ЕЩЁ ОДИН одиночный прогон, и две точки распределением не станут. Настоящая ценность B не «вторая точка к 25.4 %», а `request_log` ВСЕХ единиц прогона ⇒ **доля выброшенных первых попыток ПО ЕДИНИЦАМ, а не по вызовам.** Это и пре-регистрируется. ⚠ И идёт B **ПАРАЛЛЕЛЬНО** паку о падениях, а не после: они в РАЗНЫХ слотах (бэкенд против платформы), и правило «один промт на зону» их не сталкивает | ≈$0.42 | ждёт лендинга паков 4 и 7-платформенного |
| 9 | **платформа+бэкенд** | ⭐ **ХОЛОДНЫЙ ПРОГОН B — платформа → бэкенд → файл, ВТОРОЙ раз.** Уточнение владельца 11.09: спрашивая «перепрогоны нужны», он имел в виду именно сквозной прогон, а не пере-запуск тестов. Покупает ЧЕТЫРЕ вещи, которых нет ни у кого: **(1)** миграция **v17** (`wave_selection`) пака консистентности ни разу не шла на живом прогоне — только в тестах; **(2)** прибор консистентности впервые даст числа с ЖИВОГО прогона, а не с пере-скана сохранённой базы прогона A — сегодня все `I1`/`I2` получены повторным чтением одних и тех же данных; **(3)** платформенный пак (идемпотентный резюм, терминальное состояние) проверяется сквозь, а не юнитами; **(4)** ⛔ **вторая точка по денежной дыре, и она нужна по норме, а не для красоты** — весь довод «четверть денег купила пустоту» стоит на ЧЕТЫРЁХ одиночных розыгрышах, а квирки §3д прямо говорят: смета DeepSeek с одиночного вызова может ошибиться на порядок, сметы строятся на ПОВТОРАХ. ⇒ у числа 25.4 % есть знак и нет величины, пока прогон не повторён. **Цена ≈$0.42 по образцу прогона A, машинный кап ≈$2.5.****ПРЕ-РЕГИСТРАЦИЯ ЗНАМЕНАТЕЛЯ, иначе B ответит числом на другой вопрос** (поправка старшего коллеги 11.09): B — ЕЩЁ ОДИН одиночный прогон, и две точки распределением не станут. Настоящая ценность B не «вторая точка к 25.4 %», а `request_log` ВСЕХ единиц прогона ⇒ **доля выброшенных первых попыток ПО ЕДИНИЦАМ, а не по вызовам.** Это и пре-регистрируется. ⚠ И идёт B **ПАРАЛЛЕЛЬНО** паку о падениях, а не после: они в РАЗНЫХ слотах (бэкенд против платформы), и правило «один промт на зону» их не сталкивает | ≈$0.42 | **ВЫДАН 11.09** сессии `textmachine-82`, санкция владельца получена и положена в носитель ниже |
**ПОЧЕМУ 4 ВСТАЛ ПЕРЕД 5 (решение 11.09 по мерилу владельца).** Мерило — «качественный КОНСИСТЕНТНЫЙ
перевод длинных книг». Аудит банка 11.09 показал, что **книжного вопроса о консистентности не задаёт ни один
@ -103,6 +103,29 @@ API не было: 04.09 упёрлось в потолок (`PD-440`, с тех
## Решения владельца, уже полученные (носитель — ЗДЕСЬ; заведён 11.09, прежде его не было ни в одном доке)
- ✅ **ЗА ВСЁ ПЛАТИТ ЧИТАТЕЛЬ, вопрос отложен** (слово 11.09: «пока платит за все юзер, потом будем решать
эту проблему»). ⇒ **ни один пак не строит механизм „платит продукт“** и не меняет, кому что
выставляется. Вопрос «считается ли доставленная пустота расходом читателя» снят с очереди решений и
висит ОТЛОЖЕННЫМ; носители — ряды **415** · **414** · **422**. ⚠ И рядом стоит расхождение, которое
надо будет разрешить вместе с ним: объёмный леджер СЕГОДНЯ считает флагованный юнит НЕ доставленным
(`backend/internal/pipeline/volume_test.go`, греп `TestAFlaggedUnitIsNotReportedAsDelivered`), то есть
грант за него не съеден — это поведение ПРОТИВОРЕЧИТ «платит за всё читатель» и существует ДО этих
паков. Не трогать молча: назвать при разборе отложенного вопроса.
- ✅ **НАЗВАНИЕ КНИГИ И ЗАГОЛОВКИ ГЛАВ ИДУТ БАНКОМ, подпись — АВТО либо ПОЛЬЗОВАТЕЛЬСКАЯ** (слово 11.09:
«да, это идет банком и делается автоподпись\подпись юзера просто, вот и все»). ⇒ отдельного узла
подписи для заголовков НЕ проектировать: переиспользуется механизм подписи банка, каким подписывают
термины. Носитель предмета — ряд **284**; это снимает вопрос «какая у стадии подпись», висевший
открытым с 05.09.
- ✅ **ФАБЛ-СОВЕТЧИК РАЗРЕШЁН КАЖДОЙ ЗОННОЙ СЕССИИ — ОДИН** (слово 11.09: «разрешаю 1 фабла на 1 сессию,
ну как у тебя»). Прежде потолок 12 читался как относящийся только к оркестратору. Носитель — гардрейл
`CLAUDE.md`; промты зон обязаны НАЗЫВАТЬ механизм и форму (вопросы досылаются ОДНОМУ, второго не
поднимают).
- ✅ **СКВОЗНОЙ ПРОГОН B РАЗРЕШЁН** (слово 11.09: «разрешаю», в ответ на прямой вопрос с числами —
«ждём ≈$0.42, кап ≈$2.5 холда»). ⚠ Санкция названа ЗДЕСЬ и датирована, потому что норма «согласие —
ДО траты, явным словом» уже однажды стоила смены: промты ссылались на разрешение как на факт, а в
дереве его не было ни строкой. Предусловие санкции, объявленное вместе с ней: **пре-регистрация
знаменателя** — доля выброшенных первых попыток ПО ЕДИНИЦАМ, а не по вызовам, — и **фриз HEAD и
бинарей** (`git archive`), чтобы лендинги во время прогона не двигали прибор под замером.
- ✅ **ПЛАТНЫЙ ПРОГОН A РАЗРЕШЁН** (слово 11.09: «Разрешаю»). Ожидание ≈$0.30, машинный кап ≈$2.5 (холд, см.
раздел выше). ⚠ Санкция названа ЗДЕСЬ, потому что опровергатель 11.09 поймал: промты ссылались на неё как
на факт, а в дереве её не было ни строкой — «известное со слов» не носитель.

File diff suppressed because one or more lines are too long

View file

@ -24,6 +24,8 @@
- **Активные хендофф-промты — какой файл ТВОЙ** (состав обновляется при каждом лендинге, D39.80; хроника, причины и статусы РАБОТ — CURRENT-STATE и D-лог):
| Роль | Активный промт | Статус |
|---|---|---|
| **Бэкенд · КУПЛЕНО — ЗНАЧИТ ДОСТАВЛЕНО** | [BACKEND_BOUGHT_MEANS_DELIVERED_SESSION_PROMPT.md](BACKEND_BOUGHT_MEANS_DELIVERED_SESSION_PROMPT.md) | ✍️ **написан 11.09**, выдаётся `textmachine-b1`. Предмет решён владельцем (`D39.204`): отказ резервации на РЕТРАЕ деградирует во флаг с отдельной причиной. Сегодня он уходит наверх из цикла попыток раньше `UpsertChunkStatus` ⇒ юнит виден `pending` НАВСЕГДА, прогон уходит `exit 4` при доставленных купленных юнитах. Платформенная половина — отдельным промтом, акт ОДИН на две. $0 |
| Сквозной · ХОЛОДНЫЙ ПРОГОН B | активного НЕТ | пак ОТРАБОТАН и **ПРИНЯТ 11.09** — акт **D39.252**; промт в [archive/prompts/](archive/prompts/COLD_RUN_B_SESSION_PROMPT.md) с баннером-исходом. Книга второй раз от двери до ФАЙЛА, факт **$0.396657**. ⭐ Две меры денежной дыры разошлись в РАЗНЫЕ стороны на одних данных; **85.7 % оплаченного выхода — размышление, а не текст**. ⛔ Прибор приоритета №1 солгал вердиктом по подписанному имени героя. Качество впервые судилось по ОРИГИНАЛУ рядом с переводом: 26 находок, 1 крит |
| Оркестратор | [ORCHESTRATOR_SESSION_PROMPT.md](ORCHESTRATOR_SESSION_PROMPT.md) | роль и нормы; счётчик роли — CURRENT-STATE |
| Бэкенд · ДВЕ ОСТАНОВКИ | активного НЕТ | пак ОТМЕНЁН ВЛАДЕЛЬЦЕМ 10.09 до лендинга — акт **D39.240**; промт в `archive/prompts/` с баннером. Причина — размах правки, не качество. ⚠ Предмет ЗАМЕНЁН и сузился: остановка остаётся жёсткой, **деньги терять допустимо**, но она обязана быть КОРРЕКТНОЙ (без гонок, без половинчатых состояний, верное возобновление). ⭐ Пак успел купить `D39.236``D39.238` и строку **385** |
| Платформа · ДВЕ ОСТАНОВКИ | активного НЕТ | пак ОТМЕНЁН ВЛАДЕЛЬЦЕМ 10.09 до лендинга — акт **D39.240**; промт в `archive/prompts/` с баннером. Причина — размах правки, не качество. ⚠ Предмет ЗАМЕНЁН и сузился: остановка остаётся жёсткой, **деньги терять допустимо**, но она обязана быть КОРРЕКТНОЙ (без гонок, без половинчатых состояний, верное возобновление). ⭐ Пак успел купить `D39.236``D39.238` и строку **385** |
@ -34,7 +36,7 @@
| Бэкенд · ГЕЙТ ВМЕСТО ПРОЗЫ | активного НЕТ | пак ОТРАБОТАН и **ПРИНЯТ С ДОФИКСОМ 07.09** — акт **D39.225**; промт в `archive/prompts/` с баннером-исходом |
| Бэкенд · КОНСИСТЕНТНОСТЬ, КОТОРУЮ МОЖНО ПРЕДЪЯВИТЬ | активного НЕТ | пак ОТРАБОТАН и **ПРИНЯТ С ДОФИКСОМ 11.09** — акт **D39.249** (`0997e41`, 26 путей, $0); промт в [archive/prompts/](archive/prompts/BACKEND_CONSISTENCY_YOU_CAN_SHOW_SESSION_PROMPT.md) с баннером-исходом. ⭐ **Приоритет №1 впервые ИЗМЕРИМ по отгруженному тексту.** ⛔ Первый круг приёмки — шесть выживших посадок из шести; порядок колонок ратифицирован и отменён в тот же день (правило и два триггера — ряд **406**). Каталог **169/169 RED, выживших 0**. Живое: **407** и **419** наполовину · **408** с опровергнутым мотивом · **409** не тронут · новые **425427** · **428** · **431** · **432**
| Бэкенд · СТРУКТУРА ГЛАВ (ДИЗАЙН) | активного НЕТ | пак ОТРАБОТАН и **ПРИНЯТ 11.09 БЕЗ СХОДИМОСТИ, правилом остановки** — акт **D39.245** (`1d2fab6`, 3 пути, $0); результат — `backend/docs/CHAPTER_STRUCTURE_DESIGN.md` (2062 строки), статус ПРЕДЛОЖЕНИЕ до пака стройки. Глава становится ИДЕНТИЧНОСТЬЮ; предметная часть устояла в четырёх кругах опровержения, механика исполнения — нет, и пак стройки обязан начать с прогона миграционной механики ОПЫТОМ. Промт был — [CHAPTER_STRUCTURE_DESIGN_SESSION_PROMPT.md](archive/prompts/CHAPTER_STRUCTURE_DESIGN_SESSION_PROMPT.md), выдан 11.09 (актуализирован пятью врезками + 19 правок по опровергателю: мёртвые адреса, две таксономии осей, три «уже построено», собственная зона записи `backend/docs/` с дизайном-оф-рекорд `D15.2` и прибором shiftmap). ⚠ **ПРОЕКТИРОВАНИЕ, НЕ СТРОЙКА** (`D39.136` п.3): дизайн окна большой пере-нарезки — чем адресуется глава и развязка чанкера (161) · карта осей сдвига · банк-окна на chapter-ID · контент-адресуемый resume `D15.2` · IR/адаптеры/индуктор · правило заголовка и не-CJK путь (303) · гранулярность EPUB (302) и выдача (283). Снимает временный отказ интейка книге из одной главы (`D39.221`). Пак 1 «форматы и честный словарь» ОТРАБОТАН и ПРИНЯТ 06.09 — акт **D39.210**, промт в `archive/prompts/`. ⚠ **ПАК 2 (лексиконы не-CJK, корпус, замер оракулом) — НЕ НАПИСАН и ждёт файлов владельца** |
| **Бэкенд · ЗА ОДНУ ЕДИНИЦУ ПЛАТИМ ДВАЖДЫ** | [BACKEND_FAILURE_YOU_CAN_SEE_SESSION_PROMPT.md](BACKEND_FAILURE_YOU_CAN_SEE_SESSION_PROMPT.md) | ✅ **ВЫДАН 11.09** сессии `textmachine-61` (свежая, ждала слота не читая ничего). Редакция 2. Предмет — двойная оплата единицы: на платном прогоне $0.106472 = 25.4 % цены книги ушло в выброшенные первые попытки. ⛔ **Редакция 1 стояла на ПЕРЕВЁРНУТОЙ посылке** («движок лечит болезнь возбудителем») и заказала бы отключить восстановление, которое работает 4 раза из 4; опровергатель это снял — сессия, исполнившая её буквально, отключила бы восстановление трёх чанков из четырёх, отменила `D2` п.2 и сломала `TestRetryableSubset`. ⭐ Настоящая находка редакции 2: **причин ДВЕ, лекарство одно.** Черновик — потолок мал (контроль: 6 успешных вызовов, все ≤8496 прошли сразу, все >8496 упали и прошли на удвоенном; исключений 0), редактор — размышление съело достаточный потолок (2-я попытка 11717 против упавших 16000, $0.071009 = две трети потери в одном вызове). Плюс `reasoning_tokens` не несла числа ни разу за историю проекта (0 строк при 405 базах и 58 886 строках) и молчащая подстановка цены. Ряды **414** · **415** · **422**, пин flash **413** из пака ВЫНУТ |
| Бэкенд · ЗА ОДНУ ЕДИНИЦУ ПЛАТИМ ДВАЖДЫ | активного НЕТ | пак ОТРАБОТАН и **ПРИНЯТ С ДОФИКСОМ 11.09** — акт **D39.251** (22 пути, +947/66, 8 новых тестовых, $0); промт в [archive/prompts/](archive/prompts/BACKEND_FAILURE_YOU_CAN_SEE_SESSION_PROMPT.md) с баннером-исходом. ⭐ Наблюдаемость двойной оплаты построена, **лекарство лендится ВЫКЛЮЧЕННЫМ** и достаёт один случай из четырёх — условие включения в ряду **433**. ⛔ Посылка промта была ПЕРЕВЁРНУТА и снята зоной ДО первой правки (`empty` ×3 = 89 % потери, не `length`); `reasoning_tokens` ронял НАШ адаптер, а не вендор. |
| Бэкенд · отозванный | активного НЕТ | пак КАЧЕСТВО написан и ОТОЗВАН до выдачи 05.09 (`archive/prompts/…_WITHDRAWN.md`): предмет дешевле пака — точность чекера уже измерена, пере-снять её стоит одной команды. Следующий пак — СТРУКТУРА ГЛАВ (решение владельца 05.09 «любая книга любого формата»), пишется |
| Бэкенд · прежний | активного НЕТ | пак «денежный стоп» ОТРАБОТАН и ПРИНЯТ С ДОФИКСОМ 05.09 — акт **D39.206** (`81a89e9` + `616a8e4`), промт в `archive/prompts/` с баннером исхода. Следующая работа зоны — строки **291** (флаг вместо клина, `D39.204`) · **294** (форма «сколько добавить», `D39.203`) · **296** (точечная перегенерация по промаху глоссария, после 295) |
| Платформа · ОТВЕТ, НЕ ЗАВИСЯЩИЙ ОТ ВЕЗЕНИЯ | активного НЕТ | пак ОТРАБОТАН и **ПРИНЯТ С ДВУМЯ ДОФИКСАМИ 11.09** — акт **D39.250** (`711569d`, 27 путей, 24 новых теста, $0), контрактный минор **0.15.0**; промт в [archive/prompts/](archive/prompts/PLATFORM_ANSWER_THAT_DOES_NOT_DEPEND_ON_LUCK_SESSION_PROMPT.md) с баннером-исходом. ⭐ Предмет ПЕРЕ-ОПРЕДЕЛЁН замером зоны ДО первой правки: не гонка, а ординарный путь. ⛔ Оба денежных предохранителя проверены не тем прибором; лечением стал СНОС $0-рецепта, а не починка. Закрыты `PD-448` · `PD-466` · `PD-162` · `PD-139` · `П-22` · `П-23`. Живое — эскроу `П-18` и одно названное слепое пятно (адрес вызова не пинится изнутри набора, который на хосте не бежит)

View file

@ -89,7 +89,7 @@ excision_suspect / ok) 1:1, не изобретать заново. Метрик
`configs/models.yaml` (модели/цены/таймауты, с датой проверки). В конфиг ядра входят: состав/порядок стадий,
роль→модель, версии промптов, пороги гейтов, **режим инъекции глоссария (selective | full_prefix)** (Р5
требует обе схемы), токен-бюджеты сборки контекста (инъекция, STM, overlap), лимит регенераций до эскалации,
**именованные эскалационные цепочки** (список моделей; ⚠ **испр. 10.09:** какую цепочку берёт прогон, решает ЗАГРУЗКА по `content_policy`, а не раннер — `backend/internal/config/pipeline.go:466`=`LOAD, not by the runner`; раннер лишь исполняет уже разрешённый хоп, `backend/internal/pipeline/escalation.go:125`=`st.ResolvedHop`. Слово «channel» отставлено — `backend/internal/config/pipeline.go:230`=`LegacyChannel is the RETIRED`), fan-out N для
**именованные эскалационные цепочки** (список моделей; ⚠ **испр. 10.09:** какую цепочку берёт прогон, решает ЗАГРУЗКА по `content_policy`, а не раннер — `backend/internal/config/pipeline.go:502`=`LOAD, not by the runner`; раннер лишь исполняет уже разрешённый хоп, `backend/internal/pipeline/escalation.go:125`=`st.ResolvedHop`. Слово «channel» отставлено — `backend/internal/config/pipeline.go:266`=`LegacyChannel is the RETIRED`), fan-out N для
C2, cache TTL per-стадия. В models.yaml: цены (+cache write/read), профиль таймаутов/ретраев per-модель,
`extra_body`. Зашито в раннер: циклы по главам/чанкам, ветвление по гейтам, механика эскалации/ретраев,
семантика «эскалация → re-gate → флаг» (диаграмма pipeline.puml перегейтовку не рисует — реализую re-gate

View file

@ -1,4 +1,4 @@
# Реестр D-нот — карта актуальности v2 (D1D39.250; титул — носитель головы, бампать при каждом аппенде)
# Реестр D-нот — карта актуальности v2 (D1D39.252; титул — носитель головы, бампать при каждом аппенде)
> ⚠ **Колонку «тело» `counts.py --check` НЕ сторожит по устройству:** он сверяет полноту НОМЕРОВ, а не
> место тела, поэтому колонка держится дисциплиной лендинга. Не нашёл тело по колонке — иди в слайсы,
@ -309,3 +309,5 @@
| D39.248 | 11.09 | **РАТИФИКАЦИЯ РЕШЕНИЙ ВЛАДЕЛЬЦА 11.09**, прежде живших только в рабочей записке: ⭐ **голос ВХОДИТ в приоритет №1** (форма задана — проектирует Fable 5, исполняет опус; пак обязан нести пункт «обсудить с Fable») · **потолок 12 агента Fable на сессию** (прежняя «рекомендация» отозвана) · автономия смены с мерилом «ценности продукта → архитектура → необходимый рефакторинг → без велосипедов» · ⛔ **полигон выведен из скоупа целиком**, четыре промта в архив с баннерами, скоуп смены — платформа и бэкенд. Класс: слово владельца ратифицируется нотой В ТОТ ЖЕ ДЕНЬ — за смену он повторился дважды | процесс/приоритеты |
| D39.249 | 11.09 | **АКТ: бэкенд-пак «КОНСИСТЕНТНОСТЬ, КОТОРУЮ МОЖНО ПРЕДЪЯВИТЬ» ПРИНЯТ С ДОФИКСОМ** (26 путей, $0). ⭐ Приоритет №1 впервые ИЗМЕРИМ по отгруженному тексту: `I1`/`I2` с печатаемыми знаменателями, прогон A — 69 судимо, 236/221 вхождений. ⛔ Первый круг приёмки: **шесть посадок из шести выжили**, худшая — единственный провод между прибором и прогоном не запинен ничем (`.Consistency` в тестах 0 хитов при 227 файлах). Порядок колонок ратифицирован и отменён в тот же день — правило и ДВА триггера пере-открытия в ряду 406. Числа: каталог **169/169 RED, выживших 0** (оркестратор, скоуп без потолка cgroup — 26 убийств `CONSTRAINT_MEMCG` у зоны), батарея 19 ok · 0 FAIL · 4 названных скипа. Пять классов в `CLAUDE.md`, четыре зона нашла у себя | бэкенд/приёмка |
| D39.250 | 11.09 | **АКТ: платформенный пак «ОТВЕТ ДВЕРИ НЕ ЗАВИСИТ ОТ ПОРЯДКА» ПРИНЯТ С ДВУМЯ ДОФИКСАМИ**, минор **0.15.0** (27 путей, 24 новых теста, $0). ⭐ Предмет ПЕРЕ-ОПРЕДЕЛЁН замером зоны до первой правки: не редкая гонка, а ОРДИНАРНЫЙ путь. ⛔ **Оба денежных предохранителя проверены не тем прибором** — гард судил шаблон, а проба рендерит свой $0-пайплайн строкой выше; рецепт проверен сканом вместо загрузчика (`EXIT=10`). Лечение — СНОС рецепта, а не починка: он существовал под ошибку. ⭐ Ответ лежал в дереве комментарием того же хелпера. Выжившая одна, названа с классом; харнесс зоны читал ненулевой выход как «поймано» — три ложных RED, назван, положен в дерево | платформа/приёмка |
| D39.251 | 11.09 | **АКТ: бэкенд-пак «ЗА ОДНУ ЕДИНИЦУ ПЛАТИМ ДВАЖДЫ» ПРИНЯТ С ДОФИКСОМ** (22 пути +947/66, 8 новых тестовых, +27/0 тестов, $0). ⭐ Наблюдаемость двойной оплаты построена, **лекарство лендится ВЫКЛЮЧЕННЫМ** и достаёт 1 случай из 4 — сказано словом зоны до приёмки. ⛔ **Посылка пака была перевёрнута, и сняла её зона ДО первой правки:** `empty` ×3 = $0.094835 = 89 % против `length` ×1 = $0.011637; `off` у ReasoningNone — ВЕРХ шкалы. ⛔ `reasoning_tokens` ронял НАШ адаптер (провайдер шлёт 35 из 35). Приёмка: «лендинг инертен» было обещанием, а не свойством (посадка в 4 конфига — батарея зелена); ни одна из 22 мутаций не была помечена `battery`. Числа мои: 23 пакета · 19 ok · 0 FAIL · vet 4 · lint 0 · gofmt 0 · мутации **195 RED / 1 выжившая / 0 неизмеренных**, якорей 0 из 407; деньги — двумя независимыми путями по сырому леджеру, $0.106472 = 25.4 %. ⛔ Выжившая — ЧУЖАЯ и краснеет по удаче (8/8 на дереве против **7/8** на посадке) ⇒ первое число ряду **379(б)** | бэкенд/приёмка |
| D39.252 | 11.09 | **АКТ: СКВОЗНОЙ ПРОГОН B ПРИНЯТ** — книга второй раз от двери до ФАЙЛА, факт **$0.396657** при ожидании ≈$0.42 (санкция владельца «разрешаю» получена ДО траты). ⭐ **Две меры денежной дыры разошлись в РАЗНЫЕ стороны на одних данных:** по ЕДИНИЦАМ 23.5 %→**41.2 %**, по ДЕНЬГАМ 25.4 %→**21.1 %**; первичной объявлена замороженная пре-регом мера по единицам. ⭐ **85.7 % оплаченного выхода книги — РАЗМЫШЛЕНИЕ, а не текст** (153 942 из 179 613; у 5 из 7 выброшенных потолок съеден целиком) ⇒ ряд 422 закрыт числом. ⛔ **Прибор приоритета №1 солгал на первом живом прогоне:** вердикт `split` по подписанному имени героя при 58 вхождениях в 4 падежах и НУЛЕ конкурентов — имя вердикта утверждает больше, чем меряет предикат ⇒ числа консистентности B базовой линией НЕ являются. ⭐ Качество впервые судилось по ОРИГИНАЛУ рядом с переводом: 26 находок (1 крит — подмена референта, пере-проверен мною), адъюдикация цитат 10/10, три числа читателя исправлены. Скаляр оценки — ЦИТАТОЙ с четырьмя границами, не ратификацией. Ряды 434, 436, 437, 438 | полигон/приёмка |

View file

@ -1,4 +1,4 @@
# Журнал решений оркестратора — контракт D1D39.250 (живой файл: карта · эрраты · живые тела · голова D39.124+ (подрезка D39.139); тела закрытых эр — в слайсах `docs/archive/architecture/`, указатель ниже; реестр всех нот — `05-decisions-index.md`)
# Журнал решений оркестратора — контракт D1D39.252 (живой файл: карта · эрраты · живые тела · голова D39.124+ (подрезка D39.139); тела закрытых эр — в слайсах `docs/archive/architecture/`, указатель ниже; реестр всех нот — `05-decisions-index.md`)
> **КАРТА АКТУАЛЬНОСТИ (ревизия D31, продлена до D38.2 [12.07]; исторические записи ниже НЕ переписываются — дисциплина D23.3).** Работая с контрактом (греп номера: живой файл → слайсы, целиком НЕ читать — D39.125), держи под рукой, что чем перекрыто:
> ⚠ **Эррата 09.08 (D39.125):** D39.111 п.1 предписывал промту S3 «максимум = баланс МИНУС открытые холды» — формула ОШИБОЧНА (вычитание дважды), исправлена D39.115 п.2(а): максимум = Balance КАК ЕСТЬ; тело D39.111 — в слайсе `../archive/architecture/05-decisions-D39-106-123.md` (испр. 05.09: прежнее «живёт ниже в этом файле» протухло подрезкой D39.139) (голова D39.106+).
@ -89,6 +89,50 @@
> ⚠ **Эррата 11.09-д (`D39.248` п.4) — «ЧЕТЫРЕ СТРОКИ СВЁРНУТЫ В ОДНУ» НЕВЕРНО ДВАЖДЫ, и вторая половина была не опиской, а НЕДОДЕЛКОЙ.** Пере-снято мною самоаудитом через час после лендинга ноты: полигонных строк в таблице активных промтов было **ТРИ** (`ХОЛОДНЫЙ ПРОГОН A` · `РЕМОНТ ПРИБОРА` · `фаза Д`), а не четыре — четвёртый файл (`PHASE_D_HANDOFF`) строкой таблицы никогда не был. ⛔ **И свернулись только ДВЕ:** моя правка шла от строки `РЕМОНТ ПРИБОРА` до `Фронт`, а `ХОЛОДНЫЙ ПРОГОН A` стоит в таблице ВЫШЕ и уцелел — то есть в таблице час провисели ДВЕ строки про один и тот же закрытый пак, сводная и прежняя. Строка удалена, в таблице осталась одна полигонная. ⭐ Класс — мой же, записанный в этой смене: **«моя правка на месте» не значит «сделано то, что я объявил»**; объём правки надо сверять с объявленным, а не с намерением. Поймал самоаудит ноты, не читатель.
> ⚠ **Эррата 11.09-е (`D39.247`, шапка «Что принято») — «33 файла… НЕ КОММИЧЕНЫ» УСТАРЕЛО В ТОТ ЖЕ ЧАС.** Пере-снято 11.09: улики прогона закоммичены в отдельный репозиторий книг коммитом `ef0509f` («четыре артефакта нельзя воспроизвести ни за какие деньги»), **33 файла, 4953 вставки**, все тридцать три под контролем версий (`git -C books ls-files 'gu-zhenren/door-to-file' | wc -l` = 33). ⛔ **Но они НЕ ОТПРАВЛЕНЫ: репозиторий книг на один коммит впереди `origin/main`.** Владелец санкционировал пуш словом «Да» 11.09, мой классификатор прав его не пропустил, и выполнить его должен владелец у себя: `git -C books push origin HEAD`. ⇒ единственная копия улик платного прогона живёт на ОДНОЙ машине. ⚠ Класс: утверждение о состоянии дерева, верное в момент написания, протухает молча — у него нет ни гейта, ни даты.
> ⚠ **Эррата 12.09-а (`D39.252` п.3 и ряд 436) — ДЕФЕКТ ПРИБОРА КОНСИСТЕНТНОСТИ ОКАЗАЛСЯ ВТОРЫМ ПО
> СТАРШИНСТВУ; первый — в том, что у прибора РАЗНЫЕ ЗНАМЕНАТЕЛИ на разных прогонах.** Акт назвал ложным
> ИМЯ вердикта (`split` утверждает больше, чем меряет предикат). Замер сессии прогона B от 12.09,
> пере-снятый мною своим запросом и сошедшийся дословно, показал причину старше: **банк книги есть
> ФУНКЦИЯ ЧЕРНОВИКА, а черновик — стохастический выход модели.** `retrieval_state.banknote_detail` двух
> прогонов на ОДНИХ шести чанках при побайтно одном исходнике и тождественном покупающем конфиге:
> **A 98 · B 93 · общих 74 ⇒ Жаккар 0.63**, и расходится не только состав, но и ГРАНУЛЯРНОСТЬ ключа
> (A предлагает `丙等资质`/`族长家老`/`舅父舅母`, B — их составляющие). ⇒ **`I1`/`I2` двух прогонов
> несравнимы ПО ПОСТРОЕНИЮ**, а не из-за качества прибора: двигается популяция, на которой считается
> вердикт. ⚠ И это задевает цель 6: снапшот и голден запирают ВХОД, состав банка в них не заперт.
> Носитель — ряд **439**; границы замера (одна книга, шесть чанков, одна пара прогонов) названы там же.
> ⚠ Тело ноты не переписано (`D23.3`).
> ⚠ **Эррата 11.09-и (`D39.251` п.7 и ряд 428, слово «пере-оплата» о $0.028742) — НАЗВАНИЕ БЫЛО НЕВЕРНЫМ,
> и опровергает его замер, а не мнение.** Я назвал вторую покупку терминологии на резюме «пере-оплатой
> ПРИНЯТОЙ работы», то есть деньгами за уже сделанное. Замер сессии прогона B, пере-снятый мною независимо:
> второй заход **ИЗМЕНИЛ передачу у 12 термов из 69**, и в отгруженном тексте стоят именно НОВЫЕ формы
> (`凤雏` «Молодой Феникс»→«Юный Феникс», `舅母` «жена дяди по матери»→«тётя по матери», `高脚吊楼`
> «дом на сваях»→«свайный дом» и ещё девять). ⇒ это НЕ деньги за пустоту и не дубликат, а **ЦЕНА
> ПЕРЕ-МАЙНИНГА БАНКА ПОСЛЕ ЧЕЛОВЕЧЕСКОЙ ПОДПИСИ** — работа, результат которой доехал до читателя.
> ⚠ У всех шести пар РАЗНЫЕ `request_hash`, то есть заданы разные вопросы: «единица» в этой мере
> склеивает разные предметы, и это печатается рядом с числом. ⚠ Само число верно ($0.028742 = 6.9 %
> книги; вторая ориентация «все кроме последнего» даёт $0.035723 = 8.5 %, обе печатаются), неверно было
> ИМЯ — а имя здесь решает, читается ли строка как потеря. **Хороши ли эти 12 замен — вопрос КАЧЕСТВА, и
> он задан вычитке прогона B.** ⚠ Тело ноты не переписано (`D23.3`).
> ⚠ **Эррата 11.09-з (`D39.247` п.4, числа консистентности прогона A) — ПРИБОР СВЕРЯЛСЯ НЕ С ТОЙ ВЕРСИЕЙ
> БАНКА И ВИДЕЛ МЕНЬШЕ ПОЛОВИНЫ КНИГИ; обе половины найдены 11.09, первую нашла сессия прогона B, вторую —
> я, пере-проверяя первую.** **(1) Граница.** Прибор сверял отгруженный текст со СТОП-снимком банка
> (`evidence/bankstop-paid/project.db.bank.json`), а читатель получил текст, сделанный под ФИНАЛЬНЫМ банком:
> второй заход терминологии ИЗМЕНИЛ передачу у **12 термов из 69** (пере-снято мною независимо, список сошёлся
> дословно: `凤雏` «Молодой Феникс»→«Юный Феникс» · `老嬷嬷` «старая мамка»→«старая нянька» · `高脚吊楼`
> «дом на сваях»→«свайный дом» · `舅母` «жена дяди по матери»→«тётя по матери» · `白家寨` «селение Бай»→«крепость
> рода Бай» и ещё семь). Порядок снят по `request_log`: терминология-2 кончилась 01:42:10, редакторская волна шла
> 01:43:54→01:54:44 ⇒ редактор целиком работал ПОСЛЕ коммита финального банка. В тексте стоят ФИНАЛЬНЫЕ формы.
> ⇒ вердикты «отдано НЕ банковской формой» по этим термам — артефакт сверки со старой версией, а не промах
> движка. Класс — `D39.228` п.5: фикстура обязана назвать границу, которую прошла; не покраснело нигде, потому
> что числа вышли правдоподобные. **(2) Знаменатель, и он хуже.** Прибор нашёл передачу у **40 термов из 69**,
> у **29** вернул `renderings={}` — и молча записал их в `spread=0`, то есть в консистентные. Я взял эти 29 и
> проверил текстом: финальная форма банка РЕАЛЬНО стоит в отгруженном файле у **26 из 29** (поимённо — ряд 434).
> ⇒ итоговая строка прибора «TERMS SHIPPED IN MORE THAN ONE SHAPE: 0» посчитана по 40 термам, а не по 69, и
> «расхождения нет» там неотличимо от «передачи не нашёл». Контроль прибор печатает честно («terms rendered at
> all: 40, control: 69») — **ловушка в том, что заголовок читается как утверждение о КНИГЕ.** ⚠ И моя ложная
> тревога по дороге, названная здесь, чтобы её не повторили: я объявил находкой «один терм тремя формами», а это
> оказались ДВА РАЗНЫХ исходных терма (`宗族祠堂` и `宗祖祠堂`), которые банк различает верно.
> ⚠ **Тело ноты не переписано** (`D23.3`): п.4 читать вместе с этой эрратой.
> ⚠ **Эррата 11.09-ж (`D39.246` п.5, число «0 FAIL» в рецепте стенда) — ЧИСЛО БЫЛО УСЛОВНЫМ, И УСЛОВИЕ Я НЕ НАЗВАЛ.** Моя приёмка вписала в `platform/docs/STACK_DECISIONS.md` «20 ok · 0 FAIL · 5 скипов» как свойство рецепта. Замер платформенной сессии 11.09 (контроль: 15 файлов возвращены к `git show HEAD:<file>`, два новых удалены, тот же FAIL воспроизведён) показывает: **рецепт не может дать зелёный `internal/runner` НИ ПРИ КАКОЙ конфигурации** — $0-шаблон нужен `TestTheSnapshotGuardIsLoudWithoutTheFlagsAndPassesWithThem`, и он же обнуляет проекцию цены, которую читают `TestTheRealEnginesPriceProjectionIsReadByThisBuild` и `TestWhatTheLiveManifestCarriesAndThisBuildDeclinesToRead`. Моё «0 FAIL» согласуется с ПЛАТНЫМ шаблоном плюс выполненным условием скипа. ⛔ **И под этим лежит опасность дороже неверного числа: предикат скипа неверен ПО РОДУ.** `platform/internal/runner/translate_resnapshot_live_test.go:41-43` слушает `127.0.0.1:11434` и скипает, только если порт ЗАНЯТ, — то есть отвечает на вопрос «свободен ли порт», а НЕ «разрешил ли кто-нибудь тратить деньги». Условий скипа три (переменные гейта · порт · артефакт контраста), и **хорошо провизионированный хост выполняет все три** — то есть платная ветвь рецепта сработает именно там, где всё настроено правильно. Прецедент верного рода в дереве есть: `TM_LIVE`. ⇒ раздел «Гейты батареи» остановлен ⛔-баннером зоной 11.09 до починки; носитель — `П-22`. ⭐ **Класс, и он мой: я вписал в чужой носитель число, не назвав условий, при которых оно снято.** Условное число без условия читается как свойство предмета — и следующая смена получит красное и решит, что сломала сама. Нашла зона, не я.
> ⚠ **Эррата 11.09-б (АДРЕС ИСПР. 11.09: носители — промт пака и таблица `docs/README.md`, НЕ тело `D39.247`) — «18 РАСХОЖДЕНИЙ» ЗАВЫШЕНО В 2.5 РАЗА, настоящих СЕМЬ.** Замерено бэкенд-сессией пака консистентности 11.09 по-юнитно: из восемнадцати промахов **11 ложные** — банковская форма доехала до читателя в том же юните СКЛОНЁННОЙ, а `SameStem` её не признал, потому что стеммер снимает ровно одно окончание и стеммы выходят разной длины (во всех одиннадцати один — строгий префикс другого с разницей в ОДИН знак). Пять из одиннадцати — намеренно исключённый мягкий знак (`D39.71`), и записка в дереве сама называет лечение — «anchor-gated match». ⇒ **акт остаётся верен в направлении и неверен в величине:** механизм консистентности работает и работает не везде, но НЕ-мест семь, а не восемнадцать. Носитель — строка бэклога **419**. ⚠ Класс: я опубликовал число, снятое ОДНИМ прибором, не спросив, что этот прибор не видит. ⛔ **Второй класс, пойманный ревью 11.09: эррату я адресовал НЕ ТУДА.** Числа «18» в теле `D39.247` нет ни разу — пере-снято грепом по живым докам (контроль: прибор прочёл 101 файл `docs/`, число нашлось в промте пака, в строке таблицы `docs/README.md`, в журнале и в ряду **419**). Акт говорит ДРУГУЮ величину на ДРУГОЙ популяции — «16 из 69, из них 6 отданы не банковской формой», снятую глазами по отгруженному файлу, и поправка стеммера её НЕ трогает. ⇒ поправлять надо было промт и таблицу, а акт — не надо было. ⭐ Урок: **у эрраты есть адрес, и он проверяется грепом ЧИСЛА по телу ноты, а не памятью о том, откуда я его взял.**
> ⚠ **Эррата 11.09-в (D39.247 п.4 и вопрос владельцу) — Я ПИСАЛ ПО ОСИ, КОТОРОЙ В ПРОДУКТЕ НЕТ.** Акт называл «подписанный/неподписанный ТЕРМИН» и ставил владельцу вопрос «закон ли для неподписанного». Владелец 11.09: «нет такого понятия, подписан либо ВЕСЬ банк, либо он в неконсистентном состоянии; есть галочка скипать подпись — тогда автоподписывание». Это ратифицировано `D39.144` (подписывается банк ЦЕЛИКОМ; пер-термная подпись — НЕ модель продукта; дефолт — авто-продолжение с неподписанным банком, строки едут с пометкой). ⇒ **вопрос был сформулирован неверно и снят.**Но под ним лежит РЕАЛЬНАЯ находка, и она дороже: в коде есть пер-термный автомат `auto|draft|approved`, и пост-проверка считает нарушением только `approved`**механизм решает по единице, которой в продуктовой модели не существует.** Носители — строки **407** (исправлена) и **419**. Мерить надо состоянием КНИГИ (подписана целиком либо авто-продолжение), а не статусом отдельного терма.
@ -3298,7 +3342,7 @@ bought NOTHING». ⇒ **`tmctl manifest` есть НИЖНЯЯ граница,
**3. ⛔ АКТ `D39.232` ОБЪЯВИЛ СТРОКИ ЗАКРЫТЫМИ И ОСТАВИЛ ИХ В ТАБЛИЦЕ.** Пункт 1 писал «закрыты строки 360 и 369, попутно 78» — и все три сутки простояли в бэклоге живыми. Это ровно та норма, которую эта же смена записала себе в ролевой промт («сверка носителей — тем же движением, что и акт»), и она не сработала на собственном акте автора. Пере-снято и закрыто сегодня: **78** (оплаченный `2xx` с нечитаемым телом теперь пишет СВОЮ строку `request_log` и помечается оценкой — `backend/internal/pipeline/cutcall.go:133`=`rl.Estimated, rl.EstTokens = cost > 0`), **369** (дедлайн выводится из бюджета вызова — `backend/internal/llm/attemptcut.go:269`=`func (p RetryProfile) deriveDeadline`, и эскалация, удвоившая `max_tokens`, удваивает время), **360** (предмет пака). Эррата 10.09-б в шапке.
**4. И ЧЕТВЁРТАЯ строка, закрытая тем же паком, которую не заметил никто, — 331** (движковая половина `PD-441`). Её условие закрытия — «сеттл оценки при отмене ушедшего вызова + публикация оценочных строк в `status --json`» — исполнено обеими половинами: сеттл — `cutcall.go`, публикация — `backend/internal/pipeline/status.go:911`=`rep.EstimatedRows, rep.EstimatedUSD = estimatedSpend(usage, committed)`, и оператору она печатается (`backend/cmd/tmctl/render.go:440`=`estimated-cost rows:`). ⚠ **С НАЗВАННЫМ СУЖЕНИЕМ:** различитель строки был «факт УХОДА запроса», а дерево книжит по «провайдер ОТВЕТИЛ 2xx», что уже; направление сужения — недосчёт, ратифицированный `D39.196` п.2а, и остаток несёт строка **376**. Платформенная половина «сказать больше, чем ≥» — строка **382**.
**4. И ЧЕТВЁРТАЯ строка, закрытая тем же паком, которую не заметил никто, — 331** (движковая половина `PD-441`). Её условие закрытия — «сеттл оценки при отмене ушедшего вызова + публикация оценочных строк в `status --json`» — исполнено обеими половинами: сеттл — `cutcall.go`, публикация — `backend/internal/pipeline/status.go:911`=`rep.EstimatedRows, rep.EstimatedUSD = estimatedSpend(usage, committed)`, и оператору она печатается (`backend/cmd/tmctl/render.go:444`=`estimated-cost rows:`). ⚠ **С НАЗВАННЫМ СУЖЕНИЕМ:** различитель строки был «факт УХОДА запроса», а дерево книжит по «провайдер ОТВЕТИЛ 2xx», что уже; направление сужения — недосчёт, ратифицированный `D39.196` п.2а, и остаток несёт строка **376**. Платформенная половина «сказать больше, чем ≥» — строка **382**.
**5. Инвентарь живых доков (ответ на вопрос владельца «доки актуализированы?»).** Прогнан по норме ролевого промта — ВСЕ живые доки против текущего состояния, не только тронутые сменой; прибор прочёл **94** живых `.md` вне архива. Найдено и исправлено: `docs/architecture/15-money-path.md` в ТРЁХ местах утверждал закрытый канал как открытый (пункт маршрута, строка таблицы долга, список бэклог-строк) — переписаны, и туда же внесена НОВАЯ денежная граница пака (доставка · класс `attempt_timeout` без ретрая · дедлайн из бюджета · резюм не перепокупает). Проверено и расхождений НЕ найдено: реестр требований против брифа владельца (V0V6 разобраны все; бриф не менялся с 24.08 — контроль по `ls`), таблица активных промтов, шапка-таблица `09-target-architecture.md` (транспорт в её слои не входит), `docs/research/21` (несёт ⚠-баннер о вердикте). ⚠ Остаток, который чинить НЕ мне: 13 битых `file:line`-якорей линтера — все в чужих зонах (`backend/docs`, `docs/experiments`, `frontend/docs`) либо уехали не от моих правок; адреса пере-сняты и оставлены пингами в зонных журналах.
@ -3661,3 +3705,51 @@ bought NOTHING». ⇒ **`tmctl manifest` есть НИЖНЯЯ граница,
**9. РЯДЫ.** Закрыты: **`PD-448`** (предмет пере-определён и закрыт по существу) · **`PD-466`** и **`PD-162`** — законным исходом «не строю с доводом», довод на замере (`AFTER STOP: openHolds=0 reserved=0.000000`) · **`PD-139`** — диспозиция ОДНА, выведена замером (`workdir = <books dir>/<book id>`, 5 книг из 5), класс закрыт вычерком носителя на всех площадках · **`П-22`** и **`П-23`** — схлопнулись со сносом рецепта. Живое: остаток `PD-162` = эскроу `П-18`; слепое пятно п.6.
## D39.251 — АКТ: БЭКЕНД-ПАК «ЗА ОДНУ ЕДИНИЦУ ПЛАТИМ ДВАЖДЫ» ПРИНЯТ С ДОФИКСОМ; наблюдаемость двойной оплаты построена, ЛЕКАРСТВО ЛЕНДИТСЯ ВЫКЛЮЧЕННЫМ и достаёт один случай из четырёх, а посылка пака была перевёрнута зоной ДО первой правки (11.09, оркестратор №23) ✅
**Что принято.** 22 пути в `backend/` (+947/66) плюс **8 новых файлов, все тестовые**; тестовых функций **+27/0** (в зоне 1438); своя секция в `docs/PROGRESS.md`. Вне зоны — ноль, `books` не тронуты, платных вызовов **ноль**. Исполнитель — `textmachine-61`, промт `docs/BACKEND_FAILURE_YOU_CAN_SEE_SESSION_PROMPT.md` (редакция 3).
**1. ⭐ ГЛАВНОЕ, И ОНО СКАЗАНО СЛОВОМ ЗОНЫ, А НЕ НАЙДЕНО ПРИЁМКОЙ: ЛЕНДИНГ ИНЕРТЕН.** Пак закрыл НАБЛЮДАЕМОСТЬ (видно, сколько денег и на что ушло) и построил ЛЕЧЕНИЕ (ступень усилия ниже при том же бюджете на классе `FlagEmpty`), но ручка `retries.lower_effort_on_empty` лендится с дефолтом `false`: **механизм есть, деньги продолжают течь**. Зона объявила это сама, отдельным абзацем отчёта, до всякой приёмки. ⛔ И охват назван честно: из четырёх двойных оплат прогона A лекарство достаёт **ОДНУ**`edit` ($0.071009 = 66.7 % потери) стоит на `off`, у которого шаг вниз есть; три черновых ($0.035463 = 33.3 %) уже на `low`, **дне эмитирующей шкалы, и лекарства для них в паке НЕТ**. Носитель условия включения — ряд **433**.
**2. ⛔ ПОСЫЛКА ПАКА БЫЛА ПЕРЕВЁРНУТА, И СНЯЛА ЕЁ ЗОНА ДО ПЕРВОЙ ПРАВКИ.** Моя редакция 2 строила лекарство на классе `length`; замер зоны дал обратное — `degraded='empty'` ×3 = **$0.094835 = 89 %** потери против `'length'` ×1 = **$0.011637**. Редакция 3 переписана по её возражению, и ещё три её возражения приняты: дискриминатор ред. 2 РЕТРОСПЕКТИВЕН (известен только после оплаты второй попытки, а решать надо до неё) · контроль «6 из 6» тавтологичен · `off` у `ReasoningNone`**ВЕРХ** шкалы, а не низ (провайдер при пустом значении берёт свой дефолт `high`). ⚠ Класс для смен: **пак, чья посылка перевёрнута, выглядит исполнимым до самого конца** — он ломается не на приёмке, а на первом замере, и только если сессии велено мерить ПРЕЖДЕ, чем править.
**3. ⛔ §4.3 ПРОМТА ТОЖЕ СТОЯЛ НА НЕВЕРНОЙ ПОСЫЛКЕ, И ЭТО БЫЛА НАША ПОТЕРЯ, А НЕ ВЕНДОРСКАЯ.** Я писал, что DeepSeek не присылает `reasoning_tokens`. Провайдер присылает: **35 записей из 35** несут `completion_tokens_details.reasoning_tokens`, и на 35 из 35 сходится `total == prompt + completion`. Ронял число НАШ адаптер — у `switch` не было ветки `ReasoningSubset`. ⇒ ряд **422** закрыт не «вендор не даёт», а починкой у себя.
**4. ⛔ ДВЕ НАХОДКИ ПРИЁМКИ ГОВОРИЛИ, ЧТО ПАК НЕ УДЕРЖИВАЕТ СОБСТВЕННЫХ ОБЕЩАНИЙ.** **(а)** «Лендинг инертен» было ОБЕЩАНИЕМ, а не свойством: ключ включается правкой ДАННЫХ, и посадка `lower_effort_on_empty: true` во все четыре боевых конфига оставляла батарею **23/23 зелёной**. Прецедент лежал в том же дереве у сиблинга (`backend/internal/config/echoregen_shipping_test.go`): зона взяла у образца ФОРМУ и не взяла его ГАРАНТИЮ — класс ушёл в `CLAUDE.md` как «копируя механизм, копируй его сторожа». **(б)** Ни одна из её 22 мутаций не была помечена `battery`, а `make mutations` гоняет только батарейное подмножество: «21/21 RED» был разовым прогоном смены, **а не гейтом проекта**. После дофикса все 27 помечены и входят в гейт. ⚠ Обе находки — про то, что построенное не попадает под сторожа; обе закрыты дофиксом.
**5. ⛔ НАПРАВЛЕННЫЙ ВТОРОЙ ЧИТАТЕЛЬ ЗОНЫ НАШЁЛ ДЕВЯТЬ ПОДТВЕРЖДЁННЫХ ДЕФЕКТОВ ПРИ ЗЕЛЁНОЙ БАТАРЕЕ И 15/15 МУТАЦИЙ.** Самый дорогой сидел в строке, построенной как ПРИБОР ДЕНЕГ: фильтр по `ok=0` захватывал неоплаченные обрывы (`cost=0`, причина в `err`) и `sanitizer_stripped`, чей текст ОТГРУЖАЕТСЯ — на прогоне `coldrun-v16` это 12 вызовов $0.123161 вместо 10 вызовов $0.104507, **завышение 15.1 % в сторону «потеряли больше»**. ⇒ зелёная батарея плюс полный мутационный каталог сходимостью НЕ являются; это уже третья смена подряд, где направленный читатель ловит при полной зелени.
**6. ⛔ ЕДИНСТВЕННАЯ ВЫЖИВШАЯ В МОЁМ КАТАЛОГЕ — НЕ ЕЁ, И ОНА КРАСНЕЕТ ПО УДАЧЕ.** Мой полный прогон дал 195 RED и одну выжившую: `CUTCALL-a-refusal-pays-when-the-reply-outruns-the-write`, запись прошлого пака; `internal/llm/attemptcut.go` этот пак не трогал вовсе. Вскрыл руками: на ЧИСТОМ дереве пин **8 зелёных из 8**, на своей посадке — **7 КРАСНЫХ ИЗ 8, один ЗЕЛЁНЫЙ**. Текст падения верный и про предмет; дыра в ФИКСТУРЕ — успеет ли транспорт записать `GotFirstResponseByte` раньше, чем `Do` вернёт ошибку записи, решает гонка. ⇒ прежние RED этой записи были удачей. Предмет уже стоял рядом **379(б)** с верным приёмочным столбцом — ряд получил ПЕРВОЕ ЧИСЛО, нового ряда не заведено. ⚠ Отдельного пака это не стоит: правка одной фикстуры идёт ПРИЛОЖЕНИЕМ к следующему бэкенд-промту. Срочность даёт класс: «отказ становится покупкой» — единственное направление, которое `D39.196` п.2а запрещает прямо, и сегодня гейт стережёт его с дырой в один прогон из восьми.
**7. ЧИСЛА, С УКАЗАНИЕМ ЧЬИМ ПРИБОРОМ.** **Мои, на замороженной копии `~/tm-11-accept2` (побайтово равна дереву, кроме `.env` и `bin`; `test -f go.mod` до и после, `pwd` сверен, мутатор по копии ходит ОДИН):** `make battery` **EXIT=0****19 `ok` · 0 FAIL · 4 «no test files» = 23 пакета**, `go vet` **4 вызова**, `golangci-lint` **0 issues**, `gofmt -l` **0 файлов**. Скипов **9**, названы поимённо и разобраны: 2 хелпер-процессных (структурные), 4 превращаются в **PASS**, когда копии дают данные (`TM_CHECKER_LABELS_DIR`), 3 остаются за отсутствием данных на этой машине (словарь jieba — контроль: `eval/exp16/data` содержит **0 записей**; `TM_CORPUS`; путь улик прогона A). **Мутации:** 196 записей батареи — **RED 195 · ВЫЖИВШИХ 1 · НЕИЗМЕРЕННЫХ 0**, якорей протухших **0 из 407**; все **27** новых записей пака — RED у меня тоже, и все 27 несут `battery: true` (сверено по JSON, а не по отчёту). **Дерево:** 22 пути, +947/66, 8 новых тестовых, `^func Test` **+27/0** (1 в трекаемом диффе + 26 в новых файлах), в зоне 1438. **Деньги — сам, двумя независимыми путями по сырому леджеру прогона A** (`stand/books/bk_ROEHZBD46ALFI43E/project.db`, контроль: 33 строки · 33 уникальных `id` · 17 ячеек · 2 трейса · $0.419423): путь по флагу `degraded` и путь «оплачено и НЕ принято» (`ok=0 AND cost_usd>0`, флага не касается) дают **одно и то же: $0.106472 = 25.4 % книги, 4 вызова из 27**, разложение `empty` ×3 = $0.094835 и `length` ×1 = $0.011637. ⚠ Третий путь — «ячейка, купленная дважды» — дал $0.230388 (54.9 %) и оказался НЕВЕРНО ПОСТРОЕННЫМ у меня: «последняя строка по времени» не значит «принятый ответ» (последней села бесплатная `banknote_export`), а разные `request_hash` — разные вопросы, не пере-спрос. Назван здесь, потому что число красивое и ложное, и следующая смена его повторит. **Её:** внутренние замеры пака (35 из 35 записей с `reasoning_tokens`, цена ключа 989 единиц / $9.99, включение ручки 58 единиц).
**8. РЯДЫ.** **Закрыт: 422** (колонка размышления несёт число; причина оказалась нашей, а не вендорской). **Наполовину, с названной половиной: 414** (признак отказа в `err` не заводится осознанно — решение «менять нечего, но НАЗВАТЬ и ПОСЧИТАТЬ», и счёт построен) · **415** (наблюдаемость закрыта, трата — нет; остаток ушёл в 433) · **429** (клейм снят из боевого конфига; вендор-заход по `identity` слага остаётся в 413). **Не закрыт и не должен: 433** — условие включения ручки измеримо и названо (эхо-контроль в РЕДАКТОРСКОЙ роли на плотном CJK). **Получил первое число: 379(б)** — см. п.6. **Пинги зоны, отнесены мною:** `identity` слага `deepseek-flash` требует вендор-захода (413) · флор `flash` фолдится в снапшот (415) · ряд 49а получил живого носителя в ГОЛОВЕ цепочки миграций.
**9. ГРАНИЦЫ ВЕРДИКТА — ЧЕСТНО.** Живого платного прогона в паке не было и не требовалось: всё судимо детерминированно. ⛔ Поэтому ДВА утверждения пак НЕ доказывает и доказать не мог: **(а)** что ступень усилия ниже не ломает эхо-безопасность редактора на плотном CJK — это замер, и он стоит денег (ряд 433); **(б)** что доля 25.4 % имеет ВЕЛИЧИНУ, а не только знак — она снята с одного прогона, а квирки §3д прямо говорят, что смета DeepSeek с одиночного вызова ошибается на порядок. ⇒ пак принят как МЕХАНИЗМ (ни одного удвоения без вердикта о вырождении; каждая выброшенная попытка — строкой леджера с причиной), а не как пропорция, ровно по поправке старшего коллеги к пункту 8 горизонта.
## D39.252 — АКТ: СКВОЗНОЙ ПРОГОН B ПРИНЯТ. Книга второй раз дошла от двери до файла ($0.396657), две меры денежной дыры разошлись в РАЗНЫЕ стороны на одних данных, 85.7 % оплаченного выхода оказалось размышлением, а прибор приоритета №1 солгал на первом же живом прогоне (11.09, оркестратор №23) ✅
**Что принято.** `docs/experiments/25-door-to-file-b.md` — пре-рег (заланден `4ee7f03` ДО первого цента) плюс блок результатов (+638 строк, **0 удалений: пре-рег не тронут, сверено мною командой**); `eval/cold_run_b/` — драйвер, прибор денег, README. Исполнитель — `textmachine-82`. **Факт траты `$0.396657`** при ожидании ≈$0.42, стоп-правиле сессии $1.00 и машинном капе ≈$2.5. **Санкция владельца получена ДО траты, дословно «разрешаю», 11.09**, носитель — `docs/NEAR_TERM_PLAN.md` §«Решения владельца». Фриз: клон (не воркри) на `baa06ce`, три бинаря со штампом `vcs.revision`+`vcs.modified=false` и негативным контролем.
**1. ⭐ ГЛАВНОЕ ДЛЯ МЕТОДА: ДВЕ МЕРЫ ОДНОГО СОБЫТИЯ РАЗОШЛИСЬ В ПРОТИВОПОЛОЖНЫЕ СТОРОНЫ НА ОДНИХ И ТЕХ ЖЕ ДАННЫХ.** Выброшенные оплаченные попытки: по ЕДИНИЦАМ **23.5 % → 41.2 %**, по ДЕНЬГАМ **25.4 % → 21.1 %**. B выбросил вдвое больше единиц, но дешёвых — пять черновых чанков по ~$0.012 против одного редакторского за $0.071 у A. ⇒ **подмена одной меры другой сказала бы здесь ровно противоположное правде**, и это первый случай в проекте, где расхождение мер видно НА ДАННЫХ, а не в рассуждении. ⛔ **Первичной объявлена мера ПО ЕДИНИЦАМ — она заморожена пре-регом ДО прогона**, вторая печатается рядом. Выбор знаменателя постфактум был бы подгонкой, и пре-рег существует ровно против неё. ⚠ Величина по-прежнему не заявляется: **n = 2**, и квирки §3д требуют повторов (×2.0 за 22 пары), а не двух розыгрышей. Пере-снято мною на своей копии её замороженного прибора: числа сошлись до знака.
**2. ⭐ 85.7 % ОПЛАЧЕННОГО ВЫХОДА КНИГИ — РАЗМЫШЛЕНИЕ, А НЕ ТЕКСТ.** Замер мой, по колонке `reasoning_in_completion`, впервые заполненной живым провайдером: **153 942 из 179 613 выходных токенов**; по ролям — классификатор **97.6 %**, терминолог **92.5 %**, черновик **85.3 %**, редактор **75.2 %**. У семи выброшенных вызовов размышление съело потолок: **пять из семи — ЦЕЛИКОМ**, 8496 из 8496 и 8000 из 8000. ⚠ **Это не открытие, а подтверждение пина, который с 25.07 лежит в ДАННЫХ:** `backend/configs/models.yaml`, блок `deepseek-v4-pro`, замер мини-прогона «разница в длине РАЗМЫШЛЕНИЯ… `max_output_ratio` этого не знает и знать не может». Новое здесь — что класс верен на боевых настройках всей книги и что доля названа числом. **Ряд 422 закрыт со ссылкой на пин конфига**, иначе следующая смена откроет его заново.
**3. ⛔ ПРИБОР ПРИОРИТЕТА №1 СОЛГАЛ НА ПЕРВОМ ЖЕ ЖИВОМ ПРОГОНЕ — и это носитель, а не абзац.** Пункт 9 плана покупал «первые числа `I1`/`I2` С ЖИВОГО прогона». Первое, что они показали: **вердикт `split` по ПОДПИСАННОМУ имени героя** (`方源`) во всех трёх главах. Пере-снял руками: в отгруженном тексте **58 вхождений в четырёх падежах** («Фан Юань» 36 · «Фан Юаня» 19 · «Фан Юаню» 2 · «Фан Юанем» 1), **конкурирующих транслитераций НОЛЬ**, а все голые «Фан» принадлежат ДРУГИМ термам банка (`方正`, `方之一脉`, `方家`, `方老魔`). ⛔ **И механизм я вскрыл точнее гипотезы: `split` НЕ утверждает «две передачи».** Предикат (`backend/internal/pipeline/bookconsistency.go`, греп `func (v *verdictAcc) observe`) ставит `split`, когда в главе **попаданий МЕНЬШЕ, чем срабатываний** источника. ⇒ **имя вердикта утверждает больше, чем меряет предикат**, и читающий отчёт видит «термин разъехался» там, где сказано «нашли не каждое срабатывание». Складывается со вторым замером той же смены (эррата 11.09-з): прибор **не находит передачу у 26 термов из 69**. ⇒ **числа консистентности этого прогона БАЗОВОЙ ЛИНИЕЙ НЕ ЯВЛЯЮТСЯ до починки прибора.** Ряды **434** (не найдено 26 из 69) и **436** (имя вердикта).
**4. ⛔ КАЧЕСТВО — ВПЕРВЫЕ СУДИЛОСЬ ПО ОРИГИНАЛУ РЯДОМ С ПЕРЕВОДОМ, и вот что акт говорит СВОИМ голосом.** Читатель (отдельный опус, миллионный контекст, весь перевод 30 283 знака и весь оригинал 9021 знак целиком, доказательство дочитывания напечатано) дал **26 находок: 1 крит · 11 мажоров · 14 миноров**, по приоритетам владельца 7/6/7/6. **Крит пере-проверен МНОЮ по обоим текстам и подтверждён:** `对她来讲,自己前途光明,甲等资质的可能性极大` — перевод отдал светлое будущее и задатки первого разряда СЛУЖАНКЕ («Для неё самой будущее было светлым…»), после чего «если удастся стать наложницей Фан Юаня, она сможет из рабыни превратиться в госпожу» не следует ни из чего. **Подмена референта, разрушена причинная связь абзаца.****Адъюдикация исполнена, а не обещана:** сессия механически сверила цитаты десяти находок по обоим файлам — **10 из 10 присутствуют дословно, выдуманных нет**, — и сама исправила ТРИ числа своего читателя, считанные стеммом («род 48» → **32**: в счёт попали «родители» и «родился»; «клан 7» → **6**: один был «робко **кланя**ясь»). **Существо всех трёх устояло, числа — нет.**
**СКАЛЯР ОЦЕНКИ ПРИВОДИТСЯ ЦИТАТОЙ, А НЕ РАТИФИЦИРУЕТСЯ.** Читатель сказал: «приличный любительский, на верхней границе». **Границы, в которых это сказано, называются в том же предложении: ОДИН читатель · ОДИН прогон · книга, узнаваемая моделями 4 из 5 · адъюдикация цитат исполнена.** ⚠ И различение, которое я сперва потерял, а старший коллега вернул: **адъюдикация закрывает претрейн-риск ЧИТАТЕЛЯ (он не выдумал находок), а НЕ риск ПЕРЕВОДЧИКА** — модель, знающая книгу, переводит её не так, как незнакомую, и на этой книге это не закрывается ничем. Канон прямой: эмпирика на знакомых претрейну текстах — **предварительная**. ⇒ **делает скаляр замером не рассуждение, а два дешёвых хода:** второй читатель ДРУГОГО семейства по тем же четырём единицам (author≠reviewer) и следующий платный прогон на книге, которой в претрейне НЕТ (выбор книги — вопрос владельцу).
**5. ⭐ СВЕРКА «ПРИБОР ПРОТИВ ЧИТАТЕЛЯ» РАСПАЛАСЬ НА ЧЕТЫРЕ КЛЕТКИ, и это честнее любого общего счёта.** Свалить всё в «приборы пропустили 24 из 26» значило бы отчитаться за приборы, которых нет и не предполагалось. **(а) Прибор есть, терма нет в его популяции — молчал МАЙНЕР, не энфорсер:** восьми термов, на которых читатель поймал разнобой, в банке НЕТ вовсе, включая крупнейшую находку приоритета №1 — `家族/族` «род ↔ клан». **(б) Форма банка доехала, а дефект В САМОМ БАНКЕ:** `方家 → «род Фан»` и `方之一脉 → «ветвь Фан»` обе в банке и обе отгружены, прибор доволен — но «род» в этой книге занят кланом. ⇒ **прибор спрашивает «доехала ли форма банка» и по построению НЕ МОЖЕТ спросить «верна ли сама форма»: содержание банка не судит ничто.** **(в) Прибор знает конструкцию поимённо и по ратифицированному решению её не судит:** чекер `DC2 千万` своим комментарием объявляет класс неразрешимым офлайн — а читатель с оригиналом решил его одной строкой. Это не дыра, а ТОЧНАЯ ГРАНИЦА детерминированного гейта, и её цена теперь названа в тексте. **(г) Прибора для класса нет вовсе — 20 находок из 26:** подмена референта · согласование рода · кальки идиом · регистр · утрата эха реплики · дописанный образ.
**И один прибор ИЗМЕРИЛ предмет, но не имеет порога:** сжатие абзацев (мой счёт: **233 → 162, 30 %**) — движок печатает предложений на абзац, но знает число абзацев только в ПЕРЕВОДЕ. **Самая крупная художественная потеря прогона измерима одним вычитанием, которого никто не делает.** Ряд **437**.
**6. ЧИСЛА, С УКАЗАНИЕМ ЧЬИМ ПРИБОРОМ.** **Мои, пере-снятые на артефактах прогона:** 35 строк · 28 вызовов · 7 бесплатных подстановок · `$0.396657` · числитель-1 **7 из 17 = 41.2 %** единиц при `$0.083587 = 21.1 %` денег · числитель-2 `$0.026771` / `$0.023834` · оба денежных пути совпали · размышление 153 942 из 179 613 · 58 вхождений имени в 4 падежах при 0 конкурентах · абзацы 233 → 162 · пре-рег +638/0. **Её, приняты с названным прибором:** `wave_selection` 10 строк = 6 `draft` + 4 `edit` над РАЗНЫМИ снапшотами (у черновика инъекция пуста, у редактора 21/26/23/26 попаданий) — **число сошлось с пре-регом**; `reasoning_in_completion` трёхзначно **NULL 7 · 0 = 0 · >0 = 28**, и все семь `NULL` — ровно бесплатные подстановки, где провайдера не спрашивали; банк 66 строк, судимых 65, `I1 = 6`, `I2 = 7`; 17 узлов пре-рег-таблицы ✅; каждый из 4 отгруженных юнитов найден в скачанном файле подстрокой ровно один раз, смещения возрастают, **остаток разобран ДО НУЛЯ** (46 знаков — заголовок и переводы строк). ⛔ **И собственный аудит отчёта перед сдачей: 17 несущих чисел из 17 пере-выводятся из артефактов, ни одного «по памяти».**
**7. ⛔ ЧЕГО ПРОГОН НЕ КУПИЛ — секцией, а не умолчанием.** **Эскалация не исполнялась** (решение моё: `budget_usd: 0` не трогать ради байт-в-байт тождества покупающего файла с прогоном A; ряд **435**). **Идемпотентный резюм и терминальное состояние упражнены ЧАСТИЧНО:** резюм был один, объявленный, прогон не останавливали потолком ⇒ «не упражнялось», а не «проверено». **Один гость, без второй пары, фронт заморожен.** **Судьи на пути нет** (`role: judge` живёт только в `c2`), гейт `coverage` выключен ⇒ из пяти гейтов работали четыре. **`epubcheck` не гонялся** — `java` на хосте нет; EPUB судился структурно. ⛔ **И деньги: `$0.396657` — НАША МОДЕЛЬ, а не счёт вендора.** Прогон шёл в ОФФ-ПИК, а леджер осознанно считает по ПИКОВОЙ цене (`D39.136`) ⇒ настоящий счёт за B примерно вдвое меньше, за A — нет (A шёл внутри пика). Сравнение ЛЕДЖЕРОВ честное, фраза «мы заплатили» — нет. **Назвала это сессия, до прогона.**
**8. ⭐ ВРЕМЯ У ЭКРАНА ВПЕРВЫЕ ЗАМЕРЕНО: 20 мин 06 с на 4 единицы.** `ПТ-19` называет скорость киллерфичей, а времени у экрана никто не мерил. ⚠ Экстраполяция на книгу требует знаменателя ПО ЕДИНИЦАМ, а не по главам, и здесь единиц четыре.
**9. ⭐ ЧТО СЕССИЯ СДЕЛАЛА ЛУЧШЕ ЗАКАЗА — три вещи, каждая стоила бы круга приёмки.** **(1)** Возразила составу §4.8 ДО прогона: дёрганье API на измеряемом прогоне САМО производит число, которое потом предъявляется как свойство движка ⇒ все формы ушли на $0-руку, в платном остался один объявленный резюм. **(2)** Обе миграции она исполнила живьём на ДЫМУ и **честно не приписала это B**, а пере-формулировала, что B покупает — поведение колонки на провайдере, который поле шлёт; сверх того проверила миграцию на ЗАПОЛНЕННОЙ базе (33 строки, 69 термов, `NULL` на всех ранее написанных), потому что пустая база о данных не говорит ничего. **(3)** Назвала стохастику стохастикой: классификатор дал 42 терма из 66 без типа на попытке 1, а на резюме — 22/22 · 19/19 · 24/24; **писать «регрессия» было бы верным результатом при неверной гипотезе.** ⚠ И находка оттуда же, предъявленная кодом: **банк-роли идут МИМО обеих ветвей лечения**`terminologist.go` зовёт попытку напрямую, а лекарства живут в цикле `runStage`; счётчика доли ответов классификатора нет ни одного. Ряд **438**.
**10. МОИ ОШИБКИ СМЕНЫ, названные здесь, чтобы не повторились.** **(1)** Указал сессии неверный источник ключей: сказал «значение в `env.sh`», а там КОММЕНТАРИЙ «is NOT set here either» — совпадение пришло из рекурсивного грепа по другому файлу, и я приписал его первому в списке. Класс мой и известный: **счёт по подстроке — не счёт по владению.** **(2)** Поднял ложную тревогу «один терм тремя формами», а это оказались ДВА РАЗНЫХ исходных терма (`宗族祠堂` и `宗祖祠堂`), которые банк различает верно; снял через минуты, до того как сессия понесла её читателю. **(3)** Назвал адъюдикацию цитат закрытием претрейн-риска — она закрывает риск ЧИТАТЕЛЯ, не ПЕРЕВОДЧИКА; поправил старший коллега. **(4)** Прогнал её замороженный прибор по базе без платных строк и получил `ZeroDivisionError`**непроверенная ветвь, которую прибор сам объявлял непроверенной, упала на первом же исполнении**; правка лендится отдельным коммитом как объявленная девиация фриза.

View file

@ -145,7 +145,7 @@ Go-гейт стоит на v1 без поглощения кавычек: backe
### [RESCOPE ИСПОЛНЕН] (7) Протокол гендер-твиста D5.1 (дописка 49): «redrive есть, селективного by-until_ch пути нет»
Посылка «селективного пути нет» ОПРОВЕРГНУТА: селективная пере-редактура по изменению банка + смета ПОСТРОЕНЫ и живьём проверены. `backend/internal/pipeline/repin.go:14-35`=`POINTWISE re-edit by key` — «POINTWISE re-edit by key (pack-20 point 5, D39.42 п.5)»: bank-only сдвиг снапшота + неизменный content_hash → пере-пин юнита за $0, изменённые инъекции — платно; «The same predicate drives the ESTIMATE: projectRebill uses it» (смета «N units, ~$X»); встроено в штатный резюм — stagerun.go:87 (repinnable); согласие на пере-оплату — rebill.go:11-40 (D20.2-Q2). until_ch-апдейт = правка строки банка → двигается ТОЛЬКО memory_version → ровно этот путь (D39.42 п.5: «поздняя подпись и ЛЮБАЯ правка»; D39.45: «точечная ре-редактура по ключу с $0-пере-пином — построено, живьём проверено... 20 юнитов пере-пинены за $0 при bank-only-сдвиге»). При этом верно: tmctl redrive существует (`cmd/tmctl/main.go:233`=`case "redrive":`), но его селектор — только chapter/chunk/reason по ФЛАГНУТЫМ чанкам (status.go:564-582), until_ch-пути в НЁМ нет — селективность живёт в другом инструменте (translate-резюм + repin + rebill), и это by-design правильный носитель.
Посылка «селективного пути нет» ОПРОВЕРГНУТА: селективная пере-редактура по изменению банка + смета ПОСТРОЕНЫ и живьём проверены. `backend/internal/pipeline/repin.go:14-35`=`POINTWISE re-edit by key` — «POINTWISE re-edit by key (pack-20 point 5, D39.42 п.5)»: bank-only сдвиг снапшота + неизменный content_hash → пере-пин юнита за $0, изменённые инъекции — платно; «The same predicate drives the ESTIMATE: projectRebill uses it» (смета «N units, ~$X»); встроено в штатный резюм — stagerun.go:87 (repinnable); согласие на пере-оплату — rebill.go:11-40 (D20.2-Q2). until_ch-апдейт = правка строки банка → двигается ТОЛЬКО memory_version → ровно этот путь (D39.42 п.5: «поздняя подпись и ЛЮБАЯ правка»; D39.45: «точечная ре-редактура по ключу с $0-пере-пином — построено, живьём проверено... 20 юнитов пере-пинены за $0 при bank-only-сдвиге»). При этом верно: tmctl redrive существует (`cmd/tmctl/main.go:234`=`case "redrive":`), но его селектор — только chapter/chunk/reason по ФЛАГНУТЫМ чанкам (status.go:564-582), until_ch-пути в НЁМ нет — селективность живёт в другом инструменте (translate-резюм + repin + rebill), и это by-design правильный носитель.
**Живой остаток дописки** (пере-формулировка исполнена — строка 49 бэклога и D39.66-пост-сверка п.3): только операторский ПРОТОКОЛ-документ релиза D5.1 — последовательность «выставить `until_ch` у `gender=hidden`-терма → `tmctl translate` → согласие на rebill-смету», плюс курация 白凝冰 из строки 24.

View file

@ -19,7 +19,7 @@
- **Гейт потолков — ПЕР-ВЫЗОВНЫЙ** (`Reserve` на каждый свежий attempt). На границе юнита сидел РЕПЭЙР-суб-бюджет — ужесточён до пер-вызовного с ценой вызова (строка 135 закрыта D39.131); эскалационный кап хоп НЕ прицениваает — перелёт ≤1 хопа, задокументирован и запинен (диспозиция D39.131 п.2д, реопен — живой инцидент). ⚠ **ТРЕТЬЕ семейство, которого перечень не знал (доп. ревизией 02.09): банк-роли несут СОБСТВЕННЫЕ КНИГО-ШИРОКИЕ бюджеты**`gates.terminology.budget_usd` и `gates.terminology.classify_budget_usd`. Именно оно резало ОПЛАЧЕННУЮ работу на холодном прогоне 31.08 (D39.182 §4: инцидент был на классификаторе). С 31.08 (D39.182) план прохода режется ценой партии ДО первого вызова, а не обрывается посередине; усечение ВИДНО в отчёте — поля `BatchesDropped` и `ClassifyBatchesDropped` (`backend/internal/pipeline/terminologist.go`, греп `BatchesDropped`). ⚠⚠ **И сами цифры этих суб-бюджетов в книжных конфигах КАЛИБРОВАНЫ ПОД ИЮЛЬСКИЕ ЦЕНЫ** (тот же множитель ×4.47, D39.179 п.1): на холодном прогоне 31.08 `classify_budget_usd` 0.02 оборвал классификатор дважды, а поднятый до 0.08 `escalation.budget_usd` был пробит фактом до 0.103305. То есть суб-бюджеты режут ОПЛАЧЕННУЮ работу не по замыслу, а по протухшей калибровке.
- **leftover-reserved зануляется write-open.** `store.Open` (путь записи, каждый `translate`) выполняет `recoverReservations` (`backend/internal/store/store.go:110`=`s.recoverReservations(ctx)`; сама функция — `backend/internal/store/store.go:278-279`=`UPDATE spend SET reserved_usd = 0`) — файл владеется одним процессом, значит любой reserved на открытии принадлежит несеттлённому прогону. `OpenReadOnly` этого прохода намеренно НЕ делает (`backend/internal/store/store.go:124`=`does not run that pass`) ⇒ **reserved, увиденный read-only `status` В МОМЕНТ СПАВНА, — остаток мёртвого процесса** (несущий факт формулы PD-158, см. §3). ⚠ Но НЕ «всегда»: `OpenReadOnly` построен ровно затем, чтобы `status` работал ВО ВРЕМЯ живого прогона (`backend/internal/store/store.go:122`=`allows concurrent readers while a writer is live`), и конкурентный `status` покажет ЖИВУЮ резервацию между `Reserve` и settle. Узко формулирует и сам код: «after a run crashes, reserved_usd stays non-zero until the next WRITE command» (`backend/internal/store/store.go:130`=`after a run crashes, reserved_usd stays non-zero`), и платформа — «at spawn there is no other writer … so anything reserved is by construction a leftover, never a live promise» (`platform/internal/runs/spawn.go`, греп `never a live promise`).
- **`--max-units` — ОБЪЁМНЫЙ потолок прогона, ортогональный денежному** (D39.165 §1б, принят D39.170). Ограничивает не деньги, а РАБОТУ: не больше N выходных ЮНИТОВ (гранулярность `units_total` манифеста — та же, в которой платформа продаёт главы) возьмут СЛОТ гранта в этом прогоне; юниты, отданные за $0 (резюм, ре-пин), ретраи и эскалации внутри юнита потолок не тратят. ⚠ **«Слот» ≠ «оплата», и с 03.09 это РАЗНЫЕ числа** (пак «число согласия», строка бэклога 232): юнит, который прежний прогон НАЧАЛ и не отгрузил, дописывается ВНЕ гранта, поэтому грант N оплачивает ДО 2N выходных юнитов — замерено приёмкой на живом раннере (грант 2 → `Paid()=4`, шесть вызовов провайдера). Единственный денежный бонд здесь — `--ceiling-usd`. ⛔ **Семантика переноса НЕ ратифицирована — слово владельца 03.09 «подумаем на этот счёт»**, и до его решения читать это как замеренный факт, а не как норму. Принимает только `translate`. **Остановка по объёму — ЗАВЕРШЕНИЕ (exit 0), не пауза:** словарь кодов выхода не расширялся и нового значения `Finished.Outcome` тоже нет — ⚠ **но с 31.08 признак едет ЧИСЛАМИ в кадре `finished` шва, а не только прозой** (D39.181 п.2, закон раскрытия: прозаическая строка отчёта до потребителя потока не доезжала): носитель — `Finished.Volume`, леджер доставки. Различение при этом живёт и в отчёте прогона, и в логе; отчёт разводит ДОСТАВКУ и ПЕРЕ-ДЕЛКУ. Носитель — `backend/internal/pipeline/volume.go:13`=`the VOLUME ceiling — the run's second stop`; словарь флага дословно — `backend/cmd/tmctl/invocation.go:143`=`Stopping on it is a COMPLETION (exit 0), not a pause`. ⚠ Проводка в платформу ГЕЙЧЕНА (`PD-422`): единственный писатель признака движения банка — дверь правок, рост АВТО-банка от майнинга флага не ставит.
- **`--ceiling-usd` — КНИЖНЫЙ потолок, не бюджет прогона.** Дословно из флага: «the book USD ceiling in force for THIS RUN ONLY — it OVERRIDES book.yaml `ceilings.book_usd` and is never written back. It caps the book's CUMULATIVE committed+reserved spend, not this run's increment…» (`backend/cmd/tmctl/invocation.go:142`=`the book USD ceiling in force for THIS RUN ONLY`; ⚠-коммент `backend/cmd/tmctl/main.go:260`=`--ceiling-usd is NOT a per-run budget`; многоточие закрывает обрыв цитаты — во флаге дальше стоит «, and must be > 0»). Ратификация — D39.122 п.2(в): пересчёт «пользовательский прирост → абсолют» — обязанность ПЛАТФОРМЫ, вторая денежная ось не заводится. День-потолок флаг НЕ перекрывает (PD-157). Проводка внутри: `Ceilings.BookUSD` в `Reserve` и именование сработавшего потолка в ошибке — `backend/internal/pipeline/stagerun.go:571`=`BookUSD: r.bookCeilingUSD()` и `backend/internal/pipeline/stagerun.go:658`=`overrides the book's ceilings.book_usd` (стоп = `errReserveCeiling`, `backend/internal/pipeline/escalation.go:49`=`var errReserveCeiling`).
- **`--ceiling-usd` — КНИЖНЫЙ потолок, не бюджет прогона.** Дословно из флага: «the book USD ceiling in force for THIS RUN ONLY — it OVERRIDES book.yaml `ceilings.book_usd` and is never written back. It caps the book's CUMULATIVE committed+reserved spend, not this run's increment…» (`backend/cmd/tmctl/invocation.go:142`=`the book USD ceiling in force for THIS RUN ONLY`; ⚠-коммент `backend/cmd/tmctl/main.go:261`=`--ceiling-usd is NOT a per-run budget`; многоточие закрывает обрыв цитаты — во флаге дальше стоит «, and must be > 0»). Ратификация — D39.122 п.2(в): пересчёт «пользовательский прирост → абсолют» — обязанность ПЛАТФОРМЫ, вторая денежная ось не заводится. День-потолок флаг НЕ перекрывает (PD-157). Проводка внутри: `Ceilings.BookUSD` в `Reserve` и именование сработавшего потолка в ошибке — `backend/internal/pipeline/stagerun.go:612`=`BookUSD: r.bookCeilingUSD()` и `backend/internal/pipeline/stagerun.go:699`=`overrides the book's ceilings.book_usd` (стоп = `errReserveCeiling`, `backend/internal/pipeline/escalation.go:49`=`var errReserveCeiling`).
- **Потолки — wiring, не семантика:** `Ceilings` намеренно исключены из `BriefHash` («Wiring fields (paths, ceilings, db) deliberately excluded», `backend/internal/config/book.go:345`=`Wiring fields (paths, ceilings, db) deliberately excluded`; канон — `backend/internal/config/book.go:369`=`canon := struct {`) ⇒ смена ДЕНЕЖНОГО потолка не двигает ни снапшот, ни ре-билл (D39.110 п.2б). ⚠ Верно ровно про ДЕНЬГИ: потолок СЕГМЕНТАЦИИ `edit_ceiling_out` в снапшот ВХОДИТ и меняет границы чанков (`backend/internal/pipeline/snapshot.go:34`=`EditCeilingOut int`).
- **`status --json` отдаёт фигуры платформе:** `committed_usd`, `reserved_usd`, `book_ceiling_usd`, `ceiling_pct` = 100·(committed+reserved)/book_ceiling (`backend/internal/pipeline/status.go:324`=`book_ceiling_usd,omitempty`; арифметика `ceiling_pct``backend/internal/pipeline/status.go:918`=`100 * (committed + reserved)`). Дневной фигуры в status нет (PD-157 — `platform/docs/DEFECT_REGISTER.md`, греп `PD-157`).
- **Леджер = НИЖНЯЯ граница; строка 78 снята с трекера 10.09, но её ЖИВОЙ ОСТАТОК несёт строка 377.****Испр. 10.09 — прежняя редакция этого пункта, написанная в тот же день, была НЕВЕРНА, и ошибка моя.** Она утверждала, будто оплаченный `2xx` с нечитаемым телом «прежде в `request_log` не попадал» и что это починил пак оборванных вызовов. Замер: ветвь `BilledDecodeError` писала свою строку с пометкой оценки ещё до пака — на `3f05fab^` это `stagerun.go:629`, введено коммитом `2f91b04` **24.07**, а пак этой ветви не касался вовсе (хитов `Estimated` в его диффе по `stagerun.go`**0**). И носитель я процитировал ЧУЖОЙ: `backend/internal/pipeline/cutcall.go:133` обслуживает `*llm.AttemptCutError` (`backend/internal/pipeline/stagerun.go:741`), а речь шла о `*llm.BilledDecodeError` (`stagerun.go:709`) — это разные классы, и второй до `cutcall.go` не доходит. **Что пак действительно купил:** строку `request_log` за вызов, который оборвали МЫ (прежде он падал в ветвь «ничего не куплено» и получал $0-строку) — вот это `cutcall.go:133`. **Что остаётся правдой:** леджер — нижняя граница, потому что обрыв, за которым ретрай УСПЕЛ, исчезает бесследно (строка **377**). Живой замер `D39.86` — 3 вызова из 14, неизвестность $0.015111 при леджере $0.114378.
@ -72,7 +72,7 @@
| Словарь терминов | `docs/glossary.md:58`=`**Деньги платформы:**` |
| Управляемый потолок: решение и форма | D39.110 (+поправка D39.112 п.3: п.1 — пересказ, не цитата) |
| Максимум шкалы = Balance как есть | D39.115 п.2а; эррата — шапка D-лога, `docs/architecture/05-decisions-log.md:4`=`Balance КАК ЕСТЬ` |
| Семантика `--ceiling-usd` | D39.122 п.2в; `backend/cmd/tmctl/invocation.go:142`=`the book USD ceiling in force for THIS RUN ONLY`, `backend/cmd/tmctl/main.go:260`=`--ceiling-usd is NOT a per-run budget` |
| Семантика `--ceiling-usd` | D39.122 п.2в; `backend/cmd/tmctl/invocation.go:142`=`the book USD ceiling in force for THIS RUN ONLY`, `backend/cmd/tmctl/main.go:261`=`--ceiling-usd is NOT a per-run budget` |
| Формула аргумента потолка (без reserved) · ставка · settle-формы | D39.123 п.2б/г/д; PD-158/PD-159/PD-144 в реестре |
| Код денег платформы | `platform/internal/pgstore/credits.go` · `pgstore/runs.go` · `runs/spawn.go` · `runs/reconcile.go` · `pricing/pricing.go` |
| Ратификации, на которых стоит этот док | ⚠ **добавлены 05.09: `D39.203`** (владелец: движку МОЖНО говорить «сколько добавить»; наружу идёт ДО-ВЫЗОВНАЯ ОЦЕНКА, не стоимость) · **`D39.204`** (отказ резервации на ретрае деградирует во ФЛАГ; решение принято, код НЕ тронут — строка 291) · **`D39.206`** (акт приёмки денежного стопа). ⚠ **И новые движковые денежные носители того же лендинга:** `backend/internal/pipeline/reservegate.go` (шлюз резерваций и `shortfallMicroUSD`) · `priceprojection.go` (проекция цены книги) · `internal/runevents/runevents.go` (`Ceiling.ShortfallMicroUSD`, `Finished.Money`, поток 1.3). · **D39.179** (пере-пин цен 16.08, ×4.47; норма «стабилен чистый ФАЙЛ» — п.4) · **D39.181** (закон раскрытия: признак объёмного стопа едет числом) · **D39.182** (пак «деньги и честность»: план банк-ролей режется до первого вызова; порог согласия без контура банк-ролей) |

View file

@ -1,5 +1,15 @@
# Бэкенд-пак: ЗА ОДНУ ЕДИНИЦУ ПЛАТИМ ДВАЖДЫ — и по двум РАЗНЫМ причинам, которые движок не различает
> ✅ **АРХИВ — ПАК ОТРАБОТАН И ПРИНЯТ С ДОФИКСОМ 11.09: акт `D39.251`** (22 пути `backend/` +947/66, 8 новых
> тестовых файлов, `^func Test` +27/0, $0). ⭐ Наблюдаемость двойной оплаты построена, **лекарство лендится
> ВЫКЛЮЧЕННЫМ** (`retries.lower_effort_on_empty: false`) и достаёт ОДИН случай из четырёх — сказано словом зоны
> до приёмки; условие включения — ряд **433**. ⛔ **Посылка этого промта была ПЕРЕВЁРНУТА, и сняла её зона ДО
> первой правки:** `degraded='empty'` ×3 = $0.094835 = 89 % потери против `'length'` ×1 = $0.011637 — обратное
> таблице редакции 2; `off` у `ReasoningNone`ВЕРХ шкалы, а не низ. §4.3 тоже стоял на неверной посылке:
> `reasoning_tokens` провайдер ПРИСЫЛАЕТ (35 из 35), ронял его наш адаптер. Закрыт ряд **422**; наполовину —
> **414**, **415**, **429**. ⛔ **Инструкции отсюда НЕ ИСПОЛНЯЮТСЯ** — читать только как историю предмета.
> Выдан оркестратором №23, 11.09. Зона записи — `backend/`. Пак КОДОВЫЙ, платных вызовов НЕ несёт.
> ⚠ **Редакция 2.** Первая была построена на перевёрнутой посылке («движок лечит болезнь возбудителем») и
> заказывала отключить восстановление, которое на замере работало. Опровергатель это снял; ниже —
@ -146,7 +156,7 @@
ЧИТАЕТСЯ. Второй разметки не заводить.** Найдёшь расхождение — находка, в отчёт.
А выбор лекарства этого не спрашивает: `maxTokensForAttempt(base, escalations)`
(`internal/pipeline/disposition.go:481`=`func maxTokensForAttempt`) принимает **счётчик удвоений и больше ничего**.
(`internal/pipeline/disposition.go:490`=`func maxTokensForAttempt`) принимает **счётчик удвоений и больше ничего**.
**Что построить: различение двух причин и разные ответы на них.** Приор — опровергается аргументом или
замером, и я жду именно опровержения, если оно у тебя будет:
@ -274,7 +284,7 @@ a named finding, not a silent gap». ⇒ **пер-модельная табли
протухшая редакция, не наследуй её.
**Сегодня это безвредно:** подстановка падает на ЗАПРОШЕННУЮ модель той же семьи. ⛔ Опасна не подстановка,
а её молчание. `PriceForResponse` (`internal/ledger/pricing.go:61`=`func (p *Pricer) PriceForResponse`) пробует `actual`, потом `requested`,
а её молчание. `PriceForResponse` (`internal/ledger/pricing.go:88`=`func (p *Pricer) PriceForResponse`) пробует `actual`, потом `requested`,
потом якорь — и ни одно из **двух** мест вызова (`stagerun.go:781`=`PriceForResponse(model, modelActual)`, `reprice.go:134`=`ledger.CostUSD(r.Pricer.PriceForResponse`) не различает, какая
ветвь сработала. Комментарий там же охраняет РОВНО ОДНО направление («премиальный ответ не должен биться
по дешёвому дефолту»); обратное — когда провайдер маршрутизирует ВНИЗ — не охраняет никто: запрошен `pro`,

View file

@ -0,0 +1,516 @@
# Промт: сквозной пак «ХОЛОДНЫЙ ПРОГОН B — платформа → бэкенд → ФАЙЛ, ВТОРОЙ раз»
> ✅ **АРХИВ — ПАК ОТРАБОТАН И ПРИНЯТ 11.09: акт `D39.252`.** Книга второй раз дошла от двери до ФАЙЛА,
> факт **$0.396657** при ожидании ≈$0.42; пре-рег заландился ДО первого цента (`4ee7f03`) и не тронут
> (+638/0). ⭐ Две меры денежной дыры разошлись в РАЗНЫЕ стороны на одних данных (единицы 23.5→41.2 %,
> деньги 25.4→21.1 %). ⭐ **85.7 % оплаченного выхода книги — размышление, а не текст.** ⛔ Прибор
> приоритета №1 солгал вердиктом `split` по подписанному имени героя при 58 вхождениях и нуле
> конкурентов. Качество впервые судилось по ОРИГИНАЛУ рядом с переводом: 26 находок, 1 крит.
> Ряды 434 · 436 · 437 · 438. ⛔ **Инструкции отсюда НЕ ИСПОЛНЯЮТСЯ** — читать только как историю.
> Выдан оркестратором №23 (сессия `textmachine-11`) 11.09. Пак **ПЛАТНЫЙ**, санкция владельца получена
> дословно («разрешаю») на прямой вопрос с числами: **ждём ≈$0.42, машинный кап ≈$2.5 холда**. Носитель
> санкции — `docs/NEAR_TERM_PLAN.md` §«Решения владельца, уже полученные».
## 1. Какая проблема и что решит твой результат
Месяц назад движок умел переводить, но никто не видел, как платформа ведёт настоящую книгу от двери до
файла. 11.09 это увидели один раз — **прогон A**: три главы `蛊真人`, 27 платных вызовов, **$0.419423**, книга
дошла до EPUB. Один прогон дал знак, но не величину.
**Твой прогон B покупает четыре вещи, которых нет ни у кого, и каждая — не «ещё раз то же самое»:**
1. **ДВЕ миграции схемы ни разу не шли на живом прогоне** — только в тестах, и база прогона A стоит на
`schema_version` **16**. `v17``wave_selection` (`backend/internal/store/migrate.go:484`=`CREATE TABLE IF NOT EXISTS wave_selection`): что каждая
волна ВИДЕЛА в банке, ось, которой у `retrieval_state` нет. `v18`**`reasoning_in_completion`**
(`migrate.go:519`), и ⛔ **это НЕ `reasoning_tokens`**: тот лежит в базовой схеме как
`INTEGER NOT NULL DEFAULT 0` (`migrate.go:94`) и на DeepSeek равен нулю ПО ПОСТРОЕНИЮ — адаптер
намеренно оставляет его нулём и уводит присланное провайдером число в новую колонку
(`provider_openai.go`, ветка `ReasoningSubset`). Новая колонка **НУЛЛАБЕЛЬНА и трёхзначна: `NULL`
вопрос не задан · `0` — измеренный ноль · `>0` — думанье**, и `DEFAULT 0` не поставлен намеренно, иначе
дефект вернулся бы первой же строкой. **Приёмка спрашивает ТРЁХЗНАЧНОСТЬ, а не «не ноль».**
2. **Прибор консистентности впервые даст числа С ЖИВОГО ПРОГОНА.** Сегодня все `I1`/`I2` получены
ПОВТОРНЫМ чтением одной и той же сохранённой базы прогона A. Пере-скан не то же, что прогон: он не
проходит через запись.
3. **Платформенный пак (идемпотентный резюм, терминальное состояние) проверяется СКВОЗЬ,** а не юнитами.
4. **Вторая точка по денежной дыре — но ЧЕСТНО о том, что она даёт.** Весь довод «четверть денег купила
пустоту» стоит на ОДНОМ прогоне. ⚠ И не обманывайся ссылкой на квирки §3д: там сказано, что парный
дизайн ловит эффект ×2.5 за **13 пар**, ×2.0 за 22, ×1.4 за 93 — **n = 2 этой нормы не выполняет и
выполнить не может**. ⇒ B покупает не величину, а **пре-регистрированный знаменатель и проверку ЗНАКА**.
Написать в отчёте «25.4 % подтверждено» по двум розыгрышам — ошибка, и я её не приму.
**И вот чего твой прогон НЕ покупает, чтобы ты не потратил на это ни цента:** он **НЕ** отвечает на
вопрос «безопасно ли редактору думать меньше» (ряд 433). Это парный дизайн на повторах с армом без
фактора, он стоит отдельных денег и отдельного заказа. Твой прогон идёт на БОЕВЫХ настройках как есть.
## 2. Зона записи и git
**ЭТОТ ПАК НЕ ПРАВИТ КОД НИ ОДНОЙ ЗОНЫ.** Ни `backend/`, ни `platform/`, ни `frontend/`. Найдёшь дефект —
**пингом мне, а не правкой**: прогон, чинящий найденное, перестаёт быть холодным и теряет право сравниваться
с A.
- **Ты не коммитишь вообще ничего.** Пре-рег и улики передаёшь мне, я лендлю. Порядок жёсткий:
пре-рег готов → пинг мне → **я коммичу фриз** → и только после этого первый платный вызов.
- Рабочее дерево прогона живёт **вне репозитория и вне общего скретчпада** (его чистит не только твой
процесс). Предлагаю `~/tm-coldrun-b`.
- ⚠ `books/` версионируются ОТДЕЛЬНЫМ репозиторием; в клоне их нет. Исходник читается из главного дерева
как ФАЙЛ, read-only.
## 3. Карта чтения — ЗАКОН, дальше только по её ссылкам
1. `CLAUDE.md` — целиком (ты его уже прочёл, если попал сюда правильно).
2. `docs/experiments/00-provider-quirks.md`**перед первым платным вызовом**, §3д особенно.
3. `docs/architecture/05-decisions-log.md`**только тела `D39.247`** (акт прогона A: чем он предъявлен и
где остался слеп) и **`D39.251`** (что именно построено в наблюдаемости денег и почему лекарство
выключено).
4. `platform/README.md` — как поднимается стенд.
5. Архивный промт прогона A — `docs/archive/prompts/POLYGON_COLD_RUN_A_SESSION_PROMPT.md`. ⛔ **Инструкции
оттуда НЕ ИСПОЛНЯЮТСЯ** (архив), но §4.1 и §4.2 несут ПЕРЕ-СНЯТЫЕ предусловия стенда и механику фриза,
и переписывать их сюда значило бы завести копию, которая стареет молча. Читай как СПРАВКУ и проверяй
каждое утверждение своим прибором: часть из них снята исправлениями ПОСЛЕ выдачи.
Код-факты добывай сам. `file:line` ниже — отправные точки, а не истина: **код первичен**.
## 4. Состав. Порядок фаз ЖЁСТКИЙ
### 4.0 Фаза 0 — $0-ДЫМ. **Делай РОВНО так, и это первая работа пака**
**Ни одного платного вызова, пока дым не прошёл целиком.** Довод не гигиенический: первая же
инфраструктурная осечка на платной руке сожжёт «холодность» книги — прогон уйдёт в `failed`, и следующий
старт той же книги пойдёт как ПРОДОЛЖЕНИЕ, а не как холодный.
⛔ **И ровно поэтому — дописано релеем 11.09 по возражению исполняющей сессии: ДЫМ ИДЁТ НЕ ПО ТОЙ КНИГЕ И
НЕ В ТОЙ БАЗЕ, что платный прогон.** Иначе холодность сгорит тем самым механизмом, ради защиты которого
§4.0 и написан: платный старт пойдёт продолжением дымовой книги. Отдельная книга либо отдельная база —
выбирай, но назови в пре-реге. ⚠ Это же частично снимает §5.2 (дым меняет три фактора разом).
Дым обязан доказать: стенд поднят и порт отвечает ТВОЕМУ pid · гость заведён · деньги начислены · книга
принята · прогон стартовал · кадры доехали · узел подписи ответил · резюм прошёл · **выгрузка собралась и
файл скачался**.
⛔ **И вот ловушка, на которой этот пункт разваливается, если о ней не сказать: `zeroCostPipeline`
(`platform/internal/runner/bankapply_live_test.go`) только ПЕРЕПИСЫВАЕТ конфиг на провайдера `kind: local`
он никого не ОБСЛУЖИВАЕТ.** Единственный слушатель на `127.0.0.1:11434` в дереве — внутритестовая заглушка
(`platform/internal/runner/translate_resnapshot_live_test.go`, греп `net.Listen`), а на хосте локальной
модели нет вовсе. ⇒ **тебе РАЗРЕШЕНО поднять свою заглушку на 11434** (портировав ту же, или исполнив
несущий её живой тест) — и это не обход, а штатный путь. ⚠ Не сделав этого, ты упрёшься в отказ соединения
посреди дыма, и дешёвым выходом станет «дым неприменим, иду за деньгами» — ровно то, ради предотвращения
чего §4.0 существует.
**Пин фазы 0 — ЗАГРУЗЧИК, а не текстовый скан.** Счёт платных слагов в получившемся конфиге оставь
КОНТРОЛЬНОЙ величиной (ноль против ненулевого счёта тех же слагов в `backend/configs/pipeline-c1.yaml`), но
**сам пин — движок, который этот файл ЗАГРУЗИЛ бесплатным глаголом**. Довод замерен и стоил платформенной
зоне круга приёмки: её рецепт был «проверен гардом» — сканом имён — и производил файл, который движок
отказывается грузить (`escalate_to must differ from the primary model`, `EXIT=10`). Имя в конфиге и
загружаемый конфиг — разные предметы.
### 4.1 Предусловия — **проверь КАЖДОЕ своим прибором, список от прогона A мог устареть**
Известное на 11.09 (справка, не истина): локальной модели на машине НЕТ (ни `ollama`, ни слушателя на
11434) ⇒ настоящие узлы проходятся только платно · кластер Postgres поднят на `-h /tmp -p 55433 -U postgres`,
ОС-роль `ubuntu-26` в нём НЕ заведена · четыре переменные окружения решают, существует ли путь вообще
(`TM_PLATFORM_EXPORT_FORMATS` пуст по умолчанию ⇒ ручки выгрузки не монтируются, и последней трети пути для
тебя нет) · `epubcheck`/`java` на хосте нет и поставить их нельзя.
⛔ **АРТЕФАКТ КОНТРАСТА МАЙНИНГА — и здесь я в первой редакции ПЕРЕВЕРНУЛ факт, поймал опровергатель.**
`CheckMiningContrast` делает **ТОЛЬКО `os.Stat`** (`backend/internal/config/pipeline.go:558`=`func (p *Pipeline) CheckMiningContrast`) —
никакой sha он не сверяет и **удовлетворится фальшивым кандидатом**, который лежит рядом на диске
(`~/tm-p9-work/cfg/contrast-zh.txt`, 992 байта). ⇒ **sha256 сверяешь ТЫ, руками**: рецепт и эталон —
`docs/experiments/16-bank-mining.md:183`=`Словарь-артефакт (контраст)` (jieba 0.42.1 `dict.txt`,
`7197c3211ddd98962b036cdf40324d1ea2bfaa12bd028e68faa70111a88e12a8`). ⚠ И ловушка имени: в конфигах файл
зовётся `mining-contrast.zh.txt` и резолвится ОТ КАТАЛОГА `pipeline.yaml`, а источник —
`jieba_dict_general_zh.txt`. **Неверный артефакт молча меняет контур банка, то есть портит ровно те числа
консистентности, ради которых B и покупается.**
⛔ **ПИН ДО ФРИЗА, дешёвый и обязательный: `GET /v0/capabilities` показывает НЕПУСТОЙ `export_formats`.**
Выгрузка гейтится ТРЕМЯ условиями сразу (`platform/internal/config/config.go`, греп `ExportsEnabled`), а не
одной переменной, и проба возможностей — единственный дешёвый способ узнать, что выполнены все три, ДО того
как двинутся деньги.
**Ключи провайдера.** Значение `TM_PLATFORM_ENGINE_KEYS_PATH` даю я по твоему пингу. **Нет значения ⇒
СТОП ДО фриза.** `.env` читать запрещено гардрейлом.
### 4.2 Фриз — **делай РОВНО так**
**Фриз = sha коммита ПЛЮС бинари, собранные из ЛОКАЛЬНОГО КЛОНА** (`git clone <репо> <путь>`). Довод —
главное дерево движут параллельные сессии.
**Не `git worktree`.** Бинарь из ЛИНКОВАННОГО воркри не несёт VCS-штампа **ВООБЩЕ** (замер: 24 строки
`go version -m`, из них `vcs.*` — 0), потому что `.git` воркри — ФАЙЛ и поиск корня VCS в Go его пропускает.
⇒ правило «отказывать при `vcs.modified=true`» на таком бинаре выполняется ВСЕГДА и не проверяет ничего.
У клона `.git` — настоящий каталог, и штамп живой. **Третье условие отказа обязательно: отсутствие строк
`vcs.*`.** Перед сборкой — `test ! -e /tmp/.git`; `GOFLAGS=-buildvcs=false` в фриз-сборке **ЗАПРЕЩЁН**.
⚠ Штамп снимается **на КАЖДУЮ попытку**, а не один раз: резюм может сменить бинарь под тем же прогоном —
сверяй по `run_attempts`, а не по тому, что лежит на PATH в конце.
⛔ **ПОРЯДОК ЖЁСТКИЙ, и он важен потому, что пре-рег обязан НАЗЫВАТЬ тот самый sha, который поедет:**
клон → записал sha и собрал бинари → написал пре-рег, НАЗЫВАЮЩИЙ этот sha → отдал мне → **я коммичу**
ты сверил, что заландженный пре-рег называет твой sha → и только теперь первый платный вызов.
**Что делать, если я молчу.** Канал может отсутствовать — это нормальный случай, а не авария. **Ждёшь
меня до конца своего хода, потом ОСТАНАВЛИВАЕШЬСЯ и пишешь отчёт** с готовым пре-регом и пометкой «жду
лендинга фриза и значения `TM_PLATFORM_ENGINE_KEYS_PATH`». ⛔ **Без обоих платная фаза не начинается ни при
каких обстоятельствах** — и это не формальность: пре-рег, не заландженный ДО денег, перестаёт быть
пре-регом.
**Что фриз НЕ запрещает:** потолки правятся **БЕЗ пере-фриза** — фриз-гейт смотрит на ПОКУПАЮЩИЙ файл, а
не на кассу. Меняется ЧТО покупается или чем судится — новый фриз; меняется СКОЛЬКО тратить — объявление в
отчёте. Правка собственного драйвера после фриза — тоже новый фриз.
### 4.3 Пре-регистрация — **главный рубеж пака, и он дороже самого прогона**
**ЗНАМЕНАТЕЛЬ ПРЕ-РЕГИСТРИРУЕТСЯ ДО ДЕНЕГ, иначе B ответит числом на другой вопрос.** B — ЕЩЁ ОДИН
одиночный прогон, и две точки распределением не станут. Настоящая ценность B не «вторая точка к 25.4 %», а
`request_log` ВСЕХ единиц прогона ⇒ **доля выброшенных первых попыток ПО ЕДИНИЦАМ, а не по вызовам.**
**Мера задана мной, делай РОВНО так** (я снял её на прогоне A своим прибором, числа ниже — твой базис
сравнения):
- **единица** = `(chapter, chunk_idx, stage, role)`. ⚠ **Единица ОХВАТЫВАЕТ попытки и охватывает их
намеренно** — номера попытки в ключе нет. И знай асимметрию: у стадии `terminology` все строки прогона A
несут `chapter = 0`, то есть `chunk_idx` там — индекс майнинг-батча, а не чанк главы. Одно слово
«единица» покрывает два разных предмета; называй, какой именно, в каждом числе.
- **знаменатель** = единицы, за которые заплачено хоть раз (`sum(cost_usd) > 0`). ⚠ На прогоне A этот
фильтр **не отсеял НИЧЕГО** (17 единиц всего и 17 платных) — значит его поведение ни разу не проверялось,
и на B единица из одних чекпойнтов может появиться впервые. Печатай ОБА счёта.
- **ЧИСЛИТЕЛЬ ПЕРВЫЙ — выброшенная покупка:** единицы, где есть хоть одна оплаченная и НЕ принятая попытка
(`ok = 0 AND cost_usd > 0`). **Прогон A: 4 из 17 = 23.5 %**, тогда как по ВЫЗОВАМ то же событие даёт
4 из 27 = **14.8 %**. Две меры расходятся в полтора раза — вот почему мера объявляется заранее.
- ⛔ **ЧИСЛИТЕЛЬ ВТОРОЙ, и первая редакция этого промта его СЛЕПО ПРОПУСКАЛА** (нашёл опровергатель, я
пере-снял сам): **единицы, за ПРИНЯТЫЙ результат которых заплачено больше одного раза** (`ok = 1`,
ячейка куплена дважды). На прогоне A это вся стадия терминологии, пере-купленная на резюме:
**$0.028742 = 6.9 % цены книги**, и мера «выброшенная первая попытка» этого НЕ ВИДИТ вовсе — строки
`ok = 1`. ⚠ Черновик на том же резюме приехал бесплатно (6 строк `tm_hit = 1`), терминология — нет.
**Обе доли печатаются отдельно; складывать их в одно «потеряли N %» НЕЛЬЗЯ — это разные беды.**
**ДВА ПУТИ К ДЕНЕЖНОМУ ЧИСЛУ — и честно о том, чего их совпадение стоит.** Путь 1 — по колонке
`degraded`; путь 2 — по `ok = 0 AND cost_usd > 0`, колонки `degraded` не касаясь. У меня на A оба дали
ровно **$0.106472 = 25.4 %** цены книги. ⛔ **Но на A они совпадают ПО ПОСТРОЕНИЮ**, а не независимо:
`degraded` там принимает ненулевое значение ровно на тех же четырёх строках. ⇒ совпадение на B — слабая
улика, а вот РАСХОЖДЕНИЕ — сильная находка: оно означает, что классификатор и признак приёмки разъехались.
⚠ И не путай числа: **25.4 % — доля ДЕНЕГ, 23.5 % — доля ЕДИНИЦ.** Это разные величины, и подменять одну
другой в отчёте нельзя.
В пре-рег идут также: **ожидаемый исход КАЖДОГО шага, написанный ДО прогона** · sha256 исходника · sha256
ФАКТИЧЕСКОГО pipeline-YAML (платформа берёт файл ЦЕЛИКОМ, пер-полевого оверрайда нет ⇒ «прогон на c1» без
sha — утверждение без носителя) · список глав с `units_total` · грант, TTL выгрузки.
⛔ **ПРЕТРЕЙН — И ЭТО ТЕПЕРЬ НЕ ЗАПРЕТ, А ПРОЦЕДУРА (слово владельца 11.09 отменило прежнюю редакцию).**
`蛊真人` узнаётся моделями 4 из 5. Прежняя редакция этого промта на этом основании запрещала клеймы о
качестве вовсе — **владелец отменил запрет: он хочет увидеть качество на боевой книге.** Но опасность
никуда не делась и становится СИЛЬНЕЕ, когда читатель — большая модель: она способна «увидеть» в
переводе то, что помнит из претрейна, и не заметить того, чего в переводе нет. ⇒ процедура вместо
запрета: **каждая находка о качестве обязана нести ДВЕ цитаты рядом — фрагмент ОРИГИНАЛА и фрагмент
ПЕРЕВОДА.** Находка, которая не может показать место в исходнике, — не находка, а воспоминание.
### 4.4 Платная фаза
**Реши сам и аргументируй:** какую книгу и сколько глав брать. ⛔ **Глав немного — слово владельца.**
⛔ **ОГРАНИЧЕНИЕ, ОБЪЯВЛЕННОЕ ЗАРАНЕЕ (решение оркестратора 11.09, ряд 435): ЭСКАЛАЦИЯ В B НЕ ИСПОЛНЯЕТСЯ.**
`backend/configs/pipeline-c1.yaml:223`=`budget_usd: 0` держит её выключенной, а комментарий над ней требует
ручного оверрайда приёмочной сессией. **Оверрайд НЕ делаем:** байт-в-байт тождественный покупающий файл —
лучшее, что есть у сравнения A↔B, и оно дороже одного непроверенного пути. ⇒ **назови это в пре-реге
ограничением**, а не умолчи, и не считай отсутствие хопов свойством движка.
Мой приор, опровергается замером: те же главы 13 той же книги, что и в A. ⛔ **И вот ловушка, которая
стоила бы тебе ТРОЙНОГО бюджета, — первая редакция промта в неё попадала.** Файл
`books/gu-zhenren/coldrun-v16/guzhenren-ch1-10.gb18030.txt` — это **10 глав, 57838 байт**. Прогон A ел не
его, а **СРЕЗ в 17564 байта**, sha256 `ed870ba6065ce3eb…`, сохранённый как
`/home/ubuntu-26/tm-coldrun-a/material/guzhenren-ch1-3.gb18030.txt` (срез байт-в-байт равен первым 17564
байтам целого файла — проверено мною обеими sha). **Интейк главами резать НЕ умеет**: загрузишь целый файл —
купишь ×3.3 текста против объявленных ≈$0.42. ⇒ **режешь файл сам и кладёшь его sha в пре-рег.**
⭐ **Границу среза я пере-снял по возражению исполняющей сессии (байтовая граница в GB18030 могла разрубить
многобайтный символ) — она ЧИСТАЯ:** срез декодируется целиком, **9021 знак**, кончается на границе абзаца,
и несёт **ровно 3 заголовка**`第一节` · `第二节` · `第三节` (в целом файле их 10; контроль печатается
рядом). ⚠ **И ловушка имени, на которой я сам получил ложный ноль:** глава здесь размечена иероглифом
**节**, а не 章 — мой первый греп искал `第…章` и нашёл 0. Инвентарь разметки — `backend/internal/lang/data/cjk-section.txt`.
**ДВА ПОТОЛКА, а не один, и первая редакция называла только платформенный.** Холд платформы ≈$2.5 — и
**движковые потолки в `book.yaml`**: у прогона A стояло `book_usd: 1.25`, `day_usd: 2.50`. Прогон
остановится на ДВИЖКОВОМ потолке раньше, чем на платформенном, и «упёрся в потолок» будет про число,
которого ты не ждал. Назови в пре-реге ОБА и скажи, какой ставишь ты.
**A↔B СРАВНИМЫ НЕ ПО КНИГЕ — И ЭТО НАДО ЗАПИСАТЬ ОГРАНИЧЕНИЕМ, А НЕ ЗАМОЛЧАТЬ.** Между фризом прогона A
(`b0f5d89`) и сегодняшней головой — **107 коммитов и 52 файла движка** (+7678/85), включая `stagerun.go`,
`disposition.go`, `quality.go`, `httpllm.go`, `provider_openai.go`, `requestlog.go` и `models.yaml`, то есть
ровно денежный путь. **B меряет ДРУГОЙ движок.** Одинаковая книга делает сравнимыми ТЕКСТЫ, но не числа
денег. ⇒ в пре-рег: sha движка B, число коммитов между ним и A, и прямая строка «двойная оплата могла
измениться из-за наших правок, а не из-за провайдера».
**Приёмка каждого узла — по ТРЁМ независимым следам** (Postgres · диск и systemd · HTTP), не по
самоотчёту движка.
**Живость длинного прогона проверяется АКТИВНО:** не жди финального уведомления — раз в пару часов сверяй
рост числа вызовов и трат по леджеру ДВУМЯ чтениями. **Зависшая задача от идущей по сигналу завершения
неотличима, и у зависания нет цвета.**
**СЛЕДИ ЗА ЛОГАМИ И ЛОГИКОЙ ЖИВЬЁМ, а не только по итогу (слово владельца 11.09).** Прогон — это не
«запустил и жди файл»: смотри, что движок пишет, пока он это пишет. Каждое предупреждение, каждый флаг,
каждая регенерация, каждый незнакомый `finish_reason` — читаются ТОГДА, а не в разборе. Вопрос к каждому:
**согласуется ли поведение с тем, что, по твоему чтению кода, должно было произойти?** Расхождение логики и
лога — находка, даже если прогон кончился успешно. Веди это списком по ходу: «время · что увидел · чего
ожидал · что это значит».
**Деньги: ждём ≈$0.42, кап ≈$2.5 холда.** Упёрся в потолок — **СТОП и пинг мне**, не режь молча и не
проси подъёма «по проекции»: прогнозный запрос оставляет ложный след «владелец согласился на пере-оплату»,
которой не было.
### 4.5 ⭐ ВЫЧИТКА ВСЕГО ПЕРЕВОДА — отдельным опус-агентом с МИЛЛИОННЫМ контекстом
⛔ **Это заказ владельца дословно, и это главная качественная работа пака.**
**Делай РОВНО так в части формы:**
1. Подними **отдельного агента на opus** (модель задавай ЯВНО) и **загрузи ему в контекст ЦЕЛИКОМ**:
**(а)** весь получившийся перевод и **(б)** весь исходный китайский текст тех же глав.
**Контекст НЕ ЭКОНОМИТЬ** — слово владельца: «пусть не экономит контекст, ему должно на всё хватить».
Никаких выжимок, никаких «первых N абзацев», никакого пересказа вместо текста. Режешь текст — режешь
находки, и молча.
2. Его мандат — **искать МАЖОРЫ, которые жёстко критуют по тексту и его качеству**, в ПОРЯДКЕ ПРИОРИТЕТА
владельца (он назвал его сам, и порядок несущий):
1. **КОНСИСТЕНТНОСТЬ банка памяти и терминов** — один и тот же термин/имя/титул, приехавший в разных
местах по-разному; термин, разошедшийся с банком; забытая форма.
2. **ОТСУТСТВИЕ ВЫДУМОК** — текст, которого в оригинале НЕТ: дописанные предложения, пояснения от себя,
«додуманные» связки, исчезнувшие куски (пропуск — та же выдумка, только с другим знаком).
3. **ХУДОЖЕСТВЕННОСТЬ И ЧИТАЕМОСТЬ** получившегося русского текста.
4. **ОТСУТСТВИЕ TRANSLATIONESE** — кальки, порядок слов оригинала, «китайский синтаксис русскими словами».
3. ⛔ **Форма КАЖДОЙ находки — две цитаты рядом:** фрагмент ОРИГИНАЛА и фрагмент ПЕРЕВОДА, плюс глава/юнит,
плюс какой из четырёх приоритетов нарушен, плюс severity. **Находка без цитаты из исходника не
принимается** — см. абзац о претрейне выше: модель знает эту книгу и способна «вспомнить» то, чего в
нашем переводе нет.
4. **Решаешь сам и аргументируешь:** сколько таких читателей поднять и как разделить между ними работу
(один на всё · по приоритетам · по главам с перекрытием). Мой приор, опровергается доводом: **один агент
на ВЕСЬ текст сразу** — консистентность банка по построению не видна тому, кто читает главу отдельно.
Если делишь — объясни, чем компенсируешь потерю сквозного взгляда.
**И назови ОТРИЦАТЕЛЬНЫЙ результат тоже:** «выдумок не нашёл» — это результат, но только рядом с
контрольной величиной («прочитано столько-то знаков перевода против стольких-то знаков оригинала»).
⛔ **ДОКАЗАТЕЛЬСТВО ДОЧИТЫВАНИЯ — обязательно, дописано релеем 11.09 по возражению исполняющей сессии.**
Инструменты чтения РЕЖУТ длинные файлы молча, и тогда «выдумок не нашёл» становится утверждением о
НЕПРОЧИТАННОМ, неотличимым от находки. ⇒ читатель обязан напечатать: **знаков оригинала · знаков перевода ·
ПОСЛЕДНЕЕ предложение оригинала и ПОСЛЕДНЕЕ предложение перевода**. Не напечатал — его отчёт не принимается.
**Контроль для среза глав 13: 9021 знак оригинала** (снято мною).
### 4.6 ⭐ СВЕРКА ПРИБОРОВ С ВЫЧИТКОЙ — ради этого прогон и стоит своих денег
⛔ **ОДНА КЛЕТКА ЭТОЙ ТАБЛИЦЫ ИЗВЕСТНА ЗАРАНЕЕ — замер оркестратора 11.09 на артефактах прогона A, ряд 434.**
Прибор консистентности нашёл передачу у **40 термов из 69**, у **29** вернул `renderings={}` — и молча
записал их в `spread = 0`, то есть в КОНСИСТЕНТНЫЕ. Проверка текстом: финальная форма банка реально стоит в
отгруженном файле у **26 из 29**. ⇒ его итоговая строка «TERMS SHIPPED IN MORE THAN ONE SHAPE: 0»
посчитана по 40 термам, а не по 69, и **«расхождения нет» у него неотличимо от «передачи не нашёл»**.
⚠ Матчер ловит СЛОВО, а не форму: у `高脚吊楼` он вернул «Свайные», тогда как текст несёт «свайного дома /
свайные дома / свайных домов».
**для тебя это значит три вещи, и все три в пре-рег:** знаменатель прибора печатается ВСЕГДА («нашёл у M
из N») · ненайденные **проверяются текстом**, а не засчитываются в консистентные · и **эти 26 термов —
твой готовый список кандидатов** на клетку «прибор смолчал, читатель нашёл». ⚠ Опус-читателю список НЕ
давай: он находка о ПРИБОРЕ, а не о тексте, и подсказка сузила бы его взгляд.
У движка есть СВОИ приборы качества: детерминированные $0-гейты, отчёт качества, счёт расхождения форм
(`I1`/`I2`), флаги и колонка `degraded`, банк памяти и пост-проверка. **Все они утверждают что-то о тексте.
Твоя работа — спросить, совпадает ли их мнение с тем, что увидел читатель.**
Построй таблицу 2×2 и заполни её именами находок:
| | **читатель нашёл** | **читатель не нашёл** |
|---|---|---|
| **прибор сказал** | подтверждение | ложная тревога прибора |
| **прибор смолчал** | ⛔ **СЛЕПАЯ ПОВЕРХНОСТЬ** | согласие |
⛔ **Интересны ДВЕ клетки, и левая нижняя — самая дорогая: это дефект, который сегодня уезжает читателю
молча.** Каждую такую назови отдельно: что именно прошло мимо, какой прибор обязан был это поймать, и
почему не поймал — нет механизма, есть но выключен, есть но смотрит не туда.
### 4.7 КОД, КОТОРЫЙ ОТРАБОТАЛ — читать с оглядкой на получившийся текст
**Не ревью движка вообще, а ревью ТОГО ПУТИ, который реально исполнился на этой книге.** Порядок обратный
обычному: сначала находка в тексте, потом код, который её пропустил.
Назови: какие стадии и гейты реально бежали · какие не бежали и почему · где в коде живёт механизм,
отвечающий за каждый найденный мажор. ⚠ Пример связи, которую ждут: читатель нашёл разъехавшийся термин ⇒
читается путь инъекции банка и пост-проверка ⇒ ответ «механизма нет» / «механизм есть, но решает по
единице, которой в продукте нет» / «механизм есть и промолчал вот здесь».
**Кода НЕ ПРАВИТЬ** (§2). Находка = пинг мне со строками.
### 4.8 ДЁРГАТЬ API ПЛАТФОРМЫ ПО-РАЗНОМУ — не один счастливый путь
Слово владельца: «пусть подёргает по-разному апишку через платформу». Один проход по счастливому пути
доказывает, что дверь открывается, и ничего не говорит о том, что за ней.
⛔ **ГДЕ ЭТО ДЕЛАТЬ — исправлено релеем 11.09 по возражению исполняющей сессии, и возражение верное.**
Дёрганье API на ИЗМЕРЯЕМОМ прогоне САМО ПРОИЗВОДИТ число, которое потом предъявляется как свойство движка:
резюм и второй старт — ровно то, что числитель-2 §4.3 (пере-покупка ПРИНЯТЫХ единиц) и меряет. ⇒
**делай РОВНО так:**
- **ВСЕ формы дёрганья — на $0-РУКЕ** (стенд с заглушкой): API там тот же самый, провайдер подменён, и
стоит это НОЛЬ. Никаких оснований экономить на числе форм.
- **В ИЗМЕРЯЕМОМ платном прогоне — РОВНО ОДИН резюм, объявленный в пре-реге.** У прогона A резюм БЫЛ
(две попытки прогона в леджере, терминология пере-куплена на второй), поэтому один резюм не портит
сравнение, а СОХРАНЯЕТ его: без него A и B меряли бы разное.
**Решаешь сам, что дёргать; мой приор — не менее шести РАЗНЫХ форм:** заказ в главах против заказа в
ЗНАКАХ (это разные ветки контракта и разные счётчики) · повтор того же запроса (идемпотентность) · резюм ·
старт второго прогона при идущем первом · выгрузка во ВСЕХ смонтированных форматах ·
узел подписи банка · отказные пути (несуществующая книга · нехватка баланса · курсор с мусором).
**Ожидаемый исход КАЖДОГО дёрганья пишется ДО него.** Иначе это не проверка, а экскурсия: любой ответ
двери задним числом выглядит правильным.
### 4.9 Фаза 3 — $0 после прогона. **Состав задан РОВНО так; чем именно предъявлять — решаешь сам**
Прибор консистентности по ОТГРУЖЕННОМУ тексту существует и строить его НЕ НАДО — он живёт в
`backend/internal/pipeline/bookconsistency.go` и достаётся как `QualityReport().Consistency`; печатает его
`backend/cmd/tmctl/render.go`. Снимай `I1`/`I2` **с печатаемыми знаменателями** (число без знаменателя в
этом паке не принимается).
Дальше: выгрузка · структурное чтение EPUB (zip + `container.xml` + `nav`; `epubcheck` на хосте нет, и это
законно) · **обе новые миграции предъявлены ДАННЫМИ**: `wave_selection` несёт строки ОБЕИХ волн (они
выбирают над РАЗНЫМИ банками — черновик над базовым с исключёнными намайненными, редактор над обогащённым),
а `reasoning_in_completion` предъявляется **ТРЁХЗНАЧНО**: сколько строк `NULL`, сколько `0`, сколько `>0`.
Не гонись за «не нулём»: `0` здесь — законный измеренный ответ, и отличать его от `NULL` — весь смысл
миграции.
### 4.10 ⭐ НАХОДКИ ВНЕ ЗАКАЗА — и это отдельный заказ владельца, а не приятный побочный эффект
Слово владельца 11.09: **«важно, чтоб она находила проблемы даже там, где мы не ждём»**. Поэтому здесь стоит
вопрос, у которого НЕТ заранее известного ответа, и он задаётся в КАЖДОЙ фазе, а не один раз в конце:
> ⛔ **Что в этом прогоне стоит дороже, работает хуже или ведёт себя страннее, чем должно бы, — и о чём
> НИКТО не спрашивал?**
**Ради чего вопрос заведён — реальный случай, и он стоил четверти цены книги.** Удорожание из-за падающих
запросов НА НАШИХ СОБСТВЕННЫХ настройках никто не заказывал искать: просто кто-то посмотрел в леджер без
гипотезы и увидел, что 25.4 % денег ушло в выброшенные первые попытки. Ни один тест не был красным, ни один
гейт не сработал, в отчёте стояла зелень. **Находка такого рода ценнее исполнения заказа**, потому что
заказанное найдут и без тебя.
**Что докладывать (слово владельца, дословно по составу):** **криты · мажоры · регрессии · баги · жёсткие
точки улучшения.** Каждое — **АРГУМЕНТИРОВАННО**: что видел · чем предъявлено (`file:line`, строка лога,
запрос и ответ, число из леджера) · почему это именно крит/мажор, а не вкусовщина · чего это стоит в
деньгах, тексте или доверии читателя.
**И следи за КОДОМ, а не только за выдачей.** Ты читаешь логи и результаты движка — значит у тебя в руках
единственная в проекте позиция, с которой видно и поведение, и его причину сразу. Увидел странность в
выдаче — иди в код, который её произвёл, и назови место. Увидел в коде путь, который на этой книге НЕ
исполнился, хотя должен был, — это тоже находка. ⛔ **Кода не править** (§2): находка = пинг мне со строками.
## 5. Где этот пак мягкий — четыре места, назвал я, веер выбираешь ты
Мандат самопроверки — **исполнением**, субагенты разрешены явно. Направление:
1. **Сборщик, который молчит, и сборщик, который прочитал ноль, — неотличимы.** Засей в стендовую БД
заведомо ненулевой банк и заведомо удержанный юнит и потребуй, чтобы сборщики стали КРАСНЫМИ. Не
покрасневший на подсадке сборщик в платную фазу НЕ ДОПУСКАЕТСЯ.
2. **Дым меняет ТРИ фактора разом** (провайдер + книга + база) ⇒ при провале платной руки он не разделяет
причину. Где дёшево — меняй по одному.
3. **Отрицательный замер обязан доказать, что спросил существующее.** Рядом с каждым нулём ПЕЧАТАЙ
контрольную величину. И помни две ловушки среды: `grep` здесь — обёртка над ugrep, чтит `.gitignore` и
**не видит `books/`**; `sqlite3` как CLI на машине НЕТ — только `python3` + `mode=ro`.
4. **Число, выведенное из соседнего поля, не краснеет нигде.** Если в строке есть поле, прямо отвечающее на
вопрос (номер попытки живёт в `trace_id`), любой ВЫВОД ответа — по времени, по порядку, по соседству —
уже дефект метода, даже когда сходится.
### ⭐ Адверсариальная стойка — то, чем оркестратор судит чужую работу; суди ею свою
Владелец просил передать это прямо. Ниже не правила пака, а СПОСОБ СМОТРЕТЬ, и он ловит то, чего не ловят
ни тесты, ни второй круг:
- ⛔ **Автор и ревьюер — разные роли, даже когда это один ты.** Перечтение собственной работы рубежом НЕ
считается. Поднимай отдельного читателя, которому НАЗВАНО, где мягко, и не показывай ему свои выводы
раньше, чем он прочтёт предмет.
- ⛔ **Спрашивай у аномалии, о ЧЁМ она — о предмете или о твоём приборе.** Вмешательство в замер производит
ложные улики, похожие на находки: у прошлой смены запись «непонятный исход» оказалась здоровой, а сломан
был цикл того, кто мерил.
- ⛔ **Верный результат при неверном методе не краснеет нигде.** «Сошлось» — не доказательство: сходятся и
по счастливому порядку строк. Спрашивай «чем это НАЗВАНО в самой строке?» прежде, чем вычислять.
- ⛔ **«Не воспроизвелось» — не «недостижимо».** Верный прогон при неверной гипотезе неотличим от «дефекта
нет». Назови ЦЕПЬ звеньев от входа к следствию и проверь каждое, прежде чем писать «не воспроизводится».
- ⛔ **Утверждение о молчании вакуумно.** «Ошибки не было», «лишнего не купили», «предупреждений нет» —
проверяемо только рядом с фикстурой или контролем, где это ОБЯЗАНО было прозвучать.
- ⛔ **Заимствованное число проверяется не на существование, а на ТУ ЛИ КЛЕТКУ.** Та же роль, та же модель,
та же стадия, тот же режим? Прошлый пак чуть не включил денежный механизм по замеру из чужой роли.
- ⛔ **Зелёная батарея плюс полный каталог мутаций сходимостью НЕ являются.** Три смены подряд направленный
второй читатель находил 6, 7 и 9 настоящих дефектов при полной зелени.
- ⭐ **И главный вопрос отчёта — не «что не получилось», а «что ты знаешь и не сказала».** На прямой вопрос
сессия однажды назвала ДВЕНАДЦАТЬ слепых поверхностей своего прибора вместо одной, и две из двенадцати
не прозвучали бы без вопроса: одну она видела в собственном выводе и прошла мимо, вторую знала из своего
же комментария в коде.
## 6. Предметные оси ревью
Выбери 13 и назови какие: **деньги под гонкой** · **граница «холодности»** (что в прогоне перестало быть
первым разом) · **провенанс каждого числа** (чем снято, каким прибором, на какой клетке).
## 7. Записка-план ДО работы
Перед первой командой — записка: фазы, что чем предъявляется, где ждёшь осечки. Комплектность против
заказа сверяй механически, а не памятью.
## 8. Заявление = команда
Каждое число и каждая категорика отчёта идут **с командой, которой получены**. Приёмка пере-снимает.
## 9. Эхо-протокол старта
**Первым действием** — ≤10 строк СВОИМИ словами: что понял · что считаешь опасным · что считаешь неверным.
Дословный пересказ подтверждает канал, но не понимание, и ошибку промта повторяет вместе с ним.
**Адрес бери из `/tmp/textmachine-channel`** — блок с `role=оркестратор`, — а не из этого промта: имя сессии
не переживает рестарт окружения, и вписанное сюда имя однажды окажется мёртвым. Перед отправкой сверься с
`ListAgents`: файл переживает смерть сессии, а `ListAgents` — нет.
## 10. Что НЕ удалось — обязательная секция отчёта
И вторая её половина, без которой она вырождена: **«где прибор слеп и я это знаю»** — что невидимо · почему
не чинил · чем закрывается. ⭐ Формулировка прошлой смены, идущая нормой: **«в коде названо, в отчёте нет —
значит для следующей смены НЕ названо».**
**И ОТДЕЛЬНОЙ секцией — находки вне заказа (§4.10):** криты · мажоры · регрессии · баги · жёсткие точки
улучшения, каждая аргументированно и с носителем. ⚠ Эта секция обязана быть непустой ИЛИ нести строку
«искал вот так, не нашёл» с перечислением того, где искал: «находок нет» и «не смотрел» в отчёте выглядят
одинаково.
## 11. Канал вопросов и право отказаться
Конфликт промта с кодом или доками — **пинг мне, не интерпретация**. И у тебя есть право сказать «этого
делать не надо» с аргументом: прошлые три сессии возражали, и в двух случаях правы были они.
### ⭐ Старший коллега — Fable 5. **Разрешён ОДИН на сессию (слово владельца 11.09)**
У тебя есть право поднять **одного** агента на модели **`fable`** и держать его как СОВЕТЧИКА при сомнениях —
тем же способом, каким это делает оркестратор. Владелец разрешил ровно одного на сессию.
**Как с ним работать, и форма здесь важнее числа:**
- **Поднимаешь ОДНОГО и держишь его весь пак.** Ценность этого агента — в НАКОПЛЕННОМ контексте смены:
он помнит, что ты уже решил и почему. ⛔ **Второго не поднимай** — вопросы ДОСЫЛАЮТСЯ первому (`SendMessage`
по его id), а не адресуются свежему. Свежий агент вместо накопленного — потеря именно того, ради чего он
заведён.
- **Модель задавай ЯВНО** (`model: "fable"`), иначе она унаследуется и ты не будешь знать, что у тебя
работает.
- **О чём его спрашивать:** сомнение в решении · развилка, где оба пути выглядят законными · «не заказываю
ли я уже построенное» · «не противоречит ли это ратифицированному» · спорная формулировка в отчёте.
**Спрашивай С КОНТЕКСТОМ и со СВОЕЙ рекомендацией** — вопрос без твоего варианта ответа даёт совет ни
о чём.
- ⛔ **Он советчик, а не источник истины: его ответ проверяется деревом.** У оркестратора он за смену
ошибся дважды на фактах (называл платный прогон бесплатным, объявлял ряд новым предметом) — и оба раза
его ВЫВОД оставался верным. Проверяй посылки, принимай выводы по существу.
- **Что он дал оркестратору за эту смену** — для калибровки, чего от него ждать: поймал, что я собирался
завести новый ряд бэклога под предмет, у которого ряд уже был; развернул порядок паков доводом, который
я не назвал; и назвал условие, без которого два прогона стали бы несравнимы.
## 12. Прямой канал
Механизм — `CLAUDE.md` §«Связь между сессиями». Впиши свой блок в `/tmp/textmachine-channel` ПЕРВЫМ
действием. Нужной роли нет ⇒ канала нет, и это нормально: НЕ опрашивай сессии подряд.
## 13. Критерий завершённости
У каждого пункта заказа — исход (сделано · не делаю с доводом · пинг) · круги СОШЛИСЬ (последний не дал
НОВЫХ находок) · числа сняты ПОСЛЕ последней правки · всё живое в ДЕРЕВЕ или у меня, а не в письме ·
явное **«работа завершена, править не планирую»**. Без последнего пак считается идущим.

View file

@ -0,0 +1,971 @@
# Эксперимент 25. Холодный прогон B — «от двери до ФАЙЛА» во ВТОРОЙ раз, и впервые с вычиткой
> **Пре-регистрация.** Всё, что ниже строки «ГРАНИЦА ФРИЗА», написано ДО первого платного вызова и
> закоммичено фриз-коммитом. Результаты дописываются ПОСЛЕ прогона отдельной секцией и ничего в
> пре-реге не правят: расхождение НАЗЫВАЕТСЯ, а не подгоняется.
**Зона:** сквозной прогон · **Пак:** `docs/COLD_RUN_B_SESSION_PROMPT.md` (редакция `3fce077`) · **Дата:** 11.09.2026
**Предмет:** четыре вещи, которых нет ни у кого, плюс заказ владельца на ВЫЧИТКУ боевого текста.
## 0. Что именно покупается, и чего этот прогон НЕ покупает
**Покупается:** (1) первое исполнение двух миграций схемы НА ПЛАТНОМ ПРОГОНЕ · (2) числа
консистентности С ПРОГОНА, а не пере-сканом сохранённой базы A · (3) платформенный пак СКВОЗЬ ·
(4) пре-регистрированный ЗНАМЕНАТЕЛЬ денежной дыры и проверка её ЗНАКА · (5) ⭐ вычитка ВСЕГО
перевода против ВСЕГО оригинала отдельным опус-читателем и сверка ПРИБОРОВ движка с тем, что он увидел.
**НЕ покупается и не оплачивается ни центом:** ответ на «безопасно ли редактору думать меньше»
(ряд 433) — это парный дизайн, отдельные деньги, отдельный заказ. B идёт на боевых настройках как есть.
**НЕ покупается ВЕЛИЧИНА доли выброшенного.** B — ЕЩЁ ОДИН одиночный прогон. Квирки §3д: при sd
логарифма 0.82 парный дизайн ловит эффект ×2.5 за **13 пар**, ×2.0 за 22, ×1.4 за 93. **n = 2 этой
нормы не выполняет и выполнить не может.** Писать «25.4 % подтверждено» по двум розыгрышам — ошибка.
## 1. ГРАНИЦА ФРИЗА — что зафиксировано до первого цента
| Предмет | Значение |
|---|---|
| Фриз-sha (КОД, которым собраны бинари) | `baa06cef7b2b30b8f7cbe031f3d0fabc03a01a55` |
| Фриз-дерево | локальный **клон** `~/tm-coldrun-b/freeze` (0 грязных строк; контроль — главное дерево в тот же момент 1) |
| Бинари | `tmctl` · `tmplatformd` · `tmplatformctl` · `stub` · `zeropipe`**все пять** из клона, у каждого `vcs.revision=baa06ce…`, `vcs.modified=false`, строк `vcs.*` = 3. Негативный контроль: та же сборка из ГЛАВНОГО дерева даёт `vcs.modified=true`. ⚠ Прогон A объявил девиацией, что два его инструмента собраны НЕ из фриза (§12.5) — здесь эта девиация закрыта построением |
| Исходник (оригинал) | `books/gu-zhenren/coldrun-v16/guzhenren-ch1-10.gb18030.txt`, 57838 байт, sha256 `0b5f9b0266d8c32ac717a41211d2ed15d4f2a07f37a701fb686e255ab6c89f37` |
| Исходник (СРЕЗ, что покупается) | главы 13, **17564 байта**, sha256 `ed870ba6065ce3eb4eec12e80238efbc2be8511f38334ab1d92d5693f4ef4df9`. Нарезан МНОЙ из оригинала (`head -c`), байт-в-байт равен срезу прогона A (`cmp`). **Граница чистая:** декодируется целиком в **9021 знак**, кончается на границе абзаца, несёт **ровно 3** заголовка `第…节` при **10** в целом файле (контроль напечатан рядом) |
| Фактический pipeline-YAML | `~/tm-coldrun-b/mirror/cfg/pipeline-c1.yaml`, sha256 **`a46b33ee65b7713b4fbf86f64c788ba6b11eb0a219170c145a6205088156ad49`**, байт-в-байт равен `backend/configs/pipeline-c1.yaml` фриза (`cmp`) |
| Реестр моделей | `~/tm-coldrun-b/mirror/cfg/models.yaml`, sha256 `835af7b2d8830e6058b650ab742603d228febbd663978d88e0fe496ff78b5c1c` |
| Артефакт контраста | `mining-contrast.zh.txt` = jieba 0.42.1 `dict.txt`, sha256 `7197c3211ddd98962b036cdf40324d1ea2bfaa12bd028e68faa70111a88e12a8`, 349 046 строк. ⚠ `CheckMiningContrast` делает ТОЛЬКО `os.Stat` и удовлетворился бы фальшивым кандидатом — sha сверена руками |
| Шаблон книги | `~/tm-coldrun-b/book-template-c1.yaml`, sha256 `4118a43059dba64a45290db73252a1306af341ff4c429100314256c95ca2f24d` |
| Потолки книги (ДВИЖКОВЫЕ, в шаблоне) | `book_usd: 1.25` · `day_usd: 2.50` — те же, что у A, ради сравнимости |
| Потолок ПЛАТФОРМЫ | холд ≈$2.24 (формула ниже), грант учётки **$2.50** |
| Стоп-правило СЕССИИ | факт **$1.00** — раньше обоих потолков; достигнут ⇒ СТОП и пинг, не решение сессии |
### 1.1 ⭐ ДВЕ КАРТЫ СРАВНИМОСТИ A↔B, которых у прогона A не было
1. **ПОКУПАЮЩИЙ файл тождествен.** sha `a46b33ee…` — та же, что в пре-реге прогона A. Что именно
покупается, между прогонами не изменилось ни на байт.
2. **ЦЕНОВАЯ МОДЕЛЬ тождественна.** `models.yaml` фриза A и фриза B различаются **ровно на 10 строк,
и все десять — КОММЕНТАРИИ** (не-комментарных изменённых строк: **0**). ⇒ долларовые числа A и B
считаны одним прайсом и сравнимы напрямую.
3. ⛔ **А КОД — НЕТ, и это записывается ОГРАНИЧЕНИЕМ, а не замалчивается.** Между `b0f5d89` (фриз A) и
`baa06ce`: **118 коммитов**, `backend/` **52 файла** (+7678/85), `platform/` **30 файлов**
(+3945/158), включая денежный путь. **B меряет ДРУГОЙ движок.** Прямой строкой:
**двойная оплата могла измениться из-за НАШИХ правок, а не из-за провайдера.**
### 1.2 Срез по $0-манифесту (`tmctl manifest --json`, бинарь фриза B)
| Глава | `units_total` | `chunks_total` | `expected_usd` | знаков |
|---|---|---|---|---|
| 1 `第一节:纵身亡魔心仍不悔` | 2 | 2 | 0.032424 | 3287 |
| 2 `第二节:逆光阴五百年觉悟` | 1 | 2 | 0.028459 | 3051 |
| 3 `第三节:请一边玩蛋去` | 1 | 2 | 0.023237 | 2421 |
| **книга** | **4** | **6** | `expected_usd 2.08412008` · `book_once_usd 2.00` · `step_max_usd 0.13647876` | 8759 |
Смета ТЕКСТА — сумма глав, **$0.084120**; `book_once_usd` $2.00 это ПОТОЛКИ контура банка
(`gates.terminology.budget_usd 1.00` + `classify_budget_usd 1.00`), а не смета.
**Все семь чисел этой таблицы совпали с манифестом прогона A до последнего знака** — при другом
бинаре, другом каталоге и другом `book_id`. Это сильная улика тождества покупаемого.
## 2. ⛔ МЕРА ДЕНЕЖНОЙ ДЫРЫ — пре-регистрируется ЗДЕСЬ, до первого цента
**Прибор ПРОВЕРЕН ДО ПРИМЕНЕНИЯ:** те же запросы, прогнанные по сохранённой базе прогона A, дают
его числа. Это не «похоже», а пере-снято: 33 строки · 33 уникальных `id` · 2 трейса · $0.419423.
* **единица** = `(chapter, chunk_idx, stage, role)`. Номера попытки в ключе НЕТ намеренно: единица
ОХВАТЫВАЕТ попытки. ⚠ У стадии `terminology` все строки несут `chapter = 0`, то есть `chunk_idx`
там — индекс майнинг-батча, а не чанк главы. Одно слово покрывает ДВА предмета; в каждом числе
называется, какой именно.
* **знаменатель** = единицы, за которые заплачено хоть раз (`sum(cost_usd) > 0`). **Печатаются ОБА
счёта.** На A фильтр не отсеял ничего (17 и 17) ⇒ его поведение не проверялось ни разу.
* **ЧИСЛИТЕЛЬ ПЕРВЫЙ — выброшенная покупка:** единицы, где есть хоть одна оплаченная и НЕ принятая
попытка (`ok = 0 AND cost_usd > 0`). **База A: 4 из 17 = 23.5 % ЕДИНИЦ**, тогда как по ВЫЗОВАМ то
же событие даёт 4 из 27 = **14.8 %**. Две меры расходятся в полтора раза — потому мера и объявлена.
* **ЧИСЛИТЕЛЬ ВТОРОЙ — ПЕРЕ-МАЙНИНГ БАНКА, и он НЕ называется «пере-оплатой».** Единицы, за
ПРИНЯТЫЙ результат которых заплачено больше одного раза (`ok = 1`). На A это вся стадия
терминологии. **Главное число — «все платежи КРОМЕ ПЕРВОГО» = $0.028742 (6.9 %), нижняя граница;
рядом печатается «все КРОМЕ ПОСЛЕДНЕГО» = $0.035723 (8.5 %).** Разница 24 %, и обе ориентации
получены ПОРЯДКОМ — тем самым методом, на котором прошлая смена получила ложные 54.9 %. Поля,
называющего вызов-источник, у строк банка НЕТ (`glossary` 21 колонка, вызова среди них нет;
`glossary_revisions` 0 строк), поэтому ориентация выбирается явно и объявляется.
**И почему это НЕ «деньги за пустоту»** (замерено мной на артефактах A ДО прогона B): второй
заход терминологии ИЗМЕНИЛ передачу у **12 термов из 69**, и **11 из 12 уехали читателю именно в
ФИНАЛЬНОЙ форме** (стоп-форма — 0 из 12; один терм не встретился ни в какой). Это ЦЕНА
ПЕРЕ-МАЙНИНГА БАНКА ПОСЛЕ ЧЕЛОВЕЧЕСКОЙ ПОДПИСИ, а не выброшенная покупка.
* **ДВА ПУТИ К ДЕНЕЖНОМУ ЧИСЛУ.** Путь 1 — по колонке `degraded`; путь 2 — по `ok = 0 AND cost_usd > 0`,
колонки `degraded` не касаясь. На A оба дали **$0.106472 = 25.4 %**, но совпадают они ПО
ПОСТРОЕНИЮ. ⇒ совпадение на B — слабая улика; **РАСХОЖДЕНИЕ — сильная находка**: классификатор и
признак приёмки разъехались. ⚠ **25.4 % — доля ДЕНЕГ, 23.5 % — доля ЕДИНИЦ**, подменять нельзя.
## 3. ⛔ ДВЕ СЛЕПЫЕ ПОВЕРХНОСТИ ПРИБОРА КОНСИСТЕНТНОСТИ, НАЗВАННЫЕ ДО ПРОГОНА
Обе найдены на артефактах прогона A, обе меняют то, как прибор B строится.
**(а) ГРАНИЦА БАНКА.** Прибор A сверял отгруженный текст со СТОП-ПРЕДЛОЖЕНИЕМ банка
(`project.db.bank.json`, снят в момент подписи), а читатель получил текст, сделанный под ФИНАЛЬНЫМ
банком: терминология пере-майнится ПОСЛЕ подписи, и редакторская волна идёт после неё
(по `request_log` прогона A: терминология-2 кончается 01:42:10, редактор идёт 01:43:54 → 01:54:44).
**прибор B читает ФИНАЛЬНЫЙ банк и ПЕЧАТАЕТ, какую версию он прочёл.** Класс — `D39.228` п.5.
**(б) ЗНАМЕНАТЕЛЬ САМОГО ПРИБОРА.** На A он нашёл передачу у **40 термов из 69**, а у **29** вернул
пусто — и записал их в «консистентные». Его итог «отдано более чем одной формой: 0» посчитан по 40,
а читается как утверждение о книге. ⇒ **прибор B печатает всегда: термов всего N · передачу нашёл у
M · пусто у K, и K проверяется ТЕКСТОМ**, а не принимается за консистентность.
## 4. Пре-рег-таблица: узел → что ДОЛЖНО произойти → чем предъявлено → что считается провалом
| # | Узел | Ожидание (числом или строкой) | Чем предъявлено | Провал |
|---|---|---|---|---|
| 1 | Стенд — мой | слушатель `127.0.0.1:8098` принадлежит МОЕМУ pid; на `11434` в платной фазе НЕ слушает НИКТО | `ss -ltnp` + pid-файл | чужой процесс на порту ⇒ СТОП |
| 2 | Код — из фриза | у ПЯТИ бинарей `vcs.revision=baa06ce…`, `modified=false`, строк `vcs.*` > 0 | `go version -m` | любое из трёх не так ⇒ СТОП |
| 3 | Гость и деньги | ОТДЕЛЬНАЯ платная учётка; грант $2.50 одной строкой `credit_ledger.kind='grant'` | `tmplatformctl balance` + строка леджера, ФИЛЬТРОВАННАЯ по учётке | дымовые деньги попали в счёт ⇒ число недействительно |
| 4 | Интейк | `201`, `chapter_count = 3`; ⚠ `character_count` ≈ 9677 (счёт интейка по потоку, ПРИБЛИЗИТЕЛЬНЫЙ), точный — `source_chars = 8759` | тело 201 + строка `books` | не 3 главы ⇒ СТОП до оплаты |
| 5 | Срез совпал | `units_total = 4`, по главам **2/1/1**, `chunks_total = 6` | `chapters`+`units` в Postgres против §1.2 | расхождение ⇒ СТОП до оплаты |
| 6 | Цена и бонд | `term_consistency_funded: true`, `verdict: covers_all`, `affordable_chapters: 3`, холд **2 241 631 µUSD** = ⌈84 121 × 1.25⌉ + 136 479 + 2 000 000 | `GET /v0/books/{id}/run-options`, тело в файле | `false` ⇒ СТОП до оплаты |
| 7 | Старт | `202`, `status: translating`, `ordered_chapters: 3`, `runs.bond_funded = t`. ⚠ **Ожидается, что ответ старта скажет `term_consistency_funded: false`** при уже выставленном `bond_funded = t` — находка 7 прогона A; ВОСПРОИЗВЕДЕНА мной на $0-руке | ответ + строка `runs` | отказ старта ⇒ СТОП и пинг |
| 8 | Ключи живы | первый платный вызов не отказан по авторизации | `request_log.err` на первой строке | отказ провайдера ⇒ СТОП и пинг (не наша поломка) |
| 9 | Кадры | `events.jsonl` несёт `hello · progress · unit_done · spend · bank_stop · finished`; `run_attempts.last_seq` растёт **ПО ПОПЫТКАМ** | счёт кадров по типам, per-attempt | кадров нет при живом юните ⇒ дефект |
| 10 | Банк-стоп | `runs.status = awaiting_bank`, exit **3**, `.bank.json` несёт непустой `proposed[]` и `terms: []` | статус + exit + КОПИЯ сайдкара (резюм её уничтожает) | стоп не наступил ⇒ исход «узел не наступил», не провал |
| 11 | Проекция банка | `bank_terms` = **0** ПОКА прогон не закрыт и **> 0** ПОСЛЕ | `GET …/bank` дважды + `select count(*)` | ноль ПОСЛЕ закрытия ⇒ дефект |
| 12 | Дверь правок | `preview:true``changed:true`, на диске НИЧЕГО; `preview:false``state: applied` + `mined-delta.yaml` | тела обоих ответов В ФАЙЛАХ + sha файла до/после | преview написал на диск ⇒ дефект |
| 13 | Резюм (**РОВНО ОДИН**) | `202`, вторая строка `run_attempts`, `exit_code = 0` | `run_attempts` | не возобновился ⇒ дефект |
| 14 | Книга переведена | `units.state = translated` у всех **4** юнитов | `select state, count(*)` + контрольный счёт юнитов книги | любой не `translated` ⇒ книга не доведена |
| 15 | Сборка | `complete: true`, `pending/withheld/incomplete/stale/ghost = 0`, `stale_unknown: false`, `config_drift: false` | stdout `tmctl build` (на API этих полей НЕТ) | любое не так ⇒ книга не целая |
| 16 | Выгрузка | `exports.state = ready`, `exports.complete = t`, `size_bytes` = числу скачанных байт, ОБА формата | строка `exports` + длина скачанного | `complete` не `t` ⇒ дверь отдала книгу с дырой |
| 17 | ФАЙЛ | байты скачаны, sha256 напечатан; EPUB читается структурно (zip + `container.xml` + `nav` + spine = 3) | sha256 + разбор zip | не открывается ⇒ провал |
| 18 | Человек прочитал | первый абзац — связный русский текст | цитата в отчёте | не текст ⇒ провал |
| 19 | Деньги сошлись | Σ `request_log.cost_usd` ≈ кадр `spend` ≈ Σ `settlement` ЭТОЙ учётки ≈ (грант баланс); открытых резерваций **0** | четыре счёта в µUSD рядом, ВСЕ фильтрованы по учётке | расхождение — НАЗВАТЬ, не подгонять |
| 20 | Ничего не реплеено | `tm_hit = 1` печатается С РАЗБИВКОЙ ПО ПОПЫТКАМ **по `trace_id`** (он несёт номер попытки), `distinct model_actual` не содержит `local-*` | запросы к `project.db` | есть `local-*` ⇒ прогон измерил заглушку |
| 21 | **v17 `wave_selection`** | **10 строк = 6 `draft` + 4 `edit`**, и волны стоят над РАЗНЫМИ снапшотами (черновик — базовый банк, редактор — обогащённый) | `select wave, count(*)` + `snapshot_id` + `injected_srcs` | строк нет ⇒ **пинг, а не ноль в отчёте**: писатель гейтится `r.memory != nil` |
| 22 | **v18 `reasoning_in_completion`** | **ТРЁХЗНАЧНО**: сколько `NULL`, сколько `0`, сколько `>0`, сумма = числу строк. Ожидание: на DeepSeek `>0` преобладает | три счёта + контроль суммы | «не ноль» вместо трёхзначности ⇒ вопрос не задан |
| 23 | Консистентность | `I1`/`I2` С ПЕЧАТАЕМЫМИ знаменателями + «термов N · передачу нашёл у M · пусто у K» + **какую версию банка прочёл прибор** | `QualityReport().Consistency` + свой счёт по отгруженному тексту | число без знаменателя в этом паке не принимается |
| 24 | ⭐ Вычитка | опус-читатель получает ВЕСЬ перевод и ВЕСЬ оригинал; печатает знаков оригинала (**контроль: 9021**), знаков перевода, ПОСЛЕДНЕЕ предложение обоих | его отчёт | не напечатал доказательство дочитывания ⇒ отчёт не принимается |
| 25 | ⭐ Сверка приборов | таблица 2×2 заполнена ИМЕНАМИ находок; левая нижняя клетка («прибор смолчал, читатель нашёл») названа поимённо | §4.6 отчёта | клетка пуста без довода ⇒ сверка не сделана |
## 5. Стоп-правила (остановка и пинг, не решение сессии)
1. фактическая трата дошла до **$1.00** (1 000 000 µUSD);
2. `runs.paused_reason` НЕПУСТО — любое из четырёх (`run_limit_reached · credit_exhausted · daily_ceiling · ceiling_unknown`);
3. срез не совпал с §1.2 — стоп ДО оплаты;
4. `term_consistency_funded: false` в `run-options` — стоп ДО оплаты;
5. бинарь не из фриза — стоп до устранения;
6. что-либо слушает `127.0.0.1:11434` в платной фазе — стоп: прогон измерял бы заглушку.
## 6. ⛔ ОГРАНИЧЕНИЯ, ОБЪЯВЛЕННЫЕ ДО ПРОГОНА
1. **ЭСКАЛАЦИЯ В B НЕ ИСПОЛНЯЕТСЯ.** `pipeline-c1.yaml:223` несёт `escalation.budget_usd: 0`, а
комментарий строкой `:221` гласит: «Приёмочная сессия 蛊真人 ОБЯЗАНА выставить budget_usd>0».
Решение оркестратора: **не трогать**, потому что тождество покупающего файла и ценовой модели
(§1.1) стоит дороже одного непроверенного пути. ⇒ B, как и A, о работе эскалации не говорит
НИЧЕГО. Сам этот факт идёт находкой вне заказа: механизм построен, ратифицирован и не может
исполниться ни на одном боевом конфиге без ручного оверрайда, которого никто ни разу не сделал.
2. **Судьи на пути нет** (`role: judge` живёт только в `c2`), **гейт `coverage` выключен** ⇒ из пяти
гейтов работают четыре. Проверяемый путь: черновик → редактор → четыре $0-гейта → выгрузка.
3. **`epubcheck` не гоняется** — `java` на хосте нет и поставить нельзя. EPUB судится структурно.
4. **ТРИ следа — не три свидетеля.** Postgres платформы МАТЕРИАЛИЗУЕТСЯ из `events.jsonl` движка, а
текст и файл платформа берёт теми же вербами `tmctl`. По-настоящему независимы только скачанные
байты и их sha256, плюс systemd-маркеры выхода. Биллинга провайдера у нас нет вовсе.
5. **Доллары — НАША модель поверх токенов провайдера.** Провайдер сообщает ТОКЕНЫ; цену умножаем мы,
по пину `models.yaml`. На 11.09 пин flash был ВЫШЕ вендорской страницы на 13 % (D39.247 п.3).
6. **Пик/офф-пик.** Прогон A шёл 01:2901:54 UTC в пятницу — ВНУТРИ пикового окна DeepSeek
(0104 и 0610 UTC, пн–пт). B идёт вечером пятницы, то есть в ОФФ-ПИК. Леджер осознанно считает
по ПИКОВОЙ цене (`D39.136`: потолок обязан срабатывать раньше) ⇒ **записанная цена B будет вдвое
выше настоящего счёта, а записанная цена A — нет.** На сравнение ЛЕДЖЕРОВ это не влияет (оба
пиковые), на «сколько мы заплатили» — влияет вдвое. Называется здесь, чтобы не открылось потом.
7. **ПРЕТРЕЙН — процедура, а не запрет.** `蛊真人` узнаётся моделями 4 из 5. **Каждая находка о
качестве обязана нести ДВЕ цитаты рядом — фрагмент ОРИГИНАЛА и фрагмент ПЕРЕВОДА.** Находка,
которая не может показать место в исходнике, — не находка, а воспоминание.
## 7. Инструменты (замораживаются вместе с этим пре-регом)
* `eval/cold_run_b/door.py` — драйвер: гейт · вход · грант · интейк · опции · старт · вахта · улика ·
подпись · резюм · выгрузка · произвольная проба. **Каждый запрос И каждый ответ ложатся в
`evidence/http/` ДО того, как вернутся вызывающему** — дефект харнесса A, из-за которого пять
строк его пре-рег-таблицы остались без носителя (§10.12 его отчёта).
* `eval/cold_run_b/measure.py` — прибор денег и единиц §2. **Пере-снимает числа прогона A на его же
базе** как самопроверку перед применением к B.
* Из фриза A переиспользуются без правки: `eval/door_to_file/stub` ($0-заглушка провайдера),
`eval/door_to_file/zeropipe` (рендер $0-пайплайна). Оба собраны из клона B и несут его штамп.
## 8. Что уже исполнено ДО фриза и чем предъявлено ($0)
* **`GET /v0/capabilities``export_formats: ["txt","epub"]`** (непусто ⇒ все три гейта выгрузки
выполнены), `contract_version 0.15.0`, `bank_corrections_enabled: true`.
* **Пин фазы 0 — ЗАГРУЗЧИК, а не скан.** Движок ЗАГРУЗИЛ рендер $0-пайплайна бесплатным вербом
(`tmctl manifest --json`, EXIT=0). Контрольная величина рядом: достижимых ПЛАТНЫХ моделей в
`pipeline-c1.yaml`**8**, в рендере — **0**, счёт по РАЗОБРАННОМУ YAML, а не по тексту.
* **$0-ДЫМ ПРОЙДЕН ЦЕЛИКОМ на ОТДЕЛЬНОЙ книге** (синтетический zh-текст, 1206 байт, 4 главы,
sha256 `10637234312895da4665ed0dff84f25dfacdffd842871425b1bba1cc612b80da`) и на ОТДЕЛЬНОЙ учётке:
стенд · гость · грант · интейк 201 · опции (4 главы, 4 юнита, бонд профинансирован) · старт 202 ·
кадры · банк-стоп (exit 3) · дверь правок (preview НИЧЕГО не пишет — проверено sha файла до и
после) · резюм (exit 0) · выгрузка ОБОИХ форматов, `complete = t`, файлы скачаны.
* ⭐ **ОБЕ МИГРАЦИИ УЖЕ ИСПОЛНИЛИСЬ ЖИВЬЁМ, и это честно называется здесь, а не приписывается B.**
На дымовой базе: `wave_selection`**8 строк = 4 `draft` + 4 `edit`**, волны стоят над РАЗНЫМИ
снапшотами (`6dabf7e5…` без инъекции против `76670592…` с тремя термами) ⇒ **ось, которой у
`retrieval_state` нет, работает.** `reasoning_in_completion``NULL` на всех 16 строках, и это
ВЕРНЫЙ трёхзначный ответ: локальная заглушка поля не сообщает. ⇒ **B покупает не «исполнилась ли
миграция», а её поведение НА ПРОВАЙДЕРЕ, КОТОРЫЙ ЭТО ПОЛЕ ШЛЁТ.**
* ⭐ **МИГРАЦИЯ ПРОВЕРЕНА НА ЗАПОЛНЕННОЙ БАЗЕ, а не только на пустой.** Копия базы прогона A
(v16, 33 строки `request_log`, 69 строк `glossary`) открыта бинарём B: `v16 -> v18`, EXIT=0, строки
целы (33 и 69), `wave_selection` создана пустой, **`reasoning_in_completion` = `NULL` на всех 33
ранее написанных строках** — ровно то, что миграция обещает. Оригинал не тронут (sha сверена
до и после). Пустая база про данные не говорит ничего; эта — говорит.
---
# РЕЗУЛЬТАТЫ (дописано ПОСЛЕ прогона; ничего выше не правлено)
> ⚠ **О НУМЕРАЦИИ.** Секции РЕЗУЛЬТАТОВ нумеруются своим рядом и с §5 по §8 совпадают номерами с
> секциями пре-регистрации — та заморожена фриз-коммитом и правке не подлежит, поэтому вместо
> пере-нумерации замороженного текста здесь разведены ССЫЛКИ: ссылка на пре-рег всегда пишется
> «§N пре-рега», ссылка на секцию результатов с номером 58 — «§N результатов», а §9 и дальше
> однозначны сами по себе (в пре-реге таких номеров нет).
**Книга дошла до файла.** `蛊真人`, главы 13, zh→ru, прогон `run_Q6DSXGS7TW52TFNO` над книгой
`bk_NH6275ZIRFFTLZHG`: старт **22:48:38**, финиш **23:08:44** (**20 мин 06 с**), две попытки
(банк-стоп exit 3 и резюм exit 0), `status = ready`, `paused_reason` пусто, `failure_reason` пусто.
Все **4** юнита `translated`, обе выгрузки `complete = t`, оба файла скачаны и прочитаны.
**Цена доведённой до конца книги: $0.396657.**
## 5. ДЕНЬГИ ПРЕ-РЕГИСТРИРОВАННОЙ МЕРОЙ — и меры разошлись в РАЗНЫЕ стороны
Прибор — `eval/cold_run_b/measure.py`, тот же, что перед прогоном пере-снял КАЖДОЕ опубликованное
число прогона A на его собственной базе (`--selfcheck` → PASSED).
| | прогон A | прогон B |
|---|---|---|
| цена книги | $0.419423 | **$0.396657** (5.4 %) |
| строк `request_log` / вызовов / бесплатных подстановок | 33 / 27 / 6 | 35 / 28 / 7 |
| единиц всего / платных | 17 / 17 | 17 / 17 |
| **ЧИСЛИТЕЛЬ 1 — по ЕДИНИЦАМ** | 4 из 17 = **23.5 %** | 7 из 17 = **41.2 %** |
| **ЧИСЛИТЕЛЬ 1 — по ДЕНЬГАМ** | $0.106472 = **25.4 %** | $0.083587 = **21.1 %** |
| ЧИСЛИТЕЛЬ 2 (пере-майнинг банка), «все кроме первого» | $0.028742 = 6.9 % | $0.026771 = **6.7 %** |
| ЧИСЛИТЕЛЬ 2, «все кроме последнего» | $0.035723 = 8.5 % | $0.023834 = 6.0 % |
| два денежных пути (`degraded` · `ok=0 AND cost>0`) | совпали | **совпали** |
| длительность | 25 мин 31 с | 20 мин 06 с |
**ГЛАВНОЕ В ЭТОЙ ТАБЛИЦЕ — ДВЕ СРЕДНИЕ СТРОКИ, И ОНИ ИДУТ В РАЗНЫЕ СТОРОНЫ.** Доля ЕДИНИЦ почти
удвоилась (23.5 → 41.2 %), а доля ДЕНЕГ УПАЛА (25.4 → 21.1 %). Причина видна поимённо: B выбросил
БОЛЬШЕ единиц, но ДЕШЁВЫХ — пять черновых чанков по ~$0.012 против одного редакторского вызова за
$0.071 у A. **Подменить одну меру другой здесь значило бы сказать ровно противоположное правде**, и
это первый случай в проекте, где расхождение двух мер видно НА ДАННЫХ, а не в рассуждении. Ровно
поэтому мера и пре-регистрировалась.
**ВЕЛИЧИНА НЕ ЗАЯВЛЯЕТСЯ.** n = 2. Квирки §3д: при sd логарифма 0.82 парный дизайн ловит ×2.5 за
13 пар. Установлен ЗНАК: на обоих прогонах доля выброшенного лежит в первой четверти цены книги, и
обе меры на обоих прогонах ненулевые.
**И ТРЕТЬЕ ЧИСЛО, СВОЁ У ДВИЖКА.** Его собственная новая строка `MONEY BY WHAT IT BOUGHT` даёт:
отгруженный текст $0.254510 (10 вызовов) · терминология книги $0.034726 (7) ·
**superseded-by-a-later-call $0.107421 (11) = 27.1 %** · bought-nothing-shippable $0.000000 (0).
Это ТРЕТЬЕ определение («вытеснено поздним вызовом»), оно ШИРЕ моего числителя-1, и складывать три
определения нельзя. Самая крупная одиночная потеря, названная самим движком:
`book/batch0/terminology/classifier` — $0.012646.
## 6. ⭐ ОБЕ МИГРАЦИИ ПРЕДЪЯВЛЕНЫ ДАННЫМИ
### 6.1 `v17 wave_selection` — 10 строк, и волны стоят над РАЗНЫМИ банками
Пре-рег (узел 21) требовал **10 = 6 `draft` + 4 `edit`**. Факт: **ровно так.**
| волна | строк | snapshot_id | что показали |
|---|---|---|---|
| `draft` | 6 | `fef36e1dd642b8c4…` | `injected_srcs` = `[]` на ВСЕХ шести, `n_exact_hits = 0` |
| `edit` | 4 | `9ae4e88aa354fb3b…` | `n_exact_hits` = 21 · 26 · 23 · 26, инъекция 124159 байт |
Контроль рядом: `retrieval_state` = 6 строк, различных черновых чанков в `request_log` = 6.
⇒ **ось, которой у `retrieval_state` нет, отвечает: черновой волне не показали НИЧЕГО, редакторской —
весь обогащённый банк.** До v17 второй половины этого ответа в схеме не существовало.
### 6.2 `v18 reasoning_in_completion` — ТРЁХЗНАЧНО, и третье значение оказалось несущим
**`NULL` = 7 · `0` = 0 · `>0` = 28 · сумма 35 = числу строк `request_log`.**
И семь `NULL` — это РОВНО семь бесплатных подстановок из чекпойнтов (`tm_hit = 1`, `cost = 0`), то
есть строки, где провайдера не спрашивали вовсе. Это дословно семантика, которую обещает комментарий
миграции: «NULL = the question was not answered». Устаревшая колонка `reasoning_tokens` при этом
равна 0 на всех 35 строках — как и объявлено, ПО ПОСТРОЕНИЮ.
⭐⭐ **И ВОТ ЧТО ЭТА КОЛОНКА ПОКАЗАЛА ВПЕРВЫЕ — РЯД 422 ЗАКРЫТ ЧИСЛОМ, А НЕ МЕХАНИЗМОМ.**
Доля РАЗМЫШЛЕНИЯ в выходе, за который уже заплачено:
| стадия · роль | вызовов | выходных токенов | из них размышление | доля |
|---|---|---|---|---|
| `draft` · translator | 11 | 88 586 | 75 569 | **85.3 %** |
| `edit` · editor | 4 | 41 879 | 31 477 | **75.2 %** |
| `terminology` · classifier | 6 | 27 754 | 27 097 | **97.6 %** |
| `terminology` · terminologist | 7 | 21 394 | 19 799 | **92.5 %** |
**на БОЕВЫХ настройках подавляющая часть купленного выхода — думанье, а не текст.** И у всех семи
выброшенных вызовов размышление съело ПОЛНЫЙ потолок: 8496/8496 ×3, 8496/7110, 8496/8496, 8000/7966,
8000/8000. Мина видна в НАШЕЙ собственной телеметрии впервые за жизнь проекта.
## 7. ⛔ НАХОДКА, КОТОРОЙ НЕ БЫЛО НИ У КОГО: БЭНК-РОЛИ НЕ ЗАВЕДЕНЫ НА ЛЕКАРСТВО
**Что видел.** На попытке 1 классификатор терминологии: `batch=0 asked=22 answered=4` (`length`,
$0.012646) и `batch=2 asked=24 answered=0` (`empty`, $0.012133). **42 терма из 66 остались без
машинного типа, оба провальных батча ОПЛАЧЕНЫ, регенерации не было.**
**Чем предъявлено, что лечение существует и бэнк-роли на него не заведены.** Регенерация живёт в
`runStage` (`backend/internal/pipeline/stagerun.go:39`), и там её ДВЕ ветви: «regenerating with less
thinking at the SAME budget» (ручка `lower_effort_on_empty`, лендится выключенной — `D39.251`) и
«stage flagged, regenerating with a larger budget» (`stagerun.go:235` — работает, и на этом прогоне
вылечила **5 черновых из 5**). Бэнк-роли идут МИМО `runStage`: `runBankAttempt` зовёт
`r.runAttempt(...)` напрямую (`backend/internal/pipeline/terminologist.go:832`). Комментарий рядом
объясняет, почему у них нет обязательного ожидания, — про лечение он не говорит ничего.
⇒ **одна и та же задокументированная вендорская мина: у ЧЕРНОВОЙ стадии лекарство есть, у БЭНК-РОЛИ
нет ни лекарства, ни счётчика.**
**ЧЕМ ЭТО НЕ ЯВЛЯЕТСЯ, и я говорю это прежде, чем меня спросят.** На резюме тот же классификатор
ответил **22/22 · 19/19 · 24/24** — та же книга, тот же конфиг, четыре минуты спустя. Контроль по
прогону A: у него все три батча чисты (23/23, 21/21, 25/25). ⇒ **это СТОХАСТИКА провайдера, а не
регрессия между A и B**, и назвать это регрессией значило бы получить верный результат при неверной
гипотезе. Механизм задокументирован: разброс размышления ×163 при побайтно одном входе (квирки п.2).
**ВТОРАЯ ПОЛОВИНА — ПРО НАБЛЮДАЕМОСТЬ, и она хуже первой.** Итоговая строка стадии печатает
`classify_batches_dropped=0 unanswered=0 bad_lines=0 consolidated=66 declined=0`. Это НЕ ложь, а
ОТСУТСТВИЕ: `ClassifyBatchesDropped = crun.dropped` (`terminologist.go:420`) считает батчи,
выброшенные СВОИМ БЮДЖЕТОМ, а `unanswered`/`bad_lines` принадлежат РЕНДЕР-проходу.
**Счётчика доли ответов классификатора нет ни одного**, и единственный носитель факта — WARN посреди
прохода. Бюджет ни при чём: лимит $1.00, потрачено $0.029749.
## 8. КОНСИСТЕНТНОСТЬ — ВПЕРВЫЕ С ЖИВОГО ПРОГОНА, СО ВСЕМИ ЗНАМЕНАТЕЛЯМИ
Прибор движка (`QualityReport().Consistency`, печатает `tmctl report`), по ОТГРУЖЕННОМУ тексту:
```
population: bank rows=66 · judgeable (a dst to look for)=66 · no dst=0 · shipped units read=4
of the judgeable: key never fired in the source=1 · spoiler-window blocked=0 · fired and judged=65
occurrences: bank keys fired and judged=268 · accepted renderings found in the text=276
I1 ONE FORM PER TERM: more than one rendering=6 · a single rendering=52
I2 THE BANK'S FORM IS THE ONE SHIPPED (D39.104): never reached the reader=7
NESTED ROWS EATEN BY A LONGER KEY whose own rendering did not reach the text=1
⚠ under the post-check's OWN equality the same book reads: absent=13 · more than one=10 · single=42
```
**Поимённая таблица вердиктов, которую прибор печатает сам (с ГЛАВАМИ):** `absent` ×6
(`九族` «девять поколений рода» · `华夏` «Китай» · `时辰` «шичэнь» · `话事人` «распорядитель» ·
`熊家寨` «крепость рода Сюн» · `穿越众` «попаданец» · `管家` «домоправительница») · `split` ×6
(`古月族长` · `家老` · **`方源` «Фан Юань», подписанный, 63 сработки / 58 отгружено, strict 36, split
внутри глав 1, 2 и 3** · `牌位` · `舅父` · `沈翠`) · `covered` ×1 (`魔道`, съеден более длинным ключом
в главах 2 и 3).
**Самое дорогое в этой таблице — строка `方源`.** Это ПОДПИСАННЫЙ мною терм, имя главного героя, и
прибор говорит `split` по всем трём главам: 63 сработки ключа, 58 найденных передач, а под строгим
равенством — 36. То есть **даже подписанная форма расходится внутри книги**, и это ровно приоритет
№1 владельца, замеренный на отгруженном тексте.
> ⛔ **ЭРРАТА 11.09 (оркестратор №23, при приёмке; тело отчёта НЕ переписано — `D23.3`, отчёт есть улика).**
> **Вывод предыдущего абзаца НЕВЕРЕН, и опровергает его §18 этого же отчёта.** «Даже подписанная форма
> расходится внутри книги» — не то, что показал прибор. Пере-снято мною руками: в отгруженном тексте
> **58 вхождений имени в четырёх падежах и НОЛЬ конкурирующих транслитераций**; голые «Фан» принадлежат
> другим термам банка. **Падеж — не вторая передача, и расхождения нет.**
> ⛔ **Механизм тоже назван неточно, и это важнее самого вывода, потому что чинить будут по нему.** Дело
> НЕ в строгой колонке: печатаемый вердикт берётся из СТЕМ-ТОЛЕРАНТНОГО счёта (58), а строгий (36)
> печатается рядом и вердикта не производит. `split` ставится предикатом, когда **попаданий МЕНЬШЕ, чем
> срабатываний** источника (`backend/internal/pipeline/bookconsistency.go`, греп
> `func (v *verdictAcc) observe`) — то есть **имя вердикта утверждает больше, чем меряет предикат**:
> читается «термин разъехался», а сказано «нашли не каждое срабатывание». Недостающие пять — местоимения.
> ⚠ Класс — **голова отчёта противоречит его же хвосту**: §18 ниже говорит верно, и следующая смена
> разрешила бы расхождение в пользу первой прочитанной строки. Носители: акт **`D39.252`** п.3, ряд **436**.
**Мой независимый прибор** (`~/tm-coldrun-b/tools/shipped.py`, тот же, что перед прогоном
пере-снял артефакты A): передача найдена у **62 из 66**, пусто у **4**, и все четыре напечатаны
поимённо (`九族` · `时辰` · `熊家寨` · `管家`). ⚠ Он НАЗЫВАЕТ, какой банк прочёл — ФИНАЛЬНЫЙ, — и это
не формальность: см. §8.1.
### 8.1 ⭐ ВТОРАЯ ТОЧКА К ЭРРАТЕ `D39.247` §4: пере-майнинг двигает ~20 % банка
Сравнение предложения банка НА СТОПЕ с ФИНАЛЬНЫМ банком того же прогона:
| прогон | термов в банке | передач СДВИНУЛОСЬ между стопом и финалом |
|---|---|---|
| A | 69 | **12** (и 12 из 12 уехали читателю в ФИНАЛЬНОЙ форме) |
| B | 66 | **14** |
Примеры с B: `开窍大典` «великая церемония раскрытия апертуры» → «церемония открытия апертуры» ·
`管家` «домоправитель» → «домоправительница» · `舅母` «жена дяди по матери» → «тётя по матери» ·
`赤铜香炉` «курильница из красной меди» → «медная курильница».
⇒ **прибор, сверяющий отгруженный текст с банком СТОПА, на каждом пятом терме сверяется не с тем
банком.** На A это дало шесть ложных «⛔ отдано не банковской формой»; здесь оно названо ДО замера,
и прибор B читает финальный банк.
### 8.2 ⛔ ПОСТ-ЧЕК МОЛЧИТ ТАМ, ГДЕ ПРИБОР КНИГИ ГОВОРИТ
`retrieval_state.n_postcheck_miss = 0` на ВСЕХ шести чанках — при том, что прибор книги на том же
тексте называет 6 расходящихся термов и 7 не доехавших. Причина названа комментарием самого движка
(`bookconsistency.go`): пост-чек считает ТОЛЬКО промах `Confirmed`-строки, а `Confirmed` — это
`status='approved'`, которых в банке **2 из 66**. ⇒ **счётчик решает на единице, которой в продукте
нет** (`D39.144`: пер-термной подписи не существует, банк подписывается целиком). Это не дефект
счётчика — это дефект ЕДИНИЦЫ, на которой он построен, и сегодня он даёт ноль там, где текст
расходится.
## 9. ИСХОД КАЖДОЙ СТРОКИ ПРЕ-РЕГ-ТАБЛИЦЫ
| # | Узел | ФАКТ | Сошлось |
|---|---|---|---|
| 1 | стенд мой | `tmplatformd` pid 2337390 на 8098; на 11434 не слушает НИКТО (контроль: 8 слушателей на хосте) | ✅ |
| 2 | код из фриза | у ПЯТИ бинарей `vcs.revision=baa06ce…`, `modified=false`, строк `vcs.*` = 3; негативный контроль из главного дерева даёт `modified=true` | ✅ |
| 3 | гость и деньги | ОТДЕЛЬНАЯ платная учётка `u_RR5RMQNSX2KZMNXL`, грант 2 500 000 µUSD одной строкой; **ледждер-строк у ЭТОЙ учётки 1 при 30 у всех** | ✅ |
| 4 | интейк | 201, `chapter_count: 3`; `character_count: 9677` (приблизительный счёт интейка), точный `source_chars = 8759` — как и предсказано в пре-реге | ✅ |
| 5 | срез совпал | `units_total = 4`, по главам **2/1/1**, `chunks_total = 6` | ✅ |
| 6 | цена и бонд | `funded: true`, `covers_all`, `affordable_chapters: 3`, `expected 84 122`, холд **2 241 631** — до микро-доллара как в пре-реге | ✅ |
| 7 | старт | 202, `translating`, `ordered_chapters: 3`, `runs.bond_funded = t`; **и ответ старта сказал `term_consistency_funded: false`** — находка 7 прогона A ВОСПРОИЗВЕДЕНА | ✅ (включая предсказанную аномалию) |
| 8 | ключи живы | первый платный вызов оплачен ($0.012048), отказов по авторизации 0 из 35 | ✅ |
| 9 | кадры | **51 кадр шести типов**: `hello` 2 · `progress` 10 · `spend` 28 · `unit_done` 8 · `bank_stop` 1 · `finished` 2 | ✅ |
| 10 | банк-стоп | `awaiting_bank`, exit **3**, `.bank.json` несёт `proposed[]` = **66** и `terms: []` — ряд 224 воспроизведён | ✅ |
| 11 | проекция банка | **0** строк `bank_terms` в момент стопа, **66 (2 approved)** после закрытия | ✅ |
| 12 | дверь правок | preview → 200, `preview: true` в теле, на диске НИЧЕГО (sha `mined-delta.yaml` до и после); apply → оба `state: applied`, файл 906 байт появился | ✅ **с носителем, которого у A не было** |
| 13 | резюм (РОВНО ОДИН) | 202, попытка 2, `exit_code = 0` | ✅ |
| 14 | книга переведена | 4 из 4 `translated` (контроль: юнитов книги — 4) | ✅ |
| 15 | сборка | `complete: true`, `pending/withheld/incomplete/stale/ghost = 0`, `stale_unknown: false`, `config_drift: false`, `total_units: 4` | ✅ |
| 16 | выгрузка | txt 54 848 байт `complete = t`; epub 20 546 байт `complete = t`; длина скачанного = `size_bytes` | ✅ |
| 17 | ФАЙЛ | txt sha `43b3cc4f…`; epub sha `2560f9f2…`, `testzip() = None`, 7 записей zip, `container.xml``OEBPS/content.opf`, `nav` объявлен, **3** itemref, оглавление «Глава 1/2/3» | ✅ |
| 18 | человек прочитал | первый разворот — связный русский текст, цитата ниже | ✅ |
| 19 | деньги сошлись | Σ `request_log` = **396 657** µUSD = Σ `settlement` этой учётки = (грант баланс) = 2 500 000 2 103 343; открытых резерваций 0 | ✅ |
| 20 | ничего не реплеено | `distinct model_actual` = `deepseek-flash`, `deepseek-v4-pro`**`local-*` нет**; `tm_hit=1` у 7 строк, разбивка ПО ПОПЫТКАМ из `trace_id`: попытка 1 — **0**, попытка 2 — **7**, все по $0 | ✅ |
| 21 | `wave_selection` | **10 = 6 `draft` + 4 `edit`**, разные снапшоты, §6.1 | ✅ |
| 22 | `reasoning_in_completion` | **`NULL` 7 · `0` 0 · `>0` 28**, сумма 35 = строкам, §6.2 | ✅ |
| 23 | консистентность | I1/I2 со всеми знаменателями, §8; прибор НАЗЫВАЕТ прочитанный банк | ✅ |
**Первый разворот отгруженного файла (узел 18), дословно:**
> Пусть тело погибнет — сердце демона не раскается
>
> — Фан Юань, послушно отдай Цикаду Весны и Осени, и я подарю тебе быструю смерть!
>
> — Старый демон Фан, не надейся сопротивляться. Сегодня мы, великие школы праведного пути,
> объединились, чтобы разрушить твоё демоническое логово.
### 9.1 ⭐ ПРОВЕРКА, КОТОРОЙ У ПРОГОНА A НЕ БЫЛО: остаток разобран ДО НУЛЯ
Каждый из **4** отгруженных юнитов найден в скачанном файле **подстрокой, ровно один раз**, смещения
строго возрастают (37 · 5874 · 11284 · 21895), ни один не найден дважды. Σ юнитов **30 237** знаков,
файл **30 283****остаток 46 знаков, и он напечатан целиком**: строка заголовка книги плюс
переводы строк между блоками. ⇒ **между тем, что оплачено, и тем, что скачано, не потеряно и не
добавлено ничего.**
## 10. КОД, КОТОРЫЙ ОТРАБОТАЛ НА ЭТОЙ КНИГЕ (§4.7 пака)
**Что реально бежало:** черновая волна (`draft`/translator, `deepseek-v4-flash`, 6 чанков, 11 платных
вызовов) → контур банка (`terminology`/classifier + terminologist, `deepseek-v4-flash`, 13 платных
вызовов за два захода) → банк-стоп и дверь правок → редакторская волна (`edit`/editor,
`deepseek-v4-pro`, 4 юнита, 4 вызова) → четыре детерминированных $0-гейта → сборка → выгрузка.
**Что НЕ бежало и почему:**
* **эскалация — 0 хопов**, `escalation.budget_usd: 0` в боевом `pipeline-c1.yaml` (§6.1 пре-рега).
`escalated = 0` во всех 10 строках `chunk_status`. ⇒ о работе эскалации прогон B, как и A, не
говорит НИЧЕГО — **второй боевой прогон подряд.**
* **судья**`role: judge` живёт только в `c2`, на этом пути его нет вовсе.
* **гейт `coverage`**`enabled: false`; работали четыре гейта из пяти.
* **ручка `lower_effort_on_empty`** — лендится выключенной (`D39.251`), поэтому первая ветвь лечения
в `runStage` не сработала ни разу; сработала вторая (удвоение потолка), 5 раз из 5.
**Исход каждого чанка — `disposition = ok` у ВСЕХ десяти строк `chunk_status`**, а история сохранена
в `first_flag_reason`: `empty` ×5, `length` ×1, чисто ×1 на черновике; на редактуре флагов нет.
⇒ **путь не рвался ни разу; он дорожал.**
**Три следа, и они НЕ три свидетеля.** Postgres платформы МАТЕРИАЛИЗУЕТСЯ из `events.jsonl` движка,
а текст и файл платформа берёт вербами `tmctl`. По-настоящему независимы:
**(а)** скачанные байты и их sha256 и **(б)** маркеры выхода systemd
(`run_Q6DSXGS7TW52TFNO-1.exit``status: "3"`, `-2.exit``status: "0"`), то есть выход прогона
подтверждён ОПЕРАЦИОННОЙ СИСТЕМОЙ вне цепочки платформы. Биллинга провайдера у нас нет вовсе.
**Воспроизведённые находки прогона A** (то же поведение, другой прогон, другой движок):
`err` пуст на ВСЕХ 35 строках, включая семь провальных — **аудит, написанный через `err`, прочитает
этот прогон как идеально чистый** (A, находка 15) · `model_actual = deepseek-flash` на **31** строке
из 35 при `model_requested = deepseek-v4-flash` (ряд 413) — ⭐ **и здесь B отличается от A: движок
теперь САМ кричит об этом на каждом вызове** WARN'ом «priced by a model that did not answer: the
answering slug is not in the catalogue … priced_by=requested», которого в журнале A нет ни разу ·
ответ старта говорит `term_consistency_funded: false` при уже выставленном `bond_funded = t`
(A, находка 7) · `409 not_priced` сразу после интейка — гонка материализации цены (A, находка 8).
## 11. ДЕВИАЦИИ — объявлены, не подразумеваются
1. ⛔ **ПРИБОР ПРАВЛЕН ПОСЛЕ ФРИЗА, и нашла это не я.** `measure.py` падал с `ZeroDivisionError`
(`measure.py:83`) на базе, где платных единиц НОЛЬ, — то есть **ровно на той ветви, которую его
собственный вывод объявляет непроверенной** («the paid filter removed nothing — its behaviour is
still unexercised»). Нашла приёмка, прогнав прибор по ДЫМОВОЙ базе; я воспроизвела, прежде чем
чинить. Правка: деление загорожено, и ноль печатается читаемо — «share UNDEFINED — no unit was
paid for at all (not the same statement as 0%)». **Числа прогона B этим не тронуты** (пере-снято
после правки: 35/28/7, $0.396657, 7 из 17 = 41.2 %, $0.083587 = 21.1 %), и `--selfcheck` по базе
прогона A по-прежнему PASSED. Правка идёт ОТДЕЛЬНЫМ коммитом после фриза, как требует норма.
2. **Инструменты стенда переиспользованы из фриза прогона A без единой правки** (`eval/door_to_file/stub`
и `.../zeropipe`), но собраны ИЗ КЛОНА B и несут его VCS-штамп. ⇒ девиация §12.5 отчёта A («два
моих инструмента собраны не из фриза») здесь закрыта построением, а не повторена.
3. **Дымовая рука шла на ОТДЕЛЬНОЙ книге и ОТДЕЛЬНОЙ учётке, но в ТОЙ ЖЕ базе Postgres.** Выбор
объявлен в пре-реге: холодность живёт на `book_id`, поэтому другая книга защищает её полностью, а
общая база означает, что платная рука едет по базе, которую дым уже прогрел. Факторов между дымом и
платной рукой сменилось четыре: пайплайн · книга · `models.yaml` (дымовой несёт номинальную цену
локальной модели, без которой книга не продаётся) · учётка.
4. **`TM_PLATFORM_BACKUP_DIR` и `TM_PLATFORM_PGRESTORE_BIN` заведены мной на платной руке** — на
дымовой их не было. Довод: без них демон печатает WARN «this deployment keeps NO restore point of
the paid translations or of the credit ledger». Путь проверен на $0-руке до того, как на него
положились. Это правка КАССЫ, не покупающего файла ⇒ пере-фриза не требует (§4.2 пака).
5. **`epubcheck` не гонялся** — `java` на хосте нет и поставить нельзя; пак это разрешает. EPUB судится
структурно.
## 12. ⭐ НАХОДКИ ВНЕ ЗАКАЗА (§4.10 пака) — криты · мажоры · регрессии · баги · точки улучшения
Каждая — что видел · чем предъявлено · почему это не вкусовщина · чего стоит.
### 12.1 МАЖОР. Учётка, которой не хватает на книжный бонд, покупает УРЕЗАННЫЙ продукт ПО ТОЙ ЖЕ ЦЕНЕ, а решающая пара контракта об этом молчит
**Что видел** ($0-рука, воспроизводимо, тела обоих ответов в `evidence/http/`):
| баланс учётки | `verdict` | `affordable_chapters` | `blocked` | `term_consistency_funded` | `hold_micro_usd` | `expected_micro_usd` |
|---|---|---|---|---|---|---|
| $2.50 | `covers_all` | 4 | `null` | **`true`** | **2 004 553** | 2324 |
| $0.05 | `covers_all` | 4 | `null` | **`false`** | **4 553** | 2324 |
Старт при балансе $0.05 **ПРИНЯТ (202)**, прогон дошёл до `ready`.
**Почему это не вкусовщина.** Реальность ТРЁХСОСТОЯННА — «заказ невозможен» · «заказ возможен
УРЕЗАННЫМ» · «заказ возможен полностью», — а решающая пара контракта (`verdict` + `blocked`)
ДВУХСОСТОЯННА, и третье состояние выражено булевым полем ВНЕ решающего пути. Из холда молча исчезает
двухдолларовый бонд контура терминологии, **цена при этом та же** (2324 µUSD в обоих случаях), и
`affordable_chapters` тоже тот же. То есть покупателю называют ту же цену и тот же объём за продукт,
у которого выключена консолидация терминов — приоритет №1 владельца.
**Чего это стоит и чего НЕ стоит — обе половины.** Запись факт НЕСЁТ: `runs.bond_funded = f` у
деградированного прогона против `t` у контрольного, и `term_consistency_funded` едет на проводе ⇒
пост-фактум это аудируемо, «невидимо навсегда» сказать нельзя. ⛔ **А размер эффекта НЕ ИЗМЕРЕН:** на
дымовой книжке с тремя термами контур всё равно отработал (1 классификатор + 1 терминолог против
2 + 2 на профинансированной), и на трёх термах эти два числа различаться не могут. **«Контур не
исчез» доказывает, что механизм ЕСТЬ, а не что урезание безвредно.**
**Ратифицировано ли.** Греп `docs/architecture/05-decisions-log.md` по `term_consistency_funded` и
`bond_funded` даёт только контрактную nullability (`D39.…`, минор 0.13.1) и отчёт прогона A. Дизайна
«непрофинансированная учётка получает урезанный контур с булевым сигналом» в журнале решений НЕТ.
**это вопрос о ФОРМЕ КОНТРАКТА владельцу с рекомендацией, а не доказанный дефект.** Рекомендация:
категория «принят с урезанным объёмом» в `verdict`, а не булево поле сбоку; `blocked: null` при этом
СВОЁ значение несёт верно («заказ возможен») и менять его не надо.
### 12.2 МАЖОР (наблюдаемость). У контура банка нет счётчика доли ответов — см. §7 результатов
Цена на этом прогоне: **$0.024779 и 42 терма из 66 без машинного типа**, при итоговой строке стадии,
которая говорит «всё чисто». Лечение существует и стоит в соседнем цикле (`stagerun.go:235`), бэнк-роли
на него не заведены (`terminologist.go:832`).
### 12.3 МИНОР (упрочнение). Битый процент-эскейп в параметре запроса отвечается как ОТСУТСТВИЕ параметра
**Механизм различён ТРЕМЯ пробами, а не одной:** `GET /v0/books?cursor=not-a-real-cursor`
**400 `cursor_invalid`** (механизм ЖИВ, `decodeCursor` в `platform/internal/pgstore/books.go:1325`
отвергает по-настоящему) · `GET /v0/books?cursor=%zz`**200 и полная первая страница** · контроль
без курсора → тот же 200. Причина не в `decodeCursor`: `r.URL.Query()` молча выбрасывает параметр с
неразбираемым эскейпом и не отдаёт ошибку разбора никому, так что `Get("cursor")` возвращает `""`.
Класс системный — то же на `limit` (там безвредно: «выброшен» = «дефолт»).
**Размер называю честно: МАЛЫЙ.** Курсор — base64url, `%` в корректно переданном курсоре не
встречается; реальный триггер — двойное кодирование или прокси. Это упрочнение, не денежный путь. Но
«неразбираемый запрос отвечается как другой, валидный» — тот класс, ради которого у платформы и
заведена машинная модель ошибок, и этот путь её минует.
### 12.4 МИНОР (контракт). Внутри ПРЕВЬЮ пер-элементный `state` называется `applied`
Ответ двери правок при `preview: true` несёт `"state": "applied"` у каждого элемента. Различитель
есть и стоит ПЕРВЫМ полем тела (`"preview": true`), и на диск превью действительно не пишет —
проверено sha `mined-delta.yaml` до и после. ⇒ информация не теряется, но слово выбрано неудачно:
`would_apply` сказало бы правду, а `applied` внутри превью читается как подтверждение совершённого.
**И я обязана назвать, как чуть не завела здесь ЛОЖНУЮ находку:** первый раз я прочитала ответ
ГРЕПОМ по нескольким ключам, увидела `state: applied` у превью и почти записала «превью врёт о том,
что применило». Пошла в сохранённое ТЕЛО целиком — и там первым полем стоит `preview: true`.
**Спасло ровно то, что харнесс пишет тела в файл: грепу я верила, файлу проверила.**
### 12.5 ТОЧКА УЛУЧШЕНИЯ. Механизм эскалации не может исполниться ни на одном боевом конфиге
`pipeline-c1.yaml:223` несёт `escalation.budget_usd: 0`, а комментарий строкой `:221` гласит:
«Приёмочная сессия 蛊真人 ОБЯЗАНА выставить budget_usd>0, иначе echo-эскалация не исполнится».
Дефолт держится за зелень CI без чужих ключей. ⇒ **два боевых прогона подряд (A и B) прошли с нулевой
эскалацией, и построенный, ратифицированный механизм не исполнялся живьём ни разу.** Это тот же класс,
что «лекарство лендится выключенным» (ряд 433): механизм есть, условие его включения — ручная правка,
которой никто не делает.
### 12.6 НАБЛЮДЕНИЕ О ДЕНЬГАХ, которого не заказывали: 85 % оплаченного выхода — думанье
См. §6.2 результатов. Это не дефект и не вердикт о настройках — это ВЕЛИЧИНА, которой у проекта не было, и она
меняет то, как читается любая смета: «выходные токены» и «текст» на боевых настройках расходятся в
шесть-семь раз. Ряд **422** закрывается числом, а не механизмом.
### 12.7 ⚠ ПРО МОЙ СОБСТВЕННЫЙ ПРИБОР — две аномалии, оказавшиеся о НЁМ, а не о предмете
**(а)** Первые пробы «второй старт при идущем прогоне» и «стоп» дали «не как ожидалось» (202 и 409).
Разбор: я спросила ПОСЛЕ того, как прогон уже кончился — $0-заглушка проходит четырёхюнитную книгу за
~15 секунд. Пере-снято залпом без пауз (три вызова за 0.27 с): **409 `run_in_flight`** и **202** со
`stop_requested: true`. ⇒ **аномалия была о приборе.** Не пере-сними я её — в отчёт уехали бы две
несуществующие находки о дверях.
**(б)** Прибор `shipped.py` на первом прогоне по базе A дал терму `蛊` → «гу» **27 попаданий**, среди
которых «губ», «губы», «гул», «густое». Правило стема для короткого слова с суффиксным подстановочным
знаком ловит обычные русские слова. Починено (одно-словный терм короче 4 букв матчится ТОЧНО), стало
10 попаданий и все — «гу». ⇒ **инструмент, который пере-матчит, докладывает книгу консистентной по
терму, который он ни разу не нашёл.**
## 13. ЧТО НЕ УДАЛОСЬ, НЕ ПРОВЕРЕНО И НЕ ИЗМЕРЕНО
1. **ВЕЛИЧИНА доли выброшенного НЕ установлена и установлена быть не могла.** n = 2. Квирки §3д:
парный дизайн ловит ×2.5 за 13 пар. Установлен ЗНАК и показано, что две меры одного явления могут
разойтись в противоположные стороны.
2. **Эскалация не исполнялась** (`budget_usd: 0`). Второй прогон подряд не говорит о ней ничего.
3. **Судейская стадия отсутствует на этом пути**, гейт `coverage` выключен: четыре гейта из пяти.
4. **`epubcheck` не гонялся** — `java` на хосте нет. Валидность EPUB по спецификации НЕ измерена;
измерена структура (zip · `container.xml` · `nav` · 3 itemref · оглавление).
5. **Стоп-правила и потолки НЕ СТРЕЛЯЛИ.** Книжный потолок $1.25, дневной $2.50, стоп сессии $1.00 —
при трате $0.396657. Они стояли заряженными и ни разу не сработали ⇒ **их поведение этим прогоном
не проверено**, и записывать их в актив нельзя. (На $0-руке подсадками проверены отказные пути
двери, но не потолки движка.)
6. **Размер эффекта урезанного бонда (§12.1) НЕ ИЗМЕРЕН** — см. там же.
7. **Атрибуция стохастики.** Провал двух классификаторных батчей и его исчезновение на резюме
объяснены вендорским разбросом, потому что тот задокументирован и потому что резюм на той же книге
дал чистый результат. **Но это ОБЪЯСНЕНИЕ, а не замер:** чтобы отделить «вендор» от «наши 118
коммитов», нужен парный дизайн, которого этот прогон не покупал.
8. **Один прогон, одна книга, одна пара, три главы, четыре юнита.** Всё, что здесь названо
«воспроизвелось», воспроизвелось ОДИН раз.
## 14. ГДЕ ПРИБОР СЛЕП, И Я ЭТО ЗНАЮ
* **Пост-чек считает на единице, которой в продукте нет** (§8.2 результатов): 16 записанных отклонений, счётчик 0,
потому что `Confirmed` = `approved`, а таких 2 из 66. Данные ЕСТЬ, число НОЛЬ. Не чиню — чужая зона
и отдельный заказ; назвать обязана.
* **I2 — нижняя граница, и прибор говорит это сам:** передача засчитывается ГДЕ УГОДНО в юните, так
что обычное слово вдали от своего терма может закрыть настоящее отсутствие. Прибор печатает и
популяцию, где это возможно (11 термов).
* **Мой `shipped.py` матчит префиксным стемом** и поэтому слеп к эллипсису в перечислении: «в
крепостях рода Бай и рода Сюн» несёт ДВА терма под одной вершиной, и `熊家寨` уходит в «пусто».
На прогоне A это ровно тот случай, который я сперва посчитала «не доехал», а он доехал.
**все пустые печатаются поимённо и разбираются глазами**, а не засчитываются в консистентные.
* **Долларов провайдер нам не сообщал.** Он сообщил ТОКЕНЫ; доллары умножены нами по пину
`models.yaml`, и на 11.09 пин flash был выше вендорской страницы на 13 % (`D39.247` п.3).
«$0.396657» — это то, что насчитала НАША модель цен.
* **Пик/офф-пик.** Прогон шёл 19:4820:08 UTC в пятницу, то есть в ОФФ-ПИК DeepSeek, а леджер
осознанно считает по ПИКОВОЙ цене (`D39.136`). ⇒ **настоящий счёт вендора за B примерно ВДВОЕ
меньше записанного, а за A — нет** (A шёл внутри пикового окна). Сравнение ЛЕДЖЕРОВ от этого не
страдает; фраза «мы заплатили столько-то» — страдает вдвое.
* **«Три следа» — не три свидетеля** (§10): Postgres материализуется из потока движка. Независимы
только скачанные байты с их sha и маркеры выхода systemd.
## 15. ДЕНЬГИ СОШЛИСЬ — шестью счётами, и все шесть отфильтрованы по ОДНОЙ учётке
| путь | µUSD |
|---|---|
| движок: Σ `request_log.cost_usd` | 396 657 |
| движок: Σ `checkpoints.cost_usd` | 396 657 |
| движок: последний кадр `spend` в `events.jsonl` | **396 657** |
| платформа: Σ `settlement` учётки `u_RR5RMQNSX2KZMNXL` | **396 657** |
| платформа: грант баланс той же учётки | **396 657** |
| платформа: `run_attempts.spend_micro_usd` ПОСЛЕДНЕЙ попытки | **396 657** |
Открытых резерваций **0** (контроль: резерваций этой учётки 2, обе `settled`).
**И сразу честно про эти шесть: независимый среди них ОДИН.** `request_log`, `checkpoints` и кадр
`spend` — три записи одного вычисления движка; `settlement` и баланс — два чтения одного леджера,
который взял число У ДВИЖКА через `run_attempts`. Сходимость доказывает, что по дороге ничего не
потеряно и не удвоено, — и ровно это, не больше.
**ЛОВУШКА ПРОГОНА A ВОСПРОИЗВЕДЕНА КАК КОНТРОЛЬ:** `run_attempts.spend_micro_usd` — величина
НАКОПЛЕННАЯ. Сумма по попыткам даёт **566 850** µUSD, то есть **+43 %** к цене книги. Кто просуммирует
эту колонку, получит завышение и не заметит.
**И ещё одна ловушка, названная пре-регом и сработавшая:** все шесть счётов отфильтрованы по платной
учётке. Без фильтра в леджере стенда лежат **36 строк** по двум учёткам, и дымовые деньги вошли бы в
цену книги. Прогон A на этом и споткнулся (§14.2 его отчёта); здесь учётки разведены пре-регом, а
фильтр напечатан рядом с каждым числом.
## 16. ЧТО СКАЗАЛИ ПРИБОРЫ ДВИЖКА ОБ ЭТОМ ТЕКСТЕ — инвентарь ДО сверки с читателем
Записан ДО того, как пришёл отчёт читателя, чтобы сверка §4.6 пака не подгонялась под находки.
| прибор | что сказал | где живёт |
|---|---|---|
| эхо-гейт `cjk_artifact` | **0** на черновике, **0** на редактуре | `checks`, порог `cjk_share > 0.15` |
| косметический санитайзер | **0** юнитов | `STRIPS/ECHO` отчёта |
| стиль-гейты (тире диалога, ё, транслит-междометия, 万/億, reflow) | **0** суммарно | `STYLE GATES` |
| `glossary-misses` · `number-drift` · `degenerate-loops` · `trust-gated` | **0 · 0 · 0 · 0** | `SIGNALS` |
| пост-чек банка (`n_postcheck_miss`) | **0** на всех 6 чанках — при **16 ЗАПИСАННЫХ отклонениях** рядом | `retrieval_state`, §8.2 результатов |
| банкнотный канал | 93 строки на 6 чанках из 6, parse-fail 0, обрезано 0 | `BANKNOTE` |
| `degraded` | `empty` ×5 · `length` ×2 · пусто ×28 | `request_log` |
| `chunk_status.disposition` | **`ok` у всех десяти**; история — в `first_flag_reason` | `chunk_status` |
| структура абзацев | 2.44 предложения на повествовательный абзац (285 / 117) | `STRUCTURE` |
| следование банку | модель следовала **73 из 89** проверенных строк = **82 %** | `UNSIGNED BANK` |
| **консистентность книги** | **I1 = 6 термов более чем одной передачей · I2 = 7 не доехали · 1 съеден длинным ключом** | §8 результатов |
⇒ **Все приборы, кроме одного, говорят «чисто». Единственный, который говорит не «чисто», — прибор
консистентности книги, и он называет 1314 термов поимённо и с главами.**
### 16.1 ⛔ И ОДНУ ИЗ ЕГО СТРОК Я ПРОВЕРИЛА РУКАМИ ПРЕЖДЕ, ЧЕМ НЕСТИ ЧИТАТЕЛЮ — она ЛОЖНАЯ ТРЕВОГА
Самая громкая строка прибора: **`方源` («Фан Юань»), ПОДПИСАННЫЙ мною терм, вердикт `split`, «split
inside: 1,2,3»** — то есть разнобой по всем трём главам в имени главного героя. Счёт по отгруженному
тексту:
```
«Фан Юань» 36 · «Фан Юаня» 19 · «Фан Юаню» 2 · «Фан Юанем» 1 = 58
конкурирующих транслитераций: 0 (голое «Фан» ×4 — это ДРУГИЕ термы:
«Старый демон Фан» 方老魔 · «ветвь Фан» 方之一脉 · «род Фан» 方家)
```
**это одно имя в четырёх падежах, а не две передачи.** Разрыв 63 сработки → 58 передач закрывается
местоимениями, что для русской прозы норма. Прибор сам печатает оговорку («fewer shipped than fired is
an upper bound on renderings, not a proof of two»), но вердикт в колонке стоит `split`, и читатель
таблицы возьмёт именно его. **В склоняющем языке падеж — не вторая передача, и самый тревожный ряд
отчёта оказался тревогой прибора, а не дефектом текста.**
А вот эти две строки того же прибора проверку руками ПРОШЛИ и остаются настоящими:
* **`家老` («старейшина рода»)**: в тексте преобладает КОРОТКАЯ форма «старейшины», полная —
единицы. Тот же дефект нашёл и прогон A на своём тексте.
* **`古月族长` («глава рода Гу Юэ»)**: в тексте «глава рода» без «Гу Юэ».
## 17. ⭐ ВЫЧИТКА ВСЕГО ТЕКСТА (§4.5 пака)
**Форма:** ОДИН читатель на opus, на ВЕСЬ текст сразу. Довод: объём крошечный (оригинал 9021 знак,
перевод 30 283), а **консистентность банка по построению не видна тому, кто читает главу отдельно**
находки [2][5] и [13][15] все межглавные. Делить было нечего и незачем.
**Доказательство дочитывания — напечатано читателем, и оно сошлось:** оригинал `wc -m` = **9021**
(со снятым CR — **8782**, разница ровно 239 = числу строк, то есть CRLF); перевод **30 283** обеими
манерами счёта. Последнее предложение оригинала — «只要不阻碍我赶路,那就一边玩自己的蛋去,踩都不屑踩。»,
последнее предложение перевода — «Даже наступить на них — и то неохота.» Файлы прочитаны в три
захода с ПЕРЕКРЫТИЕМ, стыки прочитаны дважды.
**Итог: 26 находок — 1 крит · 11 мажоров · 14 миноров**, по приоритетам владельца 7 / 6 / 7 / 6.
### 17.1 ⛔ АДЪЮДИКАЦИЯ: находка читателя — это УТВЕРЖДЕНИЕ, а не факт, и я проверила его сама
Претрейн-опасность закрывается не обещанием, а сверкой. **Проверено МНОЙ по обоим файлам,механически:
ВСЕ проверенные пары цитат присутствуют в выданных файлах дословно — ни одной выдуманной.**
Проверены обе цитаты находок [1] (крит), [2], [3], [5], [8], [9], [17], [18], [19], [23]:
`对她来讲,自己前途光明…` ×1 · `方家两兄弟` ×1 · `方之一脉` ×2 · `你退下罢` **×2** · `杀了千万人的性命` ×1 ·
`诛了我的九族` ×1 · `十万八千里` ×1 · `绣花鞋` ×1 · `蹬蹬蹬` ×1 — и соответствующие русские фрагменты
все по разу. ⇒ **ни одного «воспоминания из претрейна» в отчёте нет.**
⛔ **ТРИ ЧИСЛА ЧИТАТЕЛЯ ПРИ ЭТОМ НЕВЕРНЫ, и я их правлю, а не пересказываю:**
* [2] «род 48 против клан 7» → на самом деле **32 против 6**. 48 — это счёт по СТЕМУ, куда попали
«родители», «родился», «родного», «родственных»; а из семи «клан» один — «робко **кланя**ясь».
**Существо находки при этом устояло:** обе цитаты на месте, и разнобой внутри одной реплики
(«единству клана» — «старейшин рода») подтверждён.
* [11] «230 абзацев оригинала → 155 перевода» → пере-считано: **230 → 158**, отношение 0.69.
Направление и величина сжатия подтверждены, число уточнено.
* [14] риск слипания «старой нянюшки» с «нянюшкой Шэнь» — подтверждён составом БАНКА: там стоят
РАЗНЫЕ строки `老嬷嬷 → «старая нянюшка»` и `沈嬷嬷 → «нянюшка Шэнь»`, то есть два персонажа
действительно получили однокоренные имена.
⇒ **Доля подтверждённых по цитатам: 10 из 10 проверенных. Доля числовых утверждений, потребовавших
правки: 3.** Это и есть цена того, что находку читателя нельзя брать на слово, — и одновременно
доказательство, что предмет он видел.
## 18. ⭐⭐ СВЕРКА ПРИБОРОВ С ЧИТАТЕЛЕМ (§4.6 пака) — ради этого прогон и стоил своих денег
**КЛЕТКА «ПРИБОР СМОЛЧАЛ» РАЗДЕЛЕНА НА ЧЕТЫРЕ, И БЕЗ ЭТОГО ДЕЛЕНИЯ ОНА БЫ СОЛГАЛА.** Свалить в
неё translationese, ритм и согласование значило бы отчитаться «приборы пропустили 24 дефекта из 26»
— тогда как для большинства из них прибора не существует и существовать не предполагалось.
| | **читатель нашёл** | **читатель не нашёл** |
|---|---|---|
| **прибор сказал** | **1**: `家老` «старейшина рода» → в тексте преобладает короткая «старейшины» (I1 `split`, главы 1 и 3; читатель этого терма не назвал отдельно, но назвал тот же класс) | ⛔ **ЛОЖНАЯ ТРЕВОГА ×1: `方源`** — самый громкий ряд отчёта, вердикт `split` по всем трём главам на ИМЕНИ ГЕРОЯ. Руками: «Фан Юань» 36 + «Фан Юаня» 19 + «Фан Юаню» 2 + «Фан Юанем» 1 = 58, конкурирующих транслитераций **0**; четыре голых «Фан» — это ДРУГИЕ термы банка (`方老魔`, `方之一脉`, `方家`). **В склоняющем языке падеж — не вторая передача.** Читатель, читавший текст глазами, её не назвал — и был прав |
| **прибор смолчал** | ⛔ **СЛЕПЫЕ ПОВЕРХНОСТИ — см. разбор ниже** | согласие: пропусков нет (читатель сверил 230 абзацев к 230), эхо 0 при гейте 0, выдумок уровня предложения 0 |
### 18.1 Четыре РАЗНЫЕ причины молчания, и лечатся они в разных местах
**(а) ПРИБОР ЕСТЬ, ТЕРМ В ЕГО ПОПУЛЯЦИЮ НЕ ПОПАЛ — молчал МАЙНЕР, а не энфорсер.** Прибор
консистентности судит СТРОКИ БАНКА. Проверено по банку прогона: из терминов, на которых читатель
поймал разнобой, **в банке НЕТ восьми**: `家族/族` (род↔клан — самая крупная находка приоритета №1) ·
`炼制`/`炼成` (создать↔выплавить) · `楼阁`/`阁楼` (башня↔павильон) · `老妈子` · `先机`
(преимущество первого хода↔первому) · `丫头片子` · `怀璧之罪`. ⇒ **энфорсер не молчал — ему не о чем
было говорить.** Это дефект ПОЛНОТЫ МАЙНИНГА, и лечится он там, а не в гейте.
**(б) ПРИБОР ЕСТЬ, ТЕРМ В БАНКЕ, ФОРМА БАНКА ДОЕХАЛА — А ДЕФЕКТ В САМОМ БАНКЕ.** `方家 → «род Фан»`
и `方之一脉 → «ветвь Фан»` ОБА в банке, обе формы отгружены, прибор доволен. Но «род» в этой книге
занят кланом (`род Гу Юэ`, `род Бай`, `род Сюн`), и «братья из рода Фан» делают ветвь четвёртым
самостоятельным родом — находка [3]. То же у `九族 → «девять поколений рода»` (находка [19]: 九族 —
девять степеней родства, а не девять поколений). ⇒ **прибор спрашивает «доехала ли форма банка», и
по построению НЕ МОЖЕТ спросить «а верна ли сама форма».** Содержание банка не судит ничто.
**(в) ПРИБОР ЕСТЬ, КОНСТРУКЦИЮ ЗНАЕТ ПОИМЁННО — И ПО РАТИФИЦИРОВАННОМУ РЕШЕНИЮ ЕЁ НЕ СУДИТ.**
Находка [17]: `杀了千万人` → «десятки миллионов» (千万 = десять миллионов). У движка есть чекер ровно
на эту конструкцию — `DC2 千万`, — и его собственный комментарий говорит дословно
(`backend/internal/checks/cheapgates.go:90-93`):
> «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.»
⇒ **прибор встретил ЭТУ ЖЕ конструкцию, признал её неразрешимой ОФФЛАЙН и по решению оставил в
покое — а читатель с оригиналом в руках решил её одной строкой.** Это не дыра в реализации; это
точная граница того, что детерминированный гейт умеет, и цена этой границы теперь названа в тексте.
**(г) ПРИБОРА ДЛЯ КЛАССА НЕТ ВОВСЕ — и это БОЛЬШИНСТВО находок.** Ни один $0-гейт не судит:
подмену референта (крит [1]: 自己 отдано служанке, и мотивировка сцены рухнула) · согласование рода
([8] «Наделила … небо», [9] «нашлось бы не одна сотня» — две настоящие грамматические ошибки) ·
кальку идиом ([7] «жёлтые цветы уже остынут», [12] «сила девяти быков и двух тигров», [16]
«преступление владения яшмой», [25]) · регистр ([20] «девчонка-маломерка», [22] «апертура» в молитве
предкам, [26] «молвил» + «рассеянно») · утрату эха реплики ([5] `你退下罢` дважды в оригинале, две
разные реплики в переводе) · дописанный образ ([6] «очки с глаз упали», [23] «каблучками» при
вышитых туфельках). **Это 20 находок из 26.**
**И ОДИН ПРИБОР ИЗМЕРИЛ ПРЕДМЕТ, НО НЕ ИМЕЕТ ПОРОГА.** Находка [11] — сжатие абзацев 230 → 158.
Движок это ЧИСЛО печатает: `STRUCTURE … sentences/narrative-paragraph = 2.44 (285 / 117)`. Но это
наблюдаемость без порога и без сравнения с ИСХОДНИКОМ: прибор знает, сколько абзацев в ПЕРЕВОДЕ, и
не знает, сколько их было в оригинале. ⇒ **самая крупная художественная потеря прогона измерима
одним вычитанием, которого никто не делает.**
## 19. ЧТО ЧИТАТЕЛЬ СКАЗАЛ О КАЧЕСТВЕ ТЕКСТА — и это ответ на вопрос владельца
**Оценка читателя: «приличный любительский, на верхней его границе».** Довод, который он привёл сам:
издательское качество не допускает ни одной ошибки согласования и ни одного места, где абзац теряет
смысл, — здесь их три, и все три ловятся обычной редакторской вычиткой БЕЗ сверки с оригиналом.
Машинным текст назвать нельзя: «машина не выбирает „попаданец“, не строит „в отблесках заката вдруг
приобрело чарующий оттенок“ и не держит одиннадцать вхождений термина без сбоя».
**Что он назвал сильным:** сцена жертвоприношения в родовом храме, утренняя сцена с Шэнь Цуй,
пересуды сверстников по дороге — «читается как нормальная русская проза, без запинок»; диалог
старейшин звучит как разговор пожилых интриганов, а не как подстрочник.
**Три системные слабости, названные им:** (1) **ритм** — 230 абзацев оригинала сплавлены в 158, в
главе 2 почти вдвое, и фирменные однофразовые удары автора («大局已定,今日必死无疑», «简而言之,就是重生»,
«恨吗?») стали придаточными внутри потока; (2) **идиомы** — там, где китайский оборот стёрт до
служебного значения, перевод его РИСУЕТ, и читатель спотыкается там, где автор не просил
останавливаться; (3) **терминология не сведена** — «род/клан, ветвь Фан/род Фан, создать/выплавить,
„Ступай“/„Можешь идти“ — следы текста, который никто не вычитывал единым проходом с глоссарием».
**Его три первоочередные правки:** абзац про Шэнь Цуй (крит) · свести глоссарий одним проходом
(закрывает почти весь приоритет №1) · вернуть абзацную разбивку и переписать четыре кальки по смыслу.
**И честная рамка вокруг этой оценки.** Она снята ОДНИМ читателем на ОДНОЙ книге из трёх глав, без
второго независимого суждения о качестве (адъюдикация §17.1 проверяла ЦИТАТЫ и ЧИСЛА, а не вкус).
Шкала «издательское / приличное любительское / машинное» — его, не наша ратифицированная.
## 20. СОСТОЯНИЕ СДАЧИ
**Сделано и предъявлено:** книга проведена от двери до файла и прочитана ($0.396657, 20 мин 06 с) ·
исход есть у каждой из 23 машинных строк пре-рег-таблицы (§9 результатов), а две последние её строки — 24 «вычитка» и 25 «сверка приборов» — закрыты §17 и §18 · деньги сведены шестью счётами с
названной оговоркой про независимость (§15 результатов) · обе миграции предъявлены ДАННЫМИ, и v18
дала числу ряда 422 первое значение (§6 результатов) · денежная мера пре-регистрирована, прибор
само-проверен на прогоне A до применения, и две меры одного явления разошлись в разные стороны
(§5 результатов) · вычитка всего текста опус-читателем с доказательством дочитывания и адъюдикацией
цитат (§17) · сверка приборов с читателем разделена на четыре РАЗНЫЕ причины молчания (§18) · находки
вне заказа с носителями (§12 результатов) · девиации объявлены, включая правку моего прибора после
фриза (§11 результатов) · что не удалось и где прибор слеп (§1314 результатов).
**Где артефакты:**
| Что | Где | В git |
|---|---|---|
| Инструменты (драйвер, прибор денег, прибор отгруженного текста, пакет читателя) | `eval/cold_run_b/` | фриз `4ee7f03` + правка `measure.py` отдельным коммитом |
| Пре-регистрация и этот разбор | `docs/experiments/25-door-to-file-b.md` | пре-рег`4ee7f03`; результаты лендит оркестратор |
| Стенд, следы, 54 сохранённых HTTP-тела, копии БД, отгруженные файлы | `~/tm-coldrun-b/` (вне git, вне `/tmp`) | нет |
| Живой журнал прогона («время · что увидел · чего ожидал · что это значит») | `~/tm-coldrun-b/evidence/live-journal.md` | нет |
| Ожидания §4.8, написанные ДО проб, и исход каждой | `~/tm-coldrun-b/evidence/checks/api-expectations.md` | нет |
**Что оставлено включённым:** стенд жив (`tmplatformd` на `127.0.0.1:8098`, pid в
`~/tm-coldrun-b/stand/daemon.pid`, база `tm_coldrun_b` в кластере `/tmp:55433`, клон фриза в
`~/tm-coldrun-b/freeze`) — чтобы любое число этого отчёта можно было пере-снять теми же командами.
Гасить по слову оркестратора и **только по pid**.
**Чего я не делала и не буду:** не чинила ничего из найденного — ни в движке, ни в платформе, ни в
банке. Пак запрещает прогону чинить, и это правильно: прогон, чинящий найденное, перестаёт быть
холодным и теряет право сравниваться с A. Единственная правка кода за пак — мой СОБСТВЕННЫЙ прибор
после фриза, и она объявлена девиацией (§11 п.1 результатов).
**Работа завершена, править не планирую.**
---
# ДОПИСКА 12.09 — по прямому вопросу «что ты знаешь и не сказала»
> ⚠ Дописка, а не правка: ничего выше не тронуто. Поводом послужил вопрос оркестратора №23 после
> закрытия пака (норма: «в коде названо, в отчёте нет — значит для следующей смены НЕ названо»).
## Д1. ⭐⭐ БАНК КНИГИ НЕДЕТЕРМИНИРОВАН ПО ПОСТРОЕНИЮ — замерено, а не предположено
В §18.1(а) сказано, что восьми термов читателя «нет в банке», и причина названа только для части.
Пере-снято по обеим базам, два разных механизма:
**(а) ЧТО ОТСЕКАЕТ КОНТРАСТ — и это НЕ порог.** Греп по `mining-contrast.zh.txt` (jieba 0.42.1,
**349 046 строк** — контроль): `家族` · `族` · `炼制` · `楼阁` · `先机` · `老妈子` — **ВСЕ в словаре
общекитайского**, а `方源` · `古月山寨` · `丫头片子` · `怀璧之罪` — нет. ⇒ **шесть из восьми термов
читателя отсечены потому, что они ОБЫЧНЫЕ КИТАЙСКИЕ СЛОВА.** Контраст спрашивает «характерно ли слово
для ЭТОГО текста на фоне китайского вообще»; продукт спрашивает «нужна ли слову ОДНА закреплённая
русская передача на всю книгу». **Для `家族` первый ответ „нет“, второй „да“** — и крупнейшая находка
приоритета №1 (род↔клан, 9 + 29 вхождений в исходнике) не отсеяна порогом, а исключена ПО ПОСТРОЕНИЮ.
**(б) А РАСХОЖДЕНИЕ БАНКОВ A И B — СОВСЕМ ДРУГОЕ, и оно глубже.** Банки: общих **53**, только в A —
**16**, только в B — **13**, на побайтно одном исходнике и тождественном покупающем конфиге
(sha `a46b33ee…` у обоих). Granularity-парами объясняются лишь **4** (`丙等资质``资质` ·
`四更时``四更` · `族长家老``族长` · `贴身丫鬟``丫鬟`). И частотой это не объясняется:
**`舅父舅母` встречается в исходнике 14 раз, есть в банке A и отсутствует в банке B.**
**МЕХАНИЗМ ЗАКРЫТ ЗАМЕРОМ, $0.** Кандидатов банка предлагает ЧЕРНОВИК — канал `banknote`
draft-side (`bank-mining: draft-side proposals folded`). Сверено `retrieval_state.banknote_detail`
двух прогонов НА ОДНИХ И ТЕХ ЖЕ шести чанках:
| чанк | поверхностей A | B | общих | только A | только B |
|---|---|---|---|---|---|
| ch1/0 | 16 | 19 | 15 | `天罗地网` | `牌位` `祭祀大典` `赤铜香炉` `魔头` |
| ch1/1 | 19 | 18 | 17 | `三转蛊师` `古月族长` | `将敬酒` |
| ch2/0 | 19 | **11** | 11 | `三转` `仙师` `孪生弟弟` `开窍` `怀璧之罪` `舅母` `舅父` `魔道巨擘` | — |
| ch2/1 | 15 | 12 | 11 | `唐诗宋词` `正派` `甲等资质` `邪魔` | `甲等` |
| ch3/0 | 17 | 20 | 13 | `四更时` `舅父舅母` `贴身丫鬟` `雪盐` | `丫鬟` `四更` `嬷嬷` `少爷` `时辰` `舅母` `舅父` |
| ch3/1 | 12 | 13 | 7 | `丙等资质` `丫头片子` `后宫` `族长家老` `舅父舅母` | `丙等` `家老` `族长` `舅母` `舅父` `资质` |
| **итого** | **98** | **93** | **74** | | |
**Жаккар по книге = 0.63.** ⇒ **банк книги — функция ЧЕРНОВИКА, а черновик есть стохастический выход
модели. Значит и СОСТАВ банка, и ГРАНУЛЯРНОСТЬ его ключей недетерминированы от прогона к прогону на
одном входе.** Видно и род различия: на ch3/1 прогон A предлагает СОСТАВНЫЕ ключи (`丙等资质`,
`族长家老`, `舅父舅母`), а B — их СОСТАВЛЯЮЩИЕ (`丙等`, `族长`, `舅母`, `舅父`, `资质`, `家老`); на
ch2/0 предложение B — строгое подмножество A, беднее на восемь.
**Следствие для чисел этого прогона, и оно жёстче уже записанного:** консистентность мерится ПО
СТРОКАМ БАНКА, а сами строки между прогонами разные. **Двигается не только вердикт (ряд 436) — двигается
ПОПУЛЯЦИЯ, на которой он считается.** Числа `I1`/`I2` двух прогонов сравнивать нельзя не потому, что
прибор плох, а потому, что у них разные знаменатели по построению.
## Д2. ТРИ НАХОДКИ ЧИТАТЕЛЯ, ВЫПАВШИЕ ИЗ ОТЧЁТА — и почему это хуже отброса
В отчёт вошли **22 из 26**. [4] присутствует по содержанию (в §18.1(а) термы перечислены
иероглифами). **Три отсутствуют полностью, и я их НЕ отбрасывала по доводу — они выпали при сведении
находок в КЛАССЫ:** в §18.1(г) перечислены представители класса, и перечень не сверен с полным
списком. **У отброса есть аргумент, который можно проверить; у выпадения из выжимки его нет.**
* **[10] МАЖОР, приоритет 4, `калька`.** `舅父舅母` — одно двусложное слово — отдано шестисловной
конструкцией «дядя по матери и тётя по матери», **11 раз полной парой** (плюс «тётя по матери»
отдельно ещё 6). ОРИГИНАЛ: «明天就是开窍大典,哥哥你这么晚还不休息,舅父舅母知道了,恐怕会担心的。»
ПЕРЕВОД: «Завтра ведь церемония открытия апертуры, а ты так поздно не отдыхаешь. Если дядя по
матери и тётя по матери узнают, боюсь, они будут волноваться.» В живой реплике подростка брату это
канцелярская справка о родстве.
* **[21] МИНОР, приоритет 3, `типографика`.** Два вида многоточия («…» ×10 и «...» ×2, строки 217 и
227) и одна пара ПРЯМЫХ ASCII-кавычек внутри ёлочек при 24 парах ёлочек:
«…на пути демонов нет слова "компромисс"». Правки корректора, которых не сделали.
* **[24] МИНОР, приоритет 4, `калька`.** ОРИГИНАЛ: «弟弟如卧龙升天,哥哥似凤雏落地。» ПЕРЕВОД:
«Младший — словно Лежащий дракон, вознёсшийся в небо; старший — словно Птенец феникса, павший на
землю.» `卧龙` и `凤雏` — прозвища Чжугэ Ляна и Пан Туна, для китайского читателя мгновенно
узнаваемая пара равновеликих гениев; по-русски антитеза не срабатывает, читатель видит двух
произвольных зверей.
## Д3. ЧЕТЫРЕ КОДОВЫХ НАБЛЮДЕНИЯ, НЕ ЗАВЕДЁННЫЕ НАХОДКАМИ В ОСНОВНОМ ОТЧЁТЕ
1. ⚠ **`bank_decisions` пуста при трёх `approved`.** После ДВУХ прогонов, применивших правки через
дверь, в таблице **0 строк**, при `bank_terms` с **3** `approved` (оба числа сняты запросом).
Эффект решений есть, платформенной записи «кто что решил» нет. Видела на дымовой руке и не чесала;
в код за причиной НЕ ходила, поэтому дефект это или таблица другого назначения — не установлено.
2. **ДВА флора `min_max_tokens`, и какой выигрывает — выяснила только по вопросу.**
`pipeline-c1.yaml:18` несёт `2048`, `models.yaml``8000` (flash) и `16000` (pro). На проводе:
черновик 8496 (= 8000 + 496 банк-бюджета), редактор 16000, бэнк-роли 8000 ⇒ выигрывает
пер-модельный, пайплайновый инертен.
3. **`TM_PLATFORM_SIGNUP_GRANT_USD` печатается на буте** как «(an amount; not logged)», то есть
настройка фри-тир-гранта в деплое существует — при ратифицированном «продуктовых квот и фри-тира
нет и не проектируется».
4. **`waves.workers` — РЕАЛЬНЫЙ ключ** (`internal/config/pipeline.go:98`), дефолт **1** при ≤ 0
(`:939-940`), **в боевом `pipeline-c1.yaml` не задан вовсе.** ⇒ последовательность прогона —
дефолт, а не записанное решение. Внутренний пер-модельный кап — `RateLimit.MaxConcurrency`
(`models.yaml`), выставлен только у `mistral-large-2512`.
**О поведении при `workers > 1` этот прогон не говорит НИЧЕГО:** гонки, порядок записи,
снапшот и леджер под конкуренцией НЕ СМОТРЕЛА.
## Д4. ДВА СВОЙСТВА ХОСТА, КОТОРЫХ НЕТ В ОСНОВНОМ ОТЧЁТЕ
* **`/tmp` занят на 96 % на входе и 97 % на выходе** (5.7 ГБ из 5.9, tmpfs; 4.9 ГБ — скретчпады
ЧУЖИХ сессий). Меня не задело только потому, что `TMPDIR`/`GOTMPDIR` всех сборок и движка уведены
на диск. Забитый tmpfs маскируется под сломанную сборку.
* **`TM_PLATFORM_RUN_MEMORY_MAX` = `4G` по умолчанию** — прогон исполняется в транзиентном юните
systemd С ПОТОЛКОМ ПАМЯТИ. На трёх главах не сработало; на книге в 2283 главы это первый
подозреваемый при убийстве по `CONSTRAINT_MEMCG`.
* **`loginctl show-user … Linger=no`**, хотя `platform/README.md` говорит «установка требует
`enable-linger`, иначе не стартует ни один прогон». Прогон прошёл целиком ⇒ на этом хосте
утверждение README буквально НЕ выполняется (пользовательский менеджер systemd жив сам по себе).
## Д5. ЧЕГО Я НЕ СМОТРЕЛА ВООБЩЕ
SSE-поток `/v0/books/{id}/stream` не дёргала ни разу · `tmctl backup` как канал не проверяла, хотя
`TM_PLATFORM_BACKUP_DIR` включила · `--max-units` не трогала · несмонтированные ручки
(`getRun`/`updateBook`/`deleteBook`) не проверяла · метрики на `127.0.0.1:9468` не читала ·
`regenerate_before_escalate` — не проверяла, срабатывал ли.

View file

@ -49,7 +49,7 @@
- **Langpack / пар-файл** — данные языковой пары (`backend/configs/langpacks/`); книжное в пар-слое = утечка; **общность** — ревью-вопрос «заработает ли пара/вторая книга без правки Go».
- **Ведро B/D** — корзины триажа карты общности D39.60 (B «пар-модуль без гейта» · D «удалить»).
- **tmctl** — CLI бэкенда. Полный список глаголов — ОДИН в коде (`backend/cmd/tmctl/main.go`, `dispatchCommands`), и он же печатается в usage (паритет «список ↔ usage ↔ switch» прибит тестом `cmd/tmctl/invocation_test.go`); сверка одной командой: `grep -n -A3 "dispatchCommands = " backend/cmd/tmctl/main.go`. ⚠ Копия списка стояла здесь и трижды отставала от кода — снята: носитель один, отдельные глаголы (`build`, `bank-apply`) расшифрованы строками ниже.
- **Подстановка цены** — тихий фолбэк прайсера: провайдер вернул слаг, которого в каталоге нет, и `PriceForResponse` (`backend/internal/ledger/pricing.go:61`=`func (p *Pricer) PriceForResponse`) пробует `actual`, потом `requested`, потом якорь, а ни одно из трёх мест вызова не различает, какая ветвь сработала. Пока подмена падает внутрь той же семьи, счёт верен; опасна не подстановка, а её НЕВИДИМОСТЬ — при маршрутизации провайдера ВНИЗ это «×3.3 из кармана читателя, молча». Носитель — строка 413. ⚠ Сводя такое по диску, помнить строку 428: обход файлов `*.db` умножает трату на число КОПИЙ базы (замер 11.09 — множитель 10.88×), поэтому деньги считаются по УНИКАЛЬНОМУ `(book_id, date)` либо по одной авторитетной базе.
- **Подстановка цены** — тихий фолбэк прайсера: провайдер вернул слаг, которого в каталоге нет, и `PriceForResponse` (`backend/internal/ledger/pricing.go:88`=`func (p *Pricer) PriceForResponse`) пробует `actual`, потом `requested`, потом якорь, а ни одно из трёх мест вызова не различает, какая ветвь сработала. Пока подмена падает внутрь той же семьи, счёт верен; опасна не подстановка, а её НЕВИДИМОСТЬ — при маршрутизации провайдера ВНИЗ это «×3.3 из кармана читателя, молча». Носитель — строка 413. ⚠ Сводя такое по диску, помнить строку 428: обход файлов `*.db` умножает трату на число КОПИЙ базы (замер 11.09 — множитель 10.88×), поэтому деньги считаются по УНИКАЛЬНОМУ `(book_id, date)` либо по одной авторитетной базе.
- **COGS** — себестоимость перевода; **ре-проба** — дешёвый повторный замер провайдера после аномалии (носитель события — строка 188; исходная 74 закрыта D39.91).
### Неймспейсы номеров

View file

@ -58,9 +58,10 @@
| Зона | Промт | Сессия | Состояние |
|---|---|---|---|
| бэкенд | `BACKEND_FAILURE_YOU_CAN_SEE_SESSION_PROMPT.md` (ред. 2) | **textmachine-61** | ✅ выдан 11.09; жду эхо. Прежний пак зоны ПРИНЯТ актом `D39.249` |
| бэкенд | `BACKEND_BOUGHT_MEANS_DELIVERED_SESSION_PROMPT.md` | **textmachine-b1** | ✅ **ВЫДАН 11.09** после опровергателя (21 утверждение проверено, 4 опровергнуто, 4 умолчания дописаны); жду эхо |
| платформа | — | — | 🟢 **СЛОТ СВОБОДЕН** — пак принят актом `D39.250`, минор 0.15.0 |
| полигон | — | — | ⛔ **ЗАКАЗ СНЯТ ВЛАДЕЛЬЦЕМ 11.09** («полигон не надо выдавать»); промт готов и лежит, пере-выдача — только его словом |
| сквозной (вне кодовых зон) | — | — | ✅ **ЗАКРЫТ актом `D39.252`** 11.09; промт в архиве с баннером. Стенд поднят до конца приёмки, гасить по pid |
| фронт | заморожен | — | разморозка по критерию `NEAR_TERM_PLAN.md` |
**Резерв израсходован: `textmachine-61` получила пак о падениях 11.09, как только слот освободился актом `D39.249`.**
@ -76,11 +77,7 @@
В акте будет сказано, ЧЬИМ прибором снята каждая половина: полный гейт 169/169 · RED=167 · выживших 1 ·
UNKNOWN 1 — её, дошёл один раз до правок; десять пере-снятий и стенд — мои.
**ДОЛГ ЛЕНДИНГА: голова `D39.248` бампнута в ДВУХ носителях из трёх.** `05-decisions-log.md` и
`05-decisions-index.md` заландены; **`docs/PROGRESS.md`НЕТ**: там лежит чужая незакоммиченная правка
на 337 строк (секция бэкенд-сессии), и коммитить файл целиком значило бы унести её под своим сообщением.
⇒ бамп CURRENT-STATE едет ВМЕСТЕ с лендингом бэкенд-пака, где её секция и должна приехать. До тех пор
`counts.py --check` на HEAD краснеет по голове — это ЗНАЕМОЕ расхождение, а не находка.
**ДОЛГ ЛЕНДИНГА СНОВА ОТКРЫТ (акт `D39.252`): голова бампнута в ДВУХ носителях из трёх.** Шапка `05-decisions-log.md` и титул реестра стоят на `D39.252`; **CURRENT-STATE — НЕТ**: в `docs/PROGRESS.md` лежит секция бэкенд-сессии на 151 строку из 152, и коммит файла целиком унёс бы её под моим сообщением — ровно инцидент `54f8f6c`, повторять нельзя. ⇒ бамп CURRENT-STATE едет ВМЕСТЕ с лендингом бэкенд-пака. До тех пор `counts.py --check` краснеет по голове — это ЗНАЕМОЕ расхождение, а не находка.
**В очереди, написано и ждёт слота:** `BACKEND_FAILURE_YOU_CAN_SEE_SESSION_PROMPT.md`, **редакция 2**
выдаётся СВЕЖЕЙ бэкенд-сессии, как c9 закроется актом. ⛔ **Опровергатель нашёл 17 находок, и первая
@ -101,9 +98,12 @@ UNKNOWN 1 — её, дошёл один раз до правок; десять
| 2 | платформа | правда у двери | textmachine-8e | ✅ своими словами, с контрактным пингом | ✅ закрыт актом `D39.246` (с дофиксом) |
| 3 | полигон | холодный прогон A (платный) | textmachine-23 | ✅ | ✅ закрыт актом `D39.247`; строка 16 закрыта замером |
| 4 | — | ревью МОИХ записей (не пак зоны) | textmachine-9a | — | ✅ отчёт сдан; часть находок заландена 11.09 |
| 5 | бэкенд | консистентность, которую можно предъявить | textmachine-c9 | ✅ | ⏳ дофикс после шести выживших посадок |
| 5 | бэкенд | консистентность, которую можно предъявить | textmachine-c9 | ✅ | ✅ закрыт актом `D39.249` с дофиксом (шесть посадок круга 1 выжили) |
| 6 | ~~полигон~~ | ремонт прибора | — | — | ⛔ **СНЯТ: зона вне скоупа словом владельца**, промт в архиве |
| 7 | платформа | ответ, не зависящий от везения (`PD-448` · `162` · `466` · `139`) | textmachine-37 | ✅ своими словами, 3 опасности + возражение | идёт |
| 7 | платформа | ответ, не зависящий от везения (`PD-448` · `162` · `466` · `139`) | textmachine-37 | ✅ своими словами, 3 опасности + возражение | ✅ закрыт актом `D39.250`, минор 0.15.0 |
| 8 | бэкенд | за одну единицу платим дважды | textmachine-61 | ✅ своими словами, 4 возражения — и первое ПЕРЕВЕРНУЛО посылку пака | ✅ закрыт актом `D39.251` с дофиксом |
| 9 | бэкенд | купленное доставлено (ряд 291 + приложение 379) | textmachine-b1 | ждётся | выдан 11.09 |
| 10 | сквозной | ХОЛОДНЫЙ ПРОГОН B, платный, с вычиткой всего текста | textmachine-82 | ✅ своими словами, 5 опасностей — три исправили промт | ✅ **ПРИНЯТ актом `D39.252`**, факт $0.396657 |
**ПОЛИГОН СНЯТ ВЛАДЕЛЬЦЕМ 11.09.** Я предлагал пак трижды — `ed`, `5c` (обе отработавшие, отказались ПО НОРМЕ и были правы) и свежей `37`, — после чего владелец сказал «полигон не надо выдавать». Выдача `37` ОТОЗВАНА тем же часом. ⚠ Урок не про полигон: **я трижды искал адресата для заказа, приоритет которого не подтверждал у владельца.** Слот зоны свободен ≠ работу зоны надо заказывать.

26
eval/cold_run_b/README.md Normal file
View file

@ -0,0 +1,26 @@
# `cold_run_b` — харнесс холодного прогона B
Инструменты пака «ХОЛОДНЫЙ ПРОГОН B» (`docs/COLD_RUN_B_SESSION_PROMPT.md`). Пре-регистрация,
таблица ожидаемых исходов и результаты — `docs/experiments/25-door-to-file-b.md`.
| Файл | Что делает | Чем проверен |
|---|---|---|
| `door.py` | драйвер: гейт · вход · грант · интейк · опции · старт · вахта · улика · подпись · резюм · выгрузка · произвольная проба с ЗАРАНЕЕ записанным ожиданием. **Отказывает**, а не предупреждает | $0-дым прошёл им целиком: интейк → банк-стоп → подпись → резюм → обе выгрузки → файл |
| `measure.py` | прибор денег и ЕДИНИЦ: знаменатель, оба числителя, обе ориентации второго, два пути к денежному числу | `--selfcheck` пере-снимает КАЖДОЕ опубликованное число прогона A на его же базе и отказывает, если хоть одно сдвинулось |
## Три вещи, ради которых это не копия харнесса прогона A
1. **Каждый запрос и каждый ответ ложатся в файл ДО того, как вернутся вызывающему.** Драйвер A
печатал тела на экран и не писал их никуда — пять строк его пре-рег-таблицы остались без
носителя, и целая находка состояла только из тела ответа, которого не сохранилось.
2. **Живость читается ПО ПОПЫТКАМ.** `max(last_seq)` по всем попыткам во время второй выглядит
замершим на значении первой. Номер попытки не вычисляется по времени — он НАЗВАН в `trace_id`.
3. **Деньги читаются фильтром по ОДНОЙ учётке.** Сборщик A суммировал леджер по всем учёткам стенда
и подал два гранта как один; дымовые деньги и боевые не должны встречаться ни в одном числе.
## Что переиспользуется из фриза прогона A без единой правки
`eval/door_to_file/stub` ($0-заглушка провайдера `kind: local` на `127.0.0.1:11434`) и
`eval/door_to_file/zeropipe` (рендер боевого пайплайна на $0-пару, печатает счёт достижимых ПЛАТНЫХ
моделей в обоих файлах). Оба собраны из клона B и несут его VCS-штамп — девиация §12.5 отчёта A
(«два моих инструмента собраны не из фриза») здесь закрыта построением.

508
eval/cold_run_b/door.py Executable file
View file

@ -0,0 +1,508 @@
#!/usr/bin/env python3
"""door.py — the driver of cold run B: one book from the intake door to a downloaded file.
Why this exists next to `eval/door_to_file/drive.sh` rather than as an edit of it: that harness is
frozen evidence of run A, and run A named two defects in it that this run cannot inherit.
* IT PRINTED EVERY HTTP ANSWER TO THE SCREEN AND WROTE NONE TO DISK. Five of A's pre-registration
rows ended with no carrier at all (§10.12 of its report): the `run-options` body, the 202 of the
start, the corrections door's `changed`/`applied`, and a whole finding that consisted of nothing
but a response body. Here EVERY request and EVERY answer is appended to evidence/http/ before
the caller sees it, so an assertion about a body can be re-read rather than remembered.
* ITS LIVENESS WATCH READ max(last_seq) ACROSS ATTEMPTS, so during the second attempt the number
looked frozen at the first attempt's value while work was going on. Here every liveness number
is read PER ATTEMPT, and the attempt is taken from the row, never inferred from time.
Two rules this file keeps because breaking them is what makes a run measure nothing:
* money is read filtered by ONE account. A's collector summed the ledger over every account on the
stand and reported two grants as one (§14.2); the smoke's account and the paid account must never
meet in a number.
* a claim about a count prints its control quantity beside it. "0 rows" and "no such table" look
the same in output, and in money that is the difference between "did not spend" and "did not look".
"""
import argparse, http.cookiejar, json, mimetypes, os, re, subprocess, sys, time, urllib.error, urllib.request, uuid
W = os.environ.get("W", "/home/ubuntu-26/tm-coldrun-b")
ADDR = os.environ.get("ADDR", "127.0.0.1:8098")
DSN = os.environ.get("TM_PLATFORM_DSN", "postgres://postgres@/tm_coldrun_b?host=/tmp&port=55433&sslmode=disable")
PSQL = os.path.expanduser("~/.local/pgsql/bin/psql")
HTTPDIR = os.path.join(W, "evidence", "http")
STATE = os.path.join(W, "stand", "run-state.json")
# An opener that does NOT go through the environment's proxy: this host exports a webshare proxy and
# a WinINET-style NO_PROXY that Go, curl and urllib all fail to parse, so a request to 127.0.0.1 goes
# out to the proxy and comes back 403 (provider-quirks, "Прокси на localhost").
# The jar is a FILE: each step is its own process, and a jar that lives only in memory logs in again
# on every call — which looks like a working session right up to the moment a step needs the one it
# thought it had.
JARFILE = os.environ.get("TMB_JAR", os.path.join(W, "stand", "cookies.txt"))
JAR = http.cookiejar.MozillaCookieJar(JARFILE)
try:
JAR.load(ignore_discard=True, ignore_expires=True)
except Exception:
pass
OPENER = urllib.request.build_opener(
urllib.request.ProxyHandler({}), urllib.request.HTTPCookieProcessor(JAR))
def jar_save():
os.makedirs(os.path.dirname(JARFILE), exist_ok=True)
JAR.save(ignore_discard=True, ignore_expires=True)
_seq = [0]
def state_load():
try:
with open(STATE) as f:
return json.load(f)
except FileNotFoundError:
return {}
def state_put(**kw):
s = state_load()
s.update(kw)
os.makedirs(os.path.dirname(STATE), exist_ok=True)
with open(STATE, "w") as f:
json.dump(s, f, indent=2)
return s
def sql(q):
out = subprocess.run([PSQL, DSN, "-At", "-F", "\t", "-c", q],
capture_output=True, text=True)
if out.returncode != 0:
raise SystemExit("psql refused: " + out.stderr.strip())
return [l.split("\t") for l in out.stdout.splitlines()]
def sql1(q, default=None):
rows = sql(q)
return rows[0][0] if rows and rows[0] and rows[0][0] != "" else default
def api(method, path, body=None, ctype=None, label=None, raw=False, headers=None):
"""One request. The request AND the answer are on disk before this returns."""
_seq[0] += 1
n = _seq[0]
url = f"http://{ADDR}{path}"
req = urllib.request.Request(url, data=body, method=method)
req.add_header("X-TM-Client", "coldrun-b-driver")
if ctype:
req.add_header("Content-Type", ctype)
for k, v in (headers or {}).items():
req.add_header(k, v)
t0 = time.time()
try:
r = OPENER.open(req, timeout=300)
code, data, hdrs = r.status, r.read(), dict(r.headers)
except urllib.error.HTTPError as e:
code, data, hdrs = e.code, e.read(), dict(e.headers)
except Exception as e: # connection refused, timeout, …
code, data, hdrs = 0, str(e).encode(), {}
ms = int((time.time() - t0) * 1000)
name = f"{n:03d}-{(label or path.strip('/').replace('/', '_'))[:60]}"
rec = {"n": n, "at": time.strftime("%Y-%m-%dT%H:%M:%S%z"), "method": method, "url": url,
"status": code, "ms": ms, "headers": hdrs,
"request_ctype": ctype, "request_bytes": len(body or b"")}
os.makedirs(HTTPDIR, exist_ok=True)
if raw:
with open(os.path.join(HTTPDIR, name + ".bin"), "wb") as f:
f.write(data)
rec["body_file"] = name + ".bin"
rec["body_bytes"] = len(data)
else:
try:
rec["body"] = json.loads(data.decode("utf-8"))
except Exception:
rec["body_text"] = data.decode("utf-8", "replace")[:20000]
with open(os.path.join(HTTPDIR, name + ".json"), "w") as f:
json.dump(rec, f, ensure_ascii=False, indent=2)
with open(os.path.join(W, "evidence", "http-index.jsonl"), "a") as f:
f.write(json.dumps({k: rec[k] for k in ("n", "at", "method", "url", "status", "ms")}) + "\n")
jar_save()
return code, (rec.get("body") if not raw else data), hdrs
def multipart(fields, filefield, filepath):
b = "----tmb" + uuid.uuid4().hex
out = []
for k, v in fields.items():
out.append(f"--{b}\r\nContent-Disposition: form-data; name=\"{k}\"\r\n\r\n{v}\r\n".encode())
fn = os.path.basename(filepath)
ct = mimetypes.guess_type(fn)[0] or "application/octet-stream"
out.append(f"--{b}\r\nContent-Disposition: form-data; name=\"{filefield}\"; filename=\"{fn}\"\r\n"
f"Content-Type: {ct}\r\n\r\n".encode())
out.append(open(filepath, "rb").read())
out.append(f"\r\n--{b}--\r\n".encode())
return b"".join(out), f"multipart/form-data; boundary={b}"
# ---------------------------------------------------------------- the gate
FREEZE_SHA = os.environ.get("FREEZE_SHA", "baa06cef7b2b30b8f7cbe031f3d0fabc03a01a55")
BINARIES = ["tmctl", "tmplatformd", "tmplatformctl", "stub", "zeropipe"]
def stamp(path):
out = subprocess.run(["go", "version", "-m", path], capture_output=True, text=True).stdout
vcs = [l for l in out.splitlines() if "vcs." in l]
rev = next((l.split("vcs.revision=")[1].strip() for l in vcs if "vcs.revision=" in l), None)
mod = next((l.split("vcs.modified=")[1].strip() for l in vcs if "vcs.modified=" in l), None)
return rev, mod, len(vcs), len(out.splitlines())
def step_gate(args):
"""Refuses; never warns. Three conditions on the stamp, not one — a binary built in a linked
worktree carries NO vcs.* lines, and a gate that only forbids modified=true passes it always."""
bad = []
print("=== binaries: the frozen revision, a clean tree, and a stamp that EXISTS ===")
for b in BINARIES:
p = os.path.join(W, "bin", b)
rev, mod, n, total = stamp(p)
print(f" {b:<14} revision={(rev or 'none')[:12]} modified={mod} vcs_lines={n} (control: build lines {total})")
if n == 0:
bad.append(f"{b}: no vcs.* lines — this gate would be vacuous")
if rev != FREEZE_SHA:
bad.append(f"{b}: revision is not the frozen one")
if mod != "false":
bad.append(f"{b}: built from a dirty tree")
print("=== the stand is MINE: a pid on the port, not a 200 from someone else's process ===")
listeners = subprocess.run(["ss", "-ltnp"], capture_output=True, text=True).stdout.splitlines()[1:]
def owner(port):
for l in listeners:
if f":{port} " in l:
return l.split("users:")[-1] if "users:" in l else l
return None
for port, must in ((ADDR.split(":")[1], True),):
o = owner(port)
print(f" port {port}: {o or 'NOTHING LISTENS'}")
if must and not o:
bad.append(f"port {port}: there is no stand")
stub = owner("11434")
phase = args.phase
if phase == "smoke":
print(f" port 11434 (the $0 stub, REQUIRED in this phase): {stub or 'NOTHING LISTENS'}")
if not stub:
bad.append("the smoke has no stub to answer the local provider")
else:
if stub:
bad.append("something LISTENS on the local provider's address during a PAID phase: a "
"configuration that still reached a local model would be served free prose "
"and the run would measure nothing while looking green")
print(f" port 11434: ⛔ {stub}")
else:
print(f" port 11434: nothing listens (control: {len(listeners)} listeners on this host) "
f"— a local model would now fail LOUDLY")
print("=== what will be bought: sha256 of every file that decides it ===")
for name, path, expect in args.sha or []:
if not os.path.exists(path):
print(f" {name}: ABSENT at {path}")
bad.append(f"{name} is absent")
continue
got = subprocess.run(["sha256sum", path], capture_output=True, text=True).stdout.split()[0]
ok = (expect == "" or expect == got)
print(f" {name}: {got}{'' if expect == '' else (' (matches the pre-registration)' if ok else ' ⛔ the pre-registration says ' + expect)}")
if not ok:
bad.append(f"{name}: sha does not match the pre-registration")
print("=== the freeze tree itself ===")
head = subprocess.run(["git", "-C", os.path.join(W, "freeze"), "rev-parse", "HEAD"],
capture_output=True, text=True).stdout.strip()
dirty = subprocess.run(["git", "-C", os.path.join(W, "freeze"), "status", "--porcelain"],
capture_output=True, text=True).stdout.splitlines()
print(f" HEAD {head} dirty lines: {len(dirty)}")
if os.path.exists("/tmp/.git"):
bad.append("/tmp/.git exists: a build here would lose its stamp")
if bad:
for b in bad:
print("" + b)
raise SystemExit("THE GATE REFUSED; nothing was bought")
print("GATE PASSED")
# ---------------------------------------------------------------- the steps
def step_capabilities(args):
code, body, _ = api("GET", "/v0/capabilities", label="capabilities")
print(f"HTTP {code}")
print(json.dumps(body, ensure_ascii=False, indent=2)[:2000])
if code != 200:
# "empty list" and "never answered" are the same shape in output, and this is the pin the
# whole export third of the path hangs on: say which one happened.
print(f"⛔ the capabilities probe did not answer 200 — export_formats is UNKNOWN, not empty")
return
fmts = (body or {}).get("export_formats")
print(f"export_formats = {fmts!r}{'NON-EMPTY: all three export gates are satisfied' if fmts else '⛔ EMPTY: the export doors are not mounted and the last third of the path does not exist'}")
def step_login(args):
code, body, _ = api("POST", "/auth/dev-login", label="dev-login")
print(f"dev-login HTTP {code}")
rows = sql("select id, coalesce(email,'(none)') from users order by created_at desc limit 5")
print(f" users on this stand: {len(sql('select id from users'))} (control) — newest: {rows[0] if rows else 'none'}")
if rows:
state_put(user=rows[0][0])
print(f" remembered user={rows[0][0]}")
def step_grant(args):
out = subprocess.run([os.path.join(W, "bin", "tmplatformctl"), "grant",
"--user", args.user, "--usd", args.usd, "--note", args.note],
capture_output=True, text=True)
print(out.stdout.strip() or out.stderr.strip())
bal = subprocess.run([os.path.join(W, "bin", "tmplatformctl"), "balance", "--user", args.user],
capture_output=True, text=True)
print(bal.stdout.strip() or bal.stderr.strip())
rows = sql(f"select kind, amount_micro_usd from credit_ledger where user_id='{args.user}' order by id")
total = sum(int(r[1]) for r in rows)
print(f" ledger rows for THIS account: {len(rows)} (control: rows for ALL accounts "
f"{len(sql('select id from credit_ledger'))}), sum {total} µUSD")
def step_intake(args):
body, ct = multipart({"title": args.title, "source_lang": "zh", "target_lang": "ru"}, "file", args.path)
code, b, _ = api("POST", "/v0/books", body=body, ctype=ct, label="intake")
print(f"HTTP {code}\n{json.dumps(b, ensure_ascii=False, indent=2)}")
if code != 201:
raise SystemExit("the intake refused")
state_put(book=b["id"])
sha = subprocess.run(["sha256sum", args.path], capture_output=True, text=True).stdout.split()[0]
print(f" remembered book={b['id']} source sha256={sha}")
def _await_options(book, tries=20):
for _ in range(tries):
code, b, _ = api("GET", f"/v0/books/{book}/run-options", label="run-options")
if code == 200 and isinstance(b, dict) and "order" in b:
return code, b
time.sleep(3)
return code, b
def step_options(args):
book = state_load()["book"]
code, b = _await_options(book)
print(f"HTTP {code}\n{json.dumps(b, ensure_ascii=False, indent=2)}")
order = (b or {}).get("order", {})
ch = order.get("chapters_left")
funded = order.get("term_consistency_funded")
units = sql1(f"select count(*) from units u join chapters c on c.id=u.chapter_id where c.book_id='{book}'", "0")
per = sql(f"""select c.number, count(u.id) from chapters c left join units u on u.chapter_id=c.id
where c.book_id='{book}' group by 1 order by 1""")
for n, k in per:
print(f" chapter {n}: units {k}")
print(f" units_total = {units} chapters_left = {ch} term_consistency_funded = {funded}")
bad = []
if args.chapters is not None and str(ch) != str(args.chapters):
bad.append(f"the slice is not the pre-registered one: {ch} chapters, expected {args.chapters}")
if args.units is not None and str(units) != str(args.units):
bad.append(f"units_total is {units}, the pre-registration says {args.units}")
if args.require_funded and funded is not True:
bad.append(f"term_consistency_funded is {funded}: the hold does not carry the book bond and "
f"the run would quietly lose its terminology consolidation")
if bad:
for x in bad:
print("" + x)
raise SystemExit("STOP before paying")
print("OPTIONS ACCEPTED")
def step_start(args):
book = state_load()["book"]
payload = {"stop_for_signing": bool(args.stop_for_signing)}
if args.chapters is not None:
payload["chapters"] = args.chapters
if args.characters is not None:
payload["characters"] = args.characters
hdrs = {"Idempotency-Key": args.idempotency_key} if args.idempotency_key else {}
code, b, _ = api("POST", f"/v0/books/{book}/runs", body=json.dumps(payload).encode(),
ctype="application/json", label="start", headers=hdrs)
print(f"HTTP {code}\nrequest: {json.dumps(payload, ensure_ascii=False)}\n{json.dumps(b, ensure_ascii=False, indent=2)}")
if code in (200, 201, 202) and isinstance(b, dict) and b.get("id"):
state_put(run=b["id"])
print(f" remembered run={b['id']}")
return code, b
def _engine_numbers(book):
"""What the ENGINE's own project database says. Returns (rows, calls, free_replays, µUSD)."""
d = sql1(f"select workdir from books where id='{book}'")
db = os.path.join(d, "project.db") if d else None
if not db or not os.path.exists(db):
return None, db
import sqlite3
c = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
rows = c.execute("select count(*) from request_log").fetchone()[0]
hits = c.execute("select count(*) from request_log where tm_hit=1").fetchone()[0]
cost = c.execute("select coalesce(sum(cost_usd),0) from request_log").fetchone()[0]
per = c.execute("""select substr(trace_id,-1), count(*), sum(tm_hit=1), round(sum(cost_usd)*1e6)
from request_log group by 1 order by 1""").fetchall()
c.close()
return {"rows": rows, "calls": rows - hits, "free": hits, "micro": round(cost * 1e6),
"per_attempt": per}, db
def step_watch(args):
"""Liveness is read ACTIVELY and on two axes, per ATTEMPT. A growing log is not a result, and a
hung job is indistinguishable from a running one by the completion signal alone."""
s = state_load()
book, run = s.get("book"), s.get("run")
eng, db = _engine_numbers(book)
att = sql(f"select attempt_no, last_seq, coalesce(exit_code,-999), coalesce(spend_micro_usd,0), unit_name "
f"from run_attempts where run_id='{run}' order by attempt_no")
status = sql1(f"select status from runs where id='{run}'", "?")
paused = sql1(f"select coalesce(paused_reason,'') from runs where id='{run}'", "")
events = sql1(f"select count(*) from book_events where book_id='{book}'", "0")
user = s.get("user")
settled = sql1(f"select coalesce(sum(amount_micro_usd),0) from credit_ledger where user_id='{user}' and kind='settlement'", "0")
bal = sql1(f"select coalesce(sum(amount_micro_usd),0) from credit_ledger where user_id='{user}'", "0")
print(f"{time.strftime('%H:%M:%S')} status={status} paused={paused!r} book_events={events}")
for a in att:
unit = a[4]
active = subprocess.run(["systemctl", "--user", "show", unit + ".service", "-p", "ActiveState", "--value"],
capture_output=True, text=True).stdout.strip() or "unknown"
print(f" attempt {a[0]}: last_seq={a[1]} exit={a[2]} spend(cumulative)={a[3]}µUSD unit={active}")
if eng:
print(f" engine: request_log rows={eng['rows']} (calls={eng['calls']}, free replays={eng['free']}) "
f"spend={eng['micro']}µUSD | per attempt {eng['per_attempt']}")
else:
print(f" engine: no project database yet at {db}")
print(f" platform ledger for THIS account: settlements={settled}µUSD balance={bal}µUSD "
f"(control: ledger rows for ALL accounts {sql1('select count(*) from credit_ledger','0')})")
if paused:
raise SystemExit(f"paused_reason={paused} — STOP and ping the orchestrator")
if eng and args.stop_at and eng["micro"] >= args.stop_at:
raise SystemExit(f"spend reached {eng['micro']}µUSD ≥ {args.stop_at}µUSD — STOP and ping the orchestrator")
def step_evidence(args):
"""Takes the copy the resume DESTROYS. exportBank is called again at the start of the next
attempt and overwrites .bank.json atomically; .bank-stop.txt is truncated before it is rewritten,
so it can only be read while the run stands still."""
s = state_load()
book, run = s["book"], s["run"]
d = sql1(f"select workdir from books where id='{book}'")
out = os.path.join(W, "evidence", args.label)
os.makedirs(out, exist_ok=True)
status = sql1(f"select status from runs where id='{run}'", "?")
print(f"run status while the copy is taken: {status}")
if status == "translating":
raise SystemExit("the run is still moving: .bank-stop.txt would be read half-written")
n = 0
for f in ("project.db.bank.json", "project.db.mined-signature.yaml", "project.db.bank-stop.txt",
"project.db.auto-bank.yaml", "project.db.manifest.json", "events.jsonl",
"project.db.mined-delta.yaml", "project.db"):
src = os.path.join(d, f)
if os.path.exists(src):
subprocess.run(["cp", src, out + "/"])
sha = subprocess.run(["sha256sum", src], capture_output=True, text=True).stdout.split()[0]
print(f" {f}: {sha}")
n += 1
print(f" copied {n} artefacts (control: the book directory holds {len(os.listdir(d))} entries)")
code, b, _ = api("GET", f"/v0/books/{book}/bank", label=f"bank-{args.label}")
t = (b or {}).get("terms") or []
print(f" GET bank: HTTP {code}, terms={len(t)}, total={(b or {}).get('total')}, signed={(b or {}).get('signed')}")
rows = sql(f"select count(*), count(*) filter (where status='approved') from bank_terms where book_id='{book}'")
print(f" bank_terms in Postgres: all={rows[0][0]} approved={rows[0][1]}")
def step_sign(args):
book = state_load()["book"]
doc = open(args.doc, "rb").read()
code, b, _ = api("POST", f"/v0/books/{book}/bank/corrections", body=doc,
ctype="application/json", label=f"corrections-{args.label}")
print(f"HTTP {code}\nrequest: {doc.decode('utf-8')[:800]}\n{json.dumps(b, ensure_ascii=False, indent=2)[:2000]}")
return code, b
def step_resume(args):
run = state_load()["run"]
hdrs = {"Idempotency-Key": args.idempotency_key} if args.idempotency_key else {}
code, b, _ = api("POST", f"/v0/runs/{run}/resume", body=b"{}", ctype="application/json",
label=f"resume{args.label}", headers=hdrs)
print(f"HTTP {code}\n{json.dumps(b, ensure_ascii=False, indent=2)}")
return code, b
def step_export(args):
book = state_load()["book"]
code, b, _ = api("POST", f"/v0/books/{book}/exports", body=json.dumps({"format": args.format}).encode(),
ctype="application/json", label=f"export-{args.format}-order")
print(f"order HTTP {code}: {json.dumps(b, ensure_ascii=False)}")
if code not in (200, 201, 202):
raise SystemExit("the export door refused")
eid = b["id"]
for _ in range(60):
time.sleep(2)
code, b, _ = api("GET", f"/v0/books/{book}/exports/{eid}", label=f"export-{args.format}-status")
st = (b or {}).get("state")
if st == "ready":
break
if st == "failed":
raise SystemExit(f"the export failed: {b}")
print(f"status HTTP {code}: {json.dumps(b, ensure_ascii=False)}")
code, data, _ = api("GET", f"/v0/books/{book}/exports/{eid}/content",
label=f"export-{args.format}-content", raw=True)
os.makedirs(os.path.join(W, "evidence", "file"), exist_ok=True)
p = os.path.join(W, "evidence", "file", f"book.{args.format}")
open(p, "wb").write(data)
sha = subprocess.run(["sha256sum", p], capture_output=True, text=True).stdout.split()[0]
print(f"download HTTP {code} bytes={len(data)} sha256={sha}")
row = sql(f"select id,format,state,complete,size_bytes from exports where id='{eid}'")
print(f" exports row (complete is read from Postgres — the wire deliberately does not carry it): {row}")
def step_probe(args):
"""One ad-hoc request whose EXPECTED outcome was written down before it was sent (§4.8): a door's
answer read after the fact always looks right."""
body = args.body.encode() if args.body else None
hdrs = {}
if args.idempotency_key:
hdrs["Idempotency-Key"] = args.idempotency_key
if args.header:
for h in args.header:
k, _, v = h.partition(":")
hdrs[k.strip()] = v.strip()
code, b, hh = api(args.method, args.path, body=body,
ctype=("application/json" if body else None), label=args.label, headers=hdrs)
verdict = "AS EXPECTED" if (args.expect_status is None or code == args.expect_status) else "⛔ NOT AS EXPECTED"
print(f"[{args.label}] {args.method} {args.path} -> HTTP {code} (expected {args.expect_status}) {verdict}")
txt = json.dumps(b, ensure_ascii=False)[:600] if not isinstance(b, bytes) else f"<{len(b)} bytes>"
print(f" body: {txt}")
if args.expect_contains and args.expect_contains not in txt:
print(f" ⛔ the answer does not contain {args.expect_contains!r}")
def main():
ap = argparse.ArgumentParser()
sub = ap.add_subparsers(dest="cmd", required=True)
g = sub.add_parser("gate"); g.add_argument("--phase", default="paid"); g.set_defaults(f=step_gate, sha=[])
for name, fn in (("capabilities", step_capabilities), ("login", step_login)):
p = sub.add_parser(name); p.set_defaults(f=fn)
p = sub.add_parser("grant"); p.add_argument("--user", required=True); p.add_argument("--usd", required=True)
p.add_argument("--note", default="cold run B"); p.set_defaults(f=step_grant)
p = sub.add_parser("intake"); p.add_argument("--path", required=True); p.add_argument("--title", required=True)
p.set_defaults(f=step_intake)
p = sub.add_parser("options"); p.add_argument("--chapters", type=int); p.add_argument("--units", type=int)
p.add_argument("--require-funded", action="store_true"); p.set_defaults(f=step_options)
p = sub.add_parser("start"); p.add_argument("--chapters", type=int); p.add_argument("--characters", type=int)
p.add_argument("--stop-for-signing", action="store_true"); p.add_argument("--idempotency-key")
p.set_defaults(f=step_start)
p = sub.add_parser("watch"); p.add_argument("--stop-at", type=int, default=1000000); p.set_defaults(f=step_watch)
p = sub.add_parser("evidence"); p.add_argument("--label", default="evidence"); p.set_defaults(f=step_evidence)
p = sub.add_parser("sign"); p.add_argument("--doc", required=True); p.add_argument("--label", default="sign")
p.set_defaults(f=step_sign)
p = sub.add_parser("resume"); p.add_argument("--label", default=""); p.add_argument("--idempotency-key")
p.set_defaults(f=step_resume)
p = sub.add_parser("export"); p.add_argument("--format", required=True); p.set_defaults(f=step_export)
p = sub.add_parser("probe"); p.add_argument("--method", default="GET"); p.add_argument("--path", required=True)
p.add_argument("--body"); p.add_argument("--label", required=True); p.add_argument("--expect-status", type=int)
p.add_argument("--expect-contains"); p.add_argument("--idempotency-key")
p.add_argument("--header", action="append"); p.set_defaults(f=step_probe)
a = ap.parse_args()
a.f(a)
if __name__ == "__main__":
main()

137
eval/cold_run_b/measure.py Normal file
View file

@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""measure.py — the money instrument of cold run B, pre-registered before the first cent.
It answers ONE question the call-level numbers cannot: what share of a book's UNITS was paid for and
not kept. A unit is `(chapter, chunk_idx, stage, role)` and it SPANS attempts on purpose the
attempt number is deliberately not in the key, because "we bought this cell twice" is a statement
about the cell, not about a retry.
Why it is validated against run A before it is pointed at run B: an instrument that has never
reproduced a known number is indistinguishable from one that reads the wrong column. `--selfcheck`
re-derives run A's published figures from run A's own database and refuses if any of them moves.
TWO THINGS THIS FILE REFUSES TO DO, both because a previous shift got a beautiful false number
from doing them:
* it never decides "which answer was accepted" by ORDER IN TIME. `ts` in this schema is second-
granular while `started_at` carries microseconds, so every checkpoint substitution is formally
"before" the attempt that made it. The attempt number is NAMED in `trace_id`; it is read, not
computed.
* it never calls the second numerator "over-payment". On run A the second terminology pass changed
the rendering of 12 terms out of 69 and 11 of those 12 are what the reader actually got. That is
the PRICE OF RE-MINING THE BANK AFTER A HUMAN SIGNATURE, not money bought for nothing.
"""
import argparse, collections, json, sqlite3, sys
def load(db):
c = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
c.row_factory = sqlite3.Row
rows = [dict(r) for r in c.execute("select * from request_log order by id")]
c.close()
return rows
def attempt_of(row):
"""The attempt is NAMED in the row. `trace_id` ends with it (tm-stream-<run>-<n>)."""
t = row.get("trace_id") or ""
tail = t.rsplit("-", 1)[-1]
return tail if tail.isdigit() else "?"
def measure(rows):
total = sum(r["cost_usd"] for r in rows)
units = collections.defaultdict(list)
for r in rows:
units[(r["chapter"], r["chunk_idx"], r["stage"], r["role"])].append(r)
paid = {k: v for k, v in units.items() if sum(x["cost_usd"] for x in v) > 0}
n1 = {k: v for k, v in units.items() if any(x["ok"] == 0 and x["cost_usd"] > 0 for x in v)}
n1_money = sum(x["cost_usd"] for v in n1.values() for x in v if x["ok"] == 0 and x["cost_usd"] > 0)
n2 = {k: v for k, v in units.items()
if sum(1 for x in v if x["ok"] == 1 and x["cost_usd"] > 0) > 1}
# Both orientations are printed. Neither is derived from time: rows are ordered by the primary
# key, and which one is called "the extra" is a DECLARED choice, not a discovered fact.
but_first = 0.0
but_last = 0.0
for v in n2.values():
acc = sorted([y for y in v if y["ok"] == 1 and y["cost_usd"] > 0], key=lambda z: z["id"])
but_first += sum(x["cost_usd"] for x in acc[1:])
but_last += sum(x["cost_usd"] for x in acc[:-1])
by_degraded = sum(r["cost_usd"] for r in rows if (r["degraded"] or "") != "")
by_ok0 = sum(r["cost_usd"] for r in rows if r["ok"] == 0 and r["cost_usd"] > 0)
free = [r for r in rows if r["tm_hit"] == 1]
per_attempt = collections.Counter(attempt_of(r) for r in rows)
return dict(rows=len(rows), calls=len(rows) - len(free), free=len(free), total=total,
units=len(units), paid_units=len(paid), n1=len(n1), n1_money=n1_money,
n2=len(n2), n2_but_first=but_first, n2_but_last=but_last,
by_degraded=by_degraded, by_ok0=by_ok0, per_attempt=dict(per_attempt),
n1_keys=sorted(n1), n2_keys=sorted(n2))
def report(m, label):
def pct(x):
return f"{100 * x / m['total']:.1f}%" if m["total"] else "n/a"
print(f"=== {label} ===")
print(f" rows={m['rows']} calls={m['calls']} free checkpoint replays={m['free']} "
f"total=${m['total']:.6f} per attempt (from trace_id): {m['per_attempt']}")
print(f" UNITS total={m['units']} paid (sum cost>0)={m['paid_units']} "
f"{'⚠ the paid filter removed nothing — its behaviour is still unexercised' if m['units'] == m['paid_units'] else 'the paid filter DID remove units'}")
# ⛔ THE DIVISION IS GUARDED, AND THE REASON IS THE BEST ARGUMENT THIS FILE CARRIES. Two lines above,
# this same report prints "the paid filter removed nothing — its behaviour is still unexercised"
# whenever every unit was paid for. On run A and run B that was true, so the branch where NOTHING was
# paid for never ran — and the first time it did (the $0 smoke base, and any empty project), the
# instrument died with ZeroDivisionError instead of printing its own zero. An unexercised branch of a
# MEASURING tool is not a cosmetic gap: the one reading where the share is undefined is exactly the
# reading a $0 arm produces, which is where an instrument is supposed to be rehearsed before it meets
# money. Found by the acceptance, not by the author, on a base this file had already been run against.
share = f"{100 * m['n1'] / m['paid_units']:.1f}% of units" if m["paid_units"] else \
"share UNDEFINED — no unit was paid for at all (not the same statement as 0%)"
print(f" NUMERATOR 1 — a paid attempt that was not kept: {m['n1']} of {m['paid_units']} units = "
f"{share} (money ${m['n1_money']:.6f} = {pct(m['n1_money'])})")
for k in m["n1_keys"]:
print(f" {k}")
print(f" NUMERATOR 2 — a cell whose KEPT result was paid for more than once: {m['n2']} units")
print(f" all payments but the FIRST = ${m['n2_but_first']:.6f} = {pct(m['n2_but_first'])} <- the declared number (lower bound)")
print(f" all payments but the LAST = ${m['n2_but_last']:.6f} = {pct(m['n2_but_last'])} <- the other orientation, printed beside it")
for k in m["n2_keys"]:
print(f" {k}")
print(f" MONEY, path 1 (degraded <> '') = ${m['by_degraded']:.6f} = {pct(m['by_degraded'])}")
print(f" MONEY, path 2 (ok=0 AND cost>0) = ${m['by_ok0']:.6f} = {pct(m['by_ok0'])}")
print(f" {'the two paths AGREE — a weak sign: on run A they agree BY CONSTRUCTION' if abs(m['by_degraded'] - m['by_ok0']) < 1e-9 else '⛔ the two paths DISAGREE — a STRONG finding: the classifier and the acceptance mark have come apart'}")
SELFCHECK = {"rows": 33, "calls": 27, "free": 6, "units": 17, "paid_units": 17, "n1": 4, "n2": 6}
SELFCHECK_MONEY = {"total": 0.419423, "by_degraded": 0.106472, "by_ok0": 0.106472,
"n2_but_first": 0.028742, "n2_but_last": 0.035723}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--db", required=True)
ap.add_argument("--label", default="run")
ap.add_argument("--selfcheck", action="store_true",
help="the db is run A's: refuse unless every published figure of run A comes back")
ap.add_argument("--json")
a = ap.parse_args()
m = measure(load(a.db))
report(m, a.label)
if a.json:
with open(a.json, "w") as f:
json.dump({k: v for k, v in m.items() if k not in ("n1_keys", "n2_keys")}, f, indent=2)
if a.selfcheck:
bad = [f"{k}: {m[k]} != {v}" for k, v in SELFCHECK.items() if m[k] != v]
bad += [f"{k}: {m[k]:.6f} != {v:.6f}" for k, v in SELFCHECK_MONEY.items()
if abs(m[k] - v) > 5e-7]
if bad:
print("⛔ SELFCHECK FAILED — this instrument does not reproduce run A:")
for b in bad:
print(" " + b)
sys.exit(1)
print("SELFCHECK PASSED: every published figure of run A comes back from its own database.")
if __name__ == "__main__":
main()